From 7e8cddf2085575519ecc16eed6b3710f61acc96f Mon Sep 17 00:00:00 2001 From: BlubbFish Date: Mon, 3 Aug 2026 22:31:27 +0200 Subject: [PATCH] first commit --- .gitignore | 9 + DebugGuard.cs | 281 + Guard.Numeric.cs | 1272 + Guard.Numeric.tt.bak | 132 + Guard.cs | 289 + ImageSharp.Drawing/ArcLineSegment.cs | 373 + ImageSharp.Drawing/BooleanOperation.cs | 20 + ImageSharp.Drawing/ClipPathExtensions.cs | 60 + ImageSharp.Drawing/ComplexPolygon.cs | 303 + ImageSharp.Drawing/CubicBezierLineSegment.cs | 263 + ImageSharp.Drawing/EllipsePolygon.cs | 125 + ImageSharp.Drawing/EmptyPath.cs | 56 + ImageSharp.Drawing/FlattenedPointBuilder.cs | 84 + ImageSharp.Drawing/Helpers/ArrayExtensions.cs | 36 + ImageSharp.Drawing/Helpers/MatrixUtilities.cs | 29 + .../Helpers/PolygonUtilities.cs | 126 + ImageSharp.Drawing/IInternalPathOwner.cs | 19 + ImageSharp.Drawing/ILineSegment.cs | 55 + ImageSharp.Drawing/IPath.cs | 50 + ImageSharp.Drawing/IPathCollection.cs | 25 + ImageSharp.Drawing/IPathInternals.cs | 19 + ImageSharp.Drawing/ISimplePath.cs | 22 + ImageSharp.Drawing/ImageSharp.Drawing.csproj | 37 + ImageSharp.Drawing/InternalPath.cs | 397 + ImageSharp.Drawing/IntersectionRule.cs | 20 + ImageSharp.Drawing/LineCap.cs | 17 + ImageSharp.Drawing/LineJoin.cs | 23 + ImageSharp.Drawing/LinearContour.cs | 43 + ImageSharp.Drawing/LinearGeometry.cs | 181 + ImageSharp.Drawing/LinearGeometryCache.cs | 48 + ImageSharp.Drawing/LinearGeometryInfo.cs | 52 + ImageSharp.Drawing/LinearLineSegment.cs | 144 + ImageSharp.Drawing/LinearSegment.cs | 43 + ImageSharp.Drawing/OutlinePathExtensions.cs | 119 + ImageSharp.Drawing/Path.cs | 718 + ImageSharp.Drawing/PathBuilder.cs | 787 + ImageSharp.Drawing/PathCollection.cs | 89 + ImageSharp.Drawing/PathExtensions.Internal.cs | 43 + ImageSharp.Drawing/PathExtensions.cs | 219 + ImageSharp.Drawing/PathTypes.cs | 25 + ImageSharp.Drawing/PiePolygon.cs | 120 + ImageSharp.Drawing/PointOrientation.cs | 25 + ImageSharp.Drawing/Polygon.cs | 98 + .../PolygonGeometry/ClippedShapeGenerator.cs | 114 + .../PolygonGeometry/PolygonClipperFactory.cs | 81 + .../PolygonGeometry/StrokedShapeGenerator.cs | 106 + .../Processing/Backends/ApplyBarrier.cs | 176 + .../Backends/CanvasRegionFrame{TPixel}.cs | 58 + .../Processing/Backends/CompositionCommand.cs | 215 + .../Backends/CompositionSceneCommandBase.cs | 132 + .../Backends/DEFAULT_DRAWING_BACKEND.md | 373 + .../Processing/Backends/DEFAULT_RASTERIZER.md | 458 + .../Backends/DefaultDrawingBackend.Helpers.cs | 202 + .../Backends/DefaultDrawingBackend.cs | 483 + .../Backends/DefaultDrawingBackendScene.cs | 35 + .../DefaultRasterizer.Linearizer.Outputs.cs | 147 + .../Backends/DefaultRasterizer.Linearizer.cs | 920 + .../DefaultRasterizer.RasterizableGeometry.cs | 174 + .../DefaultRasterizer.RetainedTypes.cs | 536 + .../Backends/DefaultRasterizer.Stroke.cs | 1579 + .../DefaultRasterizer.StrokeLinearizer.cs | 1158 + .../Processing/Backends/DefaultRasterizer.cs | 1766 + .../Backends/DrawingBackendScene.cs | 71 + .../Backends/DrawingCommandBatch.cs | 71 + .../Backends/FlushScene.RetainedTypes.cs | 475 + .../Processing/Backends/FlushScene.cs | 1168 + .../Backends/ICanvasFrame{TPixel}.cs | 35 + .../Processing/Backends/IDrawingBackend.cs | 57 + .../Backends/IRasterizerCoverageRowHandler.cs | 20 + .../Backends/MemoryCanvasFrame{TPixel}.cs | 45 + .../Backends/NativeCanvasFrame{TPixel}.cs | 50 + .../Processing/Backends/NativeSurface.cs | 17 + .../Backends/ParallelExecutionHelper.cs | 66 + .../Processing/Backends/RasterizerOptions.cs | 98 + .../Backends/StrokeLineSegmentCommand.cs | 136 + .../Processing/Backends/StrokePathCommand.cs | 111 + .../Backends/StrokePolylineCommand.cs | 148 + ImageSharp.Drawing/Processing/Brush.cs | 56 + .../Processing/BrushRenderer.cs | 68 + .../Processing/BrushWorkspace.cs | 73 + .../Processing/Brushes.Hatch.cs | 648 + ImageSharp.Drawing/Processing/Brushes.cs | 841 + ImageSharp.Drawing/Processing/ColorStop.cs | 34 + .../Processing/DRAWING_CANVAS.md | 392 + .../Processing/DrawingCanvas.Shapes.cs | 225 + .../Processing/DrawingCanvas.cs | 294 + .../DrawingCanvasBatcher{TPixel}.cs | 485 + .../DrawingCanvasFactoryExtensions.cs | 89 + .../Processing/DrawingCanvasState.cs | 65 + .../Processing/DrawingCanvasTimelineEntry.cs | 94 + .../Processing/DrawingCanvas{TPixel}.cs | 1641 + .../Processing/DrawingHelpers.cs | 22 + .../Processing/DrawingOperation.cs | 33 + .../Processing/DrawingOptions.cs | 73 + .../DrawingOptionsDefaultsExtensions.cs | 37 + .../Processing/EllipticGradientBrush.cs | 159 + .../Processing/GradientBrush.cs | 244 + .../Processing/GradientRepetitionMode.cs | 35 + ImageSharp.Drawing/Processing/ImageBrush.cs | 268 + .../Processing/LinearGradientBrush.cs | 199 + .../Processing/PaintExtensions.cs | 47 + .../Processing/PaintProcessor.cs | 46 + .../Processing/PaintProcessor{TPixel}.cs | 44 + .../Processing/PathGradientBrush.cs | 413 + ImageSharp.Drawing/Processing/PatternBrush.cs | 169 + ImageSharp.Drawing/Processing/PatternPen.cs | 79 + ImageSharp.Drawing/Processing/Pen.cs | 120 + ImageSharp.Drawing/Processing/PenOptions.cs | 76 + ImageSharp.Drawing/Processing/Pens.cs | 110 + .../Processing/RadialGradientBrush.cs | 455 + .../RasterizerDefaultsExtensions.cs | 72 + ImageSharp.Drawing/Processing/RecolorBrush.cs | 145 + .../RichTextGlyphRenderer.Brushes.cs | 204 + .../Processing/RichTextGlyphRenderer.cs | 970 + .../Processing/RichTextOptions.cs | 60 + ImageSharp.Drawing/Processing/RichTextRun.cs | 37 + ImageSharp.Drawing/Processing/ShapeOptions.cs | 45 + ImageSharp.Drawing/Processing/SolidBrush.cs | 119 + ImageSharp.Drawing/Processing/SolidPen.cs | 72 + .../Processing/StrokeOptions.cs | 41 + .../Processing/SweepGradientBrush.cs | 305 + ImageSharp.Drawing/RectanglePolygon.cs | 278 + ImageSharp.Drawing/RegularPolygon.cs | 83 + ImageSharp.Drawing/RoundedRectanglePolygon.cs | 142 + ImageSharp.Drawing/SegmentEnumerator.cs | 78 + ImageSharp.Drawing/SegmentInfo.cs | 20 + ImageSharp.Drawing/SplitPathExtensions.cs | 203 + ImageSharp.Drawing/StarPolygon.cs | 100 + ImageSharp.Drawing/Text/BaseGlyphBuilder.cs | 584 + ImageSharp.Drawing/Text/GlyphBuilder.cs | 28 + ImageSharp.Drawing/Text/GlyphLayerInfo.cs | 114 + ImageSharp.Drawing/Text/GlyphLayerKind.cs | 25 + .../Text/GlyphPathCollection.cs | 186 + ImageSharp.Drawing/Text/PathGlyphBuilder.cs | 72 + ImageSharp.Drawing/Text/TextBuilder.cs | 106 + ImageSharp.Drawing/Text/TextUtilities.cs | 95 + ImageSharp.sln | 49 + .../Advanced/AdvancedImageExtensions.cs | 160 + ImageSharp/Advanced/AotCompilerTools.cs | 598 + ImageSharp/Advanced/IConfigurationProvider.cs | 15 + ImageSharp/Advanced/IImageFrameVisitor.cs | 21 + ImageSharp/Advanced/IImageVisitor.cs | 40 + ImageSharp/Advanced/IPixelSource.cs | 31 + ImageSharp/Advanced/IRowIntervalOperation.cs | 18 + .../IRowIntervalOperation{TBuffer}.cs | 29 + ImageSharp/Advanced/IRowOperation.cs | 16 + ImageSharp/Advanced/IRowOperation{TBuffer}.cs | 28 + .../Advanced/ParallelExecutionSettings.cs | 103 + .../Advanced/ParallelRowIterator.Wrappers.cs | 196 + ImageSharp/Advanced/ParallelRowIterator.cs | 316 + ImageSharp/Advanced/PreserveAttribute.cs | 16 + ImageSharp/Color/Color.NamedColors.cs | 916 + ImageSharp/Color/Color.WebSafePalette.cs | 165 + ImageSharp/Color/Color.WernerPalette.cs | 138 + ImageSharp/Color/Color.cs | 638 + ImageSharp/Color/ColorHexFormat.cs | 40 + .../ChromaticAdaptionWhitePointSource.cs | 20 + ImageSharp/ColorProfiles/CieConstants.cs | 21 + ImageSharp/ColorProfiles/CieLab.cs | 221 + ImageSharp/ColorProfiles/CieLch.cs | 221 + ImageSharp/ColorProfiles/CieLchuv.cs | 220 + ImageSharp/ColorProfiles/CieLuv.cs | 233 + .../CieXyChromaticityCoordinates.cs | 88 + ImageSharp/ColorProfiles/CieXyy.cs | 190 + ImageSharp/ColorProfiles/CieXyz.cs | 204 + ImageSharp/ColorProfiles/Cmyk.cs | 203 + .../ColorProfiles/ColorConversionOptions.cs | 94 + .../ColorProfiles/ColorProfileConverter.cs | 48 + ...rProfileConverterExtensionsCieLabCieLab.cs | 99 + ...rProfileConverterExtensionsCieLabCieXyz.cs | 96 + ...olorProfileConverterExtensionsCieLabRgb.cs | 101 + ...rProfileConverterExtensionsCieXyzCieLab.cs | 96 + ...rProfileConverterExtensionsCieXyzCieXyz.cs | 88 + ...olorProfileConverterExtensionsCieXyzRgb.cs | 96 + .../ColorProfileConverterExtensionsIcc.cs | 773 + ...ofileConverterExtensionsPixelCompatible.cs | 193 + ...olorProfileConverterExtensionsRgbCieLab.cs | 101 + ...olorProfileConverterExtensionsRgbCieXyz.cs | 96 + .../ColorProfileConverterExtensionsRgbRgb.cs | 99 + .../Companding/CompandingUtilities.cs | 183 + .../Companding/GammaCompanding.cs | 57 + .../ColorProfiles/Companding/LCompanding.cs | 72 + .../Companding/Rec2020Companding.cs | 76 + .../Companding/Rec709Companding.cs | 72 + .../Companding/SRgbCompanding.cs | 72 + ImageSharp/ColorProfiles/Hsl.cs | 288 + ImageSharp/ColorProfiles/Hsv.cs | 274 + ImageSharp/ColorProfiles/HunterLab.cs | 244 + ImageSharp/ColorProfiles/IColorProfile.cs | 104 + .../ColorProfiles/IProfileConnectingSpace.cs | 18 + .../Icc/Calculators/ClutCalculator.cs | 507 + .../Icc/Calculators/ColorTrcCalculator.cs | 65 + .../CurveCalculator.CalculationType.cs | 14 + .../Icc/Calculators/CurveCalculator.cs | 48 + .../Icc/Calculators/GrayTrcCalculator.cs | 19 + .../Icc/Calculators/ISingleCalculator.cs | 17 + .../Icc/Calculators/IVector4Calculator.cs | 19 + .../LutABCalculator.CalculationType.cs | 23 + .../Icc/Calculators/LutABCalculator.cs | 159 + .../Icc/Calculators/LutCalculator.cs | 78 + .../Icc/Calculators/LutEntryCalculator.cs | 80 + .../Icc/Calculators/MatrixCalculator.cs | 26 + .../Calculators/ParametricCurveCalculator.cs | 131 + .../Icc/Calculators/TrcCalculator.cs | 41 + .../ColorProfiles/Icc/CompactSrgbV4Profile.cs | 43 + .../Icc/IccConverterBase.Checks.cs | 157 + .../Icc/IccConverterBase.ConversionMethod.cs | 66 + .../Icc/IccConverterbase.Conversions.cs | 110 + .../ColorProfiles/Icc/IccConverterbase.cs | 50 + .../Icc/IccDataToDataConverter.cs | 22 + .../Icc/IccDataToPcsConverter.cs | 22 + .../Icc/IccPcsToDataConverter.cs | 22 + .../ColorProfiles/Icc/IccPcsToPcsConverter.cs | 22 + .../KnownChromaticAdaptationMatrices.cs | 135 + ImageSharp/ColorProfiles/KnownIlluminants.cs | 77 + .../ColorProfiles/KnownRgbWorkingSpaces.cs | 113 + .../ColorProfiles/KnownYCbCrMatrices.cs | 62 + ImageSharp/ColorProfiles/Lms.cs | 179 + ImageSharp/ColorProfiles/Rgb.cs | 464 + .../RgbPrimariesChromaticityCoordinates.cs | 83 + .../VonKriesChromaticAdaptation.cs | 96 + .../WorkingSpaces/GammaWorkingSpace.cs | 70 + .../WorkingSpaces/LWorkingSpace.cs | 36 + .../WorkingSpaces/Rec2020WorkingSpace.cs | 36 + .../WorkingSpaces/Rec709WorkingSpace.cs | 36 + .../WorkingSpaces/RgbWorkingSpace.cs | 88 + .../WorkingSpaces/SRgbWorkingSpace.cs | 36 + ImageSharp/ColorProfiles/Y.cs | 143 + ImageSharp/ColorProfiles/YCbCr.cs | 195 + ImageSharp/ColorProfiles/YCbCrTransform.cs | 61 + ImageSharp/ColorProfiles/YccK.cs | 207 + ImageSharp/Common/ByteOrder.cs | 22 + ImageSharp/Common/Constants.cs | 20 + .../Common/Exceptions/ImageFormatException.cs | 35 + .../Exceptions/ImageProcessingException.cs | 41 + .../InvalidImageContentException.cs | 41 + .../Exceptions/UnknownImageFormatException.cs | 21 + .../Extensions/ConfigurationExtensions.cs | 21 + .../Common/Extensions/EnumerableExtensions.cs | 54 + .../Common/Extensions/StreamExtensions.cs | 73 + .../Common/Extensions/Vector4Extensions.cs | 21 + ImageSharp/Common/Helpers/ColorNumerics.cs | 266 + ImageSharp/Common/Helpers/DebugGuard.cs | 84 + ImageSharp/Common/Helpers/EnumUtils.cs | 50 + .../Common/Helpers/ExifResolutionValues.cs | 20 + ImageSharp/Common/Helpers/Guard.cs | 30 + ImageSharp/Common/Helpers/HexConverter.cs | 95 + ImageSharp/Common/Helpers/InliningOptions.cs | 29 + ImageSharp/Common/Helpers/Numerics.cs | 1141 + ImageSharp/Common/Helpers/RuntimeUtility.cs | 45 + .../Helpers/Shuffle/IComponentShuffle.cs | 38 + .../Common/Helpers/Shuffle/IPad3Shuffle4.cs | 100 + .../Common/Helpers/Shuffle/IShuffle3.cs | 52 + .../Common/Helpers/Shuffle/IShuffle4.cs | 185 + .../Common/Helpers/Shuffle/IShuffle4Slice3.cs | 105 + .../Common/Helpers/SimdUtils.Convert.cs | 79 + .../Common/Helpers/SimdUtils.HwIntrinsics.cs | 1160 + ImageSharp/Common/Helpers/SimdUtils.Pack.cs | 238 + .../Common/Helpers/SimdUtils.Shuffle.cs | 554 + ImageSharp/Common/Helpers/SimdUtils.cs | 102 + ImageSharp/Common/Helpers/TestHelpers.cs | 29 + ImageSharp/Common/Helpers/TolerantMath.cs | 105 + ImageSharp/Common/Helpers/UnitConverter.cs | 139 + .../Common/Helpers/Vector128Utilities.cs | 1362 + .../Common/Helpers/Vector256Utilities.cs | 486 + .../Common/Helpers/Vector512Utilities.cs | 116 + ImageSharp/Common/InlineArray.cs | 38 + ImageSharp/Common/InlineArray.tt | 38 + ImageSharp/Compression/Zlib/Adler32.cs | 436 + .../Zlib/DeflateCompressionLevel.cs | 80 + .../Compression/Zlib/DeflateThrowHelper.cs | 34 + ImageSharp/Compression/Zlib/Deflater.cs | 291 + .../Compression/Zlib/DeflaterConstants.cs | 148 + ImageSharp/Compression/Zlib/DeflaterEngine.cs | 868 + .../Compression/Zlib/DeflaterHuffman.cs | 980 + .../Compression/Zlib/DeflaterOutputStream.cs | 145 + .../Compression/Zlib/DeflaterPendingBuffer.cs | 186 + ImageSharp/Compression/Zlib/README.md | 11 + .../Compression/Zlib/ZlibDeflateStream.cs | 179 + .../Compression/Zlib/ZlibInflateStream.cs | 308 + ImageSharp/Configuration.cs | 237 + ImageSharp/Diagnostics/MemoryDiagnostics.cs | 94 + ImageSharp/Formats/AlphaAwareImageEncoder.cs | 19 + ImageSharp/Formats/AnimationUtilities.cs | 291 + ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs | 52 + ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs | 45 + ImageSharp/Formats/Bmp/BmpColorSpace.cs | 36 + ImageSharp/Formats/Bmp/BmpCompression.cs | 75 + .../Formats/Bmp/BmpConfigurationModule.cs | 18 + ImageSharp/Formats/Bmp/BmpConstants.cs | 63 + ImageSharp/Formats/Bmp/BmpDecoder.cs | 53 + ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 1666 + ImageSharp/Formats/Bmp/BmpDecoderOptions.cs | 43 + ImageSharp/Formats/Bmp/BmpEncoder.cs | 48 + ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 910 + ImageSharp/Formats/Bmp/BmpFileHeader.cs | 68 + ImageSharp/Formats/Bmp/BmpFileMarkerType.cs | 40 + ImageSharp/Formats/Bmp/BmpFormat.cs | 36 + .../Formats/Bmp/BmpImageFormatDetector.cs | 37 + ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 544 + ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs | 50 + ImageSharp/Formats/Bmp/BmpMetadata.cs | 164 + ImageSharp/Formats/Bmp/BmpRenderingIntent.cs | 36 + ImageSharp/Formats/Bmp/BmpThrowHelper.cs | 15 + ImageSharp/Formats/Bmp/README.md | 17 + .../Formats/Bmp/RleSkippedPixelHandling.cs | 25 + ImageSharp/Formats/ColorProfileHandling.cs | 26 + .../Formats/Cur/CurConfigurationModule.cs | 20 + ImageSharp/Formats/Cur/CurConstants.cs | 41 + ImageSharp/Formats/Cur/CurDecoder.cs | 49 + ImageSharp/Formats/Cur/CurDecoderCore.cs | 41 + ImageSharp/Formats/Cur/CurEncoder.cs | 20 + ImageSharp/Formats/Cur/CurEncoderCore.cs | 14 + ImageSharp/Formats/Cur/CurFormat.cs | 40 + ImageSharp/Formats/Cur/CurFrameMetadata.cs | 241 + ImageSharp/Formats/Cur/CurMetadata.cs | 162 + ImageSharp/Formats/DecoderOptions.cs | 110 + ImageSharp/Formats/EncodingType.cs | 20 + ImageSharp/Formats/EncodingUtilities.cs | 170 + .../Compressors/NoneExrCompressor.cs | 40 + .../Compressors/ZipExrCompressor.cs | 88 + .../Decompressors/B44ExrCompression.cs | 206 + .../Decompressors/NoneExrCompression.cs | 42 + .../Decompressors/Pxr24Compression.cs | 154 + .../Decompressors/RunLengthExrCompression.cs | 99 + .../Decompressors/ZipExrCompression.cs | 43 + .../Exr/Compression/ExrBaseCompression.cs | 76 + .../Exr/Compression/ExrBaseDecompressor.cs | 113 + .../Exr/Compression/ExrCompressorFactory.cs | 44 + .../Exr/Compression/ExrDecompressorFactory.cs | 45 + .../Formats/Exr/Constants/ExrCompression.cs | 63 + .../Formats/Exr/Constants/ExrImageDataType.cs | 30 + .../Formats/Exr/Constants/ExrImageType.cs | 21 + .../Formats/Exr/Constants/ExrLineOrder.cs | 25 + .../Formats/Exr/Constants/ExrPixelType.cs | 25 + ImageSharp/Formats/Exr/ExrAttribute.cs | 43 + ImageSharp/Formats/Exr/ExrBaseCompressor.cs | 37 + ImageSharp/Formats/Exr/ExrBox2i.cs | 48 + ImageSharp/Formats/Exr/ExrChannelInfo.cs | 60 + .../Formats/Exr/ExrConfigurationModule.cs | 18 + ImageSharp/Formats/Exr/ExrConstants.cs | 84 + ImageSharp/Formats/Exr/ExrDecoder.cs | 50 + ImageSharp/Formats/Exr/ExrDecoderCore.cs | 1021 + ImageSharp/Formats/Exr/ExrDecoderOptions.cs | 13 + ImageSharp/Formats/Exr/ExrEncoder.cs | 31 + ImageSharp/Formats/Exr/ExrEncoderCore.cs | 711 + ImageSharp/Formats/Exr/ExrFormat.cs | 36 + ImageSharp/Formats/Exr/ExrHeaderAttributes.cs | 109 + .../Formats/Exr/ExrImageFormatDetector.cs | 35 + ImageSharp/Formats/Exr/ExrMetadata.cs | 156 + ImageSharp/Formats/Exr/ExrThrowHelper.cs | 34 + ImageSharp/Formats/Exr/ExrUtils.cs | 54 + ImageSharp/Formats/Exr/README.md | 4 + .../Formats/FormatConnectingFrameMetadata.cs | 50 + .../Formats/FormatConnectingMetadata.cs | 65 + ImageSharp/Formats/FrameBlendMode.cs | 23 + ImageSharp/Formats/FrameColorTableMode.cs | 20 + ImageSharp/Formats/FrameDisposalMode.cs | 38 + .../Formats/Gif/GifConfigurationModule.cs | 18 + ImageSharp/Formats/Gif/GifConstants.cs | 135 + ImageSharp/Formats/Gif/GifDecoder.cs | 50 + ImageSharp/Formats/Gif/GifDecoderCore.cs | 1011 + ImageSharp/Formats/Gif/GifEncoder.cs | 25 + ImageSharp/Formats/Gif/GifEncoderCore.cs | 845 + ImageSharp/Formats/Gif/GifFormat.cs | 39 + ImageSharp/Formats/Gif/GifFrameMetadata.cs | 117 + .../Formats/Gif/GifImageFormatDetector.cs | 34 + ImageSharp/Formats/Gif/GifMetadata.cs | 120 + ImageSharp/Formats/Gif/GifThrowHelper.cs | 19 + ImageSharp/Formats/Gif/LzwDecoder.cs | 447 + ImageSharp/Formats/Gif/LzwEncoder.cs | 423 + ImageSharp/Formats/Gif/README.md | 6 + .../Sections/GifGraphicControlExtension.cs | 132 + .../Gif/Sections/GifImageDescriptor.cs | 115 + .../Sections/GifLogicalScreenDescriptor.cs | 132 + .../GifNetscapeLoopingApplicationExtension.cs | 45 + .../Sections/GifXmpApplicationExtension.cs | 107 + .../Formats/Gif/Sections/IGifExtension.cs | 29 + ImageSharp/Formats/Gif/spec-gif89a.txt | 2476 ++ ImageSharp/Formats/IAnimatedImageEncoder.cs | 43 + ImageSharp/Formats/IFormatFrameMetadata.cs | 46 + ImageSharp/Formats/IFormatMetadata.cs | 51 + ImageSharp/Formats/IImageDecoder.cs | 76 + ImageSharp/Formats/IImageEncoder.cs | 40 + ImageSharp/Formats/IImageFormat.cs | 62 + .../IImageFormatConfigurationModule.cs | 16 + ImageSharp/Formats/IImageFormatDetector.cs | 27 + ImageSharp/Formats/IQuantizingImageEncoder.cs | 50 + .../Formats/ISpecializedDecoderOptions.cs | 15 + .../Formats/ISpecializedImageDecoder{T}.cs | 59 + .../Formats/Ico/IcoConfigurationModule.cs | 20 + ImageSharp/Formats/Ico/IcoConstants.cs | 41 + ImageSharp/Formats/Ico/IcoDecoder.cs | 49 + ImageSharp/Formats/Ico/IcoDecoderCore.cs | 41 + ImageSharp/Formats/Ico/IcoEncoder.cs | 20 + ImageSharp/Formats/Ico/IcoEncoderCore.cs | 14 + ImageSharp/Formats/Ico/IcoFormat.cs | 40 + ImageSharp/Formats/Ico/IcoFrameMetadata.cs | 236 + ImageSharp/Formats/Ico/IcoMetadata.cs | 162 + ImageSharp/Formats/Icon/IconDecoderCore.cs | 315 + ImageSharp/Formats/Icon/IconDir.cs | 45 + ImageSharp/Formats/Icon/IconDirEntry.cs | 62 + ImageSharp/Formats/Icon/IconEncoderCore.cs | 198 + ImageSharp/Formats/Icon/IconFileType.cs | 20 + .../Formats/Icon/IconFrameCompression.cs | 20 + .../Formats/Icon/IconImageFormatDetector.cs | 67 + ImageSharp/Formats/ImageDecoder.cs | 360 + ImageSharp/Formats/ImageDecoderCore.cs | 292 + ImageSharp/Formats/ImageEncoder.cs | 101 + ImageSharp/Formats/ImageFormatManager.cs | 234 + ImageSharp/Formats/Jpeg/5116.DCT_Filter.pdf | Bin 0 -> 218384 bytes .../Jpeg/Components/Block8x8.Intrinsic.cs | 36 + .../Formats/Jpeg/Components/Block8x8.cs | 279 + .../Jpeg/Components/Block8x8F.ScaledCopy.cs | 480 + .../Jpeg/Components/Block8x8F.Vector128.cs | 93 + .../Jpeg/Components/Block8x8F.Vector256.cs | 157 + .../Formats/Jpeg/Components/Block8x8F.cs | 646 + .../JpegColorConverter.CmykScalar.cs | 117 + .../JpegColorConverter.CmykVector128.cs | 101 + .../JpegColorConverter.CmykVector256.cs | 101 + .../JpegColorConverter.CmykVector512.cs | 109 + .../JpegColorConverter.GrayScaleScalar.cs | 98 + .../JpegColorConverter.GrayScaleVector128.cs | 82 + .../JpegColorConverter.GrayScaleVector256.cs | 82 + .../JpegColorConverter.GrayScaleVector512.cs | 90 + .../JpegColorConverter.RgbScalar.cs | 85 + .../JpegColorConverter.RgbVector128.cs | 57 + .../JpegColorConverter.RgbVector256.cs | 57 + .../JpegColorConverter.RgbVector512.cs | 65 + .../JpegColorConverter.TiffCmykScalar.cs | 119 + .../JpegColorConverter.TiffCmykVector128.cs | 100 + .../JpegColorConverter.TiffCmykVector256.cs | 100 + .../JpegColorConverter.TiffCmykVector512.cs | 109 + .../JpegColorConverter.TiffYccKScalar.cs | 154 + .../JpegColorConverter.TiffYccKVector128.cs | 132 + .../JpegColorConverter.TiffYccKVector256.cs | 132 + .../JpegColorConverter.TiffYccKVector512.cs | 143 + .../JpegColorConverter.YCbCrScalar.cs | 122 + .../JpegColorConverter.YCbCrVector128.cs | 122 + .../JpegColorConverter.YCbCrVector256.cs | 122 + .../JpegColorConverter.YCbCrVector512.cs | 129 + .../JpegColorConverter.YccKScalar.cs | 126 + .../JpegColorConverter.YccKVector128.cs | 136 + .../JpegColorConverter.YccKVector256.cs | 136 + .../JpegColorConverter.YccKVector512.cs | 144 + .../ColorConverters/JpegColorConverterBase.cs | 522 + .../JpegColorConverterScalar.cs | 23 + .../JpegColorConverterVector.cs | 130 + .../JpegColorConverterVector128.cs | 34 + .../JpegColorConverterVector256.cs | 34 + .../JpegColorConverterVector512.cs | 112 + .../Formats/Jpeg/Components/ComponentType.cs | 11 + .../Jpeg/Components/Decoder/AdobeMarker.cs | 108 + .../Decoder/ArithmeticDecodingComponent.cs | 30 + .../Decoder/ArithmeticDecodingTable.cs | 42 + .../Decoder/ArithmeticScanDecoder.cs | 1241 + .../Decoder/ArithmeticStatistics.cs | 29 + .../ComponentProcessors/ComponentProcessor.cs | 64 + .../DirectComponentProcessor.cs | 72 + .../DownScalingComponentProcessor2.cs | 404 + .../DownScalingComponentProcessor4.cs | 346 + .../DownScalingComponentProcessor8.cs | 241 + .../Components/Decoder/HuffmanScanDecoder.cs | 791 + .../Jpeg/Components/Decoder/HuffmanTable.cs | 141 + .../Jpeg/Components/Decoder/IJpegComponent.cs | 95 + .../Components/Decoder/IJpegScanDecoder.cs | 53 + .../Jpeg/Components/Decoder/IRawJpegData.cs | 27 + .../Jpeg/Components/Decoder/JFifMarker.cs | 111 + .../Jpeg/Components/Decoder/JpegBitReader.cs | 240 + .../Jpeg/Components/Decoder/JpegComponent.cs | 137 + .../Jpeg/Components/Decoder/JpegFileMarker.cs | 67 + .../Jpeg/Components/Decoder/JpegFrame.cs | 152 + .../Components/Decoder/ProfileResolver.cs | 102 + .../Components/Decoder/SpectralConverter.cs | 137 + .../Decoder/SpectralConverter{TPixel}.cs | 284 + .../Jpeg/Components/Encoder/Component.cs | 116 + .../Components/Encoder/ComponentProcessor.cs | 259 + .../EncodingConfigs/JpegComponentConfig.cs | 29 + .../EncodingConfigs/JpegFrameConfig.cs | 43 + .../EncodingConfigs/JpegHuffmanTableConfig.cs | 20 + .../JpegQuantizationTableConfig.cs | 19 + .../Jpeg/Components/Encoder/HuffmanLut.cs | 68 + .../Components/Encoder/HuffmanScanEncoder.cs | 844 + .../Jpeg/Components/Encoder/HuffmanSpec.cs | 141 + .../Jpeg/Components/Encoder/JpegFrame.cs | 84 + .../Components/Encoder/SpectralConverter.cs | 11 + .../Encoder/SpectralConverter{TPixel}.cs | 149 + .../Components/FloatingPointDCT.Vector256.cs | 142 + .../Jpeg/Components/FloatingPointDCT.cs | 276 + .../Formats/Jpeg/Components/JpegColorSpace.cs | 45 + .../Formats/Jpeg/Components/Quantization.cs | 211 + .../Formats/Jpeg/Components/RowOctet.cs | 99 + .../Jpeg/Components/ScaledFloatingPointDCT.cs | 218 + .../Formats/Jpeg/Components/SizeExtensions.cs | 50 + .../Jpeg/Components/ZigZag.Intrinsic.cs | 323 + ImageSharp/Formats/Jpeg/Components/ZigZag.cs | 67 + ImageSharp/Formats/Jpeg/JpegColorType.cs | 62 + ImageSharp/Formats/Jpeg/JpegComData.cs | 34 + .../Formats/Jpeg/JpegConfigurationModule.cs | 18 + ImageSharp/Formats/Jpeg/JpegConstants.cs | 339 + ImageSharp/Formats/Jpeg/JpegDecoder.cs | 58 + ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 1663 + ImageSharp/Formats/Jpeg/JpegDecoderOptions.cs | 18 + .../Formats/Jpeg/JpegDecoderResizeMode.cs | 28 + ImageSharp/Formats/Jpeg/JpegEncoder.cs | 120 + .../Jpeg/JpegEncoderCore.FrameConfig.cs | 195 + ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 877 + ImageSharp/Formats/Jpeg/JpegFormat.cs | 36 + .../Formats/Jpeg/JpegImageFormatDetector.cs | 61 + ImageSharp/Formats/Jpeg/JpegMetadata.cs | 216 + ImageSharp/Formats/Jpeg/JpegThrowHelper.cs | 35 + ImageSharp/Formats/Jpeg/README.md | 8 + ImageSharp/Formats/Jpeg/itu-t81.pdf | Bin 0 -> 1057800 bytes ImageSharp/Formats/Pbm/BinaryDecoder.cs | 202 + ImageSharp/Formats/Pbm/BinaryEncoder.cs | 243 + .../Pbm/BufferedReadStreamExtensions.cs | 87 + ImageSharp/Formats/Pbm/PbmColorType.cs | 25 + ImageSharp/Formats/Pbm/PbmComponentType.cs | 25 + .../Formats/Pbm/PbmConfigurationModule.cs | 18 + ImageSharp/Formats/Pbm/PbmConstants.cs | 27 + ImageSharp/Formats/Pbm/PbmDecoder.cs | 66 + ImageSharp/Formats/Pbm/PbmDecoderCore.cs | 195 + ImageSharp/Formats/Pbm/PbmEncoder.cs | 56 + ImageSharp/Formats/Pbm/PbmEncoderCore.cs | 197 + ImageSharp/Formats/Pbm/PbmEncoding.cs | 20 + ImageSharp/Formats/Pbm/PbmFormat.cs | 36 + .../Formats/Pbm/PbmImageFormatDetector.cs | 38 + ImageSharp/Formats/Pbm/PbmMetadata.cs | 144 + ImageSharp/Formats/Pbm/PlainDecoder.cs | 262 + ImageSharp/Formats/Pbm/PlainEncoder.cs | 287 + ImageSharp/Formats/Png/Adam7.cs | 89 + .../Formats/Png/Chunks/AnimationControl.cs | 48 + ImageSharp/Formats/Png/Chunks/FrameControl.cs | 168 + ImageSharp/Formats/Png/Chunks/PngHeader.cs | 144 + ImageSharp/Formats/Png/Chunks/PngPhysical.cs | 114 + ImageSharp/Formats/Png/Chunks/PngTextData.cs | 136 + .../Formats/Png/Filters/AverageFilter.cs | 245 + ImageSharp/Formats/Png/Filters/FilterType.cs | 42 + ImageSharp/Formats/Png/Filters/NoneFilter.cs | 29 + ImageSharp/Formats/Png/Filters/PaethFilter.cs | 374 + ImageSharp/Formats/Png/Filters/SubFilter.cs | 187 + ImageSharp/Formats/Png/Filters/UpFilter.cs | 229 + ImageSharp/Formats/Png/PngBitDepth.cs | 36 + ImageSharp/Formats/Png/PngChunk.cs | 53 + ImageSharp/Formats/Png/PngChunkFilter.cs | 43 + ImageSharp/Formats/Png/PngChunkType.cs | 176 + ImageSharp/Formats/Png/PngColorType.cs | 35 + ImageSharp/Formats/Png/PngCompressionLevel.cs | 83 + .../Formats/Png/PngConfigurationModule.cs | 18 + ImageSharp/Formats/Png/PngConstants.cs | 147 + ImageSharp/Formats/Png/PngDecoder.cs | 107 + ImageSharp/Formats/Png/PngDecoderCore.cs | 2799 ++ ImageSharp/Formats/Png/PngDecoderOptions.cs | 19 + ImageSharp/Formats/Png/PngEncoder.cs | 63 + ImageSharp/Formats/Png/PngEncoderCore.cs | 1849 + ImageSharp/Formats/Png/PngEncoderHelpers.cs | 56 + ImageSharp/Formats/Png/PngFilterMethod.cs | 45 + ImageSharp/Formats/Png/PngFormat.cs | 39 + ImageSharp/Formats/Png/PngFrameMetadata.cs | 109 + .../Formats/Png/PngImageFormatDetector.cs | 29 + ImageSharp/Formats/Png/PngInterlaceMode.cs | 20 + ImageSharp/Formats/Png/PngMetadata.cs | 259 + .../Formats/Png/PngScanlineProcessor.cs | 368 + ImageSharp/Formats/Png/PngThrowHelper.cs | 55 + ImageSharp/Formats/Png/README.md | 6 + ImageSharp/Formats/Qoi/QoiChannels.cs | 20 + ImageSharp/Formats/Qoi/QoiChunk.cs | 56 + ImageSharp/Formats/Qoi/QoiColorSpace.cs | 22 + .../Formats/Qoi/QoiConfigurationModule.cs | 18 + ImageSharp/Formats/Qoi/QoiConstants.cs | 28 + ImageSharp/Formats/Qoi/QoiDecoder.cs | 46 + ImageSharp/Formats/Qoi/QoiDecoderCore.cs | 282 + ImageSharp/Formats/Qoi/QoiEncoder.cs | 34 + ImageSharp/Formats/Qoi/QoiEncoderCore.cs | 260 + ImageSharp/Formats/Qoi/QoiFormat.cs | 36 + ImageSharp/Formats/Qoi/QoiHeader.cs | 45 + .../Formats/Qoi/QoiImageFormatDetector.cs | 26 + ImageSharp/Formats/Qoi/QoiMetadata.cs | 103 + ImageSharp/Formats/Qoi/qoi-specification.pdf | Bin 0 -> 36221 bytes .../Formats/SegmentIntegrityHandling.cs | 26 + .../Formats/SpecializedImageDecoder{T}.cs | 118 + ImageSharp/Formats/Tga/README.md | 6 + ImageSharp/Formats/Tga/TGA_Specification.pdf | Bin 0 -> 181405 bytes ImageSharp/Formats/Tga/TgaBitsPerPixel.cs | 30 + ImageSharp/Formats/Tga/TgaCompression.cs | 20 + .../Formats/Tga/TgaConfigurationModule.cs | 18 + ImageSharp/Formats/Tga/TgaConstants.cs | 24 + ImageSharp/Formats/Tga/TgaDecoder.cs | 50 + ImageSharp/Formats/Tga/TgaDecoderCore.cs | 936 + ImageSharp/Formats/Tga/TgaEncoder.cs | 30 + ImageSharp/Formats/Tga/TgaEncoderCore.cs | 447 + ImageSharp/Formats/Tga/TgaFileHeader.cs | 143 + ImageSharp/Formats/Tga/TgaFormat.cs | 32 + .../Formats/Tga/TgaImageFormatDetector.cs | 64 + ImageSharp/Formats/Tga/TgaImageOrigin.cs | 27 + ImageSharp/Formats/Tga/TgaImageType.cs | 47 + .../Formats/Tga/TgaImageTypeExtensions.cs | 48 + ImageSharp/Formats/Tga/TgaMetadata.cs | 109 + ImageSharp/Formats/Tga/TgaThrowHelper.cs | 18 + .../Tiff/Compression/BitWriterUtils.cs | 62 + .../Compressors/DeflateCompressor.cs | 54 + .../Compression/Compressors/LzwCompressor.cs | 40 + .../Compression/Compressors/NoCompressor.cs | 33 + .../Compressors/PackBitsCompressor.cs | 49 + .../Compression/Compressors/PackBitsWriter.cs | 127 + .../Compressors/T4BitCompressor.cs | 143 + .../Compressors/T6BitCompressor.cs | 201 + .../Compressors/TiffCcittCompressor.cs | 536 + .../Compressors/TiffJpegCompressor.cs | 48 + .../Compression/Compressors/TiffLzwEncoder.cs | 269 + .../Decompressors/CcittReferenceScanline.cs | 155 + .../Decompressors/CcittTwoDimensionalCode.cs | 32 + .../CcittTwoDimensionalCodeType.cs | 72 + .../Decompressors/DeflateTiffCompression.cs | 103 + .../GrayJpegSpectralConverter.cs | 28 + .../Decompressors/JpegCompressionUtils.cs | 34 + .../Decompressors/JpegTiffCompression.cs | 123 + .../Compression/Decompressors/LzwString.cs | 103 + .../Decompressors/LzwTiffCompression.cs | 74 + .../Decompressors/ModifiedHuffmanBitReader.cs | 69 + .../ModifiedHuffmanTiffCompression.cs | 103 + .../Decompressors/NoneTiffCompression.cs | 35 + .../Decompressors/OldJpegTiffCompression.cs | 106 + .../Decompressors/PackBitsTiffCompression.cs | 88 + .../Decompressors/RgbJpegSpectralConverter.cs | 30 + .../Compression/Decompressors/T4BitReader.cs | 871 + .../Decompressors/T4TiffCompression.cs | 127 + .../Compression/Decompressors/T6BitReader.cs | 195 + .../Decompressors/T6TiffCompression.cs | 275 + .../TiffJpegSpectralConverter{TPixel}.cs | 60 + .../Decompressors/TiffLzwDecoder.cs | 256 + .../TiffOldJpegSpectralConverter{TPixel}.cs | 46 + .../Decompressors/WebpTiffCompression.cs | 58 + .../Tiff/Compression/FaxCompressionOptions.cs | 35 + .../Tiff/Compression/HorizontalPredictor.cs | 873 + .../Tiff/Compression/TiffBaseCompression.cs | 61 + .../Tiff/Compression/TiffBaseCompressor.cs | 47 + .../Tiff/Compression/TiffBaseDecompressor.cs | 62 + .../Tiff/Compression/TiffCompressorFactory.cs | 72 + .../Compression/TiffDecoderCompressionType.cs | 60 + .../Compression/TiffDecompressorsFactory.cs | 82 + .../Formats/Tiff/Constants/TiffCompression.cs | 114 + .../Formats/Tiff/Constants/TiffConstants.cs | 122 + .../Formats/Tiff/Constants/TiffFillOrder.cs | 20 + .../Formats/Tiff/Constants/TiffInkSet.cs | 26 + .../Tiff/Constants/TiffNewSubfileType.cs | 43 + .../Formats/Tiff/Constants/TiffOrientation.cs | 50 + .../TiffPhotometricInterpretation.cs | 79 + .../Tiff/Constants/TiffPlanarConfiguration.cs | 30 + .../Formats/Tiff/Constants/TiffPredictor.cs | 27 + .../Tiff/Constants/TiffSampleFormat.cs | 40 + .../Formats/Tiff/Constants/TiffSubfileType.cs | 25 + .../Tiff/Constants/TiffThresholding.cs | 25 + .../Formats/Tiff/Ifd/DirectoryReader.cs | 120 + ImageSharp/Formats/Tiff/Ifd/EntryReader.cs | 76 + .../BlackIsZero16TiffColor{TPixel}.cs | 67 + .../BlackIsZero1TiffColor{TPixel}.cs | 83 + .../BlackIsZero24TiffColor{TPixel}.cs | 61 + .../BlackIsZero32FloatTiffColor{TPixel}.cs | 59 + .../BlackIsZero32TiffColor{TPixel}.cs | 55 + .../BlackIsZero4TiffColor{TPixel}.cs | 40 + .../BlackIsZero8TiffColor{TPixel}.cs | 38 + .../BlackIsZeroTiffColor{TPixel}.cs | 46 + .../CieLab16PlanarTiffColor{TPixel}.cs | 145 + .../CieLab16TiffColor{TPixel}.cs | 141 + .../CieLab8PlanarTiffColor{TPixel}.cs | 45 + .../CieLab8TiffColor{TPixel}.cs | 49 + .../CmykTiffColor{TPixel}.cs | 106 + .../PaletteTiffColor{TPixel}.cs | 179 + .../Rgb161616TiffColor{TPixel}.cs | 68 + .../Rgb16PlanarTiffColor{TPixel}.cs | 66 + .../Rgb242424TiffColor{TPixel}.cs | 78 + .../Rgb24PlanarTiffColor{TPixel}.cs | 76 + .../Rgb323232TiffColor{TPixel}.cs | 69 + .../Rgb32PlanarTiffColor{TPixel}.cs | 66 + .../Rgb444TiffColor{TPixel}.cs | 54 + .../Rgb888TiffColor{TPixel}.cs | 38 + .../RgbFloat323232TiffColor{TPixel}.cs | 76 + .../RgbPlanarTiffColor{TPixel}.cs | 75 + .../RgbTiffColor{TPixel}.cs | 62 + .../Rgba16161616TiffColor{TPixel}.cs | 116 + .../Rgba16PlanarTiffColor{TPixel}.cs | 81 + .../Rgba24242424TiffColor{TPixel}.cs | 99 + .../Rgba24PlanarTiffColor{TPixel}.cs | 93 + .../Rgba32323232TiffColor{TPixel}.cs | 87 + .../Rgba32PlanarTiffColor{TPixel}.cs | 80 + .../Rgba8888TiffColor{TPixel}.cs | 60 + .../RgbaFloat32323232TiffColor{TPixel}.cs | 84 + .../RgbaPlanarTiffColor{TPixel}.cs | 98 + .../RgbaTiffColor{TPixel}.cs | 83 + .../TiffBaseColorDecoder{TPixel}.cs | 27 + .../TiffBasePlanarColorDecoder{TPixel}.cs | 27 + .../TiffColorDecoderFactory{TPixel}.cs | 497 + .../TiffColorType.cs | 295 + .../WhiteIsZero16TiffColor{TPixel}.cs | 55 + .../WhiteIsZero1TiffColor{TPixel}.cs | 83 + .../WhiteIsZero24TiffColor{TPixel}.cs | 62 + .../WhiteIsZero32FloatTiffColor{TPixel}.cs | 59 + .../WhiteIsZero32TiffColor{TPixel}.cs | 57 + .../WhiteIsZero4TiffColor{TPixel}.cs | 40 + .../WhiteIsZero8TiffColor{TPixel}.cs | 31 + .../WhiteIsZeroTiffColor{TPixel}.cs | 46 + .../YCbCrConverter.cs | 120 + .../YCbCrPlanarTiffColor{TPixel}.cs | 80 + .../YCbCrTiffColor{TPixel}.cs | 110 + ImageSharp/Formats/Tiff/README.md | 247 + .../Tiff/T-REC-T.4-198811-S!!PDF-E.pdf | Bin 0 -> 365889 bytes .../Tiff/T-REC-T.6-198811-I!!PDF-E.pdf | Bin 0 -> 112837 bytes .../Tiff/TIFF-AdobeTechNote-22032002.pdf | Bin 0 -> 317624 bytes ImageSharp/Formats/Tiff/TIFF-v6.pdf | Bin 0 -> 398722 bytes ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs | 95 + ImageSharp/Formats/Tiff/TiffBitsPerSample.cs | 183 + .../Formats/Tiff/TiffConfigurationModule.cs | 18 + ImageSharp/Formats/Tiff/TiffDecoder.cs | 50 + ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 974 + .../Tiff/TiffDecoderMetadataCreator.cs | 137 + .../Formats/Tiff/TiffDecoderOptionsParser.cs | 612 + ImageSharp/Formats/Tiff/TiffEncoder.cs | 55 + ImageSharp/Formats/Tiff/TiffEncoderCore.cs | 468 + .../Tiff/TiffEncoderEntriesCollector.cs | 447 + .../Formats/Tiff/TiffExtraSampleType.cs | 32 + ImageSharp/Formats/Tiff/TiffFormat.cs | 40 + ImageSharp/Formats/Tiff/TiffFormatType.cs | 20 + ImageSharp/Formats/Tiff/TiffFrameMetadata.cs | 230 + .../Formats/Tiff/TiffImageFormatDetector.cs | 63 + ImageSharp/Formats/Tiff/TiffMetadata.cs | 195 + ImageSharp/Formats/Tiff/TiffThrowHelper.cs | 34 + ImageSharp/Formats/Tiff/Utils/BitReader.cs | 65 + .../Formats/Tiff/Utils/TiffUtilities.cs | 126 + .../Writers/TiffBaseColorWriter{TPixel}.cs | 126 + .../Tiff/Writers/TiffBiColorWriter{TPixel}.cs | 118 + .../Tiff/Writers/TiffColorWriterFactory.cs | 35 + .../TiffCompositeColorWriter{TPixel}.cs | 58 + .../Tiff/Writers/TiffGrayL16Writer{TPixel}.cs | 29 + .../Tiff/Writers/TiffGrayWriter{TPixel}.cs | 29 + .../Tiff/Writers/TiffPaletteWriter{TPixel}.cs | 166 + .../Tiff/Writers/TiffRgbWriter{TPixel}.cs | 29 + .../Formats/Tiff/Writers/TiffStreamWriter.cs | 145 + ImageSharp/Formats/TransparentColorMode.cs | 22 + ImageSharp/Formats/Webp/AlphaDecoder.cs | 492 + ImageSharp/Formats/Webp/AlphaEncoder.cs | 136 + .../Formats/Webp/BackgroundColorHandling.cs | 21 + .../Formats/Webp/BitReader/BitReaderBase.cs | 65 + .../Formats/Webp/BitReader/Vp8BitReader.cs | 231 + .../Formats/Webp/BitReader/Vp8LBitReader.cs | 206 + .../Formats/Webp/BitWriter/BitWriterBase.cs | 218 + .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 634 + .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 179 + .../Webp/Chunks/WebpAnimationParameter.cs | 38 + .../Formats/Webp/Chunks/WebpFrameData.cs | 139 + ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs | 138 + ImageSharp/Formats/Webp/EntropyIx.cs | 24 + ImageSharp/Formats/Webp/HistoIx.cs | 35 + .../Webp/Lossless/BackwardReferenceEncoder.cs | 864 + .../Formats/Webp/Lossless/ColorCache.cs | 91 + .../Webp/Lossless/ColorSpaceTransformUtils.cs | 248 + .../Webp/Lossless/CostCacheInterval.cs | 19 + .../Formats/Webp/Lossless/CostInterval.cs | 38 + .../Formats/Webp/Lossless/CostManager.cs | 331 + ImageSharp/Formats/Webp/Lossless/CostModel.cs | 104 + .../Formats/Webp/Lossless/CrunchConfig.cs | 13 + .../Formats/Webp/Lossless/CrunchSubConfig.cs | 11 + .../Webp/Lossless/DominantCostRange.cs | 91 + .../Formats/Webp/Lossless/HTreeGroup.cs | 58 + .../Formats/Webp/Lossless/HistogramBinInfo.cs | 17 + .../Formats/Webp/Lossless/HistogramEncoder.cs | 730 + .../Formats/Webp/Lossless/HistogramPair.cs | 21 + ImageSharp/Formats/Webp/Lossless/HuffIndex.cs | 35 + .../Formats/Webp/Lossless/HuffmanCode.cs | 23 + .../Formats/Webp/Lossless/HuffmanTree.cs | 60 + .../Formats/Webp/Lossless/HuffmanTreeCode.cs | 25 + .../Formats/Webp/Lossless/HuffmanTreeToken.cs | 23 + .../Formats/Webp/Lossless/HuffmanUtils.cs | 655 + .../Formats/Webp/Lossless/LosslessUtils.cs | 1496 + .../Formats/Webp/Lossless/NearLosslessEnc.cs | 124 + ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs | 41 + .../Formats/Webp/Lossless/PixOrCopyMode.cs | 15 + .../Formats/Webp/Lossless/PredictorEncoder.cs | 1087 + .../Formats/Webp/Lossless/Vp8LBackwardRefs.cs | 29 + .../Formats/Webp/Lossless/Vp8LBitEntropy.cs | 220 + .../Formats/Webp/Lossless/Vp8LDecoder.cs | 70 + .../Formats/Webp/Lossless/Vp8LEncoder.cs | 1932 + .../Formats/Webp/Lossless/Vp8LHashChain.cs | 291 + .../Formats/Webp/Lossless/Vp8LHistogram.cs | 642 + .../Formats/Webp/Lossless/Vp8LHistogramSet.cs | 112 + .../Formats/Webp/Lossless/Vp8LLz77Type.cs | 13 + .../Formats/Webp/Lossless/Vp8LMetadata.cs | 28 + .../Formats/Webp/Lossless/Vp8LMultipliers.cs | 13 + .../Formats/Webp/Lossless/Vp8LStreaks.cs | 71 + .../Formats/Webp/Lossless/Vp8LTransform.cs | 47 + .../Webp/Lossless/Vp8LTransformType.cs | 36 + .../Webp/Lossless/WebpLosslessDecoder.cs | 1015 + .../Webp_Lossless_Bitstream_Specification.pdf | Bin 0 -> 144069 bytes .../Formats/Webp/Lossy/IntraPredictionMode.cs | 27 + ImageSharp/Formats/Webp/Lossy/LoopFilter.cs | 25 + ImageSharp/Formats/Webp/Lossy/LossyUtils.cs | 2408 + ImageSharp/Formats/Webp/Lossy/PassStats.cs | 75 + ImageSharp/Formats/Webp/Lossy/QuantEnc.cs | 817 + .../Formats/Webp/Lossy/Vp8BandProbas.cs | 27 + ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs | 14 + ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs | 24 + ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs | 345 + .../Formats/Webp/Lossy/Vp8EncIterator.cs | 945 + ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs | 264 + .../Formats/Webp/Lossy/Vp8EncSegmentHeader.cs | 33 + ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs | 1296 + ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs | 1096 + .../Formats/Webp/Lossy/Vp8FilterHeader.cs | 71 + .../Formats/Webp/Lossy/Vp8FilterInfo.cs | 82 + .../Formats/Webp/Lossy/Vp8FrameHeader.cs | 25 + ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs | 128 + ImageSharp/Formats/Webp/Lossy/Vp8Io.cs | 67 + .../Formats/Webp/Lossy/Vp8MacroBlock.cs | 20 + .../Formats/Webp/Lossy/Vp8MacroBlockData.cs | 65 + .../Formats/Webp/Lossy/Vp8MacroBlockInfo.cs | 20 + .../Formats/Webp/Lossy/Vp8MacroBlockType.cs | 11 + ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs | 101 + ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs | 138 + .../Formats/Webp/Lossy/Vp8PictureHeader.cs | 41 + ImageSharp/Formats/Webp/Lossy/Vp8Proba.cs | 41 + .../Formats/Webp/Lossy/Vp8ProbaArray.cs | 20 + .../Formats/Webp/Lossy/Vp8QuantMatrix.cs | 33 + ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs | 30 + ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs | 256 + .../Formats/Webp/Lossy/Vp8SegmentHeader.cs | 44 + .../Formats/Webp/Lossy/Vp8SegmentInfo.cs | 86 + ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs | 21 + .../Formats/Webp/Lossy/Vp8StatsArray.cs | 14 + .../Formats/Webp/Lossy/Vp8TopSamples.cs | 13 + .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 1372 + .../Formats/Webp/Lossy/YuvConversion.cs | 757 + .../Lossy/rfc6386_lossy_specification.pdf | Bin 0 -> 590738 bytes ImageSharp/Formats/Webp/Readme.md | 10 + ImageSharp/Formats/Webp/RiffHelper.cs | 141 + .../Webp/WebpAlphaCompressionMethod.cs | 17 + .../Formats/Webp/WebpAlphaFilterType.cs | 30 + .../Formats/Webp/WebpAnimationDecoder.cs | 529 + ImageSharp/Formats/Webp/WebpBitsPerPixel.cs | 20 + .../Formats/Webp/WebpChunkParsingUtils.cs | 481 + ImageSharp/Formats/Webp/WebpChunkType.cs | 65 + ImageSharp/Formats/Webp/WebpColorType.cs | 25 + ImageSharp/Formats/Webp/WebpCommonUtils.cs | 160 + .../Formats/Webp/WebpConfigurationModule.cs | 18 + ImageSharp/Formats/Webp/WebpConstants.cs | 324 + ImageSharp/Formats/Webp/WebpDecoder.cs | 58 + ImageSharp/Formats/Webp/WebpDecoderCore.cs | 405 + ImageSharp/Formats/Webp/WebpDecoderOptions.cs | 21 + ImageSharp/Formats/Webp/WebpEncoder.cs | 90 + ImageSharp/Formats/Webp/WebpEncoderCore.cs | 323 + ImageSharp/Formats/Webp/WebpEncodingMethod.cs | 60 + ImageSharp/Formats/Webp/WebpFeatures.cs | 51 + ImageSharp/Formats/Webp/WebpFileFormatType.cs | 20 + ImageSharp/Formats/Webp/WebpFormat.cs | 39 + ImageSharp/Formats/Webp/WebpFrameMetadata.cs | 88 + .../Formats/Webp/WebpImageFormatDetector.cs | 42 + ImageSharp/Formats/Webp/WebpImageInfo.cs | 78 + ImageSharp/Formats/Webp/WebpLookupTables.cs | 1674 + ImageSharp/Formats/Webp/WebpMetadata.cs | 160 + ImageSharp/Formats/Webp/WebpThrowHelper.cs | 25 + .../Webp/Webp_Container_Specification.pdf | Bin 0 -> 122392 bytes .../_Generated/ImageExtensions.Save.cs | 1253 + .../_Generated/ImageExtensions.Save.tt | 131 + .../_Generated/ImageMetadataExtensions.cs | 387 + .../_Generated/ImageMetadataExtensions.tt | 77 + .../Formats/_Generated/_Formats.ttinclude | 29 + ImageSharp/GeometryUtilities.cs | 33 + .../GraphicOptionsDefaultsExtensions.cs | 97 + ImageSharp/GraphicsOptions.cs | 90 + ImageSharp/IDeepCloneable.cs | 30 + ImageSharp/IO/BufferedReadStream.cs | 433 + ImageSharp/IO/ChunkedMemoryStream.cs | 470 + ImageSharp/IO/IFileSystem.cs | 42 + ImageSharp/IO/LocalFileSystem.cs | 36 + ImageSharp/Image.Decode.cs | 245 + ImageSharp/Image.FromBytes.cs | 170 + ImageSharp/Image.FromFile.cs | 300 + ImageSharp/Image.FromStream.cs | 356 + ImageSharp/Image.LoadPixelData.cs | 185 + ImageSharp/Image.WrapMemory.cs | 694 + ImageSharp/Image.cs | 253 + ImageSharp/ImageExtensions.Internal.cs | 28 + ImageSharp/ImageExtensions.cs | 189 + ImageSharp/ImageFrame.LoadPixelData.cs | 104 + ImageSharp/ImageFrame.cs | 102 + ImageSharp/ImageFrameCollection.cs | 269 + ImageSharp/ImageFrameCollectionExtensions.cs | 24 + ImageSharp/ImageFrameCollection{TPixel}.cs | 429 + ImageSharp/ImageFrame{TPixel}.cs | 536 + ImageSharp/ImageInfo.cs | 118 + ImageSharp/ImageSharp.csproj | 224 + ImageSharp/Image{TPixel}.cs | 491 + ImageSharp/IndexedImageFrame{TPixel}.cs | 114 + .../AllocationTrackedMemoryManager{T}.cs | 67 + ImageSharp/Memory/AllocationTrackingState.cs | 49 + .../Memory/Allocators/AllocationOptions.cs | 23 + .../Allocators/AllocationOptionsExtensions.cs | 19 + .../Allocators/Internals/BasicArrayBuffer.cs | 60 + .../Allocators/Internals/Gen2GcCallback.cs | 110 + .../Allocators/Internals/IRefCounted.cs | 20 + .../Allocators/Internals/ManagedBufferBase.cs | 47 + .../RefCountedMemoryLifetimeGuard.cs | 90 + .../Internals/SharedArrayPoolBuffer{T}.cs | 83 + ...iformUnmanagedMemoryPool.LifetimeGuards.cs | 64 + .../Internals/UniformUnmanagedMemoryPool.cs | 345 + .../Internals/UnmanagedBufferLifetimeGuard.cs | 26 + .../Internals/UnmanagedBuffer{T}.cs | 82 + .../Internals/UnmanagedMemoryHandle.cs | 131 + .../Memory/Allocators/MemoryAllocator.cs | 296 + .../Allocators/MemoryAllocatorOptions.cs | 83 + .../Allocators/SimpleGcMemoryAllocator.cs | 33 + ...iformUnmanagedMemoryPoolMemoryAllocator.cs | 162 + .../Allocators/UnmanagedMemoryAllocator.cs | 38 + ImageSharp/Memory/Buffer2DExtensions.cs | 157 + ImageSharp/Memory/Buffer2DRegion{T}.cs | 182 + ImageSharp/Memory/Buffer2D{T}.cs | 429 + ImageSharp/Memory/ByteMemoryManager{T}.cs | 57 + ImageSharp/Memory/ByteMemoryOwner{T}.cs | 53 + .../DiscontiguousBuffers/IMemoryGroup{T}.cs | 46 + .../MemoryGroupEnumerator{T}.cs | 68 + .../MemoryGroupExtensions.cs | 280 + .../MemoryGroupSpanCache.cs | 50 + .../MemoryGroupView{T}.cs | 147 + .../MemoryGroup{T}.Consumed.cs | 55 + .../MemoryGroup{T}.Owned.cs | 279 + .../DiscontiguousBuffers/MemoryGroup{T}.cs | 337 + .../DiscontiguousBuffers/SpanCacheMode.cs | 15 + .../Memory/InvalidMemoryOperationException.cs | 48 + .../Memory/MemoryAllocatorExtensions.cs | 138 + ImageSharp/Memory/MemoryOwnerExtensions.cs | 84 + ImageSharp/Memory/RowInterval.cs | 87 + .../Memory/UnmanagedMemoryManager{T}.cs | 59 + ImageSharp/Metadata/ImageFrameMetadata.cs | 180 + ImageSharp/Metadata/ImageMetadata.cs | 265 + ImageSharp/Metadata/PixelResolutionUnit.cs | 30 + .../Metadata/Profiles/CICP/CicpProfile.cs | 74 + .../Profiles/CICP/Enums/CicpColorPrimaries.cs | 86 + .../CICP/Enums/CicpMatrixCoefficients.cs | 96 + .../CICP/Enums/CicpTransferCharacteristics.cs | 109 + .../CICP/T-REC-H.273-202107-S!!PDF-E.pdf | Bin 0 -> 2632310 bytes .../Exif/DC-X008-Translation-2019-E.pdf | Bin 0 -> 2551169 bytes .../Metadata/Profiles/Exif/ExifConstants.cs | 29 + .../Metadata/Profiles/Exif/ExifDataType.cs | 99 + .../Metadata/Profiles/Exif/ExifDataTypes.cs | 48 + .../Profiles/Exif/ExifEncodedStringHelpers.cs | 142 + .../Metadata/Profiles/Exif/ExifParts.cs | 38 + .../Metadata/Profiles/Exif/ExifProfile.cs | 419 + .../Metadata/Profiles/Exif/ExifReader.cs | 716 + .../Exif/ExifTagDescriptionAttribute.cs | 60 + ImageSharp/Metadata/Profiles/Exif/ExifTags.cs | 274 + .../Profiles/Exif/ExifUcs2StringHelpers.cs | 20 + .../Metadata/Profiles/Exif/ExifWriter.cs | 454 + ImageSharp/Metadata/Profiles/Exif/README.md | 3 + .../Profiles/Exif/Tags/ExifTag.Byte.cs | 23 + .../Profiles/Exif/Tags/ExifTag.ByteArray.cs | 48 + .../Profiles/Exif/Tags/ExifTag.DoubleArray.cs | 28 + .../Exif/Tags/ExifTag.EncodedString.cs | 23 + .../Profiles/Exif/Tags/ExifTag.Long.cs | 113 + .../Profiles/Exif/Tags/ExifTag.LongArray.cs | 63 + .../Profiles/Exif/Tags/ExifTag.Number.cs | 53 + .../Profiles/Exif/Tags/ExifTag.NumberArray.cs | 33 + .../Profiles/Exif/Tags/ExifTag.Rational.cs | 173 + .../Exif/Tags/ExifTag.RationalArray.cs | 58 + .../Profiles/Exif/Tags/ExifTag.Short.cs | 243 + .../Profiles/Exif/Tags/ExifTag.ShortArray.cs | 108 + .../Exif/Tags/ExifTag.SignedRational.cs | 38 + .../Exif/Tags/ExifTag.SignedRationalArray.cs | 13 + .../Exif/Tags/ExifTag.SignedShortArray.cs | 13 + .../Profiles/Exif/Tags/ExifTag.String.cs | 278 + .../Profiles/Exif/Tags/ExifTag.Ucs2String.cs | 33 + .../Profiles/Exif/Tags/ExifTag.Undefined.cs | 78 + .../Metadata/Profiles/Exif/Tags/ExifTag.cs | 69 + .../Profiles/Exif/Tags/ExifTagValue.cs | 1726 + .../Profiles/Exif/Tags/ExifTag{TValueType}.cs | 16 + .../Profiles/Exif/Tags/UnkownExifTag.cs | 12 + .../Profiles/Exif/Values/EncodedString.cs | 115 + .../Exif/Values/ExifArrayValue{TValueType}.cs | 54 + .../Metadata/Profiles/Exif/Values/ExifByte.cs | 46 + .../Profiles/Exif/Values/ExifByteArray.cs | 65 + .../Profiles/Exif/Values/ExifDouble.cs | 47 + .../Profiles/Exif/Values/ExifDoubleArray.cs | 26 + .../Profiles/Exif/Values/ExifEncodedString.cs | 55 + .../Profiles/Exif/Values/ExifFloat.cs | 42 + .../Profiles/Exif/Values/ExifFloatArray.cs | 21 + .../Metadata/Profiles/Exif/Values/ExifLong.cs | 52 + .../Profiles/Exif/Values/ExifLong8.cs | 65 + .../Profiles/Exif/Values/ExifLong8Array.cs | 162 + .../Profiles/Exif/Values/ExifLongArray.cs | 26 + .../Profiles/Exif/Values/ExifNumber.cs | 72 + .../Profiles/Exif/Values/ExifNumberArray.cs | 130 + .../Exif/Values/ExifOrientationMode.cs | 55 + .../Profiles/Exif/Values/ExifRational.cs | 52 + .../Profiles/Exif/Values/ExifRationalArray.cs | 71 + .../Profiles/Exif/Values/ExifShort.cs | 60 + .../Profiles/Exif/Values/ExifShortArray.cs | 104 + .../Profiles/Exif/Values/ExifSignedByte.cs | 47 + .../Exif/Values/ExifSignedByteArray.cs | 21 + .../Profiles/Exif/Values/ExifSignedLong.cs | 25 + .../Profiles/Exif/Values/ExifSignedLong8.cs | 25 + .../Exif/Values/ExifSignedLong8Array.cs | 21 + .../Exif/Values/ExifSignedLongArray.cs | 21 + .../Exif/Values/ExifSignedRational.cs | 30 + .../Exif/Values/ExifSignedRationalArray.cs | 26 + .../Profiles/Exif/Values/ExifSignedShort.cs | 47 + .../Exif/Values/ExifSignedShortArray.cs | 71 + .../Profiles/Exif/Values/ExifString.cs | 47 + .../Profiles/Exif/Values/ExifUcs2String.cs | 46 + .../Profiles/Exif/Values/ExifValue.cs | 84 + .../Profiles/Exif/Values/ExifValues.cs | 292 + .../Exif/Values/ExifValue{TValueType}.cs | 52 + .../Profiles/Exif/Values/IExifValue.cs | 38 + .../Exif/Values/IExifValue{TValueType}.cs | 16 + .../Profiles/ICC/Curves/IccCurveSegment.cs | 46 + .../ICC/Curves/IccFormulaCurveElement.cs | 103 + .../ICC/Curves/IccOneDimensionalCurve.cs | 64 + .../Profiles/ICC/Curves/IccParametricCurve.cs | 167 + .../Profiles/ICC/Curves/IccResponseCurve.cs | 95 + .../ICC/Curves/IccSampledCurveElement.cs | 53 + .../ICC/DataReader/IccDataReader.Curves.cs | 218 + .../ICC/DataReader/IccDataReader.Lut.cs | 165 + .../ICC/DataReader/IccDataReader.Matrix.cs | 72 + .../IccDataReader.MultiProcessElement.cs | 84 + .../DataReader/IccDataReader.NonPrimitives.cs | 180 + .../DataReader/IccDataReader.Primitives.cs | 151 + .../DataReader/IccDataReader.TagDataEntry.cs | 846 + .../Profiles/ICC/DataReader/IccDataReader.cs | 78 + .../ICC/DataWriter/IccDataWriter.Curves.cs | 175 + .../ICC/DataWriter/IccDataWriter.Lut.cs | 116 + .../ICC/DataWriter/IccDataWriter.Matrix.cs | 170 + .../IccDataWriter.MultiProcessElement.cs | 77 + .../DataWriter/IccDataWriter.NonPrimitives.cs | 131 + .../DataWriter/IccDataWriter.Primitives.cs | 245 + .../DataWriter/IccDataWriter.TagDataEntry.cs | 922 + .../Profiles/ICC/DataWriter/IccDataWriter.cs | 246 + .../Profiles/ICC/Enums/IccClutDataType.cs | 25 + .../Profiles/ICC/Enums/IccColorSpaceType.cs | 135 + .../Profiles/ICC/Enums/IccColorantEncoding.cs | 35 + .../ICC/Enums/IccCurveMeasurementEncodings.cs | 59 + .../ICC/Enums/IccCurveSegmentSignature.cs | 20 + .../Profiles/ICC/Enums/IccDataType.cs | 83 + .../Profiles/ICC/Enums/IccDeviceAttribute.cs | 55 + .../Profiles/ICC/Enums/IccFormulaCurveType.cs | 25 + .../ICC/Enums/IccMeasurementGeometry.cs | 25 + .../Enums/IccMultiProcessElementSignature.cs | 35 + .../ICC/Enums/IccParametricCurveType.cs | 43 + .../ICC/Enums/IccPrimaryPlatformType.cs | 35 + .../Profiles/ICC/Enums/IccProfileClass.cs | 62 + .../Profiles/ICC/Enums/IccProfileFlag.cs | 40 + .../Profiles/ICC/Enums/IccProfileTag.cs | 361 + .../Profiles/ICC/Enums/IccRenderingIntent.cs | 41 + .../Profiles/ICC/Enums/IccScreeningFlag.cs | 38 + .../ICC/Enums/IccScreeningSpotType.cs | 50 + .../Profiles/ICC/Enums/IccSignatureName.cs | 175 + .../ICC/Enums/IccStandardIlluminant.cs | 55 + .../Profiles/ICC/Enums/IccStandardObserver.cs | 25 + .../Profiles/ICC/Enums/IccTypeSignature.cs | 270 + .../Exceptions/InvalidIccProfileException.cs | 32 + .../Metadata/Profiles/ICC/ICC.1-2022-05.pdf | Bin 0 -> 905961 bytes .../Metadata/Profiles/ICC/IccProfile.SRGB.cs | 347 + .../Metadata/Profiles/ICC/IccProfile.cs | 212 + .../Metadata/Profiles/ICC/IccProfileHeader.cs | 101 + ImageSharp/Metadata/Profiles/ICC/IccReader.cs | 139 + .../Metadata/Profiles/ICC/IccTagDataEntry.cs | 66 + ImageSharp/Metadata/Profiles/ICC/IccWriter.cs | 89 + .../IccBAcsProcessElement.cs | 31 + .../IccClutProcessElement.cs | 45 + .../IccCurveSetProcessElement.cs | 45 + .../IccEAcsProcessElement.cs | 30 + .../IccMatrixProcessElement.cs | 67 + .../IccMultiProcessElement.cs | 67 + .../IccChromaticityTagDataEntry.cs | 167 + .../IccColorantOrderTagDataEntry.cs | 75 + .../IccColorantTableTagDataEntry.cs | 67 + .../TagDataEntries/IccCrdInfoTagDataEntry.cs | 126 + .../TagDataEntries/IccCurveTagDataEntry.cs | 120 + .../ICC/TagDataEntries/IccDataTagDataEntry.cs | 93 + .../TagDataEntries/IccDateTimeTagDataEntry.cs | 71 + .../IccFix16ArrayTagDataEntry.cs | 68 + .../TagDataEntries/IccLut16TagDataEntry.cs | 169 + .../ICC/TagDataEntries/IccLut8TagDataEntry.cs | 169 + .../TagDataEntries/IccLutAToBTagDataEntry.cs | 306 + .../TagDataEntries/IccLutBToATagDataEntry.cs | 305 + .../IccMeasurementTagDataEntry.cs | 117 + .../IccMultiLocalizedUnicodeTagDataEntry.cs | 69 + .../IccMultiProcessElementsTagDataEntry.cs | 93 + .../IccNamedColor2TagDataEntry.cs | 160 + .../IccParametricCurveTagDataEntry.cs | 69 + .../IccProfileSequenceDescTagDataEntry.cs | 64 + ...ccProfileSequenceIdentifierTagDataEntry.cs | 69 + .../IccResponseCurveSet16TagDataEntry.cs | 85 + .../IccScreeningTagDataEntry.cs | 82 + .../IccSignatureTagDataEntry.cs | 64 + .../IccTextDescriptionTagDataEntry.cs | 172 + .../ICC/TagDataEntries/IccTextTagDataEntry.cs | 62 + .../IccUFix16ArrayTagDataEntry.cs | 68 + .../IccUInt16ArrayTagDataEntry.cs | 68 + .../IccUInt32ArrayTagDataEntry.cs | 68 + .../IccUInt64ArrayTagDataEntry.cs | 59 + .../IccUInt8ArrayTagDataEntry.cs | 68 + .../TagDataEntries/IccUcrBgTagDataEntry.cs | 89 + .../TagDataEntries/IccUnknownTagDataEntry.cs | 68 + .../IccViewingConditionsTagDataEntry.cs | 95 + .../ICC/TagDataEntries/IccXyzTagDataEntry.cs | 58 + .../Metadata/Profiles/ICC/Various/IccClut.cs | 113 + .../ICC/Various/IccColorantTableEntry.cs | 114 + .../ICC/Various/IccLocalizedString.cs | 59 + .../Metadata/Profiles/ICC/Various/IccLut.cs | 77 + .../Profiles/ICC/Various/IccNamedColor.cs | 87 + .../Profiles/ICC/Various/IccPositionNumber.cs | 80 + .../ICC/Various/IccProfileDescription.cs | 91 + .../Profiles/ICC/Various/IccProfileId.cs | 105 + .../Various/IccProfileSequenceIdentifier.cs | 44 + .../Profiles/ICC/Various/IccResponseNumber.cs | 80 + .../ICC/Various/IccScreeningChannel.cs | 93 + .../Profiles/ICC/Various/IccTagTableEntry.cs | 81 + .../Profiles/ICC/Various/IccVersion.cs | 76 + .../Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf | Bin 0 -> 364258 bytes .../Metadata/Profiles/IPTC/IptcProfile.cs | 350 + .../Profiles/IPTC/IptcRecordNumber.cs | 20 + ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs | 396 + .../Profiles/IPTC/IptcTagExtensions.cs | 158 + .../Metadata/Profiles/IPTC/IptcValue.cs | 251 + ImageSharp/Metadata/Profiles/IPTC/README.md | 11 + .../Metadata/Profiles/XMP/XmpProfile.cs | 176 + ImageSharp/PixelAccessor{TPixel}.cs | 71 + ImageSharp/PixelFormats/HalfTypeHelper.cs | 29 + .../PixelFormats/IPackedVector{TPacked}.cs | 20 + ImageSharp/PixelFormats/IPixel.cs | 165 + .../PixelFormats/PixelAlphaCompositionMode.cs | 70 + .../PixelFormats/PixelAlphaRepresentation.cs | 31 + .../DefaultPixelBlenders.Generated.cs | 36209 ++++++++++++++++ .../DefaultPixelBlenders.Generated.tt | 411 + .../PorterDuffFunctions.Generated.cs | 6054 +++ .../PorterDuffFunctions.Generated.tt | 561 + .../PixelBlenders/PorterDuffFunctions.cs | 737 + .../PixelFormats/PixelBlender{TPixel}.cs | 416 + .../PixelFormats/PixelColorBlendingMode.cs | 55 + ImageSharp/PixelFormats/PixelColorType.cs | 118 + .../PixelFormats/PixelComponentBitDepth.cs | 50 + ImageSharp/PixelFormats/PixelComponentInfo.cs | 123 + .../PixelFormats/PixelConversionModifiers.cs | 38 + .../PixelConversionModifiersExtensions.cs | 37 + .../PixelFormats/PixelImplementations/A8.cs | 171 + .../PixelImplementations/Abgr32.cs | 325 + .../PixelImplementations/Argb32.cs | 301 + .../PixelImplementations/Bgr24.cs | 200 + .../PixelImplementations/Bgr565.cs | 178 + .../PixelImplementations/Bgra32.cs | 247 + .../PixelImplementations/Bgra4444.cs | 177 + .../PixelImplementations/Bgra5551.cs | 176 + .../PixelImplementations/Byte4.cs | 185 + .../PixelImplementations/HalfSingle.cs | 160 + .../PixelImplementations/HalfVector2.cs | 189 + .../PixelImplementations/HalfVector4.cs | 188 + .../PixelFormats/PixelImplementations/L16.cs | 158 + .../PixelFormats/PixelImplementations/L8.cs | 164 + .../PixelFormats/PixelImplementations/La16.cs | 193 + .../PixelFormats/PixelImplementations/La32.cs | 216 + .../PixelImplementations/NormalizedByte2.cs | 192 + .../PixelImplementations/NormalizedByte4.cs | 191 + .../PixelImplementations/NormalizedShort2.cs | 196 + .../PixelImplementations/NormalizedShort4.cs | 194 + .../PixelOperations/A8.PixelOperations.cs | 15 + .../PixelOperations/Abgr32.PixelOperations.cs | 15 + .../PixelOperations/Argb32.PixelOperations.cs | 15 + .../PixelOperations/Bgr24.PixelOperations.cs | 15 + .../PixelOperations/Bgr565.PixelOperations.cs | 15 + .../PixelOperations/Bgra32.PixelOperations.cs | 15 + .../Bgra4444.PixelOperations.cs | 15 + .../Bgra5551.PixelOperations.cs | 15 + .../PixelOperations/Byte4.PixelOperations.cs | 15 + .../Abgr32.PixelOperations.Generated.cs | 337 + .../Abgr32.PixelOperations.Generated.tt | 17 + .../Argb32.PixelOperations.Generated.cs | 337 + .../Argb32.PixelOperations.Generated.tt | 17 + .../Bgr24.PixelOperations.Generated.cs | 337 + .../Bgr24.PixelOperations.Generated.tt | 17 + .../Bgra32.PixelOperations.Generated.cs | 337 + .../Bgra32.PixelOperations.Generated.tt | 17 + .../Bgra5551.PixelOperations.Generated.cs | 267 + .../Bgra5551.PixelOperations.Generated.tt | 17 + .../L16.PixelOperations.Generated.cs | 267 + .../L16.PixelOperations.Generated.tt | 17 + .../Generated/L8.PixelOperations.Generated.cs | 267 + .../Generated/L8.PixelOperations.Generated.tt | 17 + .../La16.PixelOperations.Generated.cs | 267 + .../La16.PixelOperations.Generated.tt | 17 + .../La32.PixelOperations.Generated.cs | 267 + .../La32.PixelOperations.Generated.tt | 17 + .../Rgb24.PixelOperations.Generated.cs | 337 + .../Rgb24.PixelOperations.Generated.tt | 17 + .../Rgb48.PixelOperations.Generated.cs | 267 + .../Rgb48.PixelOperations.Generated.tt | 17 + .../Rgba32.PixelOperations.Generated.cs | 317 + .../Rgba32.PixelOperations.Generated.tt | 17 + .../Rgba64.PixelOperations.Generated.cs | 267 + .../Rgba64.PixelOperations.Generated.tt | 17 + .../Generated/_Common.ttinclude | 208 + .../HalfSingle.PixelOperations.cs | 15 + .../HalfVector2.PixelOperations.cs | 15 + .../HalfVector4.PixelOperations.cs | 15 + .../PixelOperations/L16.PixelOperations.cs | 15 + .../PixelOperations/L8.PixelOperations.cs | 15 + .../PixelOperations/La16.PixelOperations.cs | 15 + .../PixelOperations/La32.PixelOperations.cs | 15 + .../NormalizedByte2.PixelOperations.cs | 15 + .../NormalizedByte4.PixelOperations.cs | 15 + .../NormalizedShort2.PixelOperations.cs | 15 + .../NormalizedShort4.PixelOperations.cs | 15 + .../PixelOperations/Rg32.PixelOperations.cs | 15 + .../PixelOperations/Rgb24.PixelOperations.cs | 42 + .../PixelOperations/Rgb48.PixelOperations.cs | 15 + .../Rgba1010102.PixelOperations.cs | 15 + .../PixelOperations/Rgba32.PixelOperations.cs | 66 + .../PixelOperations/Rgba64.PixelOperations.cs | 15 + .../RgbaVector.PixelOperations.cs | 58 + .../PixelOperations/Short2.PixelOperations.cs | 15 + .../PixelOperations/Short4.PixelOperations.cs | 15 + .../PixelFormats/PixelImplementations/Rg32.cs | 178 + .../PixelImplementations/Rgb24.cs | 211 + .../PixelImplementations/Rgb48.cs | 184 + .../PixelImplementations/Rgb96.cs | 204 + .../PixelImplementations/Rgba1010102.cs | 177 + .../PixelImplementations/Rgba128.cs | 201 + .../PixelImplementations/Rgba32.cs | 383 + .../PixelImplementations/Rgba64.cs | 330 + .../PixelImplementations/RgbaVector.cs | 214 + .../PixelImplementations/Short2.cs | 194 + .../PixelImplementations/Short4.cs | 199 + .../PixelOperations{TPixel}.Generated.cs | 870 + .../PixelOperations{TPixel}.Generated.tt | 144 + .../PixelOperations{TPixel}.PixelBenders.cs | 216 + .../PixelFormats/PixelOperations{TPixel}.cs | 246 + ImageSharp/PixelFormats/PixelTypeInfo.cs | 62 + ImageSharp/PixelFormats/README.md | 6 + ImageSharp/PixelFormats/RgbaComponent.cs | 30 + .../PixelFormats/Utils/PixelConverter.cs | 398 + .../Utils/Vector4Converters.Default.cs | 161 + .../Utils/Vector4Converters.RgbaCompatible.cs | 149 + .../PixelFormats/Utils/Vector4Converters.cs | 50 + ImageSharp/Primitives/ColorMatrix.Impl.cs | 209 + ImageSharp/Primitives/ColorMatrix.cs | 263 + ImageSharp/Primitives/Complex64.cs | 94 + ImageSharp/Primitives/ComplexVector4.cs | 62 + ImageSharp/Primitives/DenseMatrix{T}.cs | 279 + ImageSharp/Primitives/LongRational.cs | 224 + ImageSharp/Primitives/Matrix3x2Extensions.cs | 100 + ImageSharp/Primitives/Number.cs | 186 + ImageSharp/Primitives/Point.cs | 300 + ImageSharp/Primitives/PointF.cs | 304 + ImageSharp/Primitives/Rational.cs | 171 + ImageSharp/Primitives/Rectangle.cs | 462 + ImageSharp/Primitives/RectangleF.cs | 421 + ImageSharp/Primitives/SignedRational.cs | 189 + ImageSharp/Primitives/Size.cs | 295 + ImageSharp/Primitives/SizeF.cs | 232 + ImageSharp/Primitives/ValueSize.cs | 132 + .../Processing/AdaptiveThresholdExtensions.cs | 73 + .../Processing/AffineTransformBuilder.cs | 383 + ImageSharp/Processing/AnchorPositionMode.cs | 55 + ImageSharp/Processing/BinaryThresholdMode.cs | 25 + ImageSharp/Processing/ColorBlindnessMode.cs | 50 + .../DefaultImageProcessorContext{TPixel}.cs | 95 + .../Binarization/BinaryDitherExtensions.cs | 72 + .../Binarization/BinaryThresholdExtensions.cs | 143 + .../Convolution/BokehBlurExtensions.cs | 56 + .../Convolution/BoxBlurExtensions.cs | 65 + .../Convolution/ConvolutionExtensions.cs | 89 + .../Convolution/DetectEdgesExtensions.cs | 207 + .../Convolution/GaussianBlurExtensions.cs | 68 + .../Convolution/GaussianSharpenExtensions.cs | 68 + .../Convolution/MedianBlurExtensions.cs | 47 + .../Extensions/Dithering/DitherExtensions.cs | 153 + .../Extensions/Drawing/DrawImageExtensions.cs | 635 + .../Extensions/Effects/OilPaintExtensions.cs | 61 + .../Effects/PixelRowDelegateExtensions.cs | 101 + .../Extensions/Effects/PixelateExtensions.cs | 44 + .../Filters/BlackWhiteExtensions.cs | 32 + .../Filters/BrightnessExtensions.cs | 42 + .../Filters/ColorBlindnessExtensions.cs | 58 + .../Extensions/Filters/ContrastExtensions.cs | 42 + .../Extensions/Filters/FilterExtensions.cs | 34 + .../Extensions/Filters/GrayscaleExtensions.cs | 112 + .../Extensions/Filters/HueExtensions.cs | 34 + .../Extensions/Filters/InvertExtensions.cs | 32 + .../Filters/KodachromeExtensions.cs | 32 + .../Extensions/Filters/LightnessExtensions.cs | 42 + .../Extensions/Filters/LomographExtensions.cs | 32 + .../Extensions/Filters/OpacityExtensions.cs | 34 + .../Extensions/Filters/PolaroidExtensions.cs | 32 + .../Extensions/Filters/SaturateExtensions.cs | 42 + .../Extensions/Filters/SepiaExtensions.cs | 53 + .../HistogramEqualizationExtensions.cs | 31 + .../Overlays/BackgroundColorExtensions.cs | 67 + .../Extensions/Overlays/GlowExtensions.cs | 172 + .../Extensions/Overlays/VignetteExtensions.cs | 176 + .../ProcessingExtensions.IntegralImage.cs | 110 + .../Extensions/ProcessingExtensions.cs | 299 + .../Quantization/QuantizeExtensions.cs | 53 + .../Transforms/AutoOrientExtensions.cs | 21 + .../Extensions/Transforms/CropExtensions.cs | 34 + .../Transforms/EntropyCropExtensions.cs | 30 + .../Extensions/Transforms/FlipExtensions.cs | 22 + .../Extensions/Transforms/PadExtensions.cs | 46 + .../Extensions/Transforms/ResizeExtensions.cs | 175 + .../Extensions/Transforms/RotateExtensions.cs | 44 + .../Transforms/RotateFlipExtensions.cs | 21 + .../Extensions/Transforms/SkewExtensions.cs | 39 + .../Transforms/SwizzleExtensions.cs | 23 + .../Transforms/TransformExtensions.cs | 138 + ImageSharp/Processing/FlipMode.cs | 25 + ImageSharp/Processing/GrayscaleMode.cs | 20 + .../Processing/IImageProcessingContext.cs | 45 + .../IImageProcessingContextFactory.cs | 36 + ...IInternalImageProcessingContext{TPixel}.cs | 21 + ImageSharp/Processing/KnownDitherings.cs | 82 + .../Processing/KnownEdgeDetectorKernels.cs | 62 + ImageSharp/Processing/KnownFilterMatrices.cs | 488 + ImageSharp/Processing/KnownQuantizers.cs | 33 + ImageSharp/Processing/KnownResamplers.cs | 97 + ImageSharp/Processing/PixelRowOperation.cs | 26 + .../AdaptiveThresholdProcessor.cs | 76 + .../AdaptiveThresholdProcessor{TPixel}.cs | 129 + .../Binarization/BinaryThresholdProcessor.cs | 85 + .../BinaryThresholdProcessor{TPixel}.cs | 192 + .../Processors/CloningImageProcessor.cs | 20 + .../CloningImageProcessor{TPixel}.cs | 181 + .../Convolution/BokehBlurProcessor.cs | 162 + .../Convolution/BokehBlurProcessor{TPixel}.cs | 435 + .../Convolution/BorderWrappingMode.cs | 24 + .../Convolution/BoxBlurProcessor.cs | 71 + .../Convolution/BoxBlurProcessor{TPixel}.cs | 90 + .../Convolution2DProcessor{TPixel}.cs | 97 + .../Convolution2DRowOperation{TPixel}.cs | 197 + .../Convolution/Convolution2DState.cs | 53 + .../Convolution2PassProcessor{TPixel}.cs | 476 + .../Convolution/ConvolutionProcessor.cs | 79 + .../ConvolutionProcessorHelpers.cs | 189 + .../ConvolutionProcessor{TPixel}.cs | 239 + .../Convolution/ConvolutionState.cs | 44 + .../Convolution/EdgeDetector2DProcessor.cs | 41 + .../EdgeDetector2DProcessor{TPixel}.cs | 68 + .../EdgeDetectorCompassProcessor.cs | 41 + .../EdgeDetectorCompassProcessor{TPixel}.cs | 132 + .../Convolution/EdgeDetectorProcessor.cs | 41 + .../EdgeDetectorProcessor{TPixel}.cs | 59 + .../Convolution/GaussianBlurProcessor.cs | 120 + .../GaussianBlurProcessor{TPixel}.cs | 61 + .../Convolution/GaussianSharpenProcessor.cs | 120 + .../GaussianSharpenProcessor{TPixel}.cs | 61 + .../Processors/Convolution/Kernel.cs | 98 + .../Convolution/KernelSamplingMap.cs | 184 + .../Kernels/EdgeDetector2DKernel.cs | 102 + .../Kernels/EdgeDetectorCompassKernel.cs | 161 + .../Convolution/Kernels/EdgeDetectorKernel.cs | 77 + .../Kernels/Implementation/KayyaliKernels.cs | 32 + .../Kernels/Implementation/KirschKernels.cs | 99 + .../Implementation/LaplacianKernelFactory.cs | 30 + .../Implementation/LaplacianKernels.cs | 35 + .../Kernels/Implementation/PrewittKernels.cs | 32 + .../Implementation/RobertsCrossKernels.cs | 30 + .../Kernels/Implementation/RobinsonKernels.cs | 99 + .../Kernels/Implementation/ScharrKernels.cs | 32 + .../Kernels/Implementation/SobelKernels.cs | 32 + .../Convolution/MedianBlurProcessor.cs | 52 + .../MedianBlurProcessor{TPixel}.cs | 53 + .../Convolution/MedianConvolutionState.cs | 45 + .../Convolution/MedianRowOperation{TPixel}.cs | 182 + .../Parameters/BokehBlurKernelData.cs | 33 + .../Parameters/BokehBlurKernelDataProvider.cs | 222 + .../Parameters/BokehBlurParameters.cs | 51 + .../Processors/Convolution/ReadOnlyKernel.cs | 62 + .../Processing/Processors/Dithering/DHALF.TXT | 1331 + .../Processors/Dithering/DITHER.TXT | 547 + .../Dithering/ErrorDither.KnownTypes.cs | 187 + .../Processors/Dithering/ErrorDither.cs | 242 + .../Processors/Dithering/IDither.cs | 47 + .../IPaletteDitherImageProcessor{TPixel}.cs | 37 + .../Dithering/OrderedDither.KnownTypes.cs | 35 + .../Processors/Dithering/OrderedDither.cs | 229 + .../Dithering/OrderedDitherFactory.cs | 92 + .../Dithering/PaletteDitherProcessor.cs | 78 + .../PaletteDitherProcessor{TPixel}.cs | 113 + .../Processors/Dithering/error_diffusion.txt | 61 + ...mal-parallel-error-diffusion-dithering.pdf | Bin 0 -> 130235 bytes .../Processors/Drawing/DrawImageProcessor.cs | 138 + .../DrawImageProcessor{TPixelBg,TPixelFg}.cs | 211 + .../Processors/Effects/IPixelRowDelegate.cs | 20 + .../Effects/OilPaintingProcessor.cs | 45 + .../Effects/OilPaintingProcessor{TPixel}.cs | 188 + .../Effects/PixelRowDelegateProcessor.cs | 62 + ...lRowDelegateProcessor{TPixel,TDelegate}.cs | 105 + .../Processors/Effects/PixelateProcessor.cs | 35 + .../Effects/PixelateProcessor{TPixel}.cs | 102 + .../PositionAwarePixelRowDelegateProcessor.cs | 66 + .../Filters/AchromatomalyProcessor.cs | 18 + .../Filters/AchromatopsiaProcessor.cs | 18 + .../Processors/Filters/BlackWhiteProcessor.cs | 18 + .../Processors/Filters/BrightnessProcessor.cs | 29 + .../Processors/Filters/ContrastProcessor.cs | 29 + .../Filters/DeuteranomalyProcessor.cs | 18 + .../Filters/DeuteranopiaProcessor.cs | 18 + .../Processors/Filters/FilterProcessor.cs | 28 + .../Filters/FilterProcessor{TPixel}.cs | 85 + .../Filters/GrayscaleBt601Processor.cs | 25 + .../Filters/GrayscaleBt709Processor.cs | 25 + .../Processors/Filters/HueProcessor.cs | 25 + .../Processors/Filters/InvertProcessor.cs | 25 + .../Processors/Filters/KodachromeProcessor.cs | 18 + .../Processors/Filters/LightnessProcessor.cs | 29 + .../Processors/Filters/LomographProcessor.cs | 29 + .../Filters/LomographProcessor{TPixel}.cs | 37 + .../Processors/Filters/OpacityProcessor.cs | 25 + .../Filters/OpaqueProcessor{TPixel}.cs | 72 + .../Processors/Filters/PolaroidProcessor.cs | 29 + .../Filters/PolaroidProcessor{TPixel}.cs | 39 + .../Filters/ProtanomalyProcessor.cs | 18 + .../Processors/Filters/ProtanopiaProcessor.cs | 18 + .../Processing/Processors/Filters/README.md | 4 + .../Processors/Filters/SaturateProcessor.cs | 29 + .../Processors/Filters/SepiaProcessor.cs | 25 + .../Filters/TritanomalyProcessor.cs | 18 + .../Processors/Filters/TritanopiaProcessor.cs | 18 + .../Processors/ICloningImageProcessor.cs | 26 + .../ICloningImageProcessor{TPixel}.cs | 20 + .../Processing/Processors/IImageProcessor.cs | 29 + .../Processors/IImageProcessor{TPixel}.cs | 20 + .../Processors/ImageProcessorExtensions.cs | 43 + .../Processors/ImageProcessor{TPixel}.cs | 119 + .../AdaptiveHistogramEqualizationProcessor.cs | 42 + ...eHistogramEqualizationProcessor{TPixel}.cs | 636 + ...ogramEqualizationSlidingWindowProcessor.cs | 44 + ...alizationSlidingWindowProcessor{TPixel}.cs | 439 + .../Normalization/AutoLevelProcessor.cs | 46 + .../AutoLevelProcessor{TPixel}.cs | 224 + .../GlobalHistogramEqualizationProcessor.cs | 31 + ...lHistogramEqualizationProcessor{TPixel}.cs | 142 + .../GrayscaleLevelsRowOperation{TPixel}.cs | 66 + .../HistogramEqualizationMethod.cs | 31 + .../HistogramEqualizationOptions.cs | 52 + .../HistogramEqualizationProcessor.cs | 68 + .../HistogramEqualizationProcessor{TPixel}.cs | 149 + .../Overlays/BackgroundColorProcessor.cs | 38 + .../BackgroundColorProcessor{TPixel}.cs | 102 + .../Processors/Overlays/GlowProcessor.cs | 55 + .../Overlays/GlowProcessor{TPixel}.cs | 123 + .../Processors/Overlays/VignetteProcessor.cs | 63 + .../Overlays/VignetteProcessor{TPixel}.cs | 131 + .../Quantization/ColorMatchingMode.cs | 22 + .../DefaultPixelSamplingStrategy.cs | 158 + .../EuclideanPixelMap{TPixel,TCache}.cs | 261 + .../ExtensivePixelSamplingStrategy.cs | 31 + .../Quantization/HexadecatreeQuantizer.cs | 49 + .../HexadecatreeQuantizer{TPixel}.cs | 686 + .../Quantization/IColorIndexCache.cs | 397 + .../Quantization/IPixelSamplingStrategy.cs | 32 + .../Processors/Quantization/IQuantizer.cs | 36 + .../Quantization/IQuantizer{TPixel}.cs | 65 + .../IQuantizingPixelRowDelegate{TPixel}.cs | 22 + .../Quantization/PaletteQuantizer.cs | 79 + .../Quantization/PaletteQuantizer{TPixel}.cs | 108 + .../Quantization/QuantizeProcessor.cs | 29 + .../Quantization/QuantizeProcessor{TPixel}.cs | 58 + .../Quantization/QuantizerConstants.cs | 52 + .../Quantization/QuantizerOptions.cs | 92 + .../Quantization/QuantizerUtilities.cs | 447 + .../Quantization/WebSafePaletteQuantizer.cs | 27 + .../Quantization/WernerPaletteQuantizer.cs | 28 + .../Processors/Quantization/WuQuantizer.cs | 44 + .../Quantization/WuQuantizer{TPixel}.cs | 893 + .../Processors/Transforms/CropProcessor.cs | 35 + .../Transforms/CropProcessor{TPixel}.cs | 105 + .../DegenerateTransformException.cs | 41 + .../Transforms/EntropyCropProcessor.cs | 43 + .../EntropyCropProcessor{TPixel}.cs | 178 + .../Processors/Transforms/IResampler.cs | 34 + ...samplingTransformImageProcessor{TPixel}.cs | 22 + .../Processors/Transforms/ISwizzler.cs | 22 + .../Linear/AffineTransformProcessor.cs | 52 + .../AffineTransformProcessor{TPixel}.cs | 257 + .../Transforms/Linear/AutoOrientProcessor.cs | 17 + .../Linear/AutoOrientProcessor{TPixel}.cs | 112 + .../Transforms/Linear/FlipProcessor.cs | 28 + .../Linear/FlipProcessor{TPixel}.cs | 131 + .../Linear/GaussianEliminationSolver.cs | 88 + .../Linear/LinearTransformUtility.cs | 61 + .../Linear/ProjectiveTransformProcessor.cs | 52 + .../ProjectiveTransformProcessor{TPixel}.cs | 255 + .../Transforms/Linear/RotateProcessor.cs | 50 + .../Linear/RotateProcessor{TPixel}.cs | 285 + .../Transforms/Linear/SkewProcessor.cs | 56 + .../Transforms/Resamplers/BicubicResampler.cs | 48 + .../Transforms/Resamplers/BoxResampler.cs | 35 + .../Transforms/Resamplers/CubicResampler.cs | 111 + .../Transforms/Resamplers/LanczosResampler.cs | 67 + .../Resamplers/NearestNeighborResampler.cs | 27 + .../Resamplers/TriangleResampler.cs | 41 + .../Transforms/Resamplers/WelchResampler.cs | 40 + .../Transforms/Resize/ResizeHelper.cs | 430 + .../Transforms/Resize/ResizeKernel.cs | 188 + .../ResizeKernelMap.PeriodicKernelMap.cs | 76 + .../Transforms/Resize/ResizeKernelMap.cs | 280 + .../Transforms/Resize/ResizeProcessor.cs | 53 + .../Resize/ResizeProcessor{TPixel}.cs | 290 + .../Transforms/Resize/ResizeWorker.cs | 253 + .../Transforms/Resize/ResizeWorker.pptx | Bin 0 -> 45638 bytes .../SwizzleProcessor{TSwizzler,TPixel}.cs | 54 + .../Transforms/SwizzleProcessor{TSwizzler}.cs | 30 + .../Transforms/TransformProcessor{TPixel}.cs | 42 + .../Transforms/TransformUtilities.cs | 685 + .../Processing/ProjectiveTransformBuilder.cs | 431 + ImageSharp/Processing/ResizeMode.cs | 51 + ImageSharp/Processing/ResizeOptions.cs | 59 + ImageSharp/Processing/RotateMode.cs | 30 + ImageSharp/Processing/TaperCorner.cs | 25 + ImageSharp/Processing/TaperSide.cs | 30 + ImageSharp/Properties/AssemblyInfo.cs | 9 + ImageSharp/ReadOrigin.cs | 20 + LICENSE | 43 + PolygonClipper/ActiveEdge.cs | 194 + PolygonClipper/ActiveEdgeList.cs | 349 + PolygonClipper/ArrayBuilder{T}.cs | 180 + PolygonClipper/BooleanOperation.cs | 31 + PolygonClipper/Box2.cs | 134 + PolygonClipper/Buffer{T}.cs | 63 + PolygonClipper/ByteMemoryManager{T}.cs | 50 + PolygonClipper/Contour.cs | 282 + PolygonClipper/EdgeType.cs | 33 + PolygonClipper/FloatExtensions.cs | 76 + PolygonClipper/IntersectNode.cs | 41 + PolygonClipper/JoinWith.cs | 25 + PolygonClipper/LineCap.cs | 28 + PolygonClipper/LineJoin.cs | 41 + PolygonClipper/LocalMinima.cs | 42 + PolygonClipper/OutputRecord.cs | 163 + PolygonClipper/PathCommand.cs | 38 + PolygonClipper/PathCommandExtensions.cs | 54 + PolygonClipper/PathFlags.cs | 38 + PolygonClipper/PointInPolygonResult.cs | 25 + PolygonClipper/Polygon.cs | 192 + PolygonClipper/PolygonClipper.cs | 1234 + PolygonClipper/PolygonClipper.csproj | 38 + PolygonClipper/PolygonStroker.cs | 1250 + PolygonClipper/PolygonType.cs | 20 + PolygonClipper/PolygonUtilities.cs | 860 + PolygonClipper/ResultTransition.cs | 25 + PolygonClipper/ScanlineSchedule.cs | 158 + PolygonClipper/Segment.cs | 95 + PolygonClipper/SegmentComparer.cs | 158 + PolygonClipper/SelfIntersectionRemover.cs | 1352 + PolygonClipper/SelfIntersectionSweepLine.cs | 2611 ++ .../StablePriorityQueue{T,TComparer}.cs | 220 + PolygonClipper/StatusLine.cs | 216 + PolygonClipper/StrokeOptions.cs | 65 + PolygonClipper/StrokeVertexDistance.cs | 71 + PolygonClipper/SweepEvent.cs | 205 + PolygonClipper/SweepEventComparer.cs | 72 + PolygonClipper/SweepVertex.cs | 53 + PolygonClipper/Vertex.cs | 248 + PolygonClipper/VertexFlags.cs | 33 + PolygonClipper/VertexPoolList.cs | 342 + SharedInfrastructure.projitems | 22 + SixLabors.Fonts/ArrayBuilder{T}.cs | 193 + SixLabors.Fonts/ArraySlice{T}.cs | 209 + SixLabors.Fonts/BigEndianBinaryReader.cs | 513 + SixLabors.Fonts/Bounds.cs | 93 + SixLabors.Fonts/Buffer{T}.cs | 73 + SixLabors.Fonts/ByteMemoryManager{T}.cs | 50 + SixLabors.Fonts/CaretMovement.cs | 60 + SixLabors.Fonts/CaretPlacement.cs | 20 + SixLabors.Fonts/CaretPosition.cs | 91 + SixLabors.Fonts/ClipQuad.cs | 85 + SixLabors.Fonts/ColorFontSupport.cs | 38 + SixLabors.Fonts/DecorationPositioningMode.cs | 24 + SixLabors.Fonts/Exceptions/FontException.cs | 22 + .../Exceptions/FontFamilyNotFoundException.cs | 67 + .../Exceptions/FontsThrowHelper.cs | 27 + .../Exceptions/GlyphMissingException.cs | 22 + .../Exceptions/InvalidFontFileException.cs | 22 + .../Exceptions/InvalidFontTableException.cs | 25 + .../Exceptions/MissingFontTableException.cs | 25 + SixLabors.Fonts/FileFontMetrics.cs | 208 + SixLabors.Fonts/Font.cs | 375 + SixLabors.Fonts/FontCollection.cs | 303 + SixLabors.Fonts/FontCollectionExtensions.cs | 58 + SixLabors.Fonts/FontDescription.cs | 202 + SixLabors.Fonts/FontFamily.cs | 231 + SixLabors.Fonts/FontGlyphMetrics.cs | 535 + SixLabors.Fonts/FontMetrics.cs | 299 + SixLabors.Fonts/FontReader.cs | 218 + SixLabors.Fonts/FontRectangle.cs | 370 + SixLabors.Fonts/FontStyle.cs | 37 + SixLabors.Fonts/FontVariation.cs | 46 + SixLabors.Fonts/Glyph.cs | 54 + SixLabors.Fonts/GlyphColor.KnownColors.cs | 915 + SixLabors.Fonts/GlyphColor.cs | 252 + SixLabors.Fonts/GlyphLayout.cs | 192 + SixLabors.Fonts/GlyphLayoutData.cs | 147 + SixLabors.Fonts/GlyphLayoutMode.cs | 25 + SixLabors.Fonts/GlyphMetrics.cs | 79 + SixLabors.Fonts/GlyphPositioningCollection.cs | 459 + SixLabors.Fonts/GlyphShapingBounds.cs | 81 + SixLabors.Fonts/GlyphShapingClass.cs | 23 + SixLabors.Fonts/GlyphShapingData.cs | 269 + .../GlyphSubstitutionCollection.cs | 464 + SixLabors.Fonts/GlyphType.cs | 30 + SixLabors.Fonts/GraphemeMetrics.cs | 81 + SixLabors.Fonts/HintingMode.cs | 21 + SixLabors.Fonts/HorizontalAlignment.cs | 25 + SixLabors.Fonts/HorizontalMetrics.cs | 28 + SixLabors.Fonts/IFontCollection.cs | 149 + SixLabors.Fonts/IFontMetricsCollection.cs | 26 + SixLabors.Fonts/IGlyphShapingCollection.cs | 50 + SixLabors.Fonts/IMetricsHeader.cs | 44 + SixLabors.Fonts/IO/ZlibInflateStream.cs | 212 + .../IReadOnlySystemFontCollection.cs | 19 + SixLabors.Fonts/IReadonlyFontCollection.cs | 79 + .../IReadonlyFontMetricsCollection.cs | 55 + SixLabors.Fonts/KerningMode.cs | 26 + SixLabors.Fonts/KnownVariationAxes.cs | 48 + SixLabors.Fonts/LayoutMode.cs | 43 + SixLabors.Fonts/LayoutModeExtensions.cs | 39 + SixLabors.Fonts/LineLayout.cs | 205 + SixLabors.Fonts/LineLayoutEnumerator.cs | 67 + SixLabors.Fonts/LineMetrics.cs | 110 + SixLabors.Fonts/LogicalTextLine.cs | 52 + SixLabors.Fonts/MappedArraySlice{T}.cs | 50 + SixLabors.Fonts/Native/CFStringEncoding.cs | 23 + SixLabors.Fonts/Native/CFURLPathStyle.cs | 18 + SixLabors.Fonts/Native/CoreFoundation.cs | 125 + SixLabors.Fonts/Native/CoreText.cs | 20 + .../Native/MacSystemFontsEnumerator.cs | 93 + SixLabors.Fonts/ObjectPool{T}.cs | 129 + SixLabors.Fonts/OutlineType.cs | 10 + SixLabors.Fonts/PlaceholderGlyphMetrics.cs | 166 + SixLabors.Fonts/PreparedTextLayoutDesign.md | 534 + SixLabors.Fonts/ReadOnlyArraySlice{T}.cs | 199 + .../ReadOnlyMappedArraySlice{T}.cs | 50 + SixLabors.Fonts/Rendering/CompositeMode.cs | 183 + SixLabors.Fonts/Rendering/FillRule.cs | 20 + .../Rendering/GlyphRendererExtensions.cs | 25 + .../Rendering/GlyphRendererParameters.cs | 164 + SixLabors.Fonts/Rendering/GradientStop.cs | 33 + SixLabors.Fonts/Rendering/GradientUnits.cs | 22 + SixLabors.Fonts/Rendering/IGlyphRenderer.cs | 135 + .../Rendering/IPaintedGlyphSource.cs | 20 + SixLabors.Fonts/Rendering/Paint.cs | 165 + .../Rendering/PaintedCanvasMetadata.cs | 46 + SixLabors.Fonts/Rendering/PaintedGlyph.cs | 33 + .../Rendering/PaintedGlyphMetrics.cs | 581 + SixLabors.Fonts/Rendering/PaintedLayer.cs | 58 + SixLabors.Fonts/Rendering/PathCommand.cs | 151 + SixLabors.Fonts/Rendering/PathVerb.cs | 40 + SixLabors.Fonts/Rendering/SpreadMethod.cs | 25 + SixLabors.Fonts/Rendering/TextRenderer.cs | 57 + SixLabors.Fonts/ShapedText.cs | 52 + SixLabors.Fonts/SixLabors.Fonts.csproj | 48 + SixLabors.Fonts/StreamFontMetrics.Cff.cs | 193 + SixLabors.Fonts/StreamFontMetrics.TrueType.cs | 325 + SixLabors.Fonts/StreamFontMetrics.cs | 840 + SixLabors.Fonts/StringComparerHelpers.cs | 20 + SixLabors.Fonts/SystemFontCollection.cs | 174 + SixLabors.Fonts/SystemFonts.cs | 85 + .../AdvancedTypographicUtils.cs | 700 + .../Tables/AdvancedTypographic/AttachPoint.cs | 16 + .../AttachmentListTable.cs | 70 + .../ChainedClassSequenceRuleSetTable.cs | 60 + .../ChainedClassSequenceRuleTable.cs | 102 + .../ChainedSequenceRuleSetTable.cs | 60 + .../ChainedSequenceRuleTable.cs | 101 + .../ClassDefinitionTable.cs | 216 + .../AdvancedTypographic/ClassRangeRecord.cs | 43 + .../ClassSequenceRuleSetTable.cs | 61 + .../ClassSequenceRuleTable.cs | 65 + .../CoverageRangeRecord.cs | 43 + .../AdvancedTypographic/CoverageTable.cs | 215 + .../AdvancedTypographic/FeatureListTable.cs | 163 + .../FeatureVariationsTable.cs | 387 + .../AdvancedTypographic/GPos/AnchorTable.cs | 330 + .../AdvancedTypographic/GPos/AnchorXY.cs | 31 + .../GPos/BaseArrayTable.cs | 43 + .../AdvancedTypographic/GPos/BaseRecord.cs | 50 + .../AdvancedTypographic/GPos/Class1Record.cs | 49 + .../AdvancedTypographic/GPos/Class2Record.cs | 36 + .../GPos/ComponentRecord.cs | 50 + .../GPos/EntryExitAnchors.cs | 32 + .../GPos/EntryExitRecord.cs | 43 + .../GPos/LigatureArrayTable.cs | 50 + .../GPos/LigatureAttachTable.cs | 44 + .../GPos/LookupListTable.cs | 268 + .../GPos/LookupType1SubTable.cs | 212 + .../GPos/LookupType2SubTable.cs | 388 + .../GPos/LookupType3SubTable.cs | 298 + .../GPos/LookupType4SubTable.cs | 167 + .../GPos/LookupType5SubTable.cs | 182 + .../GPos/LookupType6SubTable.cs | 203 + .../GPos/LookupType7SubTable.cs | 319 + .../GPos/LookupType8SubTable.cs | 375 + .../GPos/LookupType9SubTable.cs | 89 + .../GPos/Mark2ArrayTable.cs | 43 + .../AdvancedTypographic/GPos/Mark2Record.cs | 49 + .../GPos/MarkArrayTable.cs | 46 + .../AdvancedTypographic/GPos/MarkRecord.cs | 48 + .../GPos/NotImplementedSubTable.cs | 29 + .../GPos/PairValueRecord.cs | 49 + .../AdvancedTypographic/GPos/ValueFormat.cs | 86 + .../AdvancedTypographic/GPos/ValueRecord.cs | 193 + .../Tables/AdvancedTypographic/GPosTable.cs | 559 + .../GSub/LookupListTable.cs | 275 + .../GSub/LookupType1SubTable.cs | 210 + .../GSub/LookupType2SubTable.cs | 165 + .../GSub/LookupType3SubTable.cs | 167 + .../GSub/LookupType4SubTable.cs | 346 + .../GSub/LookupType5SubTable.cs | 348 + .../GSub/LookupType6SubTable.cs | 408 + .../GSub/LookupType7SubTable.cs | 92 + .../GSub/LookupType8SubTable.cs | 204 + .../GSub/NotImplementedSubTable.cs | 29 + .../Tables/AdvancedTypographic/GSubTable.cs | 467 + .../AdvancedTypographic/GlyphClassDef.cs | 31 + .../GlyphDefinitionTable.cs | 225 + .../AdvancedTypographic/KnownFeatureTags.cs | 820 + .../AdvancedTypographic/LigatureCaretList.cs | 65 + .../AdvancedTypographic/LigatureGlyph.cs | 36 + .../Tables/AdvancedTypographic/LookupFlags.cs | 50 + .../AdvancedTypographic/MarkGlyphSetsTable.cs | 84 + .../AdvancedTypographic/MarkZeroingMode.cs | 25 + .../Tables/AdvancedTypographic/ScriptList.cs | 265 + .../SequenceLookupRecord.cs | 61 + .../SequenceRuleSetTable.cs | 60 + .../AdvancedTypographic/SequenceRuleTable.cs | 62 + .../Shapers/ArabicShaper.cs | 212 + .../AdvancedTypographic/Shapers/BaseShaper.cs | 101 + .../Shapers/DefaultShaper.cs | 338 + .../Shapers/HangulShaper.cs | 527 + .../Shapers/HebrewShaper.cs | 307 + .../Shapers/IndicShaper.cs | 1495 + .../Shapers/MyanmarShaper.cs | 507 + .../Shapers/ShaperFactory.cs | 133 + .../Shapers/ShapingStage.cs | 68 + .../AdvancedTypographic/Shapers/ThaiShaper.cs | 500 + .../Shapers/UniversalShaper.cs | 489 + .../SkippingGlyphIterator.cs | 155 + .../AdvancedTypographic/TableLoadingUtils.cs | 327 + .../Tables/AdvancedTypographic/Tag.cs | 112 + .../Tables/AdvancedTypographic/TagEntry.cs | 35 + .../UnicodeScriptTagMap.cs | 220 + .../Variations/AVarTable.cs | 99 + .../Variations/AxisValueMapRecord.cs | 54 + .../Variations/CVarTable.cs | 213 + .../Variations/DeltaSet.cs | 63 + .../Variations/DeltaSetIndexMap.cs | 105 + .../Variations/FVarTable.cs | 128 + .../Variations/GVarTable.cs | 196 + .../Variations/GlyphVariationData.cs | 325 + .../Variations/GlyphVariationProcessor.cs | 1114 + .../Variations/HVarTable.cs | 125 + .../Variations/InstanceRecord.cs | 80 + .../Variations/ItemVariationData.cs | 113 + .../Variations/ItemVariationStore.cs | 94 + .../Variations/MVarTable.cs | 159 + .../AdvancedTypographic/Variations/MVarTag.cs | 115 + .../Variations/MetricValueRecord.cs | 38 + .../Variations/RegionAxisCoordinates.cs | 32 + .../Variations/SegmentMapRecord.cs | 48 + .../Variations/TupleVariation.cs | 170 + .../Variations/VVarTable.cs | 138 + .../Variations/VariationAxis.cs | 38 + .../Variations/VariationAxisRecord.cs | 101 + .../Variations/VariationRegionList.cs | 112 + SixLabors.Fonts/Tables/Cff/5176.CFF.pdf | Bin 0 -> 978852 bytes SixLabors.Fonts/Tables/Cff/5177.Type2.pdf | Bin 0 -> 141228 bytes SixLabors.Fonts/Tables/Cff/Cff1Parser.cs | 692 + SixLabors.Fonts/Tables/Cff/Cff1Table.cs | 88 + SixLabors.Fonts/Tables/Cff/Cff2Font.cs | 27 + SixLabors.Fonts/Tables/Cff/Cff2Parser.cs | 251 + SixLabors.Fonts/Tables/Cff/Cff2Table.cs | 89 + SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs | 237 + SixLabors.Fonts/Tables/Cff/CffDataDicEntry.cs | 58 + .../Tables/Cff/CffEvaluationEngine.cs | 865 + SixLabors.Fonts/Tables/Cff/CffFont.cs | 38 + SixLabors.Fonts/Tables/Cff/CffGlyphData.cs | 135 + SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs | 177 + SixLabors.Fonts/Tables/Cff/CffIndexOffset.cs | 36 + SixLabors.Fonts/Tables/Cff/CffOperand.cs | 47 + SixLabors.Fonts/Tables/Cff/CffOperator.cs | 155 + SixLabors.Fonts/Tables/Cff/CffParserBase.cs | 507 + .../Tables/Cff/CffPrivateDictionary.cs | 39 + .../Tables/Cff/CffStandardStrings.cs | 423 + .../Tables/Cff/CffTopDictionary.cs | 72 + SixLabors.Fonts/Tables/Cff/CidFontInfo.cs | 63 + .../Tables/Cff/CompactFontTables.cs | 152 + SixLabors.Fonts/Tables/Cff/FDRange.cs | 45 + SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs | 100 + SixLabors.Fonts/Tables/Cff/FontDict.cs | 49 + SixLabors.Fonts/Tables/Cff/ICffTable.cs | 36 + SixLabors.Fonts/Tables/Cff/OperandKind.cs | 20 + .../Tables/Cff/OperatorOperandKind.cs | 47 + SixLabors.Fonts/Tables/Cff/RefStack{T}.cs | 172 + .../Tables/Cff/SimpleBinaryReader.cs | 80 + .../Tables/Cff/TransformingGlyphRenderer.cs | 151 + SixLabors.Fonts/Tables/Cff/Type2Operator1.cs | 171 + SixLabors.Fonts/Tables/Cff/Type2Operator2.cs | 201 + .../Tables/General/CMap/CMapSubTable.cs | 72 + .../Tables/General/CMap/EncodingRecord.cs | 56 + .../Tables/General/CMap/Format0SubTable.cs | 96 + .../Tables/General/CMap/Format12SubTable.cs | 174 + .../Tables/General/CMap/Format14SubTable.cs | 229 + .../Tables/General/CMap/Format4SubTable.cs | 280 + SixLabors.Fonts/Tables/General/CMapTable.cs | 221 + .../Tables/General/Colr/Affine2x3.cs | 61 + .../Tables/General/Colr/BaseGlyphList.cs | 63 + .../General/Colr/BaseGlyphPaintRecord.cs | 32 + .../Tables/General/Colr/BaseGlyphRecord.cs | 39 + .../Tables/General/Colr/ClipBox.cs | 22 + .../Tables/General/Colr/ClipBoxFormat1.cs | 52 + .../Tables/General/Colr/ClipBoxFormat2.cs | 72 + .../Tables/General/Colr/ClipList.cs | 155 + .../Tables/General/Colr/ClipRecord.cs | 39 + .../Tables/General/Colr/ColorLine.cs | 56 + .../Tables/General/Colr/ColorStop.cs | 50 + .../Tables/General/Colr/ColrCompositeMode.cs | 152 + .../General/Colr/ColrGlyphSourceBase.cs | 447 + .../Tables/General/Colr/ColrTable.cs | 1611 + .../Tables/General/Colr/ColrV0GlyphSource.cs | 85 + .../Tables/General/Colr/ColrV1GlyphSource.cs | 90 + SixLabors.Fonts/Tables/General/Colr/Extend.cs | 27 + .../Tables/General/Colr/LayerList.cs | 62 + .../Tables/General/Colr/LayerRecord.cs | 34 + SixLabors.Fonts/Tables/General/Colr/Paint.cs | 700 + .../Tables/General/Colr/VarAffine2x3.cs | 68 + .../Tables/General/Colr/VarColorIndex.cs | 51 + .../Tables/General/Colr/VarColorLine.cs | 57 + .../Tables/General/Colr/VarColorStop.cs | 58 + SixLabors.Fonts/Tables/General/CpalTable.cs | 121 + SixLabors.Fonts/Tables/General/HeadTable.cs | 350 + .../Tables/General/HorizontalHeadTable.cs | 212 + .../Tables/General/HorizontalMetricsTable.cs | 122 + .../Tables/General/Kern/Format0SubTable.cs | 53 + .../Tables/General/Kern/KerningCoverage.cs | 85 + .../Tables/General/Kern/KerningPair.cs | 83 + .../Tables/General/Kern/KerningSubTable.cs | 111 + .../Tables/General/Kern/KerningTable.cs | 144 + .../Tables/General/MaximumProfileTable.cs | 213 + .../Tables/General/Name/NameRecord.cs | 81 + .../Tables/General/Name/NameTable.cs | 195 + SixLabors.Fonts/Tables/General/OS2Table.cs | 627 + .../Tables/General/Post/PostNameRecord.cs | 32 + .../Tables/General/Post/PostTable.cs | 273 + .../General/Svg/SvgDocumentIndexEntry.cs | 47 + .../Tables/General/Svg/SvgGlyphSource.cs | 1735 + .../Tables/General/Svg/SvgTable.cs | 287 + .../Tables/General/VerticalHeadTable.cs | 238 + .../Tables/General/VerticalMetricsTable.cs | 127 + SixLabors.Fonts/Tables/IFontTables.cs | 130 + SixLabors.Fonts/Tables/Table.cs | 11 + SixLabors.Fonts/Tables/TableFormat.cs | 25 + SixLabors.Fonts/Tables/TableHeader.cs | 72 + SixLabors.Fonts/Tables/TableLoader.cs | 155 + .../Tables/TripleEncodingRecord.cs | 75 + SixLabors.Fonts/Tables/TripleEncodingTable.cs | 269 + .../TrueType/Glyphs/CompositeComponent.cs | 41 + .../TrueType/Glyphs/CompositeGlyphFlags.cs | 101 + .../TrueType/Glyphs/CompositeGlyphLoader.cs | 201 + .../Tables/TrueType/Glyphs/ControlPoint.cs | 69 + .../TrueType/Glyphs/EmptyGlyphLoader.cs | 36 + .../Tables/TrueType/Glyphs/GlyphLoader.cs | 36 + .../Tables/TrueType/Glyphs/GlyphTable.cs | 115 + .../Tables/TrueType/Glyphs/GlyphVector.cs | 166 + .../TrueType/Glyphs/SimpleGlyphLoader.cs | 226 + .../Tables/TrueType/Hinting/CvtTable.cs | 68 + .../Tables/TrueType/Hinting/FpgmTable.cs | 65 + .../Tables/TrueType/Hinting/PrepTable.cs | 65 + .../Hinting/TrueTypeInterpreter.OpCodes.cs | 481 + .../TrueType/Hinting/TrueTypeInterpreter.cs | 3532 ++ .../Tables/TrueType/IndexLocationTable.cs | 98 + .../Tables/TrueType/TrueTypeFontTables.cs | 190 + ...TrueTypeGlyphMetrics.CompatibilityLists.cs | 119 + .../Tables/TrueType/TrueTypeGlyphMetrics.cs | 260 + SixLabors.Fonts/Tables/TtcHeader.cs | 116 + SixLabors.Fonts/Tables/UnknownTable.cs | 23 + .../Tables/Woff/Woff2GlyphLoader.cs | 40 + .../Tables/Woff/Woff2TableHeader.cs | 24 + SixLabors.Fonts/Tables/Woff/Woff2Utils.cs | 721 + .../Tables/Woff/WoffTableHeader.cs | 87 + SixLabors.Fonts/TextAlignment.cs | 27 + SixLabors.Fonts/TextAttributes.cs | 28 + SixLabors.Fonts/TextBidiMode.cs | 20 + SixLabors.Fonts/TextBlock.Visitors.cs | 815 + SixLabors.Fonts/TextBlock.cs | 584 + SixLabors.Fonts/TextBox.cs | 85 + SixLabors.Fonts/TextDecorations.cs | 33 + SixLabors.Fonts/TextDirection.cs | 25 + SixLabors.Fonts/TextEllipsis.cs | 25 + SixLabors.Fonts/TextHit.cs | 50 + SixLabors.Fonts/TextHyphenation.cs | 25 + SixLabors.Fonts/TextInteraction.cs | 1446 + SixLabors.Fonts/TextInteractionMode.cs | 20 + SixLabors.Fonts/TextJustification.cs | 27 + SixLabors.Fonts/TextLayout.LineBreaking.cs | 673 + SixLabors.Fonts/TextLayout.Visitors.cs | 35 + SixLabors.Fonts/TextLayout.cs | 1530 + SixLabors.Fonts/TextLine.cs | 1097 + SixLabors.Fonts/TextLineBreakEnumerator.cs | 341 + SixLabors.Fonts/TextMeasurer.cs | 200 + SixLabors.Fonts/TextMetrics.cs | 229 + SixLabors.Fonts/TextOptions.cs | 249 + SixLabors.Fonts/TextPlaceholder.cs | 49 + SixLabors.Fonts/TextPlaceholderAlignment.cs | 40 + SixLabors.Fonts/TextRun.cs | 104 + SixLabors.Fonts/Unicode/ArabicJoiningClass.cs | 78 + SixLabors.Fonts/Unicode/ArabicJoiningGroup.cs | 545 + SixLabors.Fonts/Unicode/ArabicJoiningType.cs | 45 + SixLabors.Fonts/Unicode/BidiAlgorithm.cs | 1599 + SixLabors.Fonts/Unicode/BidiCharacterType.cs | 141 + SixLabors.Fonts/Unicode/BidiClass.cs | 56 + SixLabors.Fonts/Unicode/BidiData.cs | 178 + .../Unicode/BidiDictionary{T1,T2}.cs | 40 + .../Unicode/BidiPairedBracketType.cs | 30 + SixLabors.Fonts/Unicode/BidiRun.cs | 77 + SixLabors.Fonts/Unicode/CodePoint.cs | 822 + .../Unicode/EastAsianWidthClass.cs | 81 + SixLabors.Fonts/Unicode/EmojiProperties.cs | 58 + SixLabors.Fonts/Unicode/GraphemeCluster.cs | 73 + .../Unicode/GraphemeClusterClass.cs | 128 + .../Unicode/GraphemeClusterFlags.cs | 59 + .../Unicode/IndicConjunctBreakClass.cs | 47 + .../Unicode/IndicPositionalCategory.cs | 102 + .../Unicode/IndicSyllabicCategory.cs | 204 + SixLabors.Fonts/Unicode/LineBreak.cs | 54 + SixLabors.Fonts/Unicode/LineBreakClass.cs | 256 + .../Unicode/LineBreakEnumerator.cs | 2089 + SixLabors.Fonts/Unicode/MemoryExtensions.cs | 254 + SixLabors.Fonts/Unicode/README.md | 9 + .../Resources/ArabicShapingTrie.Generated.cs | 160 + .../Resources/BidiMirrorTrie.Generated.cs | 156 + .../Unicode/Resources/BidiTrie.Generated.cs | 538 + .../Resources/EastAsianWidthTrie.Generated.cs | 329 + .../Unicode/Resources/EmojiTrie.Generated.cs | 226 + .../Resources/GraphemeTrie.Generated.cs | 459 + .../IndicConjunctBreakTrie.Generated.cs | 365 + .../IndicPositionalCategoryTrie.Generated.cs | 271 + .../Resources/IndicShapingData.Generated.cs | 726 + .../Unicode/Resources/IndicShapingData.cs | 462 + .../Resources/IndicShapingTrie.Generated.cs | 617 + .../IndicSyllabicCategoryTrie.Generated.cs | 363 + .../Resources/LineBreakTrie.Generated.cs | 786 + .../Resources/MyanmarShapingData.Generated.cs | 219 + .../Unicode/Resources/ScriptTrie.Generated.cs | 819 + .../UnicodeCategoryTrie.Generated.cs | 894 + .../UniversalShapingData.Generated.cs | 298 + .../UniversalShapingTrie.Generated.cs | 534 + .../VerticalOrientationTrie.Generated.cs | 459 + .../Resources/WordBreakTrie.Generated.cs | 603 + SixLabors.Fonts/Unicode/ScriptClass.cs | 901 + .../Unicode/SpanCodePointEnumerator.cs | 50 + .../Unicode/SpanGraphemeEnumerator.cs | 663 + SixLabors.Fonts/Unicode/SpanWordEnumerator.cs | 375 + .../DeterministicFiniteAutomata.cs | 96 + .../Unicode/StateAutomation/INode.cs | 447 + .../Unicode/StateAutomation/State.cs | 36 + .../Unicode/StateAutomation/StateMachine.cs | 155 + .../Unicode/StateAutomation/SymbolTable.cs | 81 + .../Unicode/TerminalAmbiguousWidth.cs | 20 + .../Unicode/TerminalControlCharacterWidth.cs | 25 + SixLabors.Fonts/Unicode/TerminalEmojiWidth.cs | 20 + .../Unicode/TerminalWidthOptions.cs | 30 + SixLabors.Fonts/Unicode/UnicodeData.cs | 109 + SixLabors.Fonts/Unicode/UnicodeTrie.cs | 174 + SixLabors.Fonts/Unicode/UnicodeTrieBuilder.cs | 1205 + SixLabors.Fonts/Unicode/UnicodeUtility.cs | 549 + .../Unicode/VerticalOrientationType.cs | 36 + SixLabors.Fonts/Unicode/WordBreakClass.cs | 106 + SixLabors.Fonts/Unicode/WordSegment.cs | 57 + .../Utilities/EncodingIDExtensions.cs | 30 + SixLabors.Fonts/Utilities/StringLoader.cs | 36 + SixLabors.Fonts/VerticalAlignment.cs | 30 + SixLabors.Fonts/VerticalMetrics.cs | 33 + SixLabors.Fonts/WellKnownIds/EncodingIDs.cs | 45 + SixLabors.Fonts/WellKnownIds/KnownNameIds.cs | 121 + SixLabors.Fonts/WellKnownIds/PlatformIDs.cs | 35 + SixLabors.Fonts/WordBreaking.cs | 33 + SixLabors.Fonts/WordMetrics.cs | 73 + SixLabors.Fonts/WordSegmentRun.cs | 49 + SixLabors.ImageSharp.props | 11 + ThrowHelper.cs | 129 + sixlabors.imagesharp.128.png | Bin 0 -> 4747 bytes 1986 files changed, 370602 insertions(+) create mode 100644 .gitignore create mode 100644 DebugGuard.cs create mode 100644 Guard.Numeric.cs create mode 100644 Guard.Numeric.tt.bak create mode 100644 Guard.cs create mode 100644 ImageSharp.Drawing/ArcLineSegment.cs create mode 100644 ImageSharp.Drawing/BooleanOperation.cs create mode 100644 ImageSharp.Drawing/ClipPathExtensions.cs create mode 100644 ImageSharp.Drawing/ComplexPolygon.cs create mode 100644 ImageSharp.Drawing/CubicBezierLineSegment.cs create mode 100644 ImageSharp.Drawing/EllipsePolygon.cs create mode 100644 ImageSharp.Drawing/EmptyPath.cs create mode 100644 ImageSharp.Drawing/FlattenedPointBuilder.cs create mode 100644 ImageSharp.Drawing/Helpers/ArrayExtensions.cs create mode 100644 ImageSharp.Drawing/Helpers/MatrixUtilities.cs create mode 100644 ImageSharp.Drawing/Helpers/PolygonUtilities.cs create mode 100644 ImageSharp.Drawing/IInternalPathOwner.cs create mode 100644 ImageSharp.Drawing/ILineSegment.cs create mode 100644 ImageSharp.Drawing/IPath.cs create mode 100644 ImageSharp.Drawing/IPathCollection.cs create mode 100644 ImageSharp.Drawing/IPathInternals.cs create mode 100644 ImageSharp.Drawing/ISimplePath.cs create mode 100644 ImageSharp.Drawing/ImageSharp.Drawing.csproj create mode 100644 ImageSharp.Drawing/InternalPath.cs create mode 100644 ImageSharp.Drawing/IntersectionRule.cs create mode 100644 ImageSharp.Drawing/LineCap.cs create mode 100644 ImageSharp.Drawing/LineJoin.cs create mode 100644 ImageSharp.Drawing/LinearContour.cs create mode 100644 ImageSharp.Drawing/LinearGeometry.cs create mode 100644 ImageSharp.Drawing/LinearGeometryCache.cs create mode 100644 ImageSharp.Drawing/LinearGeometryInfo.cs create mode 100644 ImageSharp.Drawing/LinearLineSegment.cs create mode 100644 ImageSharp.Drawing/LinearSegment.cs create mode 100644 ImageSharp.Drawing/OutlinePathExtensions.cs create mode 100644 ImageSharp.Drawing/Path.cs create mode 100644 ImageSharp.Drawing/PathBuilder.cs create mode 100644 ImageSharp.Drawing/PathCollection.cs create mode 100644 ImageSharp.Drawing/PathExtensions.Internal.cs create mode 100644 ImageSharp.Drawing/PathExtensions.cs create mode 100644 ImageSharp.Drawing/PathTypes.cs create mode 100644 ImageSharp.Drawing/PiePolygon.cs create mode 100644 ImageSharp.Drawing/PointOrientation.cs create mode 100644 ImageSharp.Drawing/Polygon.cs create mode 100644 ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs create mode 100644 ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs create mode 100644 ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md create mode 100644 ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/FlushScene.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/NativeSurface.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/ParallelExecutionHelper.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs create mode 100644 ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs create mode 100644 ImageSharp.Drawing/Processing/Brush.cs create mode 100644 ImageSharp.Drawing/Processing/BrushRenderer.cs create mode 100644 ImageSharp.Drawing/Processing/BrushWorkspace.cs create mode 100644 ImageSharp.Drawing/Processing/Brushes.Hatch.cs create mode 100644 ImageSharp.Drawing/Processing/Brushes.cs create mode 100644 ImageSharp.Drawing/Processing/ColorStop.cs create mode 100644 ImageSharp.Drawing/Processing/DRAWING_CANVAS.md create mode 100644 ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingCanvas.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingCanvasState.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingHelpers.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingOperation.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingOptions.cs create mode 100644 ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs create mode 100644 ImageSharp.Drawing/Processing/EllipticGradientBrush.cs create mode 100644 ImageSharp.Drawing/Processing/GradientBrush.cs create mode 100644 ImageSharp.Drawing/Processing/GradientRepetitionMode.cs create mode 100644 ImageSharp.Drawing/Processing/ImageBrush.cs create mode 100644 ImageSharp.Drawing/Processing/LinearGradientBrush.cs create mode 100644 ImageSharp.Drawing/Processing/PaintExtensions.cs create mode 100644 ImageSharp.Drawing/Processing/PaintProcessor.cs create mode 100644 ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs create mode 100644 ImageSharp.Drawing/Processing/PathGradientBrush.cs create mode 100644 ImageSharp.Drawing/Processing/PatternBrush.cs create mode 100644 ImageSharp.Drawing/Processing/PatternPen.cs create mode 100644 ImageSharp.Drawing/Processing/Pen.cs create mode 100644 ImageSharp.Drawing/Processing/PenOptions.cs create mode 100644 ImageSharp.Drawing/Processing/Pens.cs create mode 100644 ImageSharp.Drawing/Processing/RadialGradientBrush.cs create mode 100644 ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs create mode 100644 ImageSharp.Drawing/Processing/RecolorBrush.cs create mode 100644 ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs create mode 100644 ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs create mode 100644 ImageSharp.Drawing/Processing/RichTextOptions.cs create mode 100644 ImageSharp.Drawing/Processing/RichTextRun.cs create mode 100644 ImageSharp.Drawing/Processing/ShapeOptions.cs create mode 100644 ImageSharp.Drawing/Processing/SolidBrush.cs create mode 100644 ImageSharp.Drawing/Processing/SolidPen.cs create mode 100644 ImageSharp.Drawing/Processing/StrokeOptions.cs create mode 100644 ImageSharp.Drawing/Processing/SweepGradientBrush.cs create mode 100644 ImageSharp.Drawing/RectanglePolygon.cs create mode 100644 ImageSharp.Drawing/RegularPolygon.cs create mode 100644 ImageSharp.Drawing/RoundedRectanglePolygon.cs create mode 100644 ImageSharp.Drawing/SegmentEnumerator.cs create mode 100644 ImageSharp.Drawing/SegmentInfo.cs create mode 100644 ImageSharp.Drawing/SplitPathExtensions.cs create mode 100644 ImageSharp.Drawing/StarPolygon.cs create mode 100644 ImageSharp.Drawing/Text/BaseGlyphBuilder.cs create mode 100644 ImageSharp.Drawing/Text/GlyphBuilder.cs create mode 100644 ImageSharp.Drawing/Text/GlyphLayerInfo.cs create mode 100644 ImageSharp.Drawing/Text/GlyphLayerKind.cs create mode 100644 ImageSharp.Drawing/Text/GlyphPathCollection.cs create mode 100644 ImageSharp.Drawing/Text/PathGlyphBuilder.cs create mode 100644 ImageSharp.Drawing/Text/TextBuilder.cs create mode 100644 ImageSharp.Drawing/Text/TextUtilities.cs create mode 100644 ImageSharp.sln create mode 100644 ImageSharp/Advanced/AdvancedImageExtensions.cs create mode 100644 ImageSharp/Advanced/AotCompilerTools.cs create mode 100644 ImageSharp/Advanced/IConfigurationProvider.cs create mode 100644 ImageSharp/Advanced/IImageFrameVisitor.cs create mode 100644 ImageSharp/Advanced/IImageVisitor.cs create mode 100644 ImageSharp/Advanced/IPixelSource.cs create mode 100644 ImageSharp/Advanced/IRowIntervalOperation.cs create mode 100644 ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs create mode 100644 ImageSharp/Advanced/IRowOperation.cs create mode 100644 ImageSharp/Advanced/IRowOperation{TBuffer}.cs create mode 100644 ImageSharp/Advanced/ParallelExecutionSettings.cs create mode 100644 ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs create mode 100644 ImageSharp/Advanced/ParallelRowIterator.cs create mode 100644 ImageSharp/Advanced/PreserveAttribute.cs create mode 100644 ImageSharp/Color/Color.NamedColors.cs create mode 100644 ImageSharp/Color/Color.WebSafePalette.cs create mode 100644 ImageSharp/Color/Color.WernerPalette.cs create mode 100644 ImageSharp/Color/Color.cs create mode 100644 ImageSharp/Color/ColorHexFormat.cs create mode 100644 ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs create mode 100644 ImageSharp/ColorProfiles/CieConstants.cs create mode 100644 ImageSharp/ColorProfiles/CieLab.cs create mode 100644 ImageSharp/ColorProfiles/CieLch.cs create mode 100644 ImageSharp/ColorProfiles/CieLchuv.cs create mode 100644 ImageSharp/ColorProfiles/CieLuv.cs create mode 100644 ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs create mode 100644 ImageSharp/ColorProfiles/CieXyy.cs create mode 100644 ImageSharp/ColorProfiles/CieXyz.cs create mode 100644 ImageSharp/ColorProfiles/Cmyk.cs create mode 100644 ImageSharp/ColorProfiles/ColorConversionOptions.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverter.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs create mode 100644 ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs create mode 100644 ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs create mode 100644 ImageSharp/ColorProfiles/Companding/GammaCompanding.cs create mode 100644 ImageSharp/ColorProfiles/Companding/LCompanding.cs create mode 100644 ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs create mode 100644 ImageSharp/ColorProfiles/Companding/Rec709Companding.cs create mode 100644 ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs create mode 100644 ImageSharp/ColorProfiles/Hsl.cs create mode 100644 ImageSharp/ColorProfiles/Hsv.cs create mode 100644 ImageSharp/ColorProfiles/HunterLab.cs create mode 100644 ImageSharp/ColorProfiles/IColorProfile.cs create mode 100644 ImageSharp/ColorProfiles/IProfileConnectingSpace.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.CalculationType.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/LutCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/MatrixCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/ParametricCurveCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs create mode 100644 ImageSharp/ColorProfiles/Icc/CompactSrgbV4Profile.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccConverterBase.Checks.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccConverterBase.ConversionMethod.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccConverterbase.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs create mode 100644 ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs create mode 100644 ImageSharp/ColorProfiles/KnownChromaticAdaptationMatrices.cs create mode 100644 ImageSharp/ColorProfiles/KnownIlluminants.cs create mode 100644 ImageSharp/ColorProfiles/KnownRgbWorkingSpaces.cs create mode 100644 ImageSharp/ColorProfiles/KnownYCbCrMatrices.cs create mode 100644 ImageSharp/ColorProfiles/Lms.cs create mode 100644 ImageSharp/ColorProfiles/Rgb.cs create mode 100644 ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs create mode 100644 ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs create mode 100644 ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs create mode 100644 ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs create mode 100644 ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs create mode 100644 ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs create mode 100644 ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs create mode 100644 ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs create mode 100644 ImageSharp/ColorProfiles/Y.cs create mode 100644 ImageSharp/ColorProfiles/YCbCr.cs create mode 100644 ImageSharp/ColorProfiles/YCbCrTransform.cs create mode 100644 ImageSharp/ColorProfiles/YccK.cs create mode 100644 ImageSharp/Common/ByteOrder.cs create mode 100644 ImageSharp/Common/Constants.cs create mode 100644 ImageSharp/Common/Exceptions/ImageFormatException.cs create mode 100644 ImageSharp/Common/Exceptions/ImageProcessingException.cs create mode 100644 ImageSharp/Common/Exceptions/InvalidImageContentException.cs create mode 100644 ImageSharp/Common/Exceptions/UnknownImageFormatException.cs create mode 100644 ImageSharp/Common/Extensions/ConfigurationExtensions.cs create mode 100644 ImageSharp/Common/Extensions/EnumerableExtensions.cs create mode 100644 ImageSharp/Common/Extensions/StreamExtensions.cs create mode 100644 ImageSharp/Common/Extensions/Vector4Extensions.cs create mode 100644 ImageSharp/Common/Helpers/ColorNumerics.cs create mode 100644 ImageSharp/Common/Helpers/DebugGuard.cs create mode 100644 ImageSharp/Common/Helpers/EnumUtils.cs create mode 100644 ImageSharp/Common/Helpers/ExifResolutionValues.cs create mode 100644 ImageSharp/Common/Helpers/Guard.cs create mode 100644 ImageSharp/Common/Helpers/HexConverter.cs create mode 100644 ImageSharp/Common/Helpers/InliningOptions.cs create mode 100644 ImageSharp/Common/Helpers/Numerics.cs create mode 100644 ImageSharp/Common/Helpers/RuntimeUtility.cs create mode 100644 ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs create mode 100644 ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs create mode 100644 ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs create mode 100644 ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs create mode 100644 ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs create mode 100644 ImageSharp/Common/Helpers/SimdUtils.Convert.cs create mode 100644 ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs create mode 100644 ImageSharp/Common/Helpers/SimdUtils.Pack.cs create mode 100644 ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs create mode 100644 ImageSharp/Common/Helpers/SimdUtils.cs create mode 100644 ImageSharp/Common/Helpers/TestHelpers.cs create mode 100644 ImageSharp/Common/Helpers/TolerantMath.cs create mode 100644 ImageSharp/Common/Helpers/UnitConverter.cs create mode 100644 ImageSharp/Common/Helpers/Vector128Utilities.cs create mode 100644 ImageSharp/Common/Helpers/Vector256Utilities.cs create mode 100644 ImageSharp/Common/Helpers/Vector512Utilities.cs create mode 100644 ImageSharp/Common/InlineArray.cs create mode 100644 ImageSharp/Common/InlineArray.tt create mode 100644 ImageSharp/Compression/Zlib/Adler32.cs create mode 100644 ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs create mode 100644 ImageSharp/Compression/Zlib/DeflateThrowHelper.cs create mode 100644 ImageSharp/Compression/Zlib/Deflater.cs create mode 100644 ImageSharp/Compression/Zlib/DeflaterConstants.cs create mode 100644 ImageSharp/Compression/Zlib/DeflaterEngine.cs create mode 100644 ImageSharp/Compression/Zlib/DeflaterHuffman.cs create mode 100644 ImageSharp/Compression/Zlib/DeflaterOutputStream.cs create mode 100644 ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs create mode 100644 ImageSharp/Compression/Zlib/README.md create mode 100644 ImageSharp/Compression/Zlib/ZlibDeflateStream.cs create mode 100644 ImageSharp/Compression/Zlib/ZlibInflateStream.cs create mode 100644 ImageSharp/Configuration.cs create mode 100644 ImageSharp/Diagnostics/MemoryDiagnostics.cs create mode 100644 ImageSharp/Formats/AlphaAwareImageEncoder.cs create mode 100644 ImageSharp/Formats/AnimationUtilities.cs create mode 100644 ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs create mode 100644 ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs create mode 100644 ImageSharp/Formats/Bmp/BmpColorSpace.cs create mode 100644 ImageSharp/Formats/Bmp/BmpCompression.cs create mode 100644 ImageSharp/Formats/Bmp/BmpConfigurationModule.cs create mode 100644 ImageSharp/Formats/Bmp/BmpConstants.cs create mode 100644 ImageSharp/Formats/Bmp/BmpDecoder.cs create mode 100644 ImageSharp/Formats/Bmp/BmpDecoderCore.cs create mode 100644 ImageSharp/Formats/Bmp/BmpDecoderOptions.cs create mode 100644 ImageSharp/Formats/Bmp/BmpEncoder.cs create mode 100644 ImageSharp/Formats/Bmp/BmpEncoderCore.cs create mode 100644 ImageSharp/Formats/Bmp/BmpFileHeader.cs create mode 100644 ImageSharp/Formats/Bmp/BmpFileMarkerType.cs create mode 100644 ImageSharp/Formats/Bmp/BmpFormat.cs create mode 100644 ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Bmp/BmpInfoHeader.cs create mode 100644 ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs create mode 100644 ImageSharp/Formats/Bmp/BmpMetadata.cs create mode 100644 ImageSharp/Formats/Bmp/BmpRenderingIntent.cs create mode 100644 ImageSharp/Formats/Bmp/BmpThrowHelper.cs create mode 100644 ImageSharp/Formats/Bmp/README.md create mode 100644 ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs create mode 100644 ImageSharp/Formats/ColorProfileHandling.cs create mode 100644 ImageSharp/Formats/Cur/CurConfigurationModule.cs create mode 100644 ImageSharp/Formats/Cur/CurConstants.cs create mode 100644 ImageSharp/Formats/Cur/CurDecoder.cs create mode 100644 ImageSharp/Formats/Cur/CurDecoderCore.cs create mode 100644 ImageSharp/Formats/Cur/CurEncoder.cs create mode 100644 ImageSharp/Formats/Cur/CurEncoderCore.cs create mode 100644 ImageSharp/Formats/Cur/CurFormat.cs create mode 100644 ImageSharp/Formats/Cur/CurFrameMetadata.cs create mode 100644 ImageSharp/Formats/Cur/CurMetadata.cs create mode 100644 ImageSharp/Formats/DecoderOptions.cs create mode 100644 ImageSharp/Formats/EncodingType.cs create mode 100644 ImageSharp/Formats/EncodingUtilities.cs create mode 100644 ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs create mode 100644 ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs create mode 100644 ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs create mode 100644 ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs create mode 100644 ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs create mode 100644 ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs create mode 100644 ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs create mode 100644 ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs create mode 100644 ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs create mode 100644 ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs create mode 100644 ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs create mode 100644 ImageSharp/Formats/Exr/Constants/ExrCompression.cs create mode 100644 ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs create mode 100644 ImageSharp/Formats/Exr/Constants/ExrImageType.cs create mode 100644 ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs create mode 100644 ImageSharp/Formats/Exr/Constants/ExrPixelType.cs create mode 100644 ImageSharp/Formats/Exr/ExrAttribute.cs create mode 100644 ImageSharp/Formats/Exr/ExrBaseCompressor.cs create mode 100644 ImageSharp/Formats/Exr/ExrBox2i.cs create mode 100644 ImageSharp/Formats/Exr/ExrChannelInfo.cs create mode 100644 ImageSharp/Formats/Exr/ExrConfigurationModule.cs create mode 100644 ImageSharp/Formats/Exr/ExrConstants.cs create mode 100644 ImageSharp/Formats/Exr/ExrDecoder.cs create mode 100644 ImageSharp/Formats/Exr/ExrDecoderCore.cs create mode 100644 ImageSharp/Formats/Exr/ExrDecoderOptions.cs create mode 100644 ImageSharp/Formats/Exr/ExrEncoder.cs create mode 100644 ImageSharp/Formats/Exr/ExrEncoderCore.cs create mode 100644 ImageSharp/Formats/Exr/ExrFormat.cs create mode 100644 ImageSharp/Formats/Exr/ExrHeaderAttributes.cs create mode 100644 ImageSharp/Formats/Exr/ExrImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Exr/ExrMetadata.cs create mode 100644 ImageSharp/Formats/Exr/ExrThrowHelper.cs create mode 100644 ImageSharp/Formats/Exr/ExrUtils.cs create mode 100644 ImageSharp/Formats/Exr/README.md create mode 100644 ImageSharp/Formats/FormatConnectingFrameMetadata.cs create mode 100644 ImageSharp/Formats/FormatConnectingMetadata.cs create mode 100644 ImageSharp/Formats/FrameBlendMode.cs create mode 100644 ImageSharp/Formats/FrameColorTableMode.cs create mode 100644 ImageSharp/Formats/FrameDisposalMode.cs create mode 100644 ImageSharp/Formats/Gif/GifConfigurationModule.cs create mode 100644 ImageSharp/Formats/Gif/GifConstants.cs create mode 100644 ImageSharp/Formats/Gif/GifDecoder.cs create mode 100644 ImageSharp/Formats/Gif/GifDecoderCore.cs create mode 100644 ImageSharp/Formats/Gif/GifEncoder.cs create mode 100644 ImageSharp/Formats/Gif/GifEncoderCore.cs create mode 100644 ImageSharp/Formats/Gif/GifFormat.cs create mode 100644 ImageSharp/Formats/Gif/GifFrameMetadata.cs create mode 100644 ImageSharp/Formats/Gif/GifImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Gif/GifMetadata.cs create mode 100644 ImageSharp/Formats/Gif/GifThrowHelper.cs create mode 100644 ImageSharp/Formats/Gif/LzwDecoder.cs create mode 100644 ImageSharp/Formats/Gif/LzwEncoder.cs create mode 100644 ImageSharp/Formats/Gif/README.md create mode 100644 ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs create mode 100644 ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs create mode 100644 ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs create mode 100644 ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs create mode 100644 ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs create mode 100644 ImageSharp/Formats/Gif/Sections/IGifExtension.cs create mode 100644 ImageSharp/Formats/Gif/spec-gif89a.txt create mode 100644 ImageSharp/Formats/IAnimatedImageEncoder.cs create mode 100644 ImageSharp/Formats/IFormatFrameMetadata.cs create mode 100644 ImageSharp/Formats/IFormatMetadata.cs create mode 100644 ImageSharp/Formats/IImageDecoder.cs create mode 100644 ImageSharp/Formats/IImageEncoder.cs create mode 100644 ImageSharp/Formats/IImageFormat.cs create mode 100644 ImageSharp/Formats/IImageFormatConfigurationModule.cs create mode 100644 ImageSharp/Formats/IImageFormatDetector.cs create mode 100644 ImageSharp/Formats/IQuantizingImageEncoder.cs create mode 100644 ImageSharp/Formats/ISpecializedDecoderOptions.cs create mode 100644 ImageSharp/Formats/ISpecializedImageDecoder{T}.cs create mode 100644 ImageSharp/Formats/Ico/IcoConfigurationModule.cs create mode 100644 ImageSharp/Formats/Ico/IcoConstants.cs create mode 100644 ImageSharp/Formats/Ico/IcoDecoder.cs create mode 100644 ImageSharp/Formats/Ico/IcoDecoderCore.cs create mode 100644 ImageSharp/Formats/Ico/IcoEncoder.cs create mode 100644 ImageSharp/Formats/Ico/IcoEncoderCore.cs create mode 100644 ImageSharp/Formats/Ico/IcoFormat.cs create mode 100644 ImageSharp/Formats/Ico/IcoFrameMetadata.cs create mode 100644 ImageSharp/Formats/Ico/IcoMetadata.cs create mode 100644 ImageSharp/Formats/Icon/IconDecoderCore.cs create mode 100644 ImageSharp/Formats/Icon/IconDir.cs create mode 100644 ImageSharp/Formats/Icon/IconDirEntry.cs create mode 100644 ImageSharp/Formats/Icon/IconEncoderCore.cs create mode 100644 ImageSharp/Formats/Icon/IconFileType.cs create mode 100644 ImageSharp/Formats/Icon/IconFrameCompression.cs create mode 100644 ImageSharp/Formats/Icon/IconImageFormatDetector.cs create mode 100644 ImageSharp/Formats/ImageDecoder.cs create mode 100644 ImageSharp/Formats/ImageDecoderCore.cs create mode 100644 ImageSharp/Formats/ImageEncoder.cs create mode 100644 ImageSharp/Formats/ImageFormatManager.cs create mode 100644 ImageSharp/Formats/Jpeg/5116.DCT_Filter.pdf create mode 100644 ImageSharp/Formats/Jpeg/Components/Block8x8.Intrinsic.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Block8x8.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Block8x8F.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ComponentType.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/JpegFileMarker.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/Component.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegComponentConfig.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegHuffmanTableConfig.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegQuantizationTableConfig.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/Quantization.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/RowOctet.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/SizeExtensions.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs create mode 100644 ImageSharp/Formats/Jpeg/Components/ZigZag.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegColorType.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegComData.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegConstants.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegDecoder.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegDecoderCore.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegDecoderOptions.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegDecoderResizeMode.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegEncoder.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegEncoderCore.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegFormat.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegMetadata.cs create mode 100644 ImageSharp/Formats/Jpeg/JpegThrowHelper.cs create mode 100644 ImageSharp/Formats/Jpeg/README.md create mode 100644 ImageSharp/Formats/Jpeg/itu-t81.pdf create mode 100644 ImageSharp/Formats/Pbm/BinaryDecoder.cs create mode 100644 ImageSharp/Formats/Pbm/BinaryEncoder.cs create mode 100644 ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs create mode 100644 ImageSharp/Formats/Pbm/PbmColorType.cs create mode 100644 ImageSharp/Formats/Pbm/PbmComponentType.cs create mode 100644 ImageSharp/Formats/Pbm/PbmConfigurationModule.cs create mode 100644 ImageSharp/Formats/Pbm/PbmConstants.cs create mode 100644 ImageSharp/Formats/Pbm/PbmDecoder.cs create mode 100644 ImageSharp/Formats/Pbm/PbmDecoderCore.cs create mode 100644 ImageSharp/Formats/Pbm/PbmEncoder.cs create mode 100644 ImageSharp/Formats/Pbm/PbmEncoderCore.cs create mode 100644 ImageSharp/Formats/Pbm/PbmEncoding.cs create mode 100644 ImageSharp/Formats/Pbm/PbmFormat.cs create mode 100644 ImageSharp/Formats/Pbm/PbmImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Pbm/PbmMetadata.cs create mode 100644 ImageSharp/Formats/Pbm/PlainDecoder.cs create mode 100644 ImageSharp/Formats/Pbm/PlainEncoder.cs create mode 100644 ImageSharp/Formats/Png/Adam7.cs create mode 100644 ImageSharp/Formats/Png/Chunks/AnimationControl.cs create mode 100644 ImageSharp/Formats/Png/Chunks/FrameControl.cs create mode 100644 ImageSharp/Formats/Png/Chunks/PngHeader.cs create mode 100644 ImageSharp/Formats/Png/Chunks/PngPhysical.cs create mode 100644 ImageSharp/Formats/Png/Chunks/PngTextData.cs create mode 100644 ImageSharp/Formats/Png/Filters/AverageFilter.cs create mode 100644 ImageSharp/Formats/Png/Filters/FilterType.cs create mode 100644 ImageSharp/Formats/Png/Filters/NoneFilter.cs create mode 100644 ImageSharp/Formats/Png/Filters/PaethFilter.cs create mode 100644 ImageSharp/Formats/Png/Filters/SubFilter.cs create mode 100644 ImageSharp/Formats/Png/Filters/UpFilter.cs create mode 100644 ImageSharp/Formats/Png/PngBitDepth.cs create mode 100644 ImageSharp/Formats/Png/PngChunk.cs create mode 100644 ImageSharp/Formats/Png/PngChunkFilter.cs create mode 100644 ImageSharp/Formats/Png/PngChunkType.cs create mode 100644 ImageSharp/Formats/Png/PngColorType.cs create mode 100644 ImageSharp/Formats/Png/PngCompressionLevel.cs create mode 100644 ImageSharp/Formats/Png/PngConfigurationModule.cs create mode 100644 ImageSharp/Formats/Png/PngConstants.cs create mode 100644 ImageSharp/Formats/Png/PngDecoder.cs create mode 100644 ImageSharp/Formats/Png/PngDecoderCore.cs create mode 100644 ImageSharp/Formats/Png/PngDecoderOptions.cs create mode 100644 ImageSharp/Formats/Png/PngEncoder.cs create mode 100644 ImageSharp/Formats/Png/PngEncoderCore.cs create mode 100644 ImageSharp/Formats/Png/PngEncoderHelpers.cs create mode 100644 ImageSharp/Formats/Png/PngFilterMethod.cs create mode 100644 ImageSharp/Formats/Png/PngFormat.cs create mode 100644 ImageSharp/Formats/Png/PngFrameMetadata.cs create mode 100644 ImageSharp/Formats/Png/PngImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Png/PngInterlaceMode.cs create mode 100644 ImageSharp/Formats/Png/PngMetadata.cs create mode 100644 ImageSharp/Formats/Png/PngScanlineProcessor.cs create mode 100644 ImageSharp/Formats/Png/PngThrowHelper.cs create mode 100644 ImageSharp/Formats/Png/README.md create mode 100644 ImageSharp/Formats/Qoi/QoiChannels.cs create mode 100644 ImageSharp/Formats/Qoi/QoiChunk.cs create mode 100644 ImageSharp/Formats/Qoi/QoiColorSpace.cs create mode 100644 ImageSharp/Formats/Qoi/QoiConfigurationModule.cs create mode 100644 ImageSharp/Formats/Qoi/QoiConstants.cs create mode 100644 ImageSharp/Formats/Qoi/QoiDecoder.cs create mode 100644 ImageSharp/Formats/Qoi/QoiDecoderCore.cs create mode 100644 ImageSharp/Formats/Qoi/QoiEncoder.cs create mode 100644 ImageSharp/Formats/Qoi/QoiEncoderCore.cs create mode 100644 ImageSharp/Formats/Qoi/QoiFormat.cs create mode 100644 ImageSharp/Formats/Qoi/QoiHeader.cs create mode 100644 ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Qoi/QoiMetadata.cs create mode 100644 ImageSharp/Formats/Qoi/qoi-specification.pdf create mode 100644 ImageSharp/Formats/SegmentIntegrityHandling.cs create mode 100644 ImageSharp/Formats/SpecializedImageDecoder{T}.cs create mode 100644 ImageSharp/Formats/Tga/README.md create mode 100644 ImageSharp/Formats/Tga/TGA_Specification.pdf create mode 100644 ImageSharp/Formats/Tga/TgaBitsPerPixel.cs create mode 100644 ImageSharp/Formats/Tga/TgaCompression.cs create mode 100644 ImageSharp/Formats/Tga/TgaConfigurationModule.cs create mode 100644 ImageSharp/Formats/Tga/TgaConstants.cs create mode 100644 ImageSharp/Formats/Tga/TgaDecoder.cs create mode 100644 ImageSharp/Formats/Tga/TgaDecoderCore.cs create mode 100644 ImageSharp/Formats/Tga/TgaEncoder.cs create mode 100644 ImageSharp/Formats/Tga/TgaEncoderCore.cs create mode 100644 ImageSharp/Formats/Tga/TgaFileHeader.cs create mode 100644 ImageSharp/Formats/Tga/TgaFormat.cs create mode 100644 ImageSharp/Formats/Tga/TgaImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Tga/TgaImageOrigin.cs create mode 100644 ImageSharp/Formats/Tga/TgaImageType.cs create mode 100644 ImageSharp/Formats/Tga/TgaImageTypeExtensions.cs create mode 100644 ImageSharp/Formats/Tga/TgaMetadata.cs create mode 100644 ImageSharp/Formats/Tga/TgaThrowHelper.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/CcittReferenceScanline.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCode.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCodeType.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanBitReader.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs create mode 100644 ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffCompression.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffConstants.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffInkSet.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs create mode 100644 ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs create mode 100644 ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs create mode 100644 ImageSharp/Formats/Tiff/Ifd/EntryReader.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24242424TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/README.md create mode 100644 ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf create mode 100644 ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf create mode 100644 ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf create mode 100644 ImageSharp/Formats/Tiff/TIFF-v6.pdf create mode 100644 ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs create mode 100644 ImageSharp/Formats/Tiff/TiffBitsPerSample.cs create mode 100644 ImageSharp/Formats/Tiff/TiffConfigurationModule.cs create mode 100644 ImageSharp/Formats/Tiff/TiffDecoder.cs create mode 100644 ImageSharp/Formats/Tiff/TiffDecoderCore.cs create mode 100644 ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs create mode 100644 ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs create mode 100644 ImageSharp/Formats/Tiff/TiffEncoder.cs create mode 100644 ImageSharp/Formats/Tiff/TiffEncoderCore.cs create mode 100644 ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs create mode 100644 ImageSharp/Formats/Tiff/TiffExtraSampleType.cs create mode 100644 ImageSharp/Formats/Tiff/TiffFormat.cs create mode 100644 ImageSharp/Formats/Tiff/TiffFormatType.cs create mode 100644 ImageSharp/Formats/Tiff/TiffFrameMetadata.cs create mode 100644 ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Tiff/TiffMetadata.cs create mode 100644 ImageSharp/Formats/Tiff/TiffThrowHelper.cs create mode 100644 ImageSharp/Formats/Tiff/Utils/BitReader.cs create mode 100644 ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs create mode 100644 ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs create mode 100644 ImageSharp/Formats/TransparentColorMode.cs create mode 100644 ImageSharp/Formats/Webp/AlphaDecoder.cs create mode 100644 ImageSharp/Formats/Webp/AlphaEncoder.cs create mode 100644 ImageSharp/Formats/Webp/BackgroundColorHandling.cs create mode 100644 ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs create mode 100644 ImageSharp/Formats/Webp/BitReader/Vp8BitReader.cs create mode 100644 ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs create mode 100644 ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs create mode 100644 ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs create mode 100644 ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs create mode 100644 ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs create mode 100644 ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs create mode 100644 ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs create mode 100644 ImageSharp/Formats/Webp/EntropyIx.cs create mode 100644 ImageSharp/Formats/Webp/HistoIx.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/ColorCache.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/ColorSpaceTransformUtils.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/CostCacheInterval.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/CostInterval.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/CostManager.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/CostModel.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/CrunchConfig.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/CrunchSubConfig.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/DominantCostRange.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HistogramBinInfo.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HistogramPair.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HuffIndex.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HuffmanCode.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HuffmanTree.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HuffmanTreeCode.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HuffmanTreeToken.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/NearLosslessEnc.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/PixOrCopyMode.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LDecoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LHashChain.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LLz77Type.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LMetadata.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LMultipliers.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LStreaks.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LTransform.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Vp8LTransformType.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossless/Webp_Lossless_Bitstream_Specification.pdf create mode 100644 ImageSharp/Formats/Webp/Lossy/IntraPredictionMode.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/LoopFilter.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/LossyUtils.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/PassStats.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/QuantEnc.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8BandProbas.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8EncSegmentHeader.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8FilterHeader.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8FilterInfo.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8FrameHeader.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Io.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8MacroBlock.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockData.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockInfo.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockType.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8PictureHeader.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Proba.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8ProbaArray.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8QuantMatrix.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8SegmentHeader.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8SegmentInfo.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8StatsArray.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/Vp8TopSamples.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/YuvConversion.cs create mode 100644 ImageSharp/Formats/Webp/Lossy/rfc6386_lossy_specification.pdf create mode 100644 ImageSharp/Formats/Webp/Readme.md create mode 100644 ImageSharp/Formats/Webp/RiffHelper.cs create mode 100644 ImageSharp/Formats/Webp/WebpAlphaCompressionMethod.cs create mode 100644 ImageSharp/Formats/Webp/WebpAlphaFilterType.cs create mode 100644 ImageSharp/Formats/Webp/WebpAnimationDecoder.cs create mode 100644 ImageSharp/Formats/Webp/WebpBitsPerPixel.cs create mode 100644 ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs create mode 100644 ImageSharp/Formats/Webp/WebpChunkType.cs create mode 100644 ImageSharp/Formats/Webp/WebpColorType.cs create mode 100644 ImageSharp/Formats/Webp/WebpCommonUtils.cs create mode 100644 ImageSharp/Formats/Webp/WebpConfigurationModule.cs create mode 100644 ImageSharp/Formats/Webp/WebpConstants.cs create mode 100644 ImageSharp/Formats/Webp/WebpDecoder.cs create mode 100644 ImageSharp/Formats/Webp/WebpDecoderCore.cs create mode 100644 ImageSharp/Formats/Webp/WebpDecoderOptions.cs create mode 100644 ImageSharp/Formats/Webp/WebpEncoder.cs create mode 100644 ImageSharp/Formats/Webp/WebpEncoderCore.cs create mode 100644 ImageSharp/Formats/Webp/WebpEncodingMethod.cs create mode 100644 ImageSharp/Formats/Webp/WebpFeatures.cs create mode 100644 ImageSharp/Formats/Webp/WebpFileFormatType.cs create mode 100644 ImageSharp/Formats/Webp/WebpFormat.cs create mode 100644 ImageSharp/Formats/Webp/WebpFrameMetadata.cs create mode 100644 ImageSharp/Formats/Webp/WebpImageFormatDetector.cs create mode 100644 ImageSharp/Formats/Webp/WebpImageInfo.cs create mode 100644 ImageSharp/Formats/Webp/WebpLookupTables.cs create mode 100644 ImageSharp/Formats/Webp/WebpMetadata.cs create mode 100644 ImageSharp/Formats/Webp/WebpThrowHelper.cs create mode 100644 ImageSharp/Formats/Webp/Webp_Container_Specification.pdf create mode 100644 ImageSharp/Formats/_Generated/ImageExtensions.Save.cs create mode 100644 ImageSharp/Formats/_Generated/ImageExtensions.Save.tt create mode 100644 ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs create mode 100644 ImageSharp/Formats/_Generated/ImageMetadataExtensions.tt create mode 100644 ImageSharp/Formats/_Generated/_Formats.ttinclude create mode 100644 ImageSharp/GeometryUtilities.cs create mode 100644 ImageSharp/GraphicOptionsDefaultsExtensions.cs create mode 100644 ImageSharp/GraphicsOptions.cs create mode 100644 ImageSharp/IDeepCloneable.cs create mode 100644 ImageSharp/IO/BufferedReadStream.cs create mode 100644 ImageSharp/IO/ChunkedMemoryStream.cs create mode 100644 ImageSharp/IO/IFileSystem.cs create mode 100644 ImageSharp/IO/LocalFileSystem.cs create mode 100644 ImageSharp/Image.Decode.cs create mode 100644 ImageSharp/Image.FromBytes.cs create mode 100644 ImageSharp/Image.FromFile.cs create mode 100644 ImageSharp/Image.FromStream.cs create mode 100644 ImageSharp/Image.LoadPixelData.cs create mode 100644 ImageSharp/Image.WrapMemory.cs create mode 100644 ImageSharp/Image.cs create mode 100644 ImageSharp/ImageExtensions.Internal.cs create mode 100644 ImageSharp/ImageExtensions.cs create mode 100644 ImageSharp/ImageFrame.LoadPixelData.cs create mode 100644 ImageSharp/ImageFrame.cs create mode 100644 ImageSharp/ImageFrameCollection.cs create mode 100644 ImageSharp/ImageFrameCollectionExtensions.cs create mode 100644 ImageSharp/ImageFrameCollection{TPixel}.cs create mode 100644 ImageSharp/ImageFrame{TPixel}.cs create mode 100644 ImageSharp/ImageInfo.cs create mode 100644 ImageSharp/ImageSharp.csproj create mode 100644 ImageSharp/Image{TPixel}.cs create mode 100644 ImageSharp/IndexedImageFrame{TPixel}.cs create mode 100644 ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs create mode 100644 ImageSharp/Memory/AllocationTrackingState.cs create mode 100644 ImageSharp/Memory/Allocators/AllocationOptions.cs create mode 100644 ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/IRefCounted.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs create mode 100644 ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs create mode 100644 ImageSharp/Memory/Allocators/MemoryAllocator.cs create mode 100644 ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs create mode 100644 ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs create mode 100644 ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs create mode 100644 ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs create mode 100644 ImageSharp/Memory/Buffer2DExtensions.cs create mode 100644 ImageSharp/Memory/Buffer2DRegion{T}.cs create mode 100644 ImageSharp/Memory/Buffer2D{T}.cs create mode 100644 ImageSharp/Memory/ByteMemoryManager{T}.cs create mode 100644 ImageSharp/Memory/ByteMemoryOwner{T}.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupEnumerator{T}.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupSpanCache.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs create mode 100644 ImageSharp/Memory/DiscontiguousBuffers/SpanCacheMode.cs create mode 100644 ImageSharp/Memory/InvalidMemoryOperationException.cs create mode 100644 ImageSharp/Memory/MemoryAllocatorExtensions.cs create mode 100644 ImageSharp/Memory/MemoryOwnerExtensions.cs create mode 100644 ImageSharp/Memory/RowInterval.cs create mode 100644 ImageSharp/Memory/UnmanagedMemoryManager{T}.cs create mode 100644 ImageSharp/Metadata/ImageFrameMetadata.cs create mode 100644 ImageSharp/Metadata/ImageMetadata.cs create mode 100644 ImageSharp/Metadata/PixelResolutionUnit.cs create mode 100644 ImageSharp/Metadata/Profiles/CICP/CicpProfile.cs create mode 100644 ImageSharp/Metadata/Profiles/CICP/Enums/CicpColorPrimaries.cs create mode 100644 ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs create mode 100644 ImageSharp/Metadata/Profiles/CICP/Enums/CicpTransferCharacteristics.cs create mode 100644 ImageSharp/Metadata/Profiles/CICP/T-REC-H.273-202107-S!!PDF-E.pdf create mode 100644 ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifParts.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifReader.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifTags.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/README.md create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Byte.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.RationalArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRational.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedRationalArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag{TValueType}.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Tags/UnkownExifTag.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifArrayValue{TValueType}.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifByte.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifDouble.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifDoubleArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifFloat.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifFloatArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifLong.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifLongArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifNumber.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifOrientationMode.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifRational.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifRationalArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifShort.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifShortArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByte.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedByteArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLongArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRational.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedRationalArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShort.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifString.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifUcs2String.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/ExifValue{TValueType}.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/IExifValue.cs create mode 100644 ImageSharp/Metadata/Profiles/Exif/Values/IExifValue{TValueType}.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Curves/IccCurveSegment.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Curves/IccFormulaCurveElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Curves/IccOneDimensionalCurve.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Curves/IccParametricCurve.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Curves/IccResponseCurve.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Curves/IccSampledCurveElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Curves.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Matrix.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.MultiProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.NonPrimitives.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Primitives.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Curves.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Lut.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Matrix.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.MultiProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.NonPrimitives.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.Primitives.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccClutDataType.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccColorSpaceType.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccColorantEncoding.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveMeasurementEncodings.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccCurveSegmentSignature.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccDataType.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccDeviceAttribute.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccFormulaCurveType.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccMeasurementGeometry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccMultiProcessElementSignature.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccParametricCurveType.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccPrimaryPlatformType.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileClass.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileFlag.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccProfileTag.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccRenderingIntent.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningFlag.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccScreeningSpotType.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccSignatureName.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardIlluminant.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccStandardObserver.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Enums/IccTypeSignature.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Exceptions/InvalidIccProfileException.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/ICC.1-2022-05.pdf create mode 100644 ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/IccProfile.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/IccReader.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/IccTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/IccWriter.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccBAcsProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccClutProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccCurveSetProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccEAcsProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMatrixProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/MultiProcessElements/IccMultiProcessElement.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantOrderTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccColorantTableTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCrdInfoTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccCurveTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDataTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccDateTimeTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccFix16ArrayTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut16TagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLut8TagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMeasurementTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiLocalizedUnicodeTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccMultiProcessElementsTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccNamedColor2TagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccParametricCurveTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceDescTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccProfileSequenceIdentifierTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccResponseCurveSet16TagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccScreeningTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccSignatureTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUFix16ArrayTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt16ArrayTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt32ArrayTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt64ArrayTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUInt8ArrayTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUcrBgTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccUnknownTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccViewingConditionsTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccXyzTagDataEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccClut.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccColorantTableEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccLocalizedString.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccNamedColor.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccPositionNumber.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccProfileDescription.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccProfileId.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccProfileSequenceIdentifier.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccResponseNumber.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccScreeningChannel.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccTagTableEntry.cs create mode 100644 ImageSharp/Metadata/Profiles/ICC/Various/IccVersion.cs create mode 100644 ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf create mode 100644 ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs create mode 100644 ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs create mode 100644 ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs create mode 100644 ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs create mode 100644 ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs create mode 100644 ImageSharp/Metadata/Profiles/IPTC/README.md create mode 100644 ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs create mode 100644 ImageSharp/PixelAccessor{TPixel}.cs create mode 100644 ImageSharp/PixelFormats/HalfTypeHelper.cs create mode 100644 ImageSharp/PixelFormats/IPackedVector{TPacked}.cs create mode 100644 ImageSharp/PixelFormats/IPixel.cs create mode 100644 ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs create mode 100644 ImageSharp/PixelFormats/PixelAlphaRepresentation.cs create mode 100644 ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs create mode 100644 ImageSharp/PixelFormats/PixelBlender{TPixel}.cs create mode 100644 ImageSharp/PixelFormats/PixelColorBlendingMode.cs create mode 100644 ImageSharp/PixelFormats/PixelColorType.cs create mode 100644 ImageSharp/PixelFormats/PixelComponentBitDepth.cs create mode 100644 ImageSharp/PixelFormats/PixelComponentInfo.cs create mode 100644 ImageSharp/PixelFormats/PixelConversionModifiers.cs create mode 100644 ImageSharp/PixelFormats/PixelConversionModifiersExtensions.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/A8.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Argb32.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Byte4.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/L16.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/L8.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/La16.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/La32.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Abgr32.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Argb32.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr24.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra32.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra4444.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra5551.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra5551.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L16.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/L8.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La16.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/La32.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb48.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba32.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgba64.PixelOperations.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La16.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/La32.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb24.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba1010102.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba32.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba64.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rg32.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Short2.cs create mode 100644 ImageSharp/PixelFormats/PixelImplementations/Short4.cs create mode 100644 ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs create mode 100644 ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt create mode 100644 ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs create mode 100644 ImageSharp/PixelFormats/PixelOperations{TPixel}.cs create mode 100644 ImageSharp/PixelFormats/PixelTypeInfo.cs create mode 100644 ImageSharp/PixelFormats/README.md create mode 100644 ImageSharp/PixelFormats/RgbaComponent.cs create mode 100644 ImageSharp/PixelFormats/Utils/PixelConverter.cs create mode 100644 ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs create mode 100644 ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs create mode 100644 ImageSharp/PixelFormats/Utils/Vector4Converters.cs create mode 100644 ImageSharp/Primitives/ColorMatrix.Impl.cs create mode 100644 ImageSharp/Primitives/ColorMatrix.cs create mode 100644 ImageSharp/Primitives/Complex64.cs create mode 100644 ImageSharp/Primitives/ComplexVector4.cs create mode 100644 ImageSharp/Primitives/DenseMatrix{T}.cs create mode 100644 ImageSharp/Primitives/LongRational.cs create mode 100644 ImageSharp/Primitives/Matrix3x2Extensions.cs create mode 100644 ImageSharp/Primitives/Number.cs create mode 100644 ImageSharp/Primitives/Point.cs create mode 100644 ImageSharp/Primitives/PointF.cs create mode 100644 ImageSharp/Primitives/Rational.cs create mode 100644 ImageSharp/Primitives/Rectangle.cs create mode 100644 ImageSharp/Primitives/RectangleF.cs create mode 100644 ImageSharp/Primitives/SignedRational.cs create mode 100644 ImageSharp/Primitives/Size.cs create mode 100644 ImageSharp/Primitives/SizeF.cs create mode 100644 ImageSharp/Primitives/ValueSize.cs create mode 100644 ImageSharp/Processing/AdaptiveThresholdExtensions.cs create mode 100644 ImageSharp/Processing/AffineTransformBuilder.cs create mode 100644 ImageSharp/Processing/AnchorPositionMode.cs create mode 100644 ImageSharp/Processing/BinaryThresholdMode.cs create mode 100644 ImageSharp/Processing/ColorBlindnessMode.cs create mode 100644 ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs create mode 100644 ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Binarization/BinaryThresholdExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Effects/OilPaintExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Effects/PixelRowDelegateExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Effects/PixelateExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/BlackWhiteExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/BrightnessExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/ColorBlindnessExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/ContrastExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/GrayscaleExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/HueExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/InvertExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/KodachromeExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/LightnessExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/LomographExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/OpacityExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/PolaroidExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/SaturateExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Filters/SepiaExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Normalization/HistogramEqualizationExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Overlays/BackgroundColorExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Overlays/GlowExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Overlays/VignetteExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs create mode 100644 ImageSharp/Processing/Extensions/ProcessingExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/AutoOrientExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/CropExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/EntropyCropExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/FlipExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/ResizeExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/RotateExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/RotateFlipExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/SkewExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/SwizzleExtensions.cs create mode 100644 ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs create mode 100644 ImageSharp/Processing/FlipMode.cs create mode 100644 ImageSharp/Processing/GrayscaleMode.cs create mode 100644 ImageSharp/Processing/IImageProcessingContext.cs create mode 100644 ImageSharp/Processing/IImageProcessingContextFactory.cs create mode 100644 ImageSharp/Processing/IInternalImageProcessingContext{TPixel}.cs create mode 100644 ImageSharp/Processing/KnownDitherings.cs create mode 100644 ImageSharp/Processing/KnownEdgeDetectorKernels.cs create mode 100644 ImageSharp/Processing/KnownFilterMatrices.cs create mode 100644 ImageSharp/Processing/KnownQuantizers.cs create mode 100644 ImageSharp/Processing/KnownResamplers.cs create mode 100644 ImageSharp/Processing/PixelRowOperation.cs create mode 100644 ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/CloningImageProcessor.cs create mode 100644 ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Convolution2DState.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/ConvolutionState.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernel.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetector2DKernel.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorCompassKernel.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorKernel.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KayyaliKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KirschKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernelFactory.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/PrewittKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobertsCrossKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobinsonKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/ScharrKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/SobelKernels.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurParameters.cs create mode 100644 ImageSharp/Processing/Processors/Convolution/ReadOnlyKernel.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/DHALF.TXT create mode 100644 ImageSharp/Processing/Processors/Dithering/DITHER.TXT create mode 100644 ImageSharp/Processing/Processors/Dithering/ErrorDither.KnownTypes.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/ErrorDither.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/IDither.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/OrderedDither.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Dithering/error_diffusion.txt create mode 100644 ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf create mode 100644 ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs create mode 100644 ImageSharp/Processing/Processors/Effects/IPixelRowDelegate.cs create mode 100644 ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs create mode 100644 ImageSharp/Processing/Processors/Effects/PixelateProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Effects/PositionAwarePixelRowDelegateProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/AchromatomalyProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/AchromatopsiaProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/BrightnessProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/ContrastProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/DeuteranomalyProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/DeuteranopiaProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/FilterProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Filters/GrayscaleBt601Processor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/HueProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/InvertProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/KodachromeProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/LightnessProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/LomographProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/LomographProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Filters/OpacityProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Filters/PolaroidProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/PolaroidProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Filters/ProtanomalyProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/ProtanopiaProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/README.md create mode 100644 ImageSharp/Processing/Processors/Filters/SaturateProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/SepiaProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/TritanomalyProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Filters/TritanopiaProcessor.cs create mode 100644 ImageSharp/Processing/Processors/ICloningImageProcessor.cs create mode 100644 ImageSharp/Processing/Processors/ICloningImageProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/IImageProcessor.cs create mode 100644 ImageSharp/Processing/Processors/IImageProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/ImageProcessorExtensions.cs create mode 100644 ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/DefaultPixelSamplingStrategy.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/ExtensivePixelSamplingStrategy.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/IPixelSamplingStrategy.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/IQuantizer.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/IQuantizingPixelRowDelegate{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs create mode 100644 ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/CropProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/DegenerateTransformException.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/IResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/IResamplingTransformImageProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/ISwizzler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resamplers/CubicResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resamplers/LanczosResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.pptx create mode 100644 ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs create mode 100644 ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs create mode 100644 ImageSharp/Processing/ProjectiveTransformBuilder.cs create mode 100644 ImageSharp/Processing/ResizeMode.cs create mode 100644 ImageSharp/Processing/ResizeOptions.cs create mode 100644 ImageSharp/Processing/RotateMode.cs create mode 100644 ImageSharp/Processing/TaperCorner.cs create mode 100644 ImageSharp/Processing/TaperSide.cs create mode 100644 ImageSharp/Properties/AssemblyInfo.cs create mode 100644 ImageSharp/ReadOrigin.cs create mode 100644 LICENSE create mode 100644 PolygonClipper/ActiveEdge.cs create mode 100644 PolygonClipper/ActiveEdgeList.cs create mode 100644 PolygonClipper/ArrayBuilder{T}.cs create mode 100644 PolygonClipper/BooleanOperation.cs create mode 100644 PolygonClipper/Box2.cs create mode 100644 PolygonClipper/Buffer{T}.cs create mode 100644 PolygonClipper/ByteMemoryManager{T}.cs create mode 100644 PolygonClipper/Contour.cs create mode 100644 PolygonClipper/EdgeType.cs create mode 100644 PolygonClipper/FloatExtensions.cs create mode 100644 PolygonClipper/IntersectNode.cs create mode 100644 PolygonClipper/JoinWith.cs create mode 100644 PolygonClipper/LineCap.cs create mode 100644 PolygonClipper/LineJoin.cs create mode 100644 PolygonClipper/LocalMinima.cs create mode 100644 PolygonClipper/OutputRecord.cs create mode 100644 PolygonClipper/PathCommand.cs create mode 100644 PolygonClipper/PathCommandExtensions.cs create mode 100644 PolygonClipper/PathFlags.cs create mode 100644 PolygonClipper/PointInPolygonResult.cs create mode 100644 PolygonClipper/Polygon.cs create mode 100644 PolygonClipper/PolygonClipper.cs create mode 100644 PolygonClipper/PolygonClipper.csproj create mode 100644 PolygonClipper/PolygonStroker.cs create mode 100644 PolygonClipper/PolygonType.cs create mode 100644 PolygonClipper/PolygonUtilities.cs create mode 100644 PolygonClipper/ResultTransition.cs create mode 100644 PolygonClipper/ScanlineSchedule.cs create mode 100644 PolygonClipper/Segment.cs create mode 100644 PolygonClipper/SegmentComparer.cs create mode 100644 PolygonClipper/SelfIntersectionRemover.cs create mode 100644 PolygonClipper/SelfIntersectionSweepLine.cs create mode 100644 PolygonClipper/StablePriorityQueue{T,TComparer}.cs create mode 100644 PolygonClipper/StatusLine.cs create mode 100644 PolygonClipper/StrokeOptions.cs create mode 100644 PolygonClipper/StrokeVertexDistance.cs create mode 100644 PolygonClipper/SweepEvent.cs create mode 100644 PolygonClipper/SweepEventComparer.cs create mode 100644 PolygonClipper/SweepVertex.cs create mode 100644 PolygonClipper/Vertex.cs create mode 100644 PolygonClipper/VertexFlags.cs create mode 100644 PolygonClipper/VertexPoolList.cs create mode 100644 SharedInfrastructure.projitems create mode 100644 SixLabors.Fonts/ArrayBuilder{T}.cs create mode 100644 SixLabors.Fonts/ArraySlice{T}.cs create mode 100644 SixLabors.Fonts/BigEndianBinaryReader.cs create mode 100644 SixLabors.Fonts/Bounds.cs create mode 100644 SixLabors.Fonts/Buffer{T}.cs create mode 100644 SixLabors.Fonts/ByteMemoryManager{T}.cs create mode 100644 SixLabors.Fonts/CaretMovement.cs create mode 100644 SixLabors.Fonts/CaretPlacement.cs create mode 100644 SixLabors.Fonts/CaretPosition.cs create mode 100644 SixLabors.Fonts/ClipQuad.cs create mode 100644 SixLabors.Fonts/ColorFontSupport.cs create mode 100644 SixLabors.Fonts/DecorationPositioningMode.cs create mode 100644 SixLabors.Fonts/Exceptions/FontException.cs create mode 100644 SixLabors.Fonts/Exceptions/FontFamilyNotFoundException.cs create mode 100644 SixLabors.Fonts/Exceptions/FontsThrowHelper.cs create mode 100644 SixLabors.Fonts/Exceptions/GlyphMissingException.cs create mode 100644 SixLabors.Fonts/Exceptions/InvalidFontFileException.cs create mode 100644 SixLabors.Fonts/Exceptions/InvalidFontTableException.cs create mode 100644 SixLabors.Fonts/Exceptions/MissingFontTableException.cs create mode 100644 SixLabors.Fonts/FileFontMetrics.cs create mode 100644 SixLabors.Fonts/Font.cs create mode 100644 SixLabors.Fonts/FontCollection.cs create mode 100644 SixLabors.Fonts/FontCollectionExtensions.cs create mode 100644 SixLabors.Fonts/FontDescription.cs create mode 100644 SixLabors.Fonts/FontFamily.cs create mode 100644 SixLabors.Fonts/FontGlyphMetrics.cs create mode 100644 SixLabors.Fonts/FontMetrics.cs create mode 100644 SixLabors.Fonts/FontReader.cs create mode 100644 SixLabors.Fonts/FontRectangle.cs create mode 100644 SixLabors.Fonts/FontStyle.cs create mode 100644 SixLabors.Fonts/FontVariation.cs create mode 100644 SixLabors.Fonts/Glyph.cs create mode 100644 SixLabors.Fonts/GlyphColor.KnownColors.cs create mode 100644 SixLabors.Fonts/GlyphColor.cs create mode 100644 SixLabors.Fonts/GlyphLayout.cs create mode 100644 SixLabors.Fonts/GlyphLayoutData.cs create mode 100644 SixLabors.Fonts/GlyphLayoutMode.cs create mode 100644 SixLabors.Fonts/GlyphMetrics.cs create mode 100644 SixLabors.Fonts/GlyphPositioningCollection.cs create mode 100644 SixLabors.Fonts/GlyphShapingBounds.cs create mode 100644 SixLabors.Fonts/GlyphShapingClass.cs create mode 100644 SixLabors.Fonts/GlyphShapingData.cs create mode 100644 SixLabors.Fonts/GlyphSubstitutionCollection.cs create mode 100644 SixLabors.Fonts/GlyphType.cs create mode 100644 SixLabors.Fonts/GraphemeMetrics.cs create mode 100644 SixLabors.Fonts/HintingMode.cs create mode 100644 SixLabors.Fonts/HorizontalAlignment.cs create mode 100644 SixLabors.Fonts/HorizontalMetrics.cs create mode 100644 SixLabors.Fonts/IFontCollection.cs create mode 100644 SixLabors.Fonts/IFontMetricsCollection.cs create mode 100644 SixLabors.Fonts/IGlyphShapingCollection.cs create mode 100644 SixLabors.Fonts/IMetricsHeader.cs create mode 100644 SixLabors.Fonts/IO/ZlibInflateStream.cs create mode 100644 SixLabors.Fonts/IReadOnlySystemFontCollection.cs create mode 100644 SixLabors.Fonts/IReadonlyFontCollection.cs create mode 100644 SixLabors.Fonts/IReadonlyFontMetricsCollection.cs create mode 100644 SixLabors.Fonts/KerningMode.cs create mode 100644 SixLabors.Fonts/KnownVariationAxes.cs create mode 100644 SixLabors.Fonts/LayoutMode.cs create mode 100644 SixLabors.Fonts/LayoutModeExtensions.cs create mode 100644 SixLabors.Fonts/LineLayout.cs create mode 100644 SixLabors.Fonts/LineLayoutEnumerator.cs create mode 100644 SixLabors.Fonts/LineMetrics.cs create mode 100644 SixLabors.Fonts/LogicalTextLine.cs create mode 100644 SixLabors.Fonts/MappedArraySlice{T}.cs create mode 100644 SixLabors.Fonts/Native/CFStringEncoding.cs create mode 100644 SixLabors.Fonts/Native/CFURLPathStyle.cs create mode 100644 SixLabors.Fonts/Native/CoreFoundation.cs create mode 100644 SixLabors.Fonts/Native/CoreText.cs create mode 100644 SixLabors.Fonts/Native/MacSystemFontsEnumerator.cs create mode 100644 SixLabors.Fonts/ObjectPool{T}.cs create mode 100644 SixLabors.Fonts/OutlineType.cs create mode 100644 SixLabors.Fonts/PlaceholderGlyphMetrics.cs create mode 100644 SixLabors.Fonts/PreparedTextLayoutDesign.md create mode 100644 SixLabors.Fonts/ReadOnlyArraySlice{T}.cs create mode 100644 SixLabors.Fonts/ReadOnlyMappedArraySlice{T}.cs create mode 100644 SixLabors.Fonts/Rendering/CompositeMode.cs create mode 100644 SixLabors.Fonts/Rendering/FillRule.cs create mode 100644 SixLabors.Fonts/Rendering/GlyphRendererExtensions.cs create mode 100644 SixLabors.Fonts/Rendering/GlyphRendererParameters.cs create mode 100644 SixLabors.Fonts/Rendering/GradientStop.cs create mode 100644 SixLabors.Fonts/Rendering/GradientUnits.cs create mode 100644 SixLabors.Fonts/Rendering/IGlyphRenderer.cs create mode 100644 SixLabors.Fonts/Rendering/IPaintedGlyphSource.cs create mode 100644 SixLabors.Fonts/Rendering/Paint.cs create mode 100644 SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs create mode 100644 SixLabors.Fonts/Rendering/PaintedGlyph.cs create mode 100644 SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs create mode 100644 SixLabors.Fonts/Rendering/PaintedLayer.cs create mode 100644 SixLabors.Fonts/Rendering/PathCommand.cs create mode 100644 SixLabors.Fonts/Rendering/PathVerb.cs create mode 100644 SixLabors.Fonts/Rendering/SpreadMethod.cs create mode 100644 SixLabors.Fonts/Rendering/TextRenderer.cs create mode 100644 SixLabors.Fonts/ShapedText.cs create mode 100644 SixLabors.Fonts/SixLabors.Fonts.csproj create mode 100644 SixLabors.Fonts/StreamFontMetrics.Cff.cs create mode 100644 SixLabors.Fonts/StreamFontMetrics.TrueType.cs create mode 100644 SixLabors.Fonts/StreamFontMetrics.cs create mode 100644 SixLabors.Fonts/StringComparerHelpers.cs create mode 100644 SixLabors.Fonts/SystemFontCollection.cs create mode 100644 SixLabors.Fonts/SystemFonts.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/AttachPoint.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/AttachmentListTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleSetTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ChainedClassSequenceRuleTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleSetTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ChainedSequenceRuleTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ClassRangeRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleSetTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ClassSequenceRuleTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/CoverageRangeRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/FeatureListTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/FeatureVariationsTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorXY.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseArrayTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/BaseRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class1Record.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Class2Record.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ComponentRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitAnchors.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/EntryExitRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureArrayTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LigatureAttachTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupListTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType1SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType4SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType5SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType6SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType7SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType8SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType9SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2ArrayTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/Mark2Record.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkArrayTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/MarkRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/NotImplementedSubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/PairValueRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueFormat.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPos/ValueRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupListTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType1SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType2SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType3SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType4SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType5SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType6SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType7SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/LookupType8SubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSub/NotImplementedSubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GSubTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GlyphClassDef.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/GlyphDefinitionTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/KnownFeatureTags.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/LigatureCaretList.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/LigatureGlyph.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/LookupFlags.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/MarkGlyphSetsTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/MarkZeroingMode.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/ScriptList.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/SequenceLookupRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleSetTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/SequenceRuleTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/BaseShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HebrewShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/IndicShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/MyanmarShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShaperFactory.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ShapingStage.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ThaiShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/UniversalShaper.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/SkippingGlyphIterator.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/TableLoadingUtils.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Tag.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/TagEntry.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/UnicodeScriptTagMap.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AVarTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/AxisValueMapRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/CVarTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSet.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/DeltaSetIndexMap.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/FVarTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GVarTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationData.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/GlyphVariationProcessor.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/HVarTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/InstanceRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationData.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/ItemVariationStore.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MVarTag.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/MetricValueRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/RegionAxisCoordinates.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/SegmentMapRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/TupleVariation.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VVarTable.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxis.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationAxisRecord.cs create mode 100644 SixLabors.Fonts/Tables/AdvancedTypographic/Variations/VariationRegionList.cs create mode 100644 SixLabors.Fonts/Tables/Cff/5176.CFF.pdf create mode 100644 SixLabors.Fonts/Tables/Cff/5177.Type2.pdf create mode 100644 SixLabors.Fonts/Tables/Cff/Cff1Parser.cs create mode 100644 SixLabors.Fonts/Tables/Cff/Cff1Table.cs create mode 100644 SixLabors.Fonts/Tables/Cff/Cff2Font.cs create mode 100644 SixLabors.Fonts/Tables/Cff/Cff2Parser.cs create mode 100644 SixLabors.Fonts/Tables/Cff/Cff2Table.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffBoundsFinder.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffDataDicEntry.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffFont.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffGlyphData.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffIndexOffset.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffOperand.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffOperator.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffParserBase.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffPrivateDictionary.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffStandardStrings.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CffTopDictionary.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CidFontInfo.cs create mode 100644 SixLabors.Fonts/Tables/Cff/CompactFontTables.cs create mode 100644 SixLabors.Fonts/Tables/Cff/FDRange.cs create mode 100644 SixLabors.Fonts/Tables/Cff/FDRangeProvider.cs create mode 100644 SixLabors.Fonts/Tables/Cff/FontDict.cs create mode 100644 SixLabors.Fonts/Tables/Cff/ICffTable.cs create mode 100644 SixLabors.Fonts/Tables/Cff/OperandKind.cs create mode 100644 SixLabors.Fonts/Tables/Cff/OperatorOperandKind.cs create mode 100644 SixLabors.Fonts/Tables/Cff/RefStack{T}.cs create mode 100644 SixLabors.Fonts/Tables/Cff/SimpleBinaryReader.cs create mode 100644 SixLabors.Fonts/Tables/Cff/TransformingGlyphRenderer.cs create mode 100644 SixLabors.Fonts/Tables/Cff/Type2Operator1.cs create mode 100644 SixLabors.Fonts/Tables/Cff/Type2Operator2.cs create mode 100644 SixLabors.Fonts/Tables/General/CMap/CMapSubTable.cs create mode 100644 SixLabors.Fonts/Tables/General/CMap/EncodingRecord.cs create mode 100644 SixLabors.Fonts/Tables/General/CMap/Format0SubTable.cs create mode 100644 SixLabors.Fonts/Tables/General/CMap/Format12SubTable.cs create mode 100644 SixLabors.Fonts/Tables/General/CMap/Format14SubTable.cs create mode 100644 SixLabors.Fonts/Tables/General/CMap/Format4SubTable.cs create mode 100644 SixLabors.Fonts/Tables/General/CMapTable.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/Affine2x3.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/BaseGlyphList.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/BaseGlyphPaintRecord.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/BaseGlyphRecord.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ClipBox.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat1.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ClipBoxFormat2.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ClipList.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ClipRecord.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ColorLine.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ColorStop.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ColrCompositeMode.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ColrGlyphSourceBase.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ColrTable.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ColrV0GlyphSource.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/ColrV1GlyphSource.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/Extend.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/LayerList.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/LayerRecord.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/Paint.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/VarAffine2x3.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/VarColorIndex.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/VarColorLine.cs create mode 100644 SixLabors.Fonts/Tables/General/Colr/VarColorStop.cs create mode 100644 SixLabors.Fonts/Tables/General/CpalTable.cs create mode 100644 SixLabors.Fonts/Tables/General/HeadTable.cs create mode 100644 SixLabors.Fonts/Tables/General/HorizontalHeadTable.cs create mode 100644 SixLabors.Fonts/Tables/General/HorizontalMetricsTable.cs create mode 100644 SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs create mode 100644 SixLabors.Fonts/Tables/General/Kern/KerningCoverage.cs create mode 100644 SixLabors.Fonts/Tables/General/Kern/KerningPair.cs create mode 100644 SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs create mode 100644 SixLabors.Fonts/Tables/General/Kern/KerningTable.cs create mode 100644 SixLabors.Fonts/Tables/General/MaximumProfileTable.cs create mode 100644 SixLabors.Fonts/Tables/General/Name/NameRecord.cs create mode 100644 SixLabors.Fonts/Tables/General/Name/NameTable.cs create mode 100644 SixLabors.Fonts/Tables/General/OS2Table.cs create mode 100644 SixLabors.Fonts/Tables/General/Post/PostNameRecord.cs create mode 100644 SixLabors.Fonts/Tables/General/Post/PostTable.cs create mode 100644 SixLabors.Fonts/Tables/General/Svg/SvgDocumentIndexEntry.cs create mode 100644 SixLabors.Fonts/Tables/General/Svg/SvgGlyphSource.cs create mode 100644 SixLabors.Fonts/Tables/General/Svg/SvgTable.cs create mode 100644 SixLabors.Fonts/Tables/General/VerticalHeadTable.cs create mode 100644 SixLabors.Fonts/Tables/General/VerticalMetricsTable.cs create mode 100644 SixLabors.Fonts/Tables/IFontTables.cs create mode 100644 SixLabors.Fonts/Tables/Table.cs create mode 100644 SixLabors.Fonts/Tables/TableFormat.cs create mode 100644 SixLabors.Fonts/Tables/TableHeader.cs create mode 100644 SixLabors.Fonts/Tables/TableLoader.cs create mode 100644 SixLabors.Fonts/Tables/TripleEncodingRecord.cs create mode 100644 SixLabors.Fonts/Tables/TripleEncodingTable.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeComponent.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphFlags.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/CompositeGlyphLoader.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/ControlPoint.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/EmptyGlyphLoader.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphLoader.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphTable.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphVector.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Glyphs/SimpleGlyphLoader.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Hinting/CvtTable.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Hinting/FpgmTable.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Hinting/PrepTable.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.OpCodes.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/IndexLocationTable.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/TrueTypeFontTables.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.CompatibilityLists.cs create mode 100644 SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs create mode 100644 SixLabors.Fonts/Tables/TtcHeader.cs create mode 100644 SixLabors.Fonts/Tables/UnknownTable.cs create mode 100644 SixLabors.Fonts/Tables/Woff/Woff2GlyphLoader.cs create mode 100644 SixLabors.Fonts/Tables/Woff/Woff2TableHeader.cs create mode 100644 SixLabors.Fonts/Tables/Woff/Woff2Utils.cs create mode 100644 SixLabors.Fonts/Tables/Woff/WoffTableHeader.cs create mode 100644 SixLabors.Fonts/TextAlignment.cs create mode 100644 SixLabors.Fonts/TextAttributes.cs create mode 100644 SixLabors.Fonts/TextBidiMode.cs create mode 100644 SixLabors.Fonts/TextBlock.Visitors.cs create mode 100644 SixLabors.Fonts/TextBlock.cs create mode 100644 SixLabors.Fonts/TextBox.cs create mode 100644 SixLabors.Fonts/TextDecorations.cs create mode 100644 SixLabors.Fonts/TextDirection.cs create mode 100644 SixLabors.Fonts/TextEllipsis.cs create mode 100644 SixLabors.Fonts/TextHit.cs create mode 100644 SixLabors.Fonts/TextHyphenation.cs create mode 100644 SixLabors.Fonts/TextInteraction.cs create mode 100644 SixLabors.Fonts/TextInteractionMode.cs create mode 100644 SixLabors.Fonts/TextJustification.cs create mode 100644 SixLabors.Fonts/TextLayout.LineBreaking.cs create mode 100644 SixLabors.Fonts/TextLayout.Visitors.cs create mode 100644 SixLabors.Fonts/TextLayout.cs create mode 100644 SixLabors.Fonts/TextLine.cs create mode 100644 SixLabors.Fonts/TextLineBreakEnumerator.cs create mode 100644 SixLabors.Fonts/TextMeasurer.cs create mode 100644 SixLabors.Fonts/TextMetrics.cs create mode 100644 SixLabors.Fonts/TextOptions.cs create mode 100644 SixLabors.Fonts/TextPlaceholder.cs create mode 100644 SixLabors.Fonts/TextPlaceholderAlignment.cs create mode 100644 SixLabors.Fonts/TextRun.cs create mode 100644 SixLabors.Fonts/Unicode/ArabicJoiningClass.cs create mode 100644 SixLabors.Fonts/Unicode/ArabicJoiningGroup.cs create mode 100644 SixLabors.Fonts/Unicode/ArabicJoiningType.cs create mode 100644 SixLabors.Fonts/Unicode/BidiAlgorithm.cs create mode 100644 SixLabors.Fonts/Unicode/BidiCharacterType.cs create mode 100644 SixLabors.Fonts/Unicode/BidiClass.cs create mode 100644 SixLabors.Fonts/Unicode/BidiData.cs create mode 100644 SixLabors.Fonts/Unicode/BidiDictionary{T1,T2}.cs create mode 100644 SixLabors.Fonts/Unicode/BidiPairedBracketType.cs create mode 100644 SixLabors.Fonts/Unicode/BidiRun.cs create mode 100644 SixLabors.Fonts/Unicode/CodePoint.cs create mode 100644 SixLabors.Fonts/Unicode/EastAsianWidthClass.cs create mode 100644 SixLabors.Fonts/Unicode/EmojiProperties.cs create mode 100644 SixLabors.Fonts/Unicode/GraphemeCluster.cs create mode 100644 SixLabors.Fonts/Unicode/GraphemeClusterClass.cs create mode 100644 SixLabors.Fonts/Unicode/GraphemeClusterFlags.cs create mode 100644 SixLabors.Fonts/Unicode/IndicConjunctBreakClass.cs create mode 100644 SixLabors.Fonts/Unicode/IndicPositionalCategory.cs create mode 100644 SixLabors.Fonts/Unicode/IndicSyllabicCategory.cs create mode 100644 SixLabors.Fonts/Unicode/LineBreak.cs create mode 100644 SixLabors.Fonts/Unicode/LineBreakClass.cs create mode 100644 SixLabors.Fonts/Unicode/LineBreakEnumerator.cs create mode 100644 SixLabors.Fonts/Unicode/MemoryExtensions.cs create mode 100644 SixLabors.Fonts/Unicode/README.md create mode 100644 SixLabors.Fonts/Unicode/Resources/ArabicShapingTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/BidiMirrorTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/BidiTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/EastAsianWidthTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/EmojiTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/GraphemeTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/IndicConjunctBreakTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/IndicPositionalCategoryTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/IndicShapingData.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/IndicShapingData.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/IndicShapingTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/IndicSyllabicCategoryTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/LineBreakTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/MyanmarShapingData.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/ScriptTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/UnicodeCategoryTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/UniversalShapingData.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/UniversalShapingTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/VerticalOrientationTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/Resources/WordBreakTrie.Generated.cs create mode 100644 SixLabors.Fonts/Unicode/ScriptClass.cs create mode 100644 SixLabors.Fonts/Unicode/SpanCodePointEnumerator.cs create mode 100644 SixLabors.Fonts/Unicode/SpanGraphemeEnumerator.cs create mode 100644 SixLabors.Fonts/Unicode/SpanWordEnumerator.cs create mode 100644 SixLabors.Fonts/Unicode/StateAutomation/DeterministicFiniteAutomata.cs create mode 100644 SixLabors.Fonts/Unicode/StateAutomation/INode.cs create mode 100644 SixLabors.Fonts/Unicode/StateAutomation/State.cs create mode 100644 SixLabors.Fonts/Unicode/StateAutomation/StateMachine.cs create mode 100644 SixLabors.Fonts/Unicode/StateAutomation/SymbolTable.cs create mode 100644 SixLabors.Fonts/Unicode/TerminalAmbiguousWidth.cs create mode 100644 SixLabors.Fonts/Unicode/TerminalControlCharacterWidth.cs create mode 100644 SixLabors.Fonts/Unicode/TerminalEmojiWidth.cs create mode 100644 SixLabors.Fonts/Unicode/TerminalWidthOptions.cs create mode 100644 SixLabors.Fonts/Unicode/UnicodeData.cs create mode 100644 SixLabors.Fonts/Unicode/UnicodeTrie.cs create mode 100644 SixLabors.Fonts/Unicode/UnicodeTrieBuilder.cs create mode 100644 SixLabors.Fonts/Unicode/UnicodeUtility.cs create mode 100644 SixLabors.Fonts/Unicode/VerticalOrientationType.cs create mode 100644 SixLabors.Fonts/Unicode/WordBreakClass.cs create mode 100644 SixLabors.Fonts/Unicode/WordSegment.cs create mode 100644 SixLabors.Fonts/Utilities/EncodingIDExtensions.cs create mode 100644 SixLabors.Fonts/Utilities/StringLoader.cs create mode 100644 SixLabors.Fonts/VerticalAlignment.cs create mode 100644 SixLabors.Fonts/VerticalMetrics.cs create mode 100644 SixLabors.Fonts/WellKnownIds/EncodingIDs.cs create mode 100644 SixLabors.Fonts/WellKnownIds/KnownNameIds.cs create mode 100644 SixLabors.Fonts/WellKnownIds/PlatformIDs.cs create mode 100644 SixLabors.Fonts/WordBreaking.cs create mode 100644 SixLabors.Fonts/WordMetrics.cs create mode 100644 SixLabors.Fonts/WordSegmentRun.cs create mode 100644 SixLabors.ImageSharp.props create mode 100644 ThrowHelper.cs create mode 100644 sixlabors.imagesharp.128.png 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 0000000000000000000000000000000000000000..a5967a02e4da31ff6dd505a25f5147d53d8d3375 GIT binary patch literal 218384 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໌Ze{J5(< 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

T3@FAaq#vO+a@UvDeG2nFz) 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1d57c76c2ddbbf0f3e9ed54ca4905f47b78aaf50 GIT binary patch literal 1057800 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≻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 z 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�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μ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�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 Rk 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�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

  • ;`k_W<1jT^Rw(V+tO7YE6Pn!caG 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 + /// 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 0000000000000000000000000000000000000000..3ffa4bd615cdb1691e063e0251cf2130ceba5406 GIT binary patch literal 36221 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..09c9a4dddade89630ba198f14a3298c00e190e40 GIT binary patch literal 181405 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�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 kP + /// 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 0000000000000000000000000000000000000000..40724dd1e36989cdc0424fd1753bbff157b1f0a6 GIT binary patch literal 365889 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%0AHN3qED!ze7slKdzq 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

    rh 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%0A?$`<8m%>IO%{mLUQAabEl93G}9yD3Ke zEN$7zc?!IKcFZ)b<-y?R{6b#hfss&`B!5&ryk zjGv3_BUx9n5`lEu6N9tsoABaJRQ&@iA(d`(Qx~;Wm&~2$`SkTer zxBc_?Mrq8zh%IJwFN#c_^u*2fHtKp<&#Tua-~%*$kpm;~M|Z`1B|65~I2MpDCt7dB zO%}W^E6A5HEtM{al)CDM^@JP}<0pq&*fwxj$YyW#0y;zKX5j@4_sXgpo)a*@JF@98eGk~Dlzx>$aAKNBpqM_j`% zSf#zSWjdsCcf(&zzqn-en$+dL`6aZ#zia%8{1`&fd7KR6VmF1@e5LMukdv2~azHwQ z(^-+Ft%sR$>OQ<1fLB0wo z(HSj_+`E0lIu9(Fez<{NbED8qg)J2-FeeR!>3`Dqs0C^}Pin09J03ViU!Xm!=LWZ%+l z?acq)kf$1&y5ssFj1Lv)$GGuy9@X*-&>N>cv0GVJE~xz=xN1-5g$*-~%Oo80$w zhC|4L$-Z9~>WJ(p+{`aN$MJL9+NRTIkc*1BSNL2G8hxeurl0R~x-wmsm@lYYW}@@o zG7M6hC65;uk6|4?zfGS;(M-t==y`Q8=37K#Oos_$c(IM)U1hmC<=@*Tc9MO+oYy^Hx?Z18`_Q5Nd-attG?(6{H!es{cYJja;p!A!Kz!7vqGN&>F5PwaZ0sb; zhlPR(P16|?47 zY|r6pU+i}XhBPpoib307suF`fz2|$F#8!nhi%}a_+q4Oyutv(VRS`BQJp6r>KZXVo zDksOBH-$RTU=I}NL*!xkV)%f|;evkEyMnHee(N>KMK5NLj<6lFV$?0pE&7!Q58+BQMv=szC8 z1T?tgro4Y2yQriP@a_11eV6eUY&^8av%Vqx9fQZVSO#Ym$#Z)lLV#!>wm6$=VO z8gVlXW@zB_?XA1>_J^>D!50j6OE%nut&NJ!8!8(+hlSR(Tf0~2*51bRN$xipn`hm- zyi%zNqa!04(b#2&`?dH}v4ER-8`ilOJj-GT=1MojqunR{y5%{=S>h>IlAoWaiKg=t zy~@!JkVJha#@|q)eBqG@;Lgg~}-& zwvWW~2lylIJ|;Kn@XP(z@hy2xZUAb7%LMGs6LC+BN5d*+PVs8|-V^N@N2Clv=Q&J=@t{kN)?e2`$o?;QLY`#4|R7&%Tw2XZBufbtj{qR zfY(5uAdwAh9VHi}H>h`pG)ZZQBKgCNF;h%w(!Ke74?wtl@=fu5tUy%kEflUHE{ejB zL+7QR?5XY*)O2vTdwGCF1GDV$OF6`ahm4H0)tTlhKzv5Pum_!fTsOQ}0~kFL8%UVx zVK+a6iFA@rFqF?I5&N^Y8#0*!$B8R|^fOCRhfKnPBwrp|tc?tt0IuU)9~m!zHFKu~ z;3MmUogYN0L$G~JD~vBdv;ja~>-Ys5M3UC9k|B9@huElS%9Nxm2=-#ZB6>mT3J&DM zDgo{!Q^JHm3+j_x`pjRg!T<-Oq$WtcI5UE?n5(I&7s8-1QE1@Tqh z7qiGpcv%&2M_<@o1&(Z13<}%hI)6o$_QPgHlp$WmEV^xr?DLUmrMD{&Jx$&({Rw-6 zMrc^|gp)Gh5xgqnnvqq!9?m2rE0)GA`c|8J9U{*D0Qfi0h%_1KxDy( z0peFGeG>@+66hoZr;$K&G&!aYk;v_Jtp<`K$?9fz7NKNITAl%BA;yMD2oMwe1fB!g zY1ti6-BJUK8ni7hKD`bh{4|>jc#WsvLUPPwHXBTx*eDkS(=87fBtVrVz}Lh5x2ia` zuS3hHD-4KNR-L=Gzy$USvUCJ2T#htXfKx2WUC1N?s_WALucdVnh*@zSR$aOY+Av!> z#7*8tHiWz+!bVU@0*|DsG=4F`ql3j0Gu1%!(#GqtiC>w72~ykCJ8$xiJKRSkvAJH= z*SHBVuC`$A6{GMaB7Vvl0sF)iSt(DQ8euU=@#AAdC_f++D$d*E!7rl#7-O;2&p;#t zmFHI=W%^98F}6@VGuj0OU@Fvoeg3azk^K8Fh(NpgS^LT91V4}Lc#u-F_*|3EOFvbd z3bwri+YU8vo7)11gyg8T#?L)p5+5f~DQO-!!)L?VTeCRHujWFpJy6{qnVc4OwFG2cK>9=;)#4cL+{uQzVbi ztf$VxykjS&Hi6=-G|S8{Z;vXvK9YNAPs(l#ef;{sm^o!`KFg0PFZ^Uc$&i+0iaG7K zfS|Si7;nO~m$M`;n55U?bpqq#C{u0|!&4EPu+q(8x*Sja@_-6cRuvD6AQMF2u*ibJ zR{teGrCu}4Db^}CJepNbr;X&wbNLJoZA7TA#`dVO+a}+Zm133RHklbRJn(qgi56;` zR{m*nGZFV!!d=-LNovPe$rVi4^bpoZ+nwEztp1{4G1jc02-eVm^u}V<$QAh9At5DU z!Ob+_Xi=uy-T%gK+;E@%p8vzjJH7uOmqVSG-f}8GYAg4w{gS^L8enEw z(vsSiY0ukfqc>fGG515$HAaJN;+w6HU(7x9Z$jaF>IF>AWkHdN!X3|&0n2*w`SY93 zY(B9er~Gukrl}z!rN6K1tju+YJU=mxop51 zki0IKJo}v{UeK7^Qssbslc#-&dHoH^LAH9K@?F+_7mB!*c1JLpJov|fL@;)gVJ1{y z_p;=?-PLCfSF*Lyxpmj}Y|+Q2s8raf24s9LjUCNFOVw;h4*dIh73F?&{y>w1dvBBgFI39IDt%f-@?$DTD#>;;b4U_3&;9Ln!Yzh=T5XLXZ>yh5Jx**8Ezc=SaP z^PTBGO!i=8B;nBg5PfC;g(E9)L!51g{t3|btv|A{ z@E!5)-&UP`i%v}w^ej9%-}>k!lQzkJvo@Sj%Wxf4*q1lxK0V2P?UmhP^;gog4pW~v z{iOXa`Cg?>N7nL8=n|D4u*@pABk~=`zdg+x0Sya4reuzysY4Tp)T~!-vH%T&-j9aH z25+8nhhmrGa4X*`pLSJi7Jal{MXNK5?)zZ7d|Pkz_MY z%63jmjCFNH{IezY_D#js7seDuEzcQ6K5JR8KD|81tsLEaZs*W5Hco#_@pCV}sIRZE zFXFak{nxOEphc&LDi;na9h7aqEDg!LoKu?u@1p&>-j+g^Qr2Ej&5{pLd#I7eO)%{(>)^Y1Qt+oVKOC?kg{mJR!U|FZ1qpaM#=E>d9oE5nSdY zCDi3(Y-4}svwr)|=QeGBdriKR_iVI`ov>k-MITLmj0_ho2m^|nJ-Tuu-!HX4N!QiH zb8u_w-b`kwsl7&NTO5~K<$tFc$i8k~+h*FvlAW#SI+x}BYNFK&@08GIU6d|Wtu<} z7amzGuarmkWHfe$`DRzzIm!tx4P)~P?_0a+t{abX>hACCJQKD}(L4JD{mFGJEckyF0KxS1wR z!#7#)u^hni(i+(k8E(H-v1bwyPo8xvY9aE{1KYk@)PX4ryP(2IK5g`^A@2caE=o5e zD1hDbdTWh%*HnEl*V1mWShkwS)i~|=+x3Pm`?4;}ZRS=~#kv^9MKBaQXm_}-tOAvE zOIL^VtYs~O?Fr+igMH-Pv{(CTw2pTL_vH_={h$|1=~+0cl0c#SEErcM^`f zzG9={xr#3Y*X6!gQNeqPsgXaOcT>Ld;SrYI)&f3#?1aXO)F(TJ_aTm8p}f1XtQ<%V zi#B{K{}Bge@{S8*(lflq(cYtVmy;VgTTMV}4#QEH& zs*a-63C>N(o2Qc9UY|~ZK+(&K7u$$=*476>t0)?L9KTahZAgK|wLQ$B8VPJ&S92)7 zz|Ce6=m}@1|MbP9*AH&^S)V0#j{LgFV9b@qHhF4Mz<9cv)jH=H39fDZN2Rp_(zqvj z@MYoogQ`%1zFOE=20J4*$Z)wcA|>$aLh2I8mUNes7_&X{r8(>ZN&CRJ9tNAwD}WZU z8PRT*r@(B%1qPc*F>l7LOd(U4i{GOgq|+GgVj1I!gWPH4snVRa29p6xY+=qnj8R;F z_sWG-e^}%Z_tg*G z+pmO&v7fnB@;EakHw$M8kkg6cL$pki6NhtWA0Ju0jDZ~~))UHDqq4ihLHv6BXN2?a zxTccNZA}fu#}ME??<1MNmN&aF?1)D<>w2ePtW^^C&c%2dCP?9O=difkkc@4e7ypD#$>AJuh^ydAY|P|wcv%X5=^2jcYCk8kFAj?0>_7W}l`cxsDys`1krwol6~M547&g^#-S9+`^R~oq}o~ly2dY1f@i9=fbcd zkqEPW?b8H+G=zJZ%e}_o|C13MV>g;WDP|4I}7NvG53PA8qvID=% zaKwp^&TKGWIGt=fw_`30h#nBkqP9F+H@zB2VgSRgdlphy3Oy$0G zU1T5vzl}OLj583|>3LjOcaVbln!(?YCTu&zTAwL`lqNVO_)7>REMKl$ZdzW^4WQc&+^jEjCC~JSZzM7Z z3*Q_4&oB!RL{aXApZGoL_Xx#EavRZ1Pg`Bl@+PK`Of>-MXu&)&z@$!oUdxl?v&3sO{n z9$sG8roTGxq1C&;V-4-!3#XhzSFDx|by{B_cb{xe{(9tUb#dG*q%Iyc&Yw5I;IOqo zUtlmUNnDDt%#MdPFZLQ#&dU-_nKefZWg5&t7L+f;WXc^MKO zKUyKNaP^;;OG%SrbBPyr6C zpwWe_znC80(@+-|C@Y=l!C>E%4Xeyk*43Km%+mrP5fkQPM``K~llL#Eo8QXJ)%fnl zII@rRc=O(>E_y-M(Pz51?XO0J>-1_p_l7T5%^U((Tl7VRzeQ*TJ2Kn%7L!j%aM*-~Oa_gk;(r zihQP-)eQHXCHH5MMY9P#Qa(Z zXN*WLq8>=2B1U+jf552u!)o8d1wjp@q4dY_m89aHZ?dmEE$#Gd9f}HMHxbE)6QZtf zj6F4RqC-~qiA8qvMeSC{lvW|fYuSg zgvb7R1=zsNg_b$CXO}lS2E$!{>Z^>BrwO|YD+>v(=?oVGL zgVs!++83uE=ujq=1&*s;PknA_Pbu>={1fzAb~i9T?unYy^^D?H($Foi%GX0Up~0`} zK}x~N&F`Iz$qjJN!)f(_HRXO4qyzp2UvKyAYb85QTFR6uY>P12a56!Fk_`lzhi)xl z-xxi`Y%liOve3)$=wuEx!(W;D-n)5P_tM-3$0T5qK33M*PM3<0zdL8N_)tRS8b3o& zyN_DmTrYS;mLw@=D7)?}ze0^1>9xle3cGNTgsHHjmM+|6*JrMkd{vJ^X`oa~N%?$w zy1)wf<^D*hLV!Xk+&xYB;!+X6u)>P%L^bo#>e0zByUu@-)(&7Ti_85-^P|(l9jO;~ z?6K&Pom}ScyWyOwpTIJe1vvuEC$`;f{Zg9i@%F&j%?Zc3fj(o=d=aeki4&@hEZXO6 z$DnD|@zxi~B+ko+^}ii0{eZpy0#_NB_cO>n5C> zij74MmyI40kjltw9IMR9acFjGqrR;*e_KC#&oRd=#8InYp&$;fpqSmeb8qUcR_{CvKmX(Q<>n`XF;<#QGC zE14m+!;y*sf{MCX2?ws`W=2Kry)qqQ=pqbN>s+HS{6g4grNGh3#$&RN6YqVQ(B_kh zm2uHsQ0ANR_i+2X)%82wigvE^C>`+ei(4;M{RMtWV^~QL`WX)%spi?moV+lv=CDuQ z`N^Dn3A;*1=t(UWG>fJKBBn*I0|%!ibgA{e<8$FRrIcYe^-;CL9*e92#|hn-QJ)FY z^ghQ6%3@kqeZZk78@}}VRc4pj1_Z4s@7)^=GT+wIoSK6Nx+_kuXqvaT@T*>bQ)6q& zSh-nn$}{nbd!=Whj##WQ_o*EeFAuYMqH43gK*wm!fP2;i+yji%lK^`hiR%8I5a1BU z1dm;Eb3Xk2jLF{d!RuK5>sXbtxjju^SBY2c5{bO$#DpV{5;mZIqy{;@39k-+E!TZQ zvuS*lUG!q@G3{d+J>!xuUwFK(-?rYWgceSUG>(XRqf{Ev1in6$xTD)d2ICifDL^+$ zY;SxF=#bq)>8XC3%iqMBP>Vn)uoZo_TmVJW!^x33RDhP(jzTC=tKoM@X>2 z3uXw)^g!lk-FrU34R1uo{os7=WWH_p2|pTm+_g$&P!bMh`NBsE;K(Uw`QP5%hZQ6M zaF#8*k>tCE0^s{N6F6;NDsBNLo1L_2R4QP=f54)Y2kQar^t7kd~byMaktBNuy{hwCRLCCGQLUlndye&ed?UUy}Zx$&~C*$)TvbTfCJzT@8s zccmoVn6u0OS)|9?Zws7Potd1oyUKWlOlRaPfzdl_?+5fLkSN>lCjU69o==i?P3W^A zk-}|R8^p*J3(gFMKRcNxntD=tbSC^$ZhyxHU@R9 zvWDTo&6ol!oZiyd1Xu?{rZRm1xIOx)VRWw*0JZE|dZIM=Jd#;L10UlKX{uZJtSO~{ zWkdlVcracBLtF@q%ZZdwt^*rA8u(_p@I;%hJY3x0aA*TuRI_IF8kx{yH+ZfHWiNbE z5EB_v5@YTrgUl>?3JsWq!j+N~K?=}W>{U{kv`L-}?6aRSza9{;GMKdZN^9#p!4747 zcutbr5?tP%6UEo!_;L@+v|ob1gq}S%P~KK{s^#qHbf%W$l#qaqW|(OQiCo=dCsckx z$gz`P^s|^>>EcgE)$%c^W#vLE>`xjfb<5qbKlRygE&LRy+=n^dm38-Uo;1YT+s4r~_xYsqv>f}|gM-GTevHXxlAA``pR0~ZNt|=veoU>$xDLK#cpBH*e9rx% zZYPuaxo`cq@MV?D&o6s*S%>8rLx(HUhgJ@y?Uvp>tu=k=sF34VcNsS&Ie|-Sll%pe z_OE?mb=BUMzx8qIhgWdXrNRvX#gbbR3m>HtFCU04=4($0^f5gDNLd1|Dvy+%+$sK0 zmdTJaIH%9|!Qale8#vJO*8O$Af)s#hGepvcBO6PrjOs#)R7{JRqlO5 zioMRwHF~}Kw%0NpbX<4!?mBR>E9H_!EybE(@0Qw5RvH#+E}Jri%aaehJ0Ey{PRfJI zWD^JBx@TAS$2DrI+gW{#dtvV(rYpPoGRrf=$6ZYKUf5uVT9*&IZWLVgIzGSH$ISk; zVvOvYA410-=T#*aYlyqJSM4ZT*~E67T)2m_x)d(S|Bvi>RtJf>O{?d{nT84lLpM3N zRYf9QTGFT|S|7eG4DX4s`8i!{1;#fg7Pci?m(1M|{@v}4cAWCw+v;}P`r_Gp z3$v1aa{oA$c}T4zI3%${tz*?iwPn&~6FpHI*p_m{({o~{ji|#lky;zLlMU$X40xbat)*(TuY!G!xyRpZyE0=R2_uO_(13K`9JMP2Csoo5m=; zY!Oa+bb7H{xQ>YQbB{`KxYd;#+(D;R-Cp;*+dDB!ZT0` zW%0htavN&TGsY`Bp0GqI#S64XK9f$80QH2qqcXxi9KDp8hl-M>}UTW*18AxtL|@*>tC^Vht8!gxD!-A?BDC` z&0fihye<%S4V0c26(=MsAoFD)U`H(3^69P}aLCU{z283Ick55vRP}(qKUJdV$VH6M zq5|MdCsXL%f3cRy|3@zV6f)v~Y9}sk2-_Y4zvk&H&VCZUBwM&M zVia^+jRY_5s}okap?Ygi4V}W$_kEP~c3RVMUgX2U!^{2jzHKKt&I2IB8SZ|n_tCgu zgry_Xw(v*bvaTl-GE26oW}Do-(dN3yal^{g2SJlg!Ci{*E%Jf2%HGNEPQ3o($`;Ki zjWo@75p=!3;D<~s`N!hXeOqKcPc$Arr~-D7$bw@x;OAlI>7G9jSgQP60WL}e@J5xhz|6te*EEnDvG@xqVB(kACU32*b0`{pM&Dpst2 z+HA=b+%V9qOyAJ)h~Xvr(yu40-YC>`@3W3ywQgu@YkTNwF|%ut3RM-My93~a&aMyi z1#W&#sgWP^j7rZ3&)pNSygXpTC289VyC45?oja`>ey=1fdqh?@KxjC?ZRF! zSC$^pZ~=Z#$rjjndXts&fx+m8%`sM*qXQ?J2dXJSW2e}Hxlu!g&0li@N0uyvYw@0k z$=&z8D19-YT9e}BKHysU!c)bdTwh+ZyXLc2=%8cCLcNfm9a}J%z zUi~Ryeyx4&#VaXA7X(M)cBmWWY7X6Qy{N0(be3H{VplTUKBP6$6w>)qsj`FmJ}aMQ z7uR18`seK(98C=;wNR!yx7+IpwP-lNDRm(GMnq(!spV^;=LdcGKh0P@%~R*$oPpB0 zkJqb2)h&N~N8;5fr}Z1yo5c3_0g(aTUWkm`Av-_ zwGDbH0X;7aRUU^-ZPvcNjwKwWzgA8EzM+=Fc=UCe7->!?j}{3W_tY)MAy z=PqBJ6%^1tZa)Rb(PJG2TS95c6lT#0vU?Wor}To(?F=eQU*9P0q%MUFMhoa*bUqxs z?1uggck|xSO-5ET)USi@+~Jhu5w|q=b_!TwT(jZ9j?jz^$TpJXDJI+!j67jYP~A>o z?V(Ya(iTm9lxdBn0_A~AN4==zsmhJDk1xKbIE&YkPDLPADp0G^x(CZjT&+<{C; zdtB#HgQWu9e)*sbo>gY94ad_O5{i47wn>?@t%*txM74u_Ky0Nj_f{hRCcv@Qy0XaEd2 zVj6Hyh8W#)RM10#^LFeo&&vSp3VPMGLkL{M@Nf2hX0XP>C^I zueC04bZG8B&dW1&M5P|@%bj42qN=enR0ys{=39c59&NkEQHSmthJmo}#^6^w0jG}%ROsuI=uL}6PAwz9Uhrvmmx zd-mh&*-`huyJXO&qYLP@Jzai=5!0~?7pS~1v@&$72dn@3S~m3Pfba!c*4T!)`R5ql zwvq`#`jefbgsKVI;J&phd?nhubgqf(C$=v$ig*|@oUNiaci``-(s>VrrOjE}_qUuW zmZ1{_8F0D9HA;%*^|1RgE}D#y03WqKMkXTOpVn|q3G?pQY;iB(T!}$UUUtyc@Blx< zc)8?nwi=b>Z$)t{m{udp1_EAb(VDv#!u=dg)c-jDB_ru`Wom|x-O4}2;gTpen|@m= zb2N3|J5pmB7%wuXS(J+Jd;0v{89Hh zB-D1ASxIaU+_T(TeE%O&53Ne4#d`T)t&>~-=t<)T(W}(%G}qfkLUv0jtIT9;RXqEa ziE5^Q61`-yAZPK-Rlurvv))?4y4Op$3Z!jQ^dM9=CUi+G@A{}+|DiQJ+o8dHC+m-S+@TK8@Lwq`G39){J2xr zAC$k-`Aa?#GYO_DU$x5pT=2#dMe)C z&mc5u6<)7dcOpnC`s@iHEZZOak93IC)aq>{ZTj}9mc-zlTby>6YV3}C)-0#^%`|G^ z&4x*bPREuaLf`W{8pjm*7RIMB)aNuMMtVkTT}qlG5cy*B{kpE)6NV3-`?nXajbHj> zi;S6XQbud}pYXSMHAx8jgQO53)x8}`^6 zrDqtQ-Evz$eORthPF9h4IXq?cp0)Qa_+oeR8S*`Ppjfs{BkF5yqxq+aXmLI36aA_C zw|A*dW)p@|^!}>Y=KPJ+b;wS0PiwyFft93}$`lKKmqeSeWmRg-OzFx8r(fUJbUdqE zm-Ts4#JsPjtE+QVCno%g)2PbpKWYKFwrz3gQVEN1;h8r|C{x_@Dz;xc*1!7M>ie9KP)Vo?2Y%Bdd@bHr14a;uEikx!D zkEa%Y*k<8Z;_C|{uYp09`?r*NnSc2VHHaMjvpq=Mv3R?ngG5Vkn&#GRT^f@Dky*R* z&18-W?%KCl!NK=Zg21=^+wWE_@qbLe`86?e{>y&`R;C?J6Dj5EyW95SK<53&C50?X zYSqDW8nLpZ@vmZ~1qNYcop*_O8Y1&#K5Y6+^@Ly%B|XvkJu_iXx376(-o}*LPy57{ z%&An=I{Jrknev-FkQ8YmA8jI^9%&+|-YwF!`G*ZdQZC6e{7ToG08^QBo7|XsNcjz! zXj23%+S++eOwddnlKCL*Px$TejeUJ~HlOwyB2qpoC!ggi@6XW|Lf#m%i#DNnC(TwGqJTLR7Fsw% zTofYMvpk_F$Q6-OfYe2~1R4Y}9ydWdi&CjcSRPtkh=f*Z4QS^=$s?Q~XtXIU3j;+| zkB~=!Zva1d0TXIP8SFaR3=)8P$M0~KGSSwc-V}E>OmN7}-;7;^P{(1D;NLW8&ABAi2B=5t~%RKr&IFnbMf37G=g^UfHv1Ph1c=PWAZE#|G9HRmcb0hGfI$#NWZ-xQW-Bt_#2};$8e0M4&Y?gr z#VEsHh=tO>LBT75lpq{X{ZUxkcwk{}8pI?6J{r$DJY9NRBBnIQSb0n~MVAQ8~gv0cjT$mZga$)gCXLW?T^*g0@ai{#2ZyoLuA{HX392t1{Ki$wdT!2?+^3T(Y>!!2*Tx z881NKu*Abkl#|Ep1!lVkr(TvEIHU5>Ox#nnY)er!X@%L&4BS&hak@3~6w%{4w3m^y#QE@o@s8Iy|Mp zzZBT#;2A{ZLKMB_(G60FG4!Qipim|x0~_~5n)TxvPeLH^_|DTBv0UVrYa1emBT~D6 zcQ}Z0esvkt$9Sl68W40agtTus!Xp_{io{gjn(^qI6F@l?;3&L52|>gZ4gaRi!bhkW?~Ia;*jrND`FL<}9RE8!+>1SNnzb|Y74Tlkl1 z5kh1I+$%k=rg~u7gyZ`jzB049F*H#qFv5!6~hS!2yaWSbicDK zXC%OD1t=7?o{I%9Fg*|z#<^WKSFT7Cg;A`5&4dEf4#?;ca-XTL6_SnlH!3)X1cX5A zz^xYE1Nd{FZ*Xu-8K}v5gu|F2`o`M<5z!S&?AIWJ6h!j_1}A!BLlh(v)hvpL*A?cW zLptQ|OvOZzTRd`bIsg38<>$BCkK;Ua9C6_dHyzc?}~ogP%- zEMDleSQ^K@XC#2i_Q!m71{~KL;vM)U4?LnRc&i5+mnAowGfsp$AfVMlg}|I{MyaSv zVLppNarBPan^!NQ2x&yINmwp=k99ROfy)xH1j-5PS!Y}e6%AvLKG|Y|VqS|nJvzyR zqGCm8rm;N2hR5-_4u=5|p-fOCzc`#FEc{j7s8%8LpK<^`2k}G=Q6hZcy@48shYvy? z3NxE9UUP0=G0PXAyaXr=_PmG+0PkXuCo#x64d!z#ht9Dgcbq9oR7-QXRYTaMVUZHX zbchOV$fFo!kcT?~91A0a(WC;B;T4e+3GiIJC17`)TBDN@XGXcf>~TtJR{VYCS-88{b) zJSKDH#fe$`w*8axtPQ9-K2V42Cgvhf&vzy8sx}!4O<5p2^SR3W*kBE=MLa z(E$+$7E>O%x1Bx9SukQ3WGxY4X7NN2eZOYYlA?(m#d0{C(TIR&;Kf0V*-pj18J7@M zhMr%T+z8qj@L;EhX-0uX!C!T9_$DD}WTd6x`0yf06uDEFTA@C2!;)Kodky?XLtd0P z1D%FmnjsVITu{!%Q!&FDLm?<1;g>RhqH>%Nl>uMG;-DNh%F*jsoUt>a0U`&}Q2M>% zn0|jKhcpKf_7gM~sca@2ZwP!aRLzQ29Er(dqLqkW!-Uj`Yet1$%4|hqFk|Adx--;q zDlrkPU+n{7hKRHEh@t77)5EhlPmmZ971_I7a8C7MiAvGzL0>cx1)ma7F5%W0xxmzY zVcz)fq<*COYiEU^=*bUMa+Hf#L==_rrZmzTaaTce5O9-I7ek?qFiu1q0TzfF!$s;_ zfiMa))A|q$m3H*y9bS@z|1}+2e5$`5DgPz;4*x@$$w$nAB}%WPFZX8V;<|1;qY{TS%FN-71vLrF zK~#iaMw_)%J+wnT37tfkX~%;N7!?B7-lB*M3ez9FxwWIkB~=h3`BHPMKk{4&`7R_T=T9E)6(DQ6VGkP3NP@npl^b4ql8qc*$yv)yyyQ*8bn&$|_A^1Ql?mX+RVLd^`sX1Q+k` zI0FN0Y~_!S1vt+NSnf>i5JWiO!fQJtLbQn=8hN-@z#z~^YuM;#!y_wIX6L}$-#rCm zFZm%TUIhY%#tTa1%D^KkYyh|*6kLmt2I7V|0osg(a8PIoo?$m+hpHXE@fDE<6%Y{( z*l3@E@l$E4$4SC&22Ie`#&GD&>k`PzaRi(;6Gq@Y1jllDLGip_r&;jm;)f`l#}QB? zvHTz%j+>(R2xT5EaADZpIW0$&<2>+DgVRt@<0c_wp$s$z{DVhrGzT&A zEGuB-VS{br`U2D%M{zvQp%Bm#cM=r}LY9+=_yrBI7zdHlcl|Pjhrwbq4;*}4K3=!w z(&mv0x@-~_FCra~_tG4g$QgXjz>cFyL<6wh&D>hzNIZVeQ6+R@z(7Uf8WIJDF@?%~ zIgU;rV6KL~cq1TE%)sMQsc7SZKSW3l@dQz`CT?tl7`srq2tSyMltaa0Ylsl#A%hFN zbz&4?G4}3I6vN{_tIW0y0b!Sfv{U$l;J|8QkDk!{h-oa*+(>ADL}8*38VOW*1V$Eb z;An^d_$9o6*@z2^gZi0V1%y@_CMp6+gz%_qpj6RkQ&@_$ZczjmsSOf6U>*d<6IySm z0BlZ)n~%WrMf__u0)omDfG}`$#v8FOf+Wf%0r{eqB+iZrGT5A95=Q_`64x1Z;RGxp zEi?9i$^17fPmy)y|KrNj^_u^GS$V4XBjmikXLuA`v3a29?wLo}CBAqws(!N(%ymjS zdu1pj^N7=|u!QP{C|}5RC9ZN!5F%Cx0EL+Zn-DVYK|@9)Dkb7!L|BM*k;nz4i#0O>&K{PTpbiaN6l`0lkO&~w z*3oDOmpyTTMIk}F@p3~G5ot!qOf#~|P(Jz|P7rZzm;h=XWD%1kdXH#W0YGW-;Ufkm z3W;Fva-i-pIQR=eWfDM=D3XC=!%Ikt!iIvN(((ul17k`)1u7^_qeX1 zUU1VP?=d)1K)uW!O27g*rb9+wzb0;2<+T?V616XmGP@VUC=obBMQ72)KzS*`Vr(pd zz7FRR1MS367n`Agp<|7jM_>w~fL}oIVcKj~k-Q5Nak2^~yMs~3B5?GHP(~L*;(DFs zTfa3T;za*)ETmquVjl)E(>bRhwM7Nwwpe4H_C{3TS!9PQhKR&1GOHe!oTDr~h{UxP z9(5pLg_+D7Xu!i_wh9~N%cDND!A&J$IXu2%Y5J>Yp&%>&<=cons11+{OH?|vTK?o| zON=Z5BaV0^QNd6ek7W=VPiacV+;O!vCz}W(A7`*lz?~T)pr-j(X9pS1XqHPC%7sJD zxILWgGXv=y&OD&|e%B1vfTG`ko<%@^wYUNq=f&GN(5Qj9!7TsGLOqUCQJjH-HJl@s zM76B=thQlGb%$=LPTr^f3RSN z0VDaKJ5Bx}yv6m1xTF;xC19>4yu}jn|6+3v99oAM;EAJg@S)3?jduT%iQJm?w-J7m z4X`=m*(Jz;W0q-=VuBGEgu>0#7MCRE*VR6#D6^lQ(JbiXwBHf_Wsw3tP(geg;{}-_ z_{eiP5)qI&y_trC6ryp0w=E#>SOCUl#G@bqX-=~o8KtbkX7HL7ng)r+aPizPI)E%< z=(ro8h&c5~GRyK`YJp;r>k+wmk{ei0#|01+=raDuG{y@abexnDKA4(S zg1&{Fm4kBrcQ+;qsX%#o#ljsOY8)wqiO54}boP2!6j6b9vJ62$h5uKmB0_rq765_& z7idx%8dG@qB2=LfW*+}hKA3ECxC1l{3hhIn8(?>{eii629^j}PV5nV47WD%R6Lwf> z5J*O!!mhwQfrua>?G+{kw$OUwJH>^bxutH8` zfGR_!l|g?wJTH)gkidrCT5?2+0f*QBb+EU^3idA04@D8}>{p`YvK4628_gkg$bm6q0Qa}x{s%wWNB*NT0f0MaP;rLC3J=@k5B~rSdZ|r~ zV%kBJJqX8uFd_2Bw!w5W>uzw0n zCc(|=z*}obn!WKo%@uA}(Gt2>6M*lx^FEYOQ)m;rMP7fS4J3UMp?r`Y=l7ZXtK4$_ z#z$M2E25V=#hz}eB@LHKb?y4GT%SC>ao^U%{)Gcyn-@ziS-$8E_;lM}2%f7ky>6IihMQ4d6k~o zdsgBoJhRem_^H%HXY}&KYWQ8ME1%RoaO*}Rb>Q1soya9Ca={139(CH(k;o>wsO2lE z{T2=}108Es=DSb(dQWxopCC>3SY;LQM;-9#JyvntuOP!az571gJS9jtXt9up^f^TK z>4)mUEec2Vg9Ym(o;W*=zSnpspkm_p>F1*ueQSwV_g{P8k-7EqSy4**Ey3$u(bgl* z=eCB7e_!(UnNH2x=+qTVa`YmdtVPf2cb3YJCJD>xTuu>LLdbI1U9rNoai}0&n&d>K{igB-2{N3+_bSs9X3_HYBXlH`<^h!{)O7f6fO?yII(3 zY*{k=%vG{^Tcae?MMTPa{=!`ae5$)bva0&#eht_Z)p#o#n_*zCbc2zjgF{&W)w{G{1oe5uPTePNn#a*@ek87|cc3yMoZ^2F#SeS&L@u>!=I*VLveXai4k^Icu{$z<)j2oylqJYi@ za!;u)n_9fN_475FRco6z!N{sLSI=Fb_Q$gzQT1=nRg*qn={LL>+iB#r)W}P?R+e;` zFLWS4FXoD&+}x8|L%lN2Hr1;Py{`)8b$+<3AoqxFOWt2IEmmQ5ZZv33ZMk$r_d0lZ z(qghJqyC9PhU?hvvH1nJ5|aiB+7uh>+6~KClAncTH&q(^`RCCJ;%(V0;cs$Zp1A$@ zT#q~Nemvg1X=#)sVb{Rcyy5%(-D9HRYnrB}w4Te6_&TX~ShUv3#$sn(FlZM6il<#E zEuSczP5wEfY5f-U=EWN{RLf!(oV)LockWmq)J}XRsd^Z{zEY}HpdDdBEGo#m9L)f(-Ap6?l0`dBhOwbCqX=T}DOpD(ov1z5G z>1_`k@;h6{L18DmnA+motmvZQIuh;a?02nZk+aLVQAw$i`jfi0L6@3sb!~|r(Byo! zQVUgAl&_KtJa|Q*Jyw02rbx3*NJzRzh2?Qeko-fpT19BgV{U(LsKg2z*@XEtLkb13 z!+QW<`M@O+@UbTTrZhZYwVVn*`J5|BX8~%zX^%a{=S19^VZ#EKq*j8_9wRTiybJw` zTO`X3z0$8P`VumvUYl(b@>XJ(zHeZh2M}DBKg60kX7A^IRy85tlb=bgrT|K^J2kmW zg~peOGjt<&DH~{zT z21-74kbt!xsX3F_7q>x&d=SF633^}nbZct{Rb zDc3@48}b?=QdS=}x+t0K+OnI>%y*^4(#a)&IzsE|u2!jMfr$(qT4tX2)ZocR50UEE z>&XX0y7!V?GXvub-tQ4#RPWmMVoPYqadOVy%zLeRV|=M6sPq=ImI4~=H`mxsO?c8K zo0Sb6I}*=@gg&*|dS7m{&>uS`9Ma!No;clRut9wJ#S4oH#b3VAdYE~RmK{K@^`g1m z0aJ{WwJY>KM^A)%S8ZP`~~MZc-clc zJ6()@_ub{)xl?f86Z3+_q!IdL$)E?yWy z5Yz=)8b6UG_tuXRih*4&IlJEcfkCaaz_6}?z}G`t7C!jr2{g^Ib?6e2 zB3{~PJ%Wf*tA5tSKdkQDzBOb>b==dV<%N!-@X?pTj(ym_Aw%<=f|1Qc}98pY7v4E%riXaJNHu7LEOY9aKNa z+IMJK>f%A~c|PMU@HAu}?SU(#$u6I>aS4Dq{+(L=ZO`X#;b|N1-*QSCi1QY^y@)=> zx7dI6l+s-lGd4VT8PHhEDIr@!ruFr$^i=~50-JKTY`nirGO6Wr5!D*_ul`svekP%x zndRX#`EKR#t2w1RuI^cT_PEE6?fS;zne+(*?Y;2CCFN%s`&!ur48Nuk*%k2%&D|BZ zggnXnxlw6#Sb@@-<7xd1?k=8b?t_oDWb=NV)+1l#y`#FNmNxkwNq@l$4^;TY8iux}P zDUn@Ljq41(9+J#!Dehx)z{SE4OmYJD;tqk;r5xG~U=CjJvX0b#aB^cr@(;drv z^KKuEH6A1^nObGgc|Lw;A$h^l%&D{1?UsT}z&J3-=g3bCl#m(PbVvP`mqZi~yMr03*8>TW`m%yP}a#OY%#8@@eN>l!rl3JUMzxAIOD z6PDE#4%?X>w2VN)7nOfMG=4QKbA5rRwCZPw8^7c_sZoVJmd(H$B6^MZfMmGbY3 z)l0iqCh$*C)kkSLk&waLkHPZ?nG+wEgBPMK^h^CBuTSS!ybGB!qt$FHiPF0LRbffFZGO8?9@ zFk%M}&F7CbTE8E!{0B+OefBbzeZ-jr z5Kr#iHV^?fU4zdAa6s8OS@rhHZS#Bmtu~Gn^3-$-h2ci~*(gs|*V9RA=n| z7Ms2-j5tq<^g3ipA^5qXL6q(pxnpgMy3e&-o-=a4yFXH?D0AwvOlJ;-V067*=3SiG z#KqOEFSnW5%W2oj>g*z4U(%zSyO+J_@~xK^lEIB5@x*njcd}IDh1d5!>#~1P1o}+0 zF1?_RCy#FJVXD_9JSg`Xt@Mg>v)+@V)3=XlU4QxHZKhAL%EgZ|M>+$~C|57Jt^DTY z+YXi62UBm2IH~{r(d&XvC}rB9t@^D@>XC1naaOTS9bzsE5+)-0{H~0NUFqXDl4fcR z1YHvOk~lX|_4X4z|Jac4B4S6v&EFl)?u;WoIWqTnjrx_k*sC8yUQpCQ<=Lju^atXD z%*%u9ti0Pt!}~NC8yOp~F6n#YqeC(ZdwGTFW)u_@PC~n%RTgDJ$Pjt>sDq}+$Ct)$ zULH@-On4`fa4T1&BI5~-o$+!XsSJMCGfo{ZR`F*xxq_bsE{!A;*jP;4FI()4Skhi> zA(?6v6d`To2c>++PmC2-OpyuTgXe;(i8~9l#~BU{4r_%yxKcx=TVxsSzj9Ze_Vk?! z;GddSyHGo_fx%Yk^{~FBPj>f8uicQ9`nElPI^fuHbSB=bZm=TsjnuK->51toJCy*vz&BDE=g$^7kh?S2<=qDT!^;5S)5#}(??!`qJA6QHh-M#H4AR*m zm4kX`f50O|H)}RfDihg-V0%BE>3jMqq2lG84P%V4HR0>>@I~`w|)i?o4S#w;tDI(P8)8A7Uh3eJ0}O z@HW{M_g>2gyJ*0{+B%<_n+2jC<{EFLT{U|3H`*{l^V>7)DYAF_vNG5-YO61;FS+Nt zX0_}T`^REu61%6jgLNj(;^T`L#STXGtI|Yg{~LiX)`je_bZtvNZt3l)(GXG?;=O!t z^ykW)y$R*~pk*w^s?D#6S={q8&GUVhQbeN2QkSJ|H#c>x&8g4bENB>)Qh&nWbA@fW z-J#5IrPjSm7p#jYvNLn2e_j!4y@p*w3~CULZ*Wm;RIE^YT$y*rI%l__Q&{gRN7=SL z0y1UJn;qwhN!YmF+??I8_H0Gyt0zxR3aknBdhl(#PL*Y0+R_E?10gTcE;ApExxiCk zo$Hf-{&4bG`r4AI_ZOe~(Fft)mR4nll~x_TDN|=e<*W1Li+B(*G&qr3V+ihBW$0wb zD&5Wadx@dJdYg2X9l<}Z1a?t^8@8<3ezLn>$N0j8Raw*nzKD>Qi_<O>bKm7x;n|_&wl(?UzT1xMRPbntw_s)-6$I6)KjRjEUR3*|pkGN; zLr2wk@S|D_95Eg%tGk;RD3x0yK|G7 z4ouT-I0~_~tflgvtUY!&j?}x-WbAI6;>Y_}#e^Nr#qW0g9PxJfN&m3OHql2+_=12X z98EuX9vPPVc1T6~@D2*~*-w)nJFm6um)w8FZF;CizDymAkJ?eC>CeO+;)~pSe8&$) z`?fJYyFAmd*->K@LD?G}r~yahD^d;~J#3%$c$NA~*V_g{)sOy`b7DH=Fg+_)@;)*hpFJk#|u7 zWyg+RjXz#<_~OTS^^k~rL%qW~VQ*rtbR@iadA`R)>4Ldx8tKq&6UCm~1Tu@#{-@j& zyA@EF%=E(bu%vwB;q@vK=cP6+cfR`9)^|@rLKrCl$5w0S=d-~s15&@9 zPk#Z8(hmy4*@1G!eQ-p5=ec%I`b4I!w9|WXM?d&b(l?(>+xd2LGtx2WUDnw0t=K&nGV@?b7L)po2m4Xf}Tu@UFl+Rc1aUBp;9AX z0#9vbCye!nub8m=$Z+hw^3Bb$rR!{XbMr_*FQsxSkzG`h4^QGSS!bWH(6j%YIQ2we znbC9VviS?9h!?$zn+=5Q^NI@Lsbk=jfq`G6*YZ7pU2^7KYWPVswns8SpI1;qOLE9;HCqP#Ds@Vl-l$l7Qno&&0*>|$U9)Kp3fLB#xyLbe(cOX@ z*%bCOS2sAjO8@-%DXE_PV@vVcA-xj=`o8W--a*YiR%>;XTWT!Mk=dsxAikC=34EkT z7BJZF`lq0IM|Hue_m=gJnHyJcCf+N9&lY+WYC^7BZmw;mSruLd(dO*p%w&HhU8nqk zUaizkOuG{dJ0G2Mmsu|TVBiXvc%-+SviTI4Oa<2to^ScE;&Ow|oS@_)x4qZQH<&k= zCv^lYhL6;-!LA10jCDy*Bc{xzUw#R_x=Zfa#!HK(C#{Y`k7QS$W%Vb)gO}-3lP47e zxAPrYu#kTuYju)j{eq3FL%jRc_IV}Hj}?f~$$C*!W@Iw8)r4|3LzDVL)nGgOx$qiI z@JilP{?!v5XXQoKe8*r^&P@8C+?3RLDDPT`w{Y{=g0WiP(#%S6!8MsdMPoPe-FgHS z>&N%cw@v-7-ynSdp>60#rzXdB>s}_b` zds|2F5@lhT+QVL_!u7pQ4M`cj%k8b!y>j%!*@2=55q7)9!p_H=4?kDUJLdws_%g9L z$GO7U0UK9%`Ie{@S^N;&;5wda@-Z>FJ?WgwKwaXiaOJg%Csi&`L&D{l7&9~1 zxV5z|HG+K3FHZz6xiTy-r`1(NVvY)z&>{vqEv%c8gY5Dxi1*%wEaY4AB)mq5Pd!Rn ze*URib&-(p8W;8R-*?}-|F+I|IR*GnkBoZ*$<15p?gk0FO2M$)arpts&?{KGOqFT1 zR>^VO(H8^>*2-~~B#a-!52!s^?-sM-R&O@h$k+lOFYG?4?!3Cd{8K*@P%>EltD9x- z!a&o>Zu8pEPqrfzu^XluPFw(Xggs1_Z+=q`$W53~0R=10HtnN|zg|X5W4*n~F2~bHPI}6y zX>5W8mMYubPkO@pGcs!H5lBlFOLtF4&m%IbD&|KHyE^`kg5{Aj?q^Rs9f3G%7!j-U zr;b3v&PQ~01&^FO3>R1mVnx_eOB;(r$BsC8>gd4TeKk}eCpCoA{~_+p?AdM%?c+ zbC!Fqyg%RH=k@*LSL56{GtV~9JoC)VIWu$qe?8q77nYE2o~ze-e>W(L7QOQGKaTf} zvoDHL)EoY$!hWl}V*H0TXKS5Vrivr9Y|oFrAfezc7JJ!h#TYo>*Qnhz>U*Gp^%9#? zX_GHBbf(zm{&*3-Fm|RxVtD5AP015t{c|h^Y78IN9_O&e=aiLHtfjJUeN?3U)Aq0W z^^K=b?AjEppYd__bkh|LH=>pv__4ZwlgoMcG+%4+sU~v$s}GuRu3A5?{P;y@`<1z4 zuODxyNO-)%uhOZY#Nk-VbrnHu^y?u{$BrBHtY)yIy56p?@~#+8w4;@5p3C4#exH8I za^#wBWpEaI$L+6HT+#IA{(;A0N6tKT^$WT1Y0<^0z8QOm9|Mr|WZ}_SqR~1$IbA|<2S#rId zB={w_%>0u38B4{L^*4JIrPUsCxATl-v5$4IW51`-#XslB9eE!Wdu9a3{KdedQ}f^T z36<44EFX6I;_QM_<7l@tsu#sOI}@6^<*qk1@4k|lvZg}Zck2zUK~G1Pr&gY-$Q_mu zyUJkfB?tF;4O`~OaQU~gCZGFkp5raIW4jsK_l1vX<|Otk>2FJN9TL8@`W`#??ulAn z%#`Uh#cBIlejX{)$d*-z5^>DimtJ<8{p80XKaxjaj8s`uxPVoclUHAf{)Hm%M!e*55w>f^|q%%GUFD-7)HM|ZB6ZI+@3^S%ZB zCaGVONcH)6q+8Xh;d0Rw-lg-X{Ki~l?v{`~Z~WxrV2Pk$EOJ2~U0 z&mX7iDWQ@9y8GWnm;1dNS*HB+>utYx3EktoI}GnQ++6rm63%V+Ikw8-n#RaT*Y=6K z4coJd>)ttNIfOZgSG`=dGcP%8Zm?XDqL}lP$bCiKn*_dUd2NSVM`=HGHNRTtm*xM! zWy9&s`fIzpO7<`Rnlb-i|Ls0@hgLt)y>jbrW8wQ^vyu|OD(R8du^)HrRXo@@*mB>e ze&(5XpDf}vG&ZKzg}Wyw$*Htlta|aQvp6g<$ih=;k9Bxa#NEFWN!o{OwCP3wO=7a8&y=OSzC{MWh2 z0>PAhZe>O$15Ql;Cwa`|Svfhs6)b~iPm+CnV2geG_3ZA)V$q4?G)vFaMk)y!ewbfQ z6bMvmS8r$&RPcAdg3oddnh&plj(pdB5iDR|%24YtPvy6UN4DL*`kk; z$(tuf{tCQU&TfXe$yKLzrUMusv-lUm~88bS{Md8HyIVMv+ zPaocIHl7Q&b;K*qSnN@B2N(Mc1Wq=0fR`b;!G^^uZS=+#NJRk_wNZ;Ta743( zT07HYg%%B`+)b~JVJuI#6 z-aJ}~S*vk-U@t;~pyf|W3|AIe z0#Hz(01k)%N>~NxNInS~1tOFqfw>K}f)c3!a&Rl8K>-ewS%XfkgG_SSA38RWh3@G< z+X!$YK&_w|o**CyP~{2%9LxbI3`-{RNkxKyuscs6xJy%IMi44Vs56T;NDdQ37zZ~- zwM3zeRs=Wln&3ctIx2l)DRMxl0g^~IZ~r=u4mnPakdabs13tN#L||qJIg#|Ho13lN2dL<~ z#3B8!U52>#L)s$)>+jwK+ajt5R4f#zzKuo_jUd4_d$>ae1RKRU;8{BVt|x{d2r7Z> zaTwUAnQ8%9diqot4oDzyiW1b8^4q`k8^a?(Ff3|oUr}=bsUV#Qq74wdB5ro21Z6gZ z1&(7JWEq6WaE3BP2SrfF3crXX(j)HFjghtT5gi0!m8`^}tOAmdg&=Q%of%=)010k} z3we-=t~jg_kOzmux>>Z%gT-MpcL#6)3OKgB6|f2J2$j#pxByHN!MEV(>sfH!G}>JP&NrmbzZc|_0k4G6p|zlsql2K zo{&@%Rz(crR8MgL6e^H4fGec;Gi@jcY@TAhAA3VlLu47nCb^IXPxPj^)`NwrAz@5t zi3p261Z$3&R7K+)bh|;&n-^qO6|BdkyFCVdVd=4J;`PW{fbQ8sAlUWxm+Dk{;6Ov) zCn_RgcK}A{kAs{+R=toaCV;IJNzB54aNW`US2uzoDMZ-F)6&_)Itj*9^TghwJE12l zLJq``L%^@e%Y(5TMHff-j5{P1)u3oF4!w0BL-Jy+xBOKJv^QlVX2^;KZJltXT8qD-i|a zm>gWE1T&P%LcP{2G9TX?tAe)_bvdZU-lEnC@}wd8F+muE;9nMG;GisMX$OMj9Zr)V za+n^jg53RtI(g896#*eHg4QPJ4gt4K<}p1t4F(d`a|u=$j3Z<ivrr8WKhz9IihPej9bNIXYz>Hp~Nj56(EkPc&g-;4$qrfMA zSrA1N540VoXB$I`VFnukonJhC&OEb7jW>6`UFd0X(pnxWX%3uG4 z<}BoqDLsJ^BpQ{0C%fDOgb&FOXdBY7Ia8?D_Lor51JWRfTFBS>Kf_GBEspVe(CcxV zC%8M_JoDGidV-C}m8p#N0>P?@Z%ca$E&?BG?V|JXY|W0ypxJIsLwmX%JmW`pi35r_ z&VIiC&`80_nr&Ne@p{O`M^GRDYDu_c2|?L4!49Uvy_=*csx(QI06Lgb;rG@&x)%EG zM`Asx_Fw{Ew=}Kj6u@_zdS(DEHpv)eJ?w|J%jnY&;xmrOWVa;leiu zIsB<18j{T=qO`DVr5h&%Zv{b_*M$>SpbXPmJUT}_P}7IrJt6mQt7VSEl`G|PG&vLj z-q4nznFB1~Am}OEmlj<3<5MsW;$#l-G$IBM4^Sc&fMcwbRZjKsxK*fGQ!N|F;uJV7 zV3?e&PXXB}BJ!w^^azqqKf@qgF~L$)ZEQ*i`neZo57mkqy;(i}p;ZFMx1eqY1mM~M zGvI^_kjH_GwGwfK2-ycifpD3yjZ#N=((K6RW2{(f6ClJKWny?l5ILi}kZA|XfLjoV z&LRvB64CJ$f@TE3Ll{^PBsmVKo>(aA0K*TpMc_aIfKO(US!|_P37le#$NxB=&?M2` zDH2$OJV65-PK*0ZZ1o5+M1qUqDWibTDv*OJ4iGlrnpqA=IbFmB@QWAVD2n%@hSTx*~~MdQPc`R)cMmY+mr_sAtb0${CN^N(sXB6*my%yo|BoK^^qa z42J@xBOz?41RjGn_)@AFQK=dt0}>WYg+r``Pss_A4wPz80jyxG3RX>&g=Q7|m04~` zroALEBB6w{6hdn(qBH;?3}{A5RV;bZ2b)qYVW6<%UC=@l3QUrPyaiUSkOrcsEO^@m zW#w2Y#nF#jLsUx%5>Aawu{G|Vi8BzU?( z83YEPW#WKBjKK{cSma!&A~J|d;5gCt*p$i;6{IG>N3>w@j(&5XfkN3)l#kP`)rlXJ zNX!G^5oSG+hM$dKSg9UQk)Zv3rZqtVgJMW4C5mT*jSTE2){20juCz?nibl8{WBVk< zp@0xugv)4u9F!&+*esONLKzt|n?qPVi2%>PCyAn3r4TA0w1KX-f_r=`ib;-$_xhXx z8zZGvByedDDVevZ@ZgR+m}x|u&6IB&J!xR30xo%rG%eC@uzb{%n;->hUoF~*5%MG^ zK{admXn1epvH%j-raa6CdM&lEvSW?87Qwwbel@xdK$gF~B$AnifHLEC?ASDD*l8=G6TL_H<&}>MyE(NUsIKd2|26sd26zN`AeC(gNkc-O^f}f)r6Qd1+ zff9#txI6}e!6djzc;AL-5`jh66`6uNSr~1CI+&)1z>!dC2n{;xQd+4zjJSv<$!76^ zD71-SI*BC26?tld&AAlI#i47T(908pEL1-7T#({;Of1@Z5Y5S|7jl!WI zKzP8x+5ir`Som51a>zZv*p!dQx`q?Nd+|D0K!k`QaJ0DSStN)cLpWm=eu6C3gVEj| zA2#mgAt_%8m4rbn4j|NC4Gnk_Z2IH0j5LPT64%JiXBsVVn(#3{2 zJw~BPfhpptB%8oQ$CvdS;h{%(EL?>wjK=B(hNefh&%dc)2-M9H!Rl#8Z#@H@k0Lwt zdUe`Ka6gF`ZRhmZ%Nt>t*`lgBqVeEA%SAudh3olZv*oZs_RUencA+18nnAL3+ z@gZ@-4H@HeNktd8FHh+4KJn{=6Zc8N z1QD4z`kn#)6wk%f~zBiO7p(WRrq`ynKj4zq9lhhKfNvk;!x5$Vi4` zLyZ91Y8OBGTw|gU(k{%eq1340FefJqPM8ax#wvFYGwmo42%ttotQQ$8^Eh)kY}9uk zN>o734B`%DigmLaO1pw9>OmYn>ba2w2udMs$&w`l)6>&WDBBrbSp4dn{a6`UvsEkL z!G*H*5~X;^wVUw9zB(~AH_<(9Pl?;gPzm$ncSr1UO&>TGxWm8X$k8t|<~$m|+3ps% z?!lZE!I9rDx5dUzuXa#;pyRY_u>9kbEwa%CM~ZU{_FsD7cBwTWUM-@by%RQ-)M$lD z7+hA@=?@#;?yf7@dgth#+O2m7zb?BXBlcM8j_vtFTil-1ZogI(s5`yH`PP^*qZa6v zq)12Ut)6{T?LkUP%F#0+gVlxz#B>cemYgUno;}x6&!{o$p9y)59S+SiRtG&R&ud?B zzG-T(pnGO}#RCKX4T3+Xx_5sSROfYd1ZV6z7;GGpT75Jm zQMSS&eE!!4&+x4Cug-R6eTe>6+0yYf`ooDAaN^AOiQVBp&bDk3Nu$ytSWJ8U#Jy`%i|cV72oL5D@IpzTSpp3$EVMuI>2@y`-kB3-Ox z{z=wPe3g*1+W**}`-gK|z6W2qB>g+j;F@4sV4g$s%(jO0)9e=xNlVqY_~0BF88top zn}>bJx1evH>EAQXY|eUnE$e%FZC9}0X74@E)`I;{n7(#T=RB3OI}(E{f8H4;IRB*m z%=^P>9zJk#!>Yaaj33R}JTlsIc2?NM8*4WEmrXlvzTW-#&Y`1B6jGDlWMs;2letpx zdU(O&VWYP0Nli8H=a?^HDzV%v(f#<{#tY_2annXfFP3Y%ptN}fKXoDJSeJl5d0m#O z&dhHw1jfzM?86i06h&0~=|5Zd6q$p<%i@=bFGhD2f1l-TSCSGg^~NMHb&Xb8nKN%J zY;sOyTb3MK5Eh@PmbHF&N-6tpKK0_Xv9jK>n`B*RqbqewD8-Fm|un|IWy^kx&1X07 zTD5GRij(o-GLy#-SQ74F!O;mj2Y4$!ki351Ti0Ozo%sRhO3s)Uw7O}?me+hbzm^G>lppjf7%XFZ`A>!Qml|WmHNzYJ2%ZObw8KcKyPzX5 zI4|m~*9C!}Eu3kzTOjDpZ-7ob`^)%>rr<8Y2f6R@EuAUX$o;ss_hErbGzSbGx z_sq<+cJ5M$OP)PSzG&N(JcIhQGhYS89Ubpa=Q=5z7&bs+fBSyp)Wr9b()L6;uR5Ba> z(VI!MQOQNOYiR3DrItef$99bf`^QnQ^`&yZPTCqb@UYF$N!_wJkJr_lRDEUk@bu?9 z8$U*_d|16gV?@A#o0iq*^NseFJ~G~NVa?F>VNGp5VY%D3KYduXsA}5%)n$EZc!tB1 z77y4}i?!cBp*zHh8hr`eI4qz?J>otA~%B zx%c6#y@H*;#zA&Ln`#&Dy(Pc&M&7U*f477?GN!Jk?M<@W(jItNUUXsBIPe%WijH!-tv*z&X}Rx7!SEE%47~^5(=Q|{y6>G4He=|nQ^TcAVuBp1Jhwf3 zc+{`4vu%+4OWu)ExvJ{p|9%{?r)a;}hm-qcjm4JLj2aMUwRh*M``^xX>L3A40;zgD8ujN zU}@K;4_%s)Zc;zSsEyp}`!XxCV)(k!kF(>Jd5(No(CRzu#})~Vdsg8!m9hSHbB3DE zn6g+=Rr*Rxo$sWIn{PLo-bmc`{gQ#o0dp(C)oor=jk0gXD6X?PG;!BIhWBoreLFD1 zZ;9fi&ClveRp%^|+n5pWqS_H@mmW8~X-z^zvy4;5x5*lRR{Q)=Tx57@{2q?&zO|=b z?JVk=);E5%oKj78tJ}#j{l-S{R=9qNsSn}vb*(E@CmQkxdWfArf6lMyqt(rrA$1?} za$}yP%=1qu+qyKNXj?~&&CS-0nJI_0B*$qwRJQT^POYvS{ztWs;k5e+XTPqJAMIeD z=e=h2>L(jkoSNm4GX2dOarsYDyFRDOJoq%Mxx3-&MTxMHERDF2d+m~Rh7KK8_59VF zISYnK-qhV;Il8KT@`ra9`VGze>6N)Nf79S&eayn{sT@6-bY`M^mSg`VmTTvwJO9wm zH`M1IG5ykEu9g$=M5|`+lg$~sdD1&elM=w`S|jmyV6sBjgPlK zKU~kd`AF5bfpJde9p<~U_qK*_Ik=rCsk3+GR<6Ex*0NuL!zz%Ley1QUywEAqIJvGedzIx}w^ue0QZgb#1q zAGfd%TSetxr4Frw%NnP;8qXa#+Cu5md6kVD1}yA|nS5q2>qkb>f!F0X0{0C#w$%2s z)jS{Z7n6RN$&KLH6fC`d(4mH1^s-~@)k8mQSpS}gd2FmwHa4oM{Y{@5)qdLSZh85K zS#=w7wr1Cia{2LVUbOLy*#lfVVoyBE`q8+)tMtk%Tlr`M#|>L_ENai3_gFo0_b*-9 zEQi7tS^b<5Gu)SD??``?0J}c#Ev|c8I57QtTCLLZRjsoYY~J2zUe5njvO0FBm!H*{ z1M)Xs2S;UE6n&rb;phyD?%~f80U> z*}@$p1_s*yxr5|a^@Qn)_k0GOG;j%dzPoRl{>Q=hLSJ%6UD6&H3_Y^o_o~Sq!8R?; za)R@D=w2Y$0DCnBd4h_`X~h%Xj~pB``2rkHp){mGp+J6>@3UzC1PlL2lG6fO3WYG2q0k}@qQTt^9WEhqvfKRswO=-$JsC`Mo+@{_{OG` zm?I*B_$>um0uG<-MAKCG;vI$&J~x9&DbB$Y(M#CQmb zjmhJ0Do~#gu_>i<$p{w#vJkknay`0X1~yU+@hLn-$$A1xDRu<{p+MycW#v+{Z6rI7 zAOcBHAizT9q@K3mlH+pn_-Gvwt`&g6Yl_eg86^%P&q6|huGUP#L?A^N&;w;IKq}Yw zR1Z9cRRaxL0W7Hoo)B>$py2 z;K#pz>y{<}h@}{kL!*G8SS5&T0wx7Yq399Z*|>H=@i>`HDxO??MM4vH3zUe57Ziz} zwL+z1N^k%(GVz2`ASrxs&8L{-q%~?JiV%QEK*k+tjbJ(WN`j#{3&(}_hm=d1MLY|& zIOS%gUvl=6DyQDOpF<$4yG$xuBd z+ktTOJz++(7ZICKJ6H>)cT{6ss?`%aBOvn-qHjkOhk6)Iv=pGt1{72e2nO3BNU@bi z+z}{D-LpXL7=r|$@KAXWb3zM<96&J_k05wLw|s24KnTEb;Nd*qgVA}6gdL1H z&J=P`iY1uxMbaq)Oc1dI5Euldl+TJ?ql%H(q_cq%gJ+^hC!~`xE|o$G0pMh%@Uhx^ zWP|6B{Z|Lx6?lKuc}1;2~i~FCHG`eDIwNdA%}-L5>yl@ z0xBa9-LWYrvy1dC0A(VR<2`g^^5g!1O^^e>DcWEC5L~Pf8SAjMJDFr)OR%_G6Uu z6cz+&k5Vehn)dHpWIR;aF%ww0$%_RJ*w`qGBl=BNJnI%I2-4#&k{GQ4yDgwR;a)J3 zC!qo!#|9Y*sDv0!@CiaJL<0fh@dL0!4HSgJq;V#SYYxZvLQCQaf#fNXD669sfM_I? zlBCcL3V=vOt(e}OOeSg?42hLT!m)%1WQy`!f-0Am6DU$(R!SpF5ggoO`%y=FxoD&SB@RwEeL}pPQGV=Iq;zV31Vc8 z&|M|0rGXj)X4nGAWgHZ$9Uib?L=No>84)lOf}W5*sSz*^Tq%4Y#U(g1uWLwYz*LB2 z9+d#_U5{X zBE#T`;b|nm1CK=XWunmKV;7bZ8JI4R=3-ZZfw-ayBH(y93jhO&d)lzzb`h0F7y*5U zBZQ4Y6vV+qXi7kg3RX|+sVGY8QBHEeOG7gwbFsGs+W|g&CJhwlIFJfsdI*v$8_kQr zVC2chN~wmTfd_lYk^ZG~Yb9)!T&yv&&L}MS#8xRS-dqlfvPgy~PPu4s5!l=r=EbI! z2d$eXK}Hr)YKPeu&5BD#)u`sh#=6GILZ+m%B)537X>5qVT!Yp-<@ez$dNfFoNV5^u zFLbFw6lN4;=h%ptB|~H&t!C{bECSq9r69SAwD=y1V7PB}YN$ril8+@@393dmvIHIo zLIu*rhzdgFa&0ctPK}m@l*yQ!+b~=pMG+zQ(!NkKwmQ&trcXFV#-Uo z5lEv-AdCi*jfl#jK~SVsGqHlTFNKRn$AuW^6V-sWZG>pybNrGsae5`Iz zJ+oBwpbON_rxim9!LSfEN)u_JltF{?xJ1(y@?{`WG6H)dO5;%p6a>oGp-qcJBm-qe zhfzwxl1hL(+(L0GsE3D0@JLLE3VmKeHqr4j+>ek(BgRL#y#&&g_L9@HlPVQ~A-qUW z2SR|*S||M$h(cEi_Upv1E~4Nprc{KZz~w}wMEoJGSS~Gya47RX6$3dKJ}NxEFxjDa z7s|nb;Kl?p-a+Oudm0j&eg?v%%cV1ErE^dct_)#~Q1;Wqw_q_k0GMzfByXCewNik5 zE;Er&i;f~Orzx^LbSKme0VSfLQqo*VY7`=a70rk6FpMu)GMG}GG~&hWiL7XZ|M!ya$ZoMgfygu>pZ7f{?@#WsY4pP@`=T5vDLjRB#y~ z+ELhqsk2E)5hnp)xJ7dbI>OdU;1cFcoMDwa>)2q)7B#D?KQ(LLoT$QB?J1u;Mz38P|CC*o8w{?36E ziD=41NCgQc1+r9!2@9Nh!KQ|Qe8Pt_6adgcLX4ia1^xRzBs30~?F#Ca0}-;IgdmJ+ zLv?eDu2_njP!GZ?5vC!!*db$kh8q@Y09=$TG*T)L%R@;5%=nN%4UB})1OToDO0$0# z5Q_>H-t&PQNxh$97iPvrS6ooUi%ufcE@F#hF2E34$C#83C|t-T=ps^B2_l;4IZhGr zrNmf-&m1iX%e)2|mpuLCi_YK!A7pw&zyi}mvB_Y^j?>u)iZU*IyoB(10!|hd2Gu!M zEumKq=_n90Vzy6J3buJQ+BdRLKDGkNYCr}m%0rnmP*qbYG#)vvOsK~U0HT*bO)q-p z;j+P?9LRGK8oF4~`a#e{m&rky*b-^>hz~i?Mb*Ru!9aO9du}Yd=Qu^Umof&KE`bPK zX`Lco5Rm?Zkm&*ioI>fG%*Vq@@*M*SfpTy;$d*Uc1i1m5h9Q+sh|i+5NSZ$7oG?es zi6|1fSIpx<3ZEuKMF^h)PgI0RLd6kmdQOu_P{PKbBpeb|#LRJL0R1WENU;Si5R3@j zp*U@DbOb9ekf0P;mN%Vn4) z$RMIvK!*|wD^%DH0FsoG5Q)Q9isWr+7RrY(spqFy`m@YTWght~4I7?a>~LYbWr%|+ z3_bLB;DHEixFG`}5U3Ot3-_pmEG!xVA{a>MzL{CE$I%@$Tnd5E8A%;0j`Xyk(-UMe zqKDu?C%JUlMRb5%yz+ON3NykbbWf@P>qA5!phLPf!qjMiNRGJm314CXE{H<{MRg-m zyGVLSXE@*pSQ^$WMX%@g%0+r2iUd^yZHz99c*Beca|rSb-!5!K6ed|tQWC^mSmZ$3 zTCs6LOTxcgM3(IS$?YU41vNChZkx_TGB5&p(ivG0JIbA5m=I9wBH^$Kz9Wf}kLRK`sRaxG+7)GY1T}B5C_d7IeHHwRzS6PE8>+9tymhF zrbBg6xc&&zNVjCmw(%#J@oA=5m0UKuzd}=^BbY31n4;E010cJJv1-Vs6EFsS2Mq@v z@|030h|>v2L4YiAD1``E1T7DakkufDm{Q<9dLp(mqfWqGQjLkA)YD-SM0ogYl^Bss zKFJv>3%?g5-3SzeC6o#TL`oEnRj7KSgrge*!!j{ppu_CLh445RQ6&w8EHaS{SHzD` zjST2ih;UH}5t~!EM-^!dBpk9<>6312Frppf%EZ`dKO>l zERUu|xmT2ctp*QdFdD@g1UDw+C#C{8lR<%m>s3_Po`Ku~D~6tjf>|M@(BU!=k5|L) zyA+b?x4;O1jlc-S!;cV$tTQYzQhXdUi*y%!ly(zYF;@f=C1XEA&qE;r1s)9vZ48%` z=%jEiM@WOqbWaS$6J$a}^v4Fhk>pW97+or|MLGu}9OAV=&twDw)go5U0EYd5L~SE8 zL^29BUrK;OnmT@~XO*8xFu`?5MDdkHw8ktR-0=aN4wy3Jg0E*(yY3qgg?5QKbp3U9AV zfspWpH?e#G(Z>BHwjeImqS2S<2~tSkXcYr-2lC)3^$SIY=|7jsCNc@3P~4835&;%r zJh9g96y2UxLXsq#sSuHZNB%?^X*8bd8wqacO7|MDn^0615{6As(Imv+A{ZzuPe_3$ z!VmyBLOL*4E*0Jpl<2FP^{8M*CMr9l+QBkt-9b8oA;!T~vI0d-gfu8i2|_%2tU;i5 z1EUxJBr&=wKr<}BfY4)Xg_4B*AZa25B)Wp~D3*ajDG(CF2;de^L}>uPqN)_kvYy}} zdWFM45PeaG1HMo5l41jx`E#$+~2KqG}D{Uf{^qLV-pp)3d$k9D#17>TqgJ{E@_ zvyv#J?QjV0*wk?pr&HZI2Mb8eLy@p_z!QDA>(xetiWLzMQ=@2u<*`NjX`CTLvgi`= zl#rD!^qr)FxC~k7D06hAPSRkLG(~KkDhiJvzEV0#5FZ4lw8!gbG76XxuPsD;~ zE~I{d6|y|^U-*WSnTJ_}by8F!x(Q*Zbf(9nmna}pCcrqD0RfCo=;jLVX;DxFk22uY7_b`;D15JzMz(o>+7JgRhL;lb1LNT3%aBXmo( zc;UN+l>`_zJ<9$luc6Lm$igAwV2A`g;+-5H-H?@qbw%Woy*mwzC_?KaGFR7AF}UZE ziPW!fn#mwX;K9(ElB^el#dgO>l+lGM3c9B!eIywoO^HL^YBF?3X3+{0QSeQQq* ztRR$#2>xxaY#|?ETc<@7Rx`6_!=o2~;gh&%%>8%$5}C|$kgBFRlG>r|3T06Kp7QB5 zut-F-9KzOufDXJZ6*8hsj4l!O12~_OK9*Ps1*pOiH;^HN@J&P>q%o(gCxks7H82zk z!leliQ%K6hnQFpm&Ab98};!l(t!vjEsP24+IccBn69gP5@KZO$_@mNDMtf zA=N{5W2jIG*MJ{@L_@S2wR+A`8r)8k)8tqcybbw^g zxLAaQLL0$!y6_cq!VQq30l)U8WRWOqBtYS6Vy(7j64j>YR3gFx2BKbqq5}NUzAQHC z_C*>ypTGpFgUA()q~@?6z`6WI)}m`$h{6zVf&(uz{#GB+O2vSq9ci;%LJ}VIPuSGr zU^p5>jo^qp;)<#cK;nff?i? zp{g4}T09le8vlo%lR1nhszgCG+OLaj%Wr%(VJSrIa@ z9;Sim!?f0p@i0ZC(qRC|B4?OPA+Yvvu@F&V;6aqAFoWLP!K2g-DK6aa*Do(YUBWAn@VB&?*rn-A_*(qSxuEbXsaIBKX(i zHpBq=Cd^K#*PgT?I}hQD*dwxSLh;CZ3&lh%al>RGgdX+-#U+UFF}Y?80#Ve737Za1 zC2I@Oz=19b+@Y>d$dLxYk%ZD7gedz2Jf>ggW2zX1h!VnJvSgpira_pkfb8Ev9$XqI z?F6GzOa~<8v|EtoOh2dg?B>!(*W)`N#qvqeF{g7ginxniW-3(AV zm8eFPRrJvp#Xy3P6`K#gM*+y^7otN)k@BNa9*splV-r3DB5e{)Jk($%KGI(-?Eg+r zFX_Lid`gVY*l$19Pb$o`e(W0MrZ!MEQ|*bNol0%U?ty9Lj!z5jURHZFa^ihsTeYFr z%Ra&PHM#_UG6(KYuNjn^mk6gM!C5pFjem?DeQ^-P7s==C>vWKVw2S_Z!zA8o?5}Nd zd!@h6fAz3m28G|E3P0y7MnuPyj2rfAhK7XYkX;*B4mh_nZG%jqdPv$Q;{rwB_KX^f zfy?S#I6y=2=+iN{DqVMY+t6f*65!L@UE6W&(8oPDEnQ1wwfmkOHcYO#a9TxIhafNh zj;5g7==Um&jlkR5VOzb|#lml|6fe%u&^B;k$LI}l%8t0m$tX~%@o4rLIsE#gOZw|y zKG=5rnR_cg^|0}ajb(GwbGIo=y6>Kr+~o5BZgmI=8YtoB(8i% zd-5b>>u%Q-YBxsy?!PalZ2nfe@iw*&yPo>Y3ts7}WcO6z(&wBf!)iUd{CtXU>>9mm zjq2ll8_T91co#Z#*O-y1iQ6Pbr*EG)V!zzDSEnpymw0{3FUdG1X%U=dJ-X`pU9XXP zbwR77)}|+Cd@UG1YQE*d`F}JUqdS6Md=GEjWmF)LJS(WE^e}o7T+y1J7%T`L5}tRm z;7@b+kdpfqf@|ShJJ^D00*jIEZJW=wp8giSeUIQuSl*j&_L5G=D$^=kb7XDh{@AYk z?tS^ihHA%Wj-G?&OI)76JJe^G>w@{E`957~g2{&_EIqwZtt^=j$8c?S{^h^!qMzQM z@L_g4W=6}08vKdc%nL2HPKcPj^!>4op$6+Dq_jfk>#z=+?!RSpcaWeh)UjH+eG6+#>$$~cLARXuf~&v^l$nC^kj$cTITYr9baW z<@^Dm659KBO_-pjIC-|q6|b|S?(CNPwCQ@R?G>wZv-I4ha7LiP-st+MNoP~`a3(!H zwW>k8{_&ABN1s~hxWX61qem$^$trc%T`I0!om5!sGgeB$wV%pbQx|868N9NRl(3XU z-2mAqhZF{c?vI=`V%GZbf3nin9?0nQP;z@5@WFn6rTeMbs|-*4swsQCX|r2#S4fZ` zQ!4LlLE?-7kz)eezGmLqaR0~G)b^$ie}W9cTEhi`7avxKwmEcE{H_pxSG~?iAgDd# z{UfjL^cX?i$m#>9;B<+0qYD`o>ooH2^t;?`JiAmN$gP+f5Zv*7^W-bvuQ=uU+t&mO zD!(u5ZV5l{VgD_7tiyu47g(0kf>y_Gp4-Oee0SV^-fX*>R(Pk(f-bKg8l%HPhMla6 zn^t9NoVrQJ%-m^$qRi4TqoxT-`+Tp(@3ivWGT-P&WU*zWOZ@12!3OKx1l%#2qb#)z zM@dzCEDBGBllkIrM(~FiG+bWU?6x_=Xh((KjjYFk%|}y4Oy6LzXiaC(Hi7f@Egj%@ z#!6Ni3EBiT=d0m(SHaK99dDubM`lm`82oWvzQdI;PEKX(j1ON58W%f+PU>7df6In| zHzsaoQVRF9LXFe*?D?Q=aZ<6>==@1VL3bbde!U+sqE8<%^1na83VojZfBA`V?P(aN z*8{9j7ID^z1$@RF&X2OQ(}we-NDMoD`KkI;+yLU3dv9k^{eL`!O89Yg@`JqEx_b2Y zc>nVds+E3KS4Rg`WZhOYIJKKC#d>(t@X!~vNTq^TF0a;g%(1PKDU-ROaD$ud@K(}( z&4`#;M+#Ss?{m-4;H_B0?AePh%W3SWT66QY*XWaOIXTCt?j60a#;7aK_wMLN&T|Kj znCRBIeb}j4fATUu9;+A>aq_#IZ|<|EgAw-v4xE~QC|37_;j1;PZ<}05TDjWeqhis= zdv6=hv&2&h$0W^R`xS={da!t(-<5l2i=|Zd4IlL=BV*6TqLhUnFU?di&wQ9wpHd`q zFiG)|;fa~G`zPHvGQqyg_PO+9RqI8ExUq@Rs{a%(KD9S>^QEp$m&`vmwS;%Re(jcT zy)UaFv)bGHhqpaFCh4bKU=azP-?vQ+sJt~f2#hV@7d@Z=AOk$;{^XtUE zm5)m+PyW!6qabTCCaU7qyjiD5Tdy@gbYM!#%Xw+}hZf%0{L=Kzw3dIXPq~@usL0DU zbOycD4~};(yOQ?$L?hn)3xUSBud09i^)dTm!ZFJ2> zJGO7Q{7_zJR(^E9;QFC`?7}$@8pe#?Hac^TY8T5_{fdo66Hyu~ z3x|!DdTu&rskGljrE+$hM$%8mQY)2;+?mHO%r+al^Q%PcfXwW=I@{NFujKM21Vf!| zhiv$9M82x7ztP$wE4AK*%8##AUt8vp>?Y%V^<9vj`M)1lw?6ioC~3def7h=idIhFI z4~9RocyHutm00Z%c7dfnDc`9}bD>VOph%pI>)_f%)>#;eAGv2b0s8gY;-;kU02Fo&vQ}u{x7<36Q~ve7}6{hM-%qL=lR9ph`ian7QU;prO3a`w+)j~KW; z$;AD&X=0@Qv)^gM6Kfv)88du+{A$0M5mzk4ou#$J12g5ue*QN8XpoMHR<2?B^)2da z66<#TzWG5s!hh)CtrPcz?=Cv-o4LQ_?Zo)%W8;thI6Zp7?Mp0QyUlkKKUrVfEWX<1 z+&*oYL4Vw;;|KXXF_SXUpE+J!OloqdY5$=EB_#&+4O*|gWzm;k78lMx8?q-~_0#Q^ z0bWD4S&iAa>EBTYBBakL-d~YlngH%#1FG}tiJ2H*HPDh;x^}#V$GMtj%jOedX!o_xgfXy zXvg^jUY;j##YXAO&I4b@@3g++^L4Y`kj-1hgx6lMuw157 zAinIN@ssiPCcmcG{&|uiJtrbbdd&F+TjwmZJ0Lav+Jej_pPXKrnXlRy5FhjEfmrCg zo40P2HBOy8SM9i}hGN+1Bd<8CnnTB*E`D)n#R}D7Qg<&&=#QJeaFFeqPlkn8pR0&F z9t@o)Fd4DrNVn$tH!EH=zu=FqN$wcXo-}-l^{{*T@c}hzQe%9@tVR?}TxL*^a8YmU znBPOv2QJ!o)O6P5yzp#U`B|^CGNR%1x+8~-c1AoobE-wL)oomQR!&pj4!6+$cP)#2 z+v{F8`c6~c{n~f4NA100>)Q&cl`f9I)41n`41cgMAbflI-XarCJGGQuN!*);u70-_ zTwkoeY5c+R+{K|o#O80Dv+U)vm|ek^?s>;}K?dbZF6oRrKX=5_cRcB;%Punx*4*;g zas1hf;FQEux@$BpJ(|0Vr@F@;TDa22wR3jLj~_1o@WzH;)fdap)bBW-{9}NAs`S{h zU=y9ni1aMyN|%46E6W`>j(eEDMQd=;6WbX|J5B9(Zjc-zZ(DG)%Q;n9$>z)V!~ORx zDLb(LNutlh1BsbquB(uMPL!nIlED)*mFO7H$C|2Cs4HnpkNyLN=; z+TY8yJ|2=9zpmhex&5d?TLORI)i@^eRCdA2m*%dno+ASeJhGduan60|s)J@;_qHVN zxOZ`r+h<+S+4f1pjWtYb#@5$3jvk-bklrZwePOcn7}XPV_nThc8TdP6yKz@tLx5+? zaqpB{a$?HWp4SXRbk7fRyF1YM_*?r#kGSC)>z}!vxe$MHPsGEsJ3l^=*%C0-!)|Qu z*T8t$!TlQ7oG|YGSN~i^qPqFojp6Dy%O&J3_OXX7Q=B*w4#hTbdobg%y|zV(MqP~MD|E3x$!o6fbUdT(;J-#MH8V`!S}@wVw3RF|I{ z)mdC)c}{-lkRyr)@~Z6JXFX>x98jfPuzP~ws{QL@MVo`w+G_Q)vtDP52ZVV|%RM9? z<}`4L>f_j|HEWXxruVZ z?-=;rvGLu&qhpPXH{6w0Px&&ao zbQYiL-Y#w9d?RWNchMWWNTy$@^Ap`+xvdH$}R zOXa6c)tIKMt%(Mf3miRNHSOFzUHulgZsB@+I(kj9_ILDfcgDH4{=TmM&Tg6uynQ_# zJ#dn}tNVI4e|cRkEln5}uJ@BSG|)72^>apt=&5T@)yF_4Cf-|Ds_PmW$gAsUO_QHG zRU6Jr)za2kt!Yd^8W_R}r@-FA(bL@{Kotuh4-&{@5hiJxI&QQg80qWjYc3<>>Uz5R z#EE+d7jgA<-{|k{t4WA#?6uy*RbG9n9^-UXd`PS4nSpk203aT6ixPp8Ve#Rtn)4j} z{7u~)eIY@cI2F)urM4bA?3MXnM@L5<{h>6Ji{b>7o|cw;&p!f_N@dc~lGmR)mH2}J z{WNWD`X0pzx&Q-tdV2cu2EYZyDL4Wf(}i5(PL#SRXI))gEF1dOo@PMHiQ-!FR4SrQ z$x8j|7#PZ4;jSrYCyS=%XGP(-X}F@;aKv9{#SrUU2Fz80KboXIHMJ$?VyFV9Ji3 zLk3#yO*#}{XS-!YL9qA3l$iMQ`OZUD{(F8vjGl+%fpM(G64nxZIVM(>V#a1-!_Cch zrN?WRd-_`}G*8yZbj}!B9`vQOe~Z?a&jy!AR753@G_Xuwus*e{L9U_Aux(32!<)Ca z+LD|49G`eP{mKiiL46|2k3XFoaesu&a3>zSPyd1P{f70?4#?0@pP?ZBF7|=V`4v^{ zKl@YGh{w3!nlSItZLObzz3LV9L;B1aZ6K~1cf~hphEHbtw9oTeM<)fGj#mj=Ed8~S z<#Sc{!apgu>bBP!zPdLsa_P=Xlh!AjoYHD^PqMEV9dDq>H`wxmAKzl&aR24$M`?>c z{HYp#cJZTs`p&6W$uXX!S@2SFefI2$HX%*@`V{)Vn6av3=-9X+1IkV-y~wt>wPK0a z?W#}X53B0hMJP}DdNvMD$u?EVimR77%v*8lp!?Xqhx3|}3{qb?Z9Ncbe&Ws@lVG`$ z8M`~Oj^-^&t$tE=u=qxb@t;B7osyd@omfK48<85w;ry zw$j0_l?!!^Dz0C5&gz?0DSlq#SD#N~p{7ZP) ziE+=oKDRwv{F&uHG@BJ9Hs4tCc*F4ot$t;W<2}T4?&O{y6}8j$o$OZjxsVO-RIW>} z5tqDk>gVist?m70OFD#3tO@@)rJvN8yOy%U6ed_GSxx8eiBkyV$<$j6II!5o$ak>e z{#C{*2P_K*#qIH3JXq#rjT=+V|%QUv_*M{l)W(^q9z6 zvq7g_OP^ovv0%+_{3Y;$COv2m?Jt$D5Dh~1wa#GZFK zx3pa1tLoPQ8xB2o-CtSnF(fD9R(McU&@t2Z*L~g_4qPy?uK$xsPgZN(-BdgGy6=yH zKSs3;-jZnfk-e3>Ml375ZDlpf`$3ZPc(;`G)oW)vb?^PHBv+Huc>0gp4mp9mV7lP+ z?qu_b-K9>E_jl|s8u4_B=af_G@rT|V5}%T_Joob5bMG_MGW3^4u6%RBB4bvDPR7@a zYs-&a*x{0YiGA(BYya1>mv6WadL8&`-Anz~JJxQyv$_09wMNN;_1EtIhtsF09h8lXnjt$;*1?dSUT8!PL=Z3@%w&GZSoZvQ7hRY6z(vU>A}XCoc&IEI}Mbqtmq8D<~Wz|PB?bAHZ^ zwH|lgNSu*aelKdzL#rtlm40;+?jq%H^AB4Op6jt-W{R2BTZ7|_q@D= z3WaG8+f7xb>L~gtHEAwCdG64;OE0Uwz5h1r2{1( z=%R+F$5wsJaeH~PNvvG#ONiob-rbvblXqX*|6!!}ikYj2+dW_6AGCL^b$+a$O-8~} z^)BAw%OZKf@kUV4eY&f?g~x622;(BRKx=eE#YG- zhr9|K6qypaVN8RbvTj%EXwO5_j(PZaxO*R2G2)Vk<>copUIku!d?VwMXJEklU#q`A zZ++Biw5fel^Oo&-6N2wwSdjDNQ}PyjmCUNwuH~m2vM;D@f6@Bl^FJ+L_UiTBaP?fk zn~cxhCkwLHpRWsF6VNbngZYN=^6u-I9&gMXHom>ua@D8@+Z+P@x@V-qghF^ zNqdvpl3kKZQ|6>(9vgRT|FMqa?#C-nSf03gQsv~KQv*(IK2^(G$}2c+aGIYwCUt*m z*BP%f@6xzw_xRKJ{B+s$=(GLKZaQ0kZq>PG8D<&RGu1OsWR1w$f4w&23-Al&F$K|>&vf~+?adgZuX4qD>)iDXKpIoJbFv! zR&?&b++DeXyuiGc+Z%6xyR-hzhr14UU*|8&e|~TAy(jl=?iW6=c#vOUT##2dtMKN- z=?`x_GI(^YNWbW6v0m|&$9j*iJkfu0_35;y*Pj_a%PyH&l3Qw2dZ)~+?7?&E=S43T zyeKJOQeIiHs^VRxOXcU69xoeTZGP4H`rj(Cs(o*y-W;lytv>m7(%Y4AN@Z5`LwHcNbR9IxjO!5?aw#sE$W|rS^4GD*NtC0zlD7t_C4u`>W|9} za~g_&F8leZ(YI0XE23$1)0t-7=DRJ6TB=(;TRYqKwU2I3`)%;Mpu@hSu5-&D@jr*V zCUs?Z+jLh6JOu(cXS{7!Q)jR9B^~lR7ArNaO-<2|)&)NHfJUI))$~^z@W@|uM5+yQ zA9pnLL=z)_SK{Dt=D^b~X4b-cT7$#d{tS!`A5~f_WgGuViYq?bOtN*=`$JaG_g49T z&YSpfN%$y_hjP(fuYNr@YWXN#xMk?(+pJR)3$E{qSHCMZap5zyjn%2M(yO1SgcScg zugXLPyaseRR2lrU6;bITi9-L z@(wARJtqn{7^XRA-?(9GibkDm73nY zmuZycw`&Cr$$6Pw|2W6Hu3gr>yDbOa6>05kC<)QYZIE_a>*(S#vQVS=aq(nz>&MpW z8Vl5?%rEwNqM0`3(Y*iD-nE5Dc9dZP=4EgZj1N%+i!tI1q34`B_tVTy?CtJJhRn^L zUfk?Oa{Ba{-FEvj_9ZhL6fcm6h!PdW7k!C>=0)-lM2YBwmk?26yu<{25D|Sa5Pc9e z2Jx$^bE`T%lbJX%!E`_DcGvmq|Nr{yuiN+kHRq{?_y7E^XPWwBkLka@>2rU)_ZM%i zU3|yK*G4x#@r9SZ)_mZ1Kl%OVfA!ne%};&z4=+9Z$(J7f;pcz#wzD@s@#S~k{3!j= zT>X(BJyMU}SNX;*Z@;zj=vO}XiF<;lTc5rC!n0>u-~0PtKe7GyhyL-x?YDmEqYp*@ z{@{zh+#dh<`A<$hbIbLw-?rZU=8Hf3%j5NjzcW1l>rX%T&X3%2-+k|DUHP|M~QbkALcKxBcr~zj@*5J3fBv58w3QU(WyZU!C%w_n&|BxBhhb zE#JNge*ECm_pML2efIbAYn|QQXc+aw2xnQX*Xd7iI_gHFQS{+S(7hrK)yWYofAGp8 zVp8|V%=u|2GvslL(f4JdZtxo5($&gHKy2I#5XjCEZ}7^3)*Ne2%|z*Eauyl1?BGU zv`3ey$pMNhd6Ep;S?~1lQiDqiAsu(d(c(gT6ot{^6^$W0SuwR3AkbI{tx+`WMA0I( z_{_G@?oWCK`^hH1MqHa(yc_Zod3`bQ)hME|UJF+a)Ffk?f09BRjmKhQU0l zp`|rkb4rcuMM6_#P=gj_Fg*{|%5sfjjcj7tob*PQ(7zUMjHG%tf_Ai8&hAz=E~VY|#p= zc=p^2qIMeGq|-h-{{eeL-`J~0*E)CIKa6(n z-)x5WUl0BE{2eQE%LnCy-d+#f$U(2$AC(Wz&Ij#5D=OnVE7s--CmCNqJ8v%0lB?7C zl{rGl^02*IZq!!ediZsA{@QrFSJt%s{r#nVb7?TVs!`wfHQmq*qXdf5=th4W9F+Q_ zcMB#7pJdkR4Elue4O)ZA`0V^V6O?|0f_phZ{m~Lvb}1b6G(6DglBuPfQE?fCWxO4$ zJqe@XN+lc)TEUpqI-_x?+l_|AUDB6qvH%3IJ=h;DE;A+>eI}jrxjVYCKNz-08gD=2 zt}$J6x~C76@|SDVL9mw^Pihk$piR2mCKQ&dz!HHwHwT06%35EbjWAx&CC#Xd7tZv0 z6D(xGm>CpH+MvIzsnv3=V~;ifhVI^|Vc)o7=(<fT8cs}aNQ{BZQ-DNJOKEWOPQB2XXtj=I4O67{?)Vd2c>qj8%(<6`IYmVYg?OS zZL?W#Y*w21URke`T78E!fAHY?4<4*;ZES#~${+0r6LmYX2vI6a=9;TYE<^E9_>IDW zX}A-8ST4;}U*T^l8g;lgwWAT)p0pA>jgC@E=E;>RD!|kNQyxiyqsmIWBCDnYH0~Kb zOs5!Viquo{LJX;mjnomR4(eV{_CsE!TpNTF=BR76mC2;jF1JlRaw5ww1=Mj$mPK17 zFYwqmH){E9!wn;ckIRjXeBGSC3chRo(Kx{NJK^gX5p~<1Q3?XHU9vo*U24HAORdne zgCGcfs$&2Y>K-{-h4ru~U}4eHl|}AR(I=<>Z`R<`=bdt#2}w zDUS1HX0GE{DW#d2D>cP&zRb*Z94nU-`d2vz00qJPHo9y_oaQNQ;$@M4YoqrCLI~Yd0XXXGF z*#`bgKg6+ZAe8J8`xu6a)ENGi^wTnyN~4xJc$2hJX8P$dLs$9?Xy+U1!7WugzL-!;ZK3-0)+TKUXUV+Zypr>5dG8vdJmw7G= zisj3IhP$pJgX#`PjAc5!1A&#)Ul{yQ;Nxo-LH{! z!H7}&9I1(x>fop0Kn zBG*6!iQ(mWekh}!(l(UAV^lhbRb;?TZM{f`Sy8Er!K$tw24HeSsN)hF1`I>Bu45EsIG#GU8wQ5C;(rEK6xH^aG~9w*)Ab8-O^>o*h|~_# zw^-V&REFVJunVkGXs?Y4OV!V&ZlP^9g(oO7Y*=5FYnz^{`i5=o~^EfCiNV3%%BV_t5O#OPMzDa2s$i&CFx?q)s(&jUBTC|Z30rL z)U|Y{ptD7(Iv-%V_tbu4W$JuyLIMjYO8Qw=(FQ0}eG+xuLVMX7p!f@V95azB!{&iA zSuPx3wG~uReFOZADRkx(I@3}6*TMi%d>8!CxPpGRx(j?^2H2l=DO@yJu$?v~8 zK~d((X}I@lgyUMIogYP8lX17xNAR2{EWp&5(2yvQ6cfJlML`mKC49<50S~#Sf%C1s zs9y|;IW@4T`feRGuj9RU(HJhXn zg-fKlWRc4Ylp?0wY7^pK1H@x$(@a@^V`HEtCi}`+Qu4!S(WYW zN~6*|ze85H8l<*WJ-<=kY?5Z9vbnRdwzGqnSMGGnNAXeGvz=(7y@>ATGC8o_7Rn17 z={UDX_rY+Yj%blb)_Qv)7AV4_gI$8SUoZ^DlaZte=At7t5$?h5sA^&xcC3leAl6!F zP+6h`5Co4(GdGchvd%fY^Wxr)J5O|d)_xmHx@dna6}to2N=QA2?rTKj$*@nx0}>*| zXgKJiLmSO1FpHf3@XQ9u@!0MVe3JAG*_*J+B>lmV1bk1O$$_io`=ewEpQUWMXAF7o zh!l!gON&(br5rK}uOVJ5>Gg7cjddc?gmqm`k*qsp$M6K5Fpx=_!=OLvbw(p}3K`={ z8regRz<7Ys3WK$gg*gg?K4FuDq!=dbxX;9(Qp|wudTfSR(2_~&!{J~Ero$odD<-Z7 z{u0xp9f8KGbAiH|soAk+N^TI(8$viGJ7_~6_nrtAm+*RCCeLCy-1#WKqLs`X1?NKo zj^up8F%sv)lz@~ANg5+KLYl~_ImFc+?4TrRqVp4J2C4Vk1S6aE6`Z`jwJtk3JB%DD zJm=OT)hFlHG?^p017qvPy)BVV3BbQVYH5DG5lL40F_bWvI+NhsDK}Jfwh&fJ zXm<{pgWP1}XOnD&P+mKku#U63oN#erkYlP{JZE#|93Ag5uC`sw5Q|Zpfv@Mh7R*X<8mS2rAgbh~X<1gm{{X?YF&M z->j`&AZyU=&Iy)jWIJId9!7h70qwMK;})5V0UH41&f#FPhwVqYptFUJjSN2D4cJBo zsZoeGh@~G}n%sa9+c{y#X~}nF$SPTp5tdqQPcIpvZz3RIn6?|vlK9dj1@R?wAmzbn{+tOpku_)L znwjfF7B~^{%)4ghI*|oVMEu6^u45+g#X%_~R8f(3X4)pZyPAdxS@b1)gtVjFBcNh- zS_^k}vq!8tI5}$28fArmH_Ci=&yanR1UA#Q^n8*R1+0>4VXNa|$Pm zfv*X77Ej4IApcfKU|= z)HSpqlTW_{JD zA+}cYDs`)F*cG?BTC1A1+RBv}*hH2A;K+PH6^R1KgLq@aRBh9+UT-ng7)oW=nE%Hy zRYN~CrkVlA)1qJoo0fp3awt8%{hP%LP9-cSB7cXXygB?dQRX$kPqPReZq19!&oLzI zSkoZkELfmel|jOcSqxk!Vb5v!Fvr)c0Ec;2H!kR2XIO758t+F@pWv@cba1^H_ddyf zXMBxRY^~yu0KuV(MRtR*(*?Q>M_XE2l8|8Lgfk>~b`3k?SIdC|y=K?26a3XS%Y3eW z?UCT3CNm^>cAYypGR>}Y3rnzgihL#e)xFLgdM(4StrBvyLtN+HwMu>vwM#}6=|<=U zkr}mTNO0n$@&9j(le^OBSN?Nmt`pgAX0DmJPGo@-5zoA9X08)i;6%hndDkmOf*sth z$zh;*B-qitJjTdU?GsQH7nJj`BFjR}9^qO*hC(u!AbW)OhvgsH#EkJ#ToG08*qSI) z?_r@#x*I(Or^M^8{!5(FT=gxt?$s-+=aA`X;RQ%F%d`x-YSC5Qv-AX~6rf}Qs>G#l K3fMQk{_!6>UWW1j literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..32fa877b13f104a4f4d94701eda484e8948c05ad GIT binary patch literal 112837 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 literal 0 HcmV?d00001 diff --git a/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf b/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e4822d4093f701d4e760479616e1ba21e0c13b0c GIT binary patch literal 317624 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ℜPsj_0>zrytHVuciAJmMAF}10E%idGp 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

    ?W&sa<32wnUYm=WsQ_;!_c&HM= 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 za*�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®qGI--L_Z7L_Fc+Xg32f<@`M)g@*{2 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 + /// Pixel decoding methods for the PBM binary encoding. + /// + internal class BinaryDecoder + { + private static L8 white = new(255); + private static L8 black = new(0); + + ///

  • Q$ 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

    rV0u6BK9PJ$= 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

    %<+cqh>0+|Th&`98 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

  • gGEL?&_#+nB(|%Wg$o2(; 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
    s>k8x2#If#2l-d0WzIO|#N||N@p2oNL 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 IZ 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�nAiqhOE(vGX+~(95Fe?uIA7i|9VA z1Mwrl@!A4X(1FoWQ}{yyvvM^%I-J8gPks-zTuIbKHDY|!*jx!Q5Q`R-xhX9sL00I+ zKucKK_;#mNqgdq!>9LO0m);7E2rIcwxAs+(#&7a32>lkN_akRk@lhPG4nEUtt0gT& zqrOHZz&|`tUJ0sfwv`oAI4QyMRcx=>N3!T-a^N&k-)B7p+mZQ^{B<1U{re`Y`yk#V z^+P1bgrUpC7K{(uVyG;gq_8c&nn=;J8Y(-p|KEItxZJBW@q#86^c zdj7zU6qC0l;cLj)m*Q2=dXR>zz5QGEwQC_YS_$eh<_}^`PTKwPrbi%7GU$cTV2fLI-)S(M${l zfxwD7vjU`hs$|Q{;V*)~=YYpJQ?jm=!0ZZw>2}8jOjyigcc3Wr2a$Oa?xTctsHz#v zs2HEo7?C#@3gL*phl&V*c!yUE`VyWPpz#pK$vb~9{Y*xR){+({(hwfvwJs%_^LdXd8+Jwwi9U!ar4Ud6$#SiE&qodVS%iw`x9Q9E2ncX&s=fczp2we~#_$JXd zy%6uMx0p&|FSz{uoQo!mQJy4jeS4a7#2e)l>q^Z!px_{Yu2nN_JB>W#PFV3F><|5X zrHcbzDfw~VX=^wxvQ4vv&I4)x4JIaSzIet%fLu^$-q9JS%GwFr;R(h>LZ-EW@El2b z<8x>`^`4k4J_nZCwtw%?{D{FF!ZoN9N^l$#(pt zV9J~Am~eD@Wk?%}BASU2mNPW~g>&ZN?L;|Wzu+O6Ur0$LnjFzON(x0{oes1h1zK0G-4iQlY7D<9C)HZ!)yGG5;7hV7!Dah##f%}?`i7G(qLulhm) zm36fpZ$xYN4kPhk#%`QKl~U!Oh*{0+S@A;_*%4xD@JzFgXE@H-69z;p!Q8&dG0 z`P3M#m(du0CmB0Kv#EXNFyl$Kcg9*Q<$@1-sOs?~()7RSML01jVB%*;vUs-gXXf-3 zkEH!Ge!tKl($Ue(m6vY$7KVMteJW1M;DAqC12*s_!h~XX?N8?m4|?Lqn?A}$J#kKJ z`CxjcfJ5!%xg`vxo%Pc>PL7VNkQ?G+(`JVvE*bv z8SEk020mFUvF#1)_VxxR-`4!rwzQHUaIEv(0X mCD;cEQS^d6WPz*2Jal;l_P@ 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