├── src ├── MozJpegFileTypeIO │ ├── resource.h │ ├── JpegMetadataWriter.h │ ├── JpegMetadataReader.h │ ├── JpegDestiniationManager.h │ ├── JpegSourceManager.h │ ├── MozJpegFileTypeIO.vcxproj.filters │ ├── JpegMetadataWriter.cpp │ ├── version.rc │ ├── JpegDestinationManager.cpp │ ├── MozJpegFileTypeIO.h │ ├── JpegSourceManager.cpp │ ├── JpegMetadataReader.cpp │ ├── MozJpegFileTypeIO.cpp │ └── MozJpegFileTypeIO.vcxproj ├── MozJpegFileType.sln.licenseheader ├── Exif │ ├── Endianess.cs │ ├── ExifColorSpace.cs │ ├── MetadataSection.cs │ ├── TagDataType.cs │ ├── EndianUtil.cs │ ├── MetadataHelpers.cs │ ├── MetadataKey.cs │ ├── ExifTagHelper.cs │ ├── ExifValueCollection.cs │ ├── IFDEntry.cs │ ├── TiffConstants.cs │ ├── TagDataTypeUtil.cs │ ├── MetadataEntry.cs │ └── ExifWriter.cs ├── Interop │ ├── MetadataType.cs │ ├── BitmapData.cs │ ├── EncodeStatus.cs │ ├── DecodeStatus.cs │ ├── JpegLibraryErrorInfo.cs │ ├── EncodeOptions.cs │ ├── ReadCallbacks.cs │ ├── MozJpeg_X64.cs │ ├── MozJpeg_X86.cs │ ├── MozJpeg_Arm64.cs │ ├── MetadataParams.cs │ ├── CallbackDelegates.cs │ ├── MozJpegStreamIO.cs │ ├── MozJpegLoadState.cs │ └── MetadataCustomMarshaler.cs ├── MozJpegFileTypeFactory.cs ├── ChromaSubsampling.cs ├── PluginSupportInfo.cs ├── Xmp │ ├── ExtendedXmpData.cs │ ├── ExtendedXmpChunk.cs │ └── XmpUtils.cs ├── MozJpegFileType.csproj ├── Properties │ └── AssemblyInfo.cs ├── .editorconfig ├── MozJpegFileType.sln ├── ImageTransform.cs ├── MozJpegFileType.cs ├── MozJpegNative.cs └── MozJpegFile.cs ├── LICENSE.txt ├── README.md ├── .gitattributes ├── Third Party Notices.txt └── .gitignore /src/MozJpegFileTypeIO/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by version.rc 4 | 5 | // Next default values for new objects 6 | // 7 | #ifdef APSTUDIO_INVOKED 8 | #ifndef APSTUDIO_READONLY_SYMBOLS 9 | #define _APS_NEXT_RESOURCE_VALUE 101 10 | #define _APS_NEXT_COMMAND_VALUE 40001 11 | #define _APS_NEXT_CONTROL_VALUE 1001 12 | #define _APS_NEXT_SYMED_VALUE 101 13 | #endif 14 | #endif 15 | -------------------------------------------------------------------------------- /src/MozJpegFileType.sln.licenseheader: -------------------------------------------------------------------------------- 1 | extensions: designer.cs generated.cs 2 | extensions: .cs .cpp .h 3 | //////////////////////////////////////////////////////////////////////// 4 | // 5 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 6 | // that saves JPEG images using the mozjpeg encoder. 7 | // 8 | // Copyright (c) 2021 Nicholas Hayes 9 | // 10 | // This file is licensed under the MIT License. 11 | // See LICENSE.txt for complete licensing and attribution information. 12 | // 13 | //////////////////////////////////////////////////////////////////////// 14 | -------------------------------------------------------------------------------- /src/Exif/Endianess.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal enum Endianess 16 | { 17 | Big = 0, 18 | Little 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exif/ExifColorSpace.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal enum ExifColorSpace : ushort 16 | { 17 | Srgb = 1, 18 | Uncalibrated = 65535 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exif/MetadataSection.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal enum MetadataSection 16 | { 17 | Image = 0, 18 | Exif, 19 | Gps, 20 | Interop 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Interop/MetadataType.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Interop 14 | { 15 | internal enum MetadataType : int 16 | { 17 | Exif = 0, 18 | Icc, 19 | StandardXmp, 20 | ExtendedXmp 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegMetadataWriter.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #pragma once 14 | 15 | #include "MozJpegFileTypeIO.h" 16 | #include 17 | #include 18 | #include 19 | 20 | void WriteMetadata(j_compress_ptr cinfo, const MetadataParams* metadata); 21 | -------------------------------------------------------------------------------- /src/Interop/BitmapData.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Interop 14 | { 15 | internal unsafe struct BitmapData 16 | { 17 | public byte* scan0; 18 | public uint width; 19 | public uint height; 20 | public uint stride; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Interop/EncodeStatus.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Interop 14 | { 15 | internal enum EncodeStatus 16 | { 17 | Ok = 0, 18 | NullParameter, 19 | OutOfMemory, 20 | JpegLibraryError, 21 | UserCanceled 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegMetadataReader.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #pragma once 14 | 15 | #include "MozJpegFileTypeIO.h" 16 | #include 17 | #include 18 | #include 19 | 20 | DecodeStatus ReadMetadata(j_decompress_ptr cinfo, const ReadCallbacks* callbacks); 21 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegDestiniationManager.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #pragma once 14 | 15 | #include "MozJpegFileTypeIO.h" 16 | #include 17 | #include 18 | #include 19 | 20 | void InitializeDestinationManager(j_compress_ptr cinfo, WriteCallback writeCallback); 21 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegSourceManager.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #pragma once 14 | 15 | #include "MozJpegFileTypeIO.h" 16 | #include 17 | #include 18 | #include 19 | 20 | void InitializeSourceManager(j_decompress_ptr cinfo, const ReadCallbacks* readCallbacks); 21 | -------------------------------------------------------------------------------- /src/Interop/DecodeStatus.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Interop 14 | { 15 | internal enum DecodeStatus 16 | { 17 | Ok = 0, 18 | NullParameter, 19 | OutOfMemory, 20 | JpegLibraryError, 21 | CallbackError, 22 | UserCanceled 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Interop/JpegLibraryErrorInfo.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Runtime.InteropServices; 14 | 15 | namespace MozJpegFileType.Interop 16 | { 17 | [StructLayout(LayoutKind.Sequential)] 18 | internal unsafe struct JpegLibraryErrorInfo 19 | { 20 | public fixed sbyte errorMessage[256]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeFactory.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | 15 | namespace MozJpegFileType 16 | { 17 | public sealed class MozJpegFileTypeFactory : IFileTypeFactory2 18 | { 19 | public FileType[] GetFileTypeInstances(IFileTypeHost host) 20 | { 21 | return new[] { new MozJpegFileTypePlugin(host) }; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Interop/EncodeOptions.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Runtime.InteropServices; 14 | 15 | namespace MozJpegFileType.Interop 16 | { 17 | [StructLayout(LayoutKind.Sequential)] 18 | internal struct EncodeOptions 19 | { 20 | public int quality; 21 | public ChromaSubsampling chromaSubsampling; 22 | [MarshalAs(UnmanagedType.U1)] 23 | public bool progressive; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exif/TagDataType.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal enum TagDataType : ushort 16 | { 17 | Byte = 1, 18 | Ascii = 2, 19 | Short = 3, 20 | Long = 4, 21 | Rational = 5, 22 | SByte = 6, 23 | Undefined = 7, 24 | SShort = 8, 25 | SLong = 9, 26 | SRational = 10, 27 | Float = 11, 28 | Double = 12, 29 | IFD = 13 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Interop/ReadCallbacks.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Runtime.InteropServices; 14 | 15 | namespace MozJpegFileType.Interop 16 | { 17 | [StructLayout(LayoutKind.Sequential)] 18 | internal sealed class ReadCallbacks 19 | { 20 | [MarshalAs(UnmanagedType.FunctionPtr)] 21 | public ReadCallback read; 22 | 23 | [MarshalAs(UnmanagedType.FunctionPtr)] 24 | public SkipBytesCallback skipBytes; 25 | 26 | [MarshalAs(UnmanagedType.FunctionPtr)] 27 | public AllocateSurfaceCallback allocateSurface; 28 | 29 | [MarshalAs(UnmanagedType.FunctionPtr)] 30 | public SetMetadataCallback setIccProfile; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021, 2022 Nicholas Hayes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ChromaSubsampling.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType 14 | { 15 | internal enum ChromaSubsampling 16 | { 17 | /// 18 | /// 4:2:0 (best compression) 19 | /// 20 | Subsampling420, 21 | 22 | /// 23 | /// 4:2:2 24 | /// 25 | Subsampling422, 26 | 27 | /// 28 | /// 4:4:4 (best quality) 29 | /// 30 | Subsampling444, 31 | 32 | /// 33 | /// YUV 4:0:0 34 | /// 35 | /// 36 | /// Used internally for gray-scale images, not shown to the user. 37 | /// 38 | Subsampling400 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/PluginSupportInfo.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | using System; 15 | using System.Reflection; 16 | 17 | namespace MozJpegFileType 18 | { 19 | public class PluginSupportInfo : IPluginSupportInfo 20 | { 21 | private readonly Assembly assembly = typeof(PluginSupportInfo).Assembly; 22 | 23 | public string Author => this.assembly.GetCustomAttribute().Copyright; 24 | 25 | public string Copyright => this.assembly.GetCustomAttribute().Description; 26 | 27 | public string DisplayName => this.assembly.GetCustomAttribute().Product; 28 | 29 | public Version Version => this.assembly.GetName().Version; 30 | 31 | public Uri WebsiteUri => new Uri("https://www.getpaint.net/redirect/plugins.html"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Xmp/ExtendedXmpData.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | using System.Collections.Generic; 15 | 16 | namespace MozJpegFileType.Xmp 17 | { 18 | internal readonly struct ExtendedXmpData 19 | { 20 | public ExtendedXmpData(byte[] standardXmpBytes, List extendedXmp) 21 | { 22 | if (standardXmpBytes is null) 23 | { 24 | throw new ArgumentNullException(nameof(standardXmpBytes)); 25 | } 26 | 27 | if (extendedXmp is null) 28 | { 29 | throw new ArgumentNullException(nameof(extendedXmp)); 30 | } 31 | 32 | this.StandardXmpBytes = standardXmpBytes; 33 | this.ExtendedXmpChunks = extendedXmp; 34 | } 35 | 36 | public byte[] StandardXmpBytes { get; } 37 | 38 | public List ExtendedXmpChunks { get; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pdn-mozjpeg 2 | 3 | A [Paint.NET](http://www.getpaint.net) filetype plugin that saves JPEG images using the [mozjpeg](https://github.com/mozilla/mozjpeg) encoder. 4 | 5 | ## Installation 6 | 7 | 1. Close Paint.NET. 8 | 2. Place MozJpegFileType.dll, MozJpegFileTypeIO_x86.dll, MozJpegFileTypeIO_ARM64.dll and MozJpegFileTypeIO_x64.dll in the Paint.NET FileTypes folder which is usually located in one the following locations depending on the Paint.NET version you have installed. 9 | 10 | Paint.NET Version | FileTypes Folder Location 11 | --------|---------- 12 | Classic | C:\Program Files\Paint.NET\FileTypes 13 | Microsoft Store | Documents\paint.net App Files\FileTypes 14 | 15 | 3. Restart Paint.NET. 16 | 4. The filetype should now be available as the "MozJpeg" item in the save dialog. 17 | 18 | ## License 19 | 20 | This project is licensed under the terms of the MIT License. 21 | See [License.txt](License.txt) for more information. 22 | 23 | # Source code 24 | 25 | ## Prerequisites 26 | 27 | * Visual Studio 2022 28 | * Paint.NET 4.3.12 or later 29 | * The `mozjpeg` package from [VCPkg](https://github.com/microsoft/vcpkg). 30 | 31 | ## Building the plugin 32 | 33 | * Open the solution 34 | * Change the PaintDotNet references in the MozJpegFileType project to match your Paint.NET install location 35 | * Update the post build events to copy the build output to the Paint.NET FileTypes folder 36 | * Build the solution -------------------------------------------------------------------------------- /src/Interop/MozJpeg_X64.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Runtime.InteropServices; 14 | 15 | namespace MozJpegFileType.Interop 16 | { 17 | internal static class MozJpeg_X64 18 | { 19 | private const string DllName = "MozJpegFileTypeIO_x64.dll"; 20 | 21 | [DllImport(DllName)] 22 | internal static extern unsafe DecodeStatus ReadImage( 23 | ReadCallbacks callbacks, 24 | ref JpegLibraryErrorInfo errorInfo); 25 | 26 | [DllImport(DllName)] 27 | internal static extern unsafe EncodeStatus WriteImage( 28 | [In] ref BitmapData bitmapData, 29 | [In] ref EncodeOptions encodeOptions, 30 | [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MetadataCustomMarshaler))] MetadataParams metadata, 31 | ref JpegLibraryErrorInfo errorInfo, 32 | [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback progressCallback, 33 | [MarshalAs(UnmanagedType.FunctionPtr)] WriteCallback writeCallback); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Interop/MozJpeg_X86.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Runtime.InteropServices; 14 | 15 | namespace MozJpegFileType.Interop 16 | { 17 | internal static class MozJpeg_X86 18 | { 19 | private const string DllName = "MozJpegFileTypeIO_x86.dll"; 20 | 21 | [DllImport(DllName)] 22 | internal static extern unsafe DecodeStatus ReadImage( 23 | ReadCallbacks callbacks, 24 | ref JpegLibraryErrorInfo errorInfo); 25 | 26 | [DllImport(DllName)] 27 | internal static extern unsafe EncodeStatus WriteImage( 28 | [In] ref BitmapData bitmapData, 29 | [In] ref EncodeOptions encodeOptions, 30 | [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MetadataCustomMarshaler))] MetadataParams metadata, 31 | ref JpegLibraryErrorInfo errorInfo, 32 | [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback progressCallback, 33 | [MarshalAs(UnmanagedType.FunctionPtr)] WriteCallback writeCallback); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Interop/MozJpeg_Arm64.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Runtime.InteropServices; 14 | 15 | namespace MozJpegFileType.Interop 16 | { 17 | internal static class MozJpeg_Arm64 18 | { 19 | private const string DllName = "MozJpegFileTypeIO_ARM64.dll"; 20 | 21 | [DllImport(DllName)] 22 | internal static extern unsafe DecodeStatus ReadImage( 23 | ReadCallbacks callbacks, 24 | ref JpegLibraryErrorInfo errorInfo); 25 | 26 | [DllImport(DllName)] 27 | internal static extern unsafe EncodeStatus WriteImage( 28 | [In] ref BitmapData bitmapData, 29 | [In] ref EncodeOptions encodeOptions, 30 | [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MetadataCustomMarshaler))] MetadataParams metadata, 31 | ref JpegLibraryErrorInfo errorInfo, 32 | [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback progressCallback, 33 | [MarshalAs(UnmanagedType.FunctionPtr)] WriteCallback writeCallback); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Xmp/ExtendedXmpChunk.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | 15 | namespace MozJpegFileType.Xmp 16 | { 17 | internal sealed class ExtendedXMPChunk : Disposable 18 | { 19 | private IArrayPoolBuffer data; 20 | 21 | public ExtendedXMPChunk(string md5Guid, uint totalLength, uint chunkOffset, IArrayPoolBuffer data) 22 | { 23 | this.MD5Guid = md5Guid; 24 | this.TotalLength = totalLength; 25 | this.ChunkOffset = chunkOffset; 26 | this.data = data; 27 | } 28 | 29 | public string MD5Guid { get; } 30 | 31 | public uint TotalLength { get; } 32 | 33 | public uint ChunkOffset { get; } 34 | 35 | public IArrayPoolBuffer Data => this.data; 36 | 37 | protected override void Dispose(bool disposing) 38 | { 39 | if (disposing) 40 | { 41 | DisposableUtil.Free(ref this.data); 42 | } 43 | 44 | base.Dispose(disposing); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Interop/MetadataParams.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | using System.Collections.Generic; 15 | using System.Runtime.InteropServices; 16 | 17 | namespace MozJpegFileType.Interop 18 | { 19 | 20 | [StructLayout(LayoutKind.Sequential)] 21 | internal sealed class MetadataParams 22 | { 23 | public byte[] iccProfile; 24 | public byte[] exif; 25 | public byte[] standardXmp; 26 | public List extendedXmpChunks; 27 | 28 | public MetadataParams(byte[] exifBytes, 29 | byte[] iccProfileBytes, 30 | byte[] standardXmpBytes, 31 | List extendedXmpChunks) 32 | { 33 | if (extendedXmpChunks is null) 34 | { 35 | throw new ArgumentNullException(nameof(extendedXmpChunks)); 36 | } 37 | 38 | this.exif = exifBytes; 39 | this.iccProfile = iccProfileBytes; 40 | this.standardXmp = standardXmpBytes; 41 | this.extendedXmpChunks = extendedXmpChunks; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exif/EndianUtil.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal static class EndianUtil 16 | { 17 | public static ushort Swap(ushort value) 18 | { 19 | return (ushort)(((value & 0xff00) >> 8) | ((value & 0x00ff) << 8)); 20 | } 21 | 22 | public static uint Swap(uint value) 23 | { 24 | return ((value & 0xff000000) >> 24) | 25 | ((value & 0x00ff0000) >> 8 ) | 26 | ((value & 0x0000ff00) << 8 ) | 27 | ((value & 0x000000ff) << 24); 28 | } 29 | 30 | public static ulong Swap(ulong value) 31 | { 32 | return ((value & 0xff00000000000000) >> 56) | 33 | ((value & 0x00ff000000000000) >> 40) | 34 | ((value & 0x0000ff0000000000) >> 24) | 35 | ((value & 0x000000ff00000000) >> 8 ) | 36 | ((value & 0x00000000ff000000) << 8 ) | 37 | ((value & 0x0000000000ff0000) << 24) | 38 | ((value & 0x000000000000ff00) << 40) | 39 | ((value & 0x00000000000000ff) << 56); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exif/MetadataHelpers.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal static class MetadataHelpers 16 | { 17 | internal static byte[] EncodeLong(uint value) 18 | { 19 | return new byte[] 20 | { 21 | (byte)(value & 0xff), 22 | (byte)(value >> 8), 23 | (byte)(value >> 16), 24 | (byte)(value >> 24) 25 | }; 26 | } 27 | 28 | internal static byte[] EncodeShort(ushort value) 29 | { 30 | return new byte[] 31 | { 32 | (byte)(value & 0xff), 33 | (byte)(value >> 8) 34 | }; 35 | } 36 | 37 | internal static bool TryDecodeShort(MetadataEntry entry, out ushort value) 38 | { 39 | if (entry.Type != TagDataType.Short || entry.LengthInBytes != 2) 40 | { 41 | value = 0; 42 | return false; 43 | } 44 | 45 | byte[] data = entry.GetDataReadOnly(); 46 | 47 | value = (ushort)(data[0] | (data[1] << 8)); 48 | 49 | return true; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Interop/CallbackDelegates.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | using System.Runtime.InteropServices; 15 | 16 | namespace MozJpegFileType.Interop 17 | { 18 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 19 | [return: MarshalAs(UnmanagedType.U1)] 20 | internal delegate bool ProgressCallback(int progress); 21 | 22 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 23 | internal delegate int ReadCallback(IntPtr data, int maxNumberOfBytesToRead); 24 | 25 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 26 | [return: MarshalAs(UnmanagedType.U1)] 27 | internal delegate bool SkipBytesCallback(int numberOfBytesToSkip); 28 | 29 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 30 | [return: MarshalAs(UnmanagedType.U1)] 31 | internal delegate bool WriteCallback(IntPtr data, UIntPtr dataSize); 32 | 33 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 34 | internal delegate IntPtr AllocateSurfaceCallback(int width, int height, out int stride); 35 | 36 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 37 | [return: MarshalAs(UnmanagedType.U1)] 38 | internal delegate bool SetMetadataCallback(IntPtr data, int size, MetadataType type); 39 | } 40 | -------------------------------------------------------------------------------- /src/MozJpegFileType.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0-windows 4 | Library 5 | false 6 | true 7 | true 8 | true 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ..\..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Base.dll 29 | 30 | 31 | ..\..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Core.dll 32 | 33 | 34 | ..\..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Data.dll 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Reflection; 14 | using System.Runtime.InteropServices; 15 | using System.Runtime.Versioning; 16 | 17 | // General Information about an assembly is controlled through the following 18 | // set of attributes. Change these attribute values to modify the information 19 | // associated with an assembly. 20 | [assembly: AssemblyTitle("MozJpegFileType")] 21 | [assembly: AssemblyDescription("")] 22 | [assembly: AssemblyConfiguration("")] 23 | [assembly: AssemblyCompany("null54")] 24 | [assembly: AssemblyProduct("MozJpegFileType")] 25 | [assembly: AssemblyCopyright("Copyright © 2022 Nicholas Hayes (aka null54)")] 26 | [assembly: AssemblyTrademark("")] 27 | [assembly: AssemblyCulture("")] 28 | 29 | // Setting ComVisible to false makes the types in this assembly not visible 30 | // to COM components. If you need to access a type in this assembly from 31 | // COM, set the ComVisible attribute to true on that type. 32 | [assembly: ComVisible(false)] 33 | 34 | // The following GUID is for the ID of the typelib if this project is exposed to COM 35 | [assembly: Guid("cce7e6c5-b93b-4f37-96ee-620b82f11431")] 36 | 37 | [assembly: SupportedOSPlatform("windows")] 38 | 39 | // Version information for an assembly consists of the following four values: 40 | // 41 | // Major Version 42 | // Minor Version 43 | // Build Number 44 | // Revision 45 | // 46 | // You can specify all the values or you can default the Build and Revision Numbers 47 | // by using the '*' as shown below: 48 | // [assembly: AssemblyVersion("1.0.*")] 49 | [assembly: AssemblyVersion("1.0.1.0")] 50 | [assembly: AssemblyFileVersion("1.0.1.0")] 51 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = crlf 8 | 9 | [*.cs] 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | # Newline settings 16 | csharp_new_line_before_open_brace = all 17 | csharp_new_line_before_else = true 18 | csharp_new_line_before_catch = true 19 | csharp_new_line_before_finally = true 20 | csharp_new_line_before_members_in_object_initializers = true 21 | csharp_new_line_before_members_in_anonymous_types = true 22 | csharp_new_line_between_query_expression_clauses = true 23 | 24 | # Expression-bodied member settings 25 | csharp_style_expression_bodied_methods = false : none 26 | csharp_style_expression_bodied_constructors = false : none 27 | csharp_style_expression_bodied_operators = false : none 28 | csharp_style_expression_bodied_properties = false : none 29 | csharp_style_expression_bodied_indexers = false : none 30 | csharp_style_expression_bodied_accessors = false : none 31 | 32 | # Code style defaults 33 | csharp_style_implicit_object_creation_when_type_is_apparent = false 34 | 35 | # Code block settings 36 | csharp_prefer_braces = true : warning 37 | 38 | # "This." qualifier settings 39 | dotnet_style_qualification_for_field = true : suggestion 40 | dotnet_style_qualification_for_property = true : suggestion 41 | dotnet_style_qualification_for_method = false : suggestion 42 | dotnet_style_qualification_for_event = true : suggestion 43 | 44 | # IDE0180: Use tuple to swap values 45 | csharp_style_prefer_tuple_swap = false 46 | 47 | [*.{cpp,h}] 48 | indent_size = 4 49 | indent_style = space 50 | trim_trailing_whitespace = true 51 | insert_final_newline = true -------------------------------------------------------------------------------- /src/Exif/MetadataKey.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | using System.Diagnostics; 15 | 16 | namespace MozJpegFileType.Exif 17 | { 18 | [DebuggerDisplay("{DebuggerDisplay, nq}")] 19 | internal readonly struct MetadataKey 20 | : IEquatable 21 | { 22 | public MetadataKey(MetadataSection section, ushort tagId) 23 | { 24 | this.Section = section; 25 | this.TagId = tagId; 26 | } 27 | 28 | public MetadataSection Section { get; } 29 | 30 | public ushort TagId { get; } 31 | 32 | [DebuggerBrowsable(DebuggerBrowsableState.Never)] 33 | private string DebuggerDisplay 34 | { 35 | get 36 | { 37 | return string.Format("{0}, Tag# {1} (0x{1:X})", this.Section, this.TagId); 38 | } 39 | } 40 | 41 | public override bool Equals(object obj) 42 | { 43 | return obj is MetadataKey other && Equals(other); 44 | } 45 | 46 | public bool Equals(MetadataKey other) 47 | { 48 | return this.Section == other.Section && this.TagId == other.TagId; 49 | } 50 | 51 | public override int GetHashCode() 52 | { 53 | int hashCode = -2103575766; 54 | 55 | unchecked 56 | { 57 | hashCode = (hashCode * -1521134295) + this.Section.GetHashCode(); 58 | hashCode = (hashCode * -1521134295) + this.TagId.GetHashCode(); 59 | } 60 | 61 | return hashCode; 62 | } 63 | 64 | public static bool operator ==(MetadataKey left, MetadataKey right) 65 | { 66 | return left.Equals(right); 67 | } 68 | 69 | public static bool operator !=(MetadataKey left, MetadataKey right) 70 | { 71 | return !(left == right); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/MozJpegFileTypeIO.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Header Files 20 | 21 | 22 | Header Files 23 | 24 | 25 | Header Files 26 | 27 | 28 | Header Files 29 | 30 | 31 | Header Files 32 | 33 | 34 | Header Files 35 | 36 | 37 | 38 | 39 | Source Files 40 | 41 | 42 | Source Files 43 | 44 | 45 | Source Files 46 | 47 | 48 | Source Files 49 | 50 | 51 | Source Files 52 | 53 | 54 | 55 | 56 | Resource Files 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Exif/ExifTagHelper.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System.Collections.Generic; 14 | 15 | namespace MozJpegFileType.Exif 16 | { 17 | internal static class ExifTagHelper 18 | { 19 | private static readonly HashSet supportedTiffImageTagsForWriting = new HashSet 20 | { 21 | // The tags related to storing offsets are included for reference, 22 | // but are not written to the EXIF blob. 23 | 24 | // Tags relating to image data structure 25 | 256, // ImageWidth 26 | 257, // ImageLength 27 | 258, // BitsPerSample 28 | 259, // Compression 29 | 262, // PhotometricInterpretation 30 | 274, // Orientation 31 | 277, // SamplesPerPixel 32 | 284, // PlanarConfiguration 33 | 530, // YCbCrSubSampling 34 | 531, // YCbCrPositioning 35 | 282, // XResolution 36 | 283, // YResolution 37 | 296, // ResolutionUnit 38 | 39 | // Tags relating to recording offset 40 | //273, // StripOffsets 41 | //278, // RowsPerStrip 42 | //279, // StripByteCounts 43 | //513, // JPEGInterchangeFormat 44 | //514, // JPEGInterchangeFormatLength 45 | 46 | // Tags relating to image data characteristics 47 | 301, // TransferFunction 48 | 318, // WhitePoint 49 | 319, // PrimaryChromaticities 50 | 529, // YCbCrCoefficients 51 | 532, // ReferenceBlackWhite 52 | 53 | // Other tags 54 | 306, // DateTime 55 | 270, // ImageDescription 56 | 271, // Make 57 | 272, // Model 58 | 305, // Software 59 | 315, // Artist 60 | 33432 // Copyright 61 | }; 62 | 63 | internal static bool CanWriteImageSectionTag(ushort tagId) 64 | { 65 | return supportedTiffImageTagsForWriting.Contains(tagId); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegMetadataWriter.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #include "JpegMetadataWriter.h" 14 | 15 | namespace 16 | { 17 | constexpr int App1Marker = JPEG_APP0 + 1; 18 | 19 | void WriteExifBlock(j_compress_ptr cinfo, const uint8_t* data, size_t dataSize) 20 | { 21 | jpeg_write_marker(cinfo, App1Marker, static_cast(data), static_cast(dataSize)); 22 | } 23 | 24 | void WriteStandardXmpBlock(j_compress_ptr cinfo, const uint8_t* data, size_t dataSize) 25 | { 26 | jpeg_write_marker(cinfo, App1Marker, static_cast(data), static_cast(dataSize)); 27 | } 28 | 29 | void WriteExtendedXmpBlocks(j_compress_ptr cinfo, const ExtendedXmpBlock* blocks, size_t blockCount) 30 | { 31 | for (size_t i = 0; i < blockCount; i++) 32 | { 33 | const ExtendedXmpBlock* block = &blocks[i]; 34 | const uint8_t* data = block->data; 35 | const size_t dataSize = block->length; 36 | 37 | jpeg_write_marker(cinfo, App1Marker, static_cast(data), static_cast(dataSize)); 38 | } 39 | } 40 | } 41 | 42 | void WriteMetadata(j_compress_ptr cinfo, const MetadataParams* metadata) 43 | { 44 | if (metadata->exif != nullptr && metadata->exifSize > 0) 45 | { 46 | WriteExifBlock(cinfo, metadata->exif, metadata->exifSize); 47 | } 48 | 49 | if (metadata->standardXmp != nullptr && metadata->standardXmpSize > 0) 50 | { 51 | WriteStandardXmpBlock(cinfo, metadata->standardXmp, metadata->standardXmpSize); 52 | 53 | if (metadata->extendedXmpBlocks != nullptr && metadata->extendedXmpBlockCount > 0) 54 | { 55 | WriteExtendedXmpBlocks(cinfo, metadata->extendedXmpBlocks, metadata->extendedXmpBlockCount); 56 | } 57 | } 58 | 59 | if (metadata->iccProfile != nullptr && metadata->iccProfileSize > 0) 60 | { 61 | jpeg_write_icc_profile(cinfo, metadata->iccProfile, static_cast(metadata->iccProfileSize)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/version.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #include "resource.h" 4 | 5 | #define APSTUDIO_READONLY_SYMBOLS 6 | ///////////////////////////////////////////////////////////////////////////// 7 | // 8 | // Generated from the TEXTINCLUDE 2 resource. 9 | // 10 | #include "winres.h" 11 | 12 | ///////////////////////////////////////////////////////////////////////////// 13 | #undef APSTUDIO_READONLY_SYMBOLS 14 | 15 | ///////////////////////////////////////////////////////////////////////////// 16 | // English (United States) resources 17 | 18 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 19 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 20 | #pragma code_page(1252) 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Version 51 | // 52 | 53 | VS_VERSION_INFO VERSIONINFO 54 | FILEVERSION 1,0,1,0 55 | PRODUCTVERSION 1,0,1,0 56 | FILEFLAGSMASK 0x3fL 57 | #ifdef _DEBUG 58 | FILEFLAGS 0x1L 59 | #else 60 | FILEFLAGS 0x0L 61 | #endif 62 | FILEOS 0x40004L 63 | FILETYPE 0x2L 64 | FILESUBTYPE 0x0L 65 | BEGIN 66 | BLOCK "StringFileInfo" 67 | BEGIN 68 | BLOCK "040904b0" 69 | BEGIN 70 | VALUE "CompanyName", "null54" 71 | VALUE "FileDescription", "MozJpegFileType IO library" 72 | VALUE "FileVersion", "1.0.1.0" 73 | VALUE "LegalCopyright", "Copyright (C) 2022 Nicholas Hayes" 74 | VALUE "OriginalFilename", "MozJpegF.dll" 75 | VALUE "ProductName", "MozJpegFileType" 76 | VALUE "ProductVersion", "1.0.1.0" 77 | END 78 | END 79 | BLOCK "VarFileInfo" 80 | BEGIN 81 | VALUE "Translation", 0x409, 1200 82 | END 83 | END 84 | 85 | #endif // English (United States) resources 86 | ///////////////////////////////////////////////////////////////////////////// 87 | 88 | 89 | 90 | #ifndef APSTUDIO_INVOKED 91 | ///////////////////////////////////////////////////////////////////////////// 92 | // 93 | // Generated from the TEXTINCLUDE 3 resource. 94 | // 95 | 96 | 97 | ///////////////////////////////////////////////////////////////////////////// 98 | #endif // not APSTUDIO_INVOKED 99 | 100 | -------------------------------------------------------------------------------- /src/Exif/ExifValueCollection.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | using System.Collections; 15 | using System.Collections.Generic; 16 | using System.Diagnostics; 17 | 18 | namespace MozJpegFileType.Exif 19 | { 20 | [DebuggerDisplay("Count = {Count}")] 21 | [DebuggerTypeProxy(typeof(ExifValueCollectionDebugView))] 22 | internal sealed class ExifValueCollection 23 | : IEnumerable 24 | { 25 | private readonly List exifMetadata; 26 | 27 | public ExifValueCollection(List items) 28 | { 29 | this.exifMetadata = items ?? throw new ArgumentNullException(nameof(items)); 30 | } 31 | 32 | public int Count => this.exifMetadata.Count; 33 | 34 | public MetadataEntry GetAndRemoveValue(MetadataKey key) 35 | { 36 | MetadataEntry value = this.exifMetadata.Find(p => p.Section == key.Section && p.TagId == key.TagId); 37 | 38 | if (value != null) 39 | { 40 | this.exifMetadata.RemoveAll(p => p.Section == key.Section && p.TagId == key.TagId); 41 | } 42 | 43 | return value; 44 | } 45 | 46 | public void Remove(MetadataKey key) 47 | { 48 | this.exifMetadata.RemoveAll(p => p.Section == key.Section && p.TagId == key.TagId); 49 | } 50 | 51 | public IEnumerator GetEnumerator() 52 | { 53 | return this.exifMetadata.GetEnumerator(); 54 | } 55 | 56 | IEnumerator IEnumerable.GetEnumerator() 57 | { 58 | return this.exifMetadata.GetEnumerator(); 59 | } 60 | 61 | private sealed class ExifValueCollectionDebugView 62 | { 63 | private readonly ExifValueCollection collection; 64 | 65 | public ExifValueCollectionDebugView(ExifValueCollection collection) 66 | { 67 | this.collection = collection ?? throw new ArgumentNullException(nameof(collection)); 68 | } 69 | 70 | [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] 71 | public MetadataEntry[] Items 72 | { 73 | get 74 | { 75 | return this.collection.exifMetadata.ToArray(); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | 65 | # GitHub Linguist override 66 | *.cs linguist-language=C# 67 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegDestinationManager.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #include "MozJpegFileTypeIO.h" 14 | #include "JpegDestiniationManager.h" 15 | 16 | namespace 17 | { 18 | constexpr size_t WriteContextBufferSize = 4096; 19 | 20 | struct JpegWriteContext 21 | { 22 | jpeg_destination_mgr mgr; 23 | 24 | WriteCallback write; 25 | JOCTET buffer[WriteContextBufferSize]; 26 | }; 27 | 28 | void init_destination(j_compress_ptr cinfo) 29 | { 30 | JpegWriteContext* ctx = reinterpret_cast(cinfo->dest); 31 | 32 | ctx->mgr.next_output_byte = ctx->buffer; 33 | ctx->mgr.free_in_buffer = WriteContextBufferSize; 34 | } 35 | 36 | boolean empty_output_buffer(j_compress_ptr cinfo) 37 | { 38 | JpegWriteContext* ctx = reinterpret_cast(cinfo->dest); 39 | 40 | if (!ctx->write(ctx->buffer, WriteContextBufferSize)) 41 | { 42 | ERREXIT(cinfo, JERR_FILE_WRITE); 43 | } 44 | 45 | ctx->mgr.next_output_byte = ctx->buffer; 46 | ctx->mgr.free_in_buffer = WriteContextBufferSize; 47 | 48 | return true; 49 | } 50 | 51 | void term_destination(j_compress_ptr cinfo) 52 | { 53 | JpegWriteContext* ctx = reinterpret_cast(cinfo->dest); 54 | size_t remaining = WriteContextBufferSize - ctx->mgr.free_in_buffer; 55 | 56 | if (remaining > 0) 57 | { 58 | if (!ctx->write(ctx->buffer, remaining)) 59 | { 60 | ERREXIT(cinfo, JERR_FILE_WRITE); 61 | } 62 | } 63 | } 64 | } 65 | 66 | void InitializeDestinationManager(j_compress_ptr cinfo, WriteCallback writeCallback) 67 | { 68 | if (cinfo->dest == nullptr) 69 | { 70 | cinfo->dest = static_cast((*cinfo->mem->alloc_small)( 71 | reinterpret_cast(cinfo), 72 | JPOOL_PERMANENT, 73 | sizeof(JpegWriteContext))); 74 | } 75 | else if (cinfo->dest->init_destination != init_destination) 76 | { 77 | // The destination manager was not created by this function. 78 | 79 | ERREXIT(cinfo, JERR_BUFFER_SIZE); 80 | } 81 | 82 | JpegWriteContext* ctx = reinterpret_cast(cinfo->dest); 83 | 84 | ctx->mgr.init_destination = init_destination; 85 | ctx->mgr.empty_output_buffer = empty_output_buffer; 86 | ctx->mgr.term_destination = term_destination; 87 | ctx->write = writeCallback; 88 | } 89 | -------------------------------------------------------------------------------- /src/Exif/IFDEntry.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | 15 | namespace MozJpegFileType.Exif 16 | { 17 | internal readonly struct IFDEntry 18 | : IEquatable 19 | { 20 | public const int SizeOf = 12; 21 | 22 | public IFDEntry(EndianBinaryReader reader) 23 | { 24 | this.Tag = reader.ReadUInt16(); 25 | this.Type = (TagDataType)reader.ReadUInt16(); 26 | this.Count = reader.ReadUInt32(); 27 | this.Offset = reader.ReadUInt32(); 28 | } 29 | 30 | public IFDEntry(ushort tag, TagDataType type, uint count, uint offset) 31 | { 32 | this.Tag = tag; 33 | this.Type = type; 34 | this.Count = count; 35 | this.Offset = offset; 36 | } 37 | 38 | public ushort Tag { get; } 39 | 40 | public TagDataType Type { get; } 41 | 42 | public uint Count { get; } 43 | 44 | public uint Offset { get; } 45 | 46 | public override bool Equals(object obj) 47 | { 48 | return obj is IFDEntry entry && Equals(entry); 49 | } 50 | 51 | public bool Equals(IFDEntry other) 52 | { 53 | return this.Tag == other.Tag && 54 | this.Type == other.Type && 55 | this.Count == other.Count && 56 | this.Offset == other.Offset; 57 | } 58 | 59 | public override int GetHashCode() 60 | { 61 | int hashCode = 1198491158; 62 | 63 | unchecked 64 | { 65 | hashCode = (hashCode * -1521134295) + this.Tag.GetHashCode(); 66 | hashCode = (hashCode * -1521134295) + this.Count.GetHashCode(); 67 | hashCode = (hashCode * -1521134295) + this.Type.GetHashCode(); 68 | hashCode = (hashCode * -1521134295) + this.Offset.GetHashCode(); 69 | } 70 | 71 | return hashCode; 72 | } 73 | 74 | public void Write(System.IO.BinaryWriter writer) 75 | { 76 | writer.Write(this.Tag); 77 | writer.Write((ushort)this.Type); 78 | writer.Write(this.Count); 79 | writer.Write(this.Offset); 80 | } 81 | 82 | public static bool operator ==(IFDEntry left, IFDEntry right) 83 | { 84 | return left.Equals(right); 85 | } 86 | 87 | public static bool operator !=(IFDEntry left, IFDEntry right) 88 | { 89 | return !(left == right); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Exif/TiffConstants.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal static class TiffConstants 16 | { 17 | internal const ushort BigEndianByteOrderMarker = 0x4d4d; 18 | internal const ushort LittleEndianByteOrderMarker = 0x4949; 19 | internal const ushort Signature = 42; 20 | 21 | internal static class Tags 22 | { 23 | internal const ushort StripOffsets = 273; 24 | internal const ushort RowsPerStrip = 278; 25 | internal const ushort StripByteCounts = 279; 26 | internal const ushort SubIFDs = 330; 27 | internal const ushort ThumbnailOffset = 513; 28 | internal const ushort ThumbnailLength = 514; 29 | internal const ushort ExifIFD = 34665; 30 | internal const ushort GpsIFD = 34853; 31 | internal const ushort InteropIFD = 40965; 32 | } 33 | 34 | internal static class Orientation 35 | { 36 | /// 37 | /// The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side 38 | /// 39 | internal const ushort TopLeft = 1; 40 | 41 | /// 42 | /// The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side. 43 | /// 44 | internal const ushort TopRight = 2; 45 | 46 | /// 47 | /// The 0th row represents the visual bottom of the image, and the 0th column represents the visual right-hand side. 48 | /// 49 | internal const ushort BottomRight = 3; 50 | 51 | /// 52 | /// The 0th row represents the visual bottom of the image, and the 0th column represents the visual left-hand side. 53 | /// 54 | internal const ushort BottomLeft = 4; 55 | 56 | /// 57 | /// The 0th row represents the visual left-hand side of the image, and the 0th column represents the visual top. 58 | /// 59 | internal const ushort LeftTop = 5; 60 | 61 | /// 62 | /// The 0th row represents the visual right-hand side of the image, and the 0th column represents the visual top. 63 | /// 64 | internal const ushort RightTop = 6; 65 | 66 | /// 67 | /// The 0th row represents the visual right-hand side of the image, and the 0th column represents the visual bottom. 68 | /// 69 | internal const ushort RightBottom = 7; 70 | 71 | /// 72 | /// The 0th row represents the visual left-hand side of the image, and the 0th column represents the visual bottom. 73 | /// 74 | internal const ushort LeftBottom = 8; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/MozJpegFileTypeIO.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | typedef bool(__stdcall* ProgressCallback)(int32_t progress); 18 | 19 | typedef int32_t(__stdcall* ReadCallback)(void* buffer, int32_t size); 20 | 21 | typedef bool(__stdcall* SkipBytesCallback)(int32_t numberOfBytesToSkip); 22 | 23 | typedef bool(__stdcall* WriteCallback)(const void* buffer, size_t size); 24 | 25 | typedef uint8_t*(__stdcall* AllocateSurfaceCallback)(int32_t width, int32_t height, int32_t* outStride); 26 | 27 | enum class MetadataType : int 28 | { 29 | Exif = 0, 30 | Icc, 31 | StandardXmp, 32 | ExtendedXmp 33 | }; 34 | 35 | typedef bool(__stdcall* SetMetadataCallback)(const void* buffer, int32_t size, MetadataType type); 36 | 37 | struct ReadCallbacks 38 | { 39 | ReadCallback read; 40 | SkipBytesCallback skipBytes; 41 | AllocateSurfaceCallback allocateSurface; 42 | SetMetadataCallback setMetadata; 43 | }; 44 | 45 | enum class DecodeStatus : int 46 | { 47 | Ok = 0, 48 | NullParameter, 49 | OutOfMemory, 50 | JpegLibraryError, 51 | CallbackError, 52 | UserCanceled 53 | }; 54 | 55 | enum class ChromaSubsampling : int 56 | { 57 | Subsampling420 = 0, 58 | Subsampling422, 59 | Subsampling444, 60 | Subsampling400 61 | }; 62 | 63 | struct EncodeOptions 64 | { 65 | int quality; 66 | ChromaSubsampling chromaSubsampling; 67 | bool progressive; 68 | }; 69 | 70 | enum class EncodeStatus : int 71 | { 72 | Ok = 0, 73 | NullParameter, 74 | OutOfMemory, 75 | JpegLibraryError, 76 | UserCanceled 77 | }; 78 | 79 | struct BitmapData 80 | { 81 | uint8_t* scan0; 82 | uint32_t width; 83 | uint32_t height; 84 | uint32_t stride; 85 | }; 86 | 87 | struct JpegLibraryErrorInfo 88 | { 89 | static const size_t maxErrorMessageLength = 255; 90 | 91 | char errorMessage[maxErrorMessageLength + 1]; 92 | }; 93 | 94 | // This must be kept in sync with the NativeExtendedXmpBlock structure in MetadataCustomMarshaler.cs. 95 | struct ExtendedXmpBlock 96 | { 97 | uint8_t* data; 98 | size_t length; 99 | }; 100 | 101 | // This must be kept in sync with the NativeMetadataParams structure in MetadataCustomMarshaler.cs. 102 | struct MetadataParams 103 | { 104 | uint8_t* exif; 105 | size_t exifSize; 106 | uint8_t* iccProfile; 107 | size_t iccProfileSize; 108 | uint8_t* standardXmp; 109 | size_t standardXmpSize; 110 | ExtendedXmpBlock* extendedXmpBlocks; 111 | size_t extendedXmpBlockCount; 112 | }; 113 | 114 | extern "C" __declspec(dllexport) DecodeStatus ReadImage( 115 | const ReadCallbacks* callbacks, 116 | JpegLibraryErrorInfo* errorInfo); 117 | 118 | extern "C" __declspec(dllexport) EncodeStatus WriteImage( 119 | const BitmapData* bgraImage, 120 | const EncodeOptions* options, 121 | const MetadataParams* metadata, 122 | JpegLibraryErrorInfo* errorInfo, 123 | ProgressCallback progressCallback, 124 | WriteCallback writeCallback); 125 | -------------------------------------------------------------------------------- /src/MozJpegFileType.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32819.101 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MozJpegFileType", "MozJpegFileType.csproj", "{CCE7E6C5-B93B-4F37-96EE-620B82F11431}" 7 | EndProject 8 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MozJpegFileTypeIO", "MozJpegFileTypeIO\MozJpegFileTypeIO.vcxproj", "{599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EAEF26B5-9015-43CA-A974-BA03D14DAE38}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Debug|ARM64 = Debug|ARM64 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|ARM64 = Release|ARM64 23 | Release|x64 = Release|x64 24 | Release|x86 = Release|x86 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|ARM64.ActiveCfg = Debug|Any CPU 30 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|ARM64.Build.0 = Debug|Any CPU 31 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|x64.ActiveCfg = Debug|Any CPU 32 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|x64.Build.0 = Debug|Any CPU 33 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|x86.ActiveCfg = Debug|Any CPU 34 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Debug|x86.Build.0 = Debug|Any CPU 35 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Release|ARM64.ActiveCfg = Release|Any CPU 38 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Release|x64.ActiveCfg = Release|Any CPU 39 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Release|x64.Build.0 = Release|Any CPU 40 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Release|x86.ActiveCfg = Release|Any CPU 41 | {CCE7E6C5-B93B-4F37-96EE-620B82F11431}.Release|x86.Build.0 = Release|Any CPU 42 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Debug|Any CPU.ActiveCfg = Debug|x64 43 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Debug|ARM64.ActiveCfg = Debug|x64 44 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Debug|ARM64.Build.0 = Debug|x64 45 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Debug|x64.ActiveCfg = Debug|x64 46 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Debug|x64.Build.0 = Debug|x64 47 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Debug|x86.ActiveCfg = Debug|Win32 48 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Debug|x86.Build.0 = Debug|Win32 49 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Release|Any CPU.ActiveCfg = Release|Win32 50 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Release|ARM64.ActiveCfg = Release|ARM64 51 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Release|ARM64.Build.0 = Release|ARM64 52 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Release|x64.ActiveCfg = Release|x64 53 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Release|x64.Build.0 = Release|x64 54 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Release|x86.ActiveCfg = Release|Win32 55 | {599E9BCE-0B46-40B8-AA1C-CE0CE67BC4BE}.Release|x86.Build.0 = Release|Win32 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {1E825196-EEF1-498F-9C26-FA20AA0BBB88} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegSourceManager.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #include "JpegSourceManager.h" 14 | 15 | namespace 16 | { 17 | constexpr int32_t ReadContextBufferSize = 4096; 18 | 19 | struct JpegReadContext 20 | { 21 | jpeg_source_mgr mgr; 22 | 23 | ReadCallback read; 24 | SkipBytesCallback skipBytes; 25 | JOCTET buffer[ReadContextBufferSize]; 26 | bool startOfFile; 27 | }; 28 | 29 | void init_source(j_decompress_ptr cinfo) 30 | { 31 | JpegReadContext* ctx = reinterpret_cast(cinfo->src); 32 | 33 | ctx->startOfFile = true; 34 | } 35 | 36 | boolean fill_input_buffer(j_decompress_ptr cinfo) 37 | { 38 | JpegReadContext* ctx = reinterpret_cast(cinfo->src); 39 | 40 | int32_t bytesRead = ctx->read(ctx->buffer, ReadContextBufferSize); 41 | 42 | if (bytesRead == 0) // End of file 43 | { 44 | if (ctx->startOfFile) 45 | { 46 | ERREXIT(cinfo, JERR_EMPTY_IMAGE); 47 | } 48 | 49 | // Insert a fake end of image marker. 50 | ctx->buffer[0] = 0xFF; 51 | ctx->buffer[1] = JPEG_EOI; 52 | bytesRead = 2; 53 | } 54 | else if (bytesRead == -1) // Other file read errors 55 | { 56 | ERREXIT(cinfo, JERR_FILE_READ); 57 | } 58 | 59 | ctx->mgr.next_input_byte = ctx->buffer; 60 | ctx->mgr.bytes_in_buffer = bytesRead; 61 | ctx->startOfFile = false; 62 | 63 | return true; 64 | } 65 | 66 | void skip_input_data(j_decompress_ptr cinfo, long num_bytes) 67 | { 68 | if (num_bytes > 0) 69 | { 70 | if (static_cast(num_bytes) > cinfo->src->bytes_in_buffer) 71 | { 72 | JpegReadContext* ctx = reinterpret_cast(cinfo->src); 73 | 74 | if (!ctx->skipBytes(num_bytes)) 75 | { 76 | ERREXIT(cinfo, JERR_FILE_READ); 77 | } 78 | 79 | // Force the buffer to be refilled. 80 | ctx->mgr.next_input_byte = nullptr; 81 | ctx->mgr.bytes_in_buffer = 0; 82 | } 83 | else 84 | { 85 | cinfo->src->next_input_byte += num_bytes; 86 | cinfo->src->bytes_in_buffer -= num_bytes; 87 | } 88 | } 89 | } 90 | 91 | void term_source(j_decompress_ptr cinfo) 92 | { 93 | // Nothing to do. 94 | } 95 | } 96 | 97 | void InitializeSourceManager(j_decompress_ptr cinfo, const ReadCallbacks* readCallbacks) 98 | { 99 | if (cinfo->src == nullptr) 100 | { 101 | cinfo->src = static_cast((*cinfo->mem->alloc_small)( 102 | reinterpret_cast(cinfo), 103 | JPOOL_PERMANENT, 104 | sizeof(JpegReadContext))); 105 | } 106 | else if (cinfo->src->init_source != init_source) 107 | { 108 | // The destination manager was not created by this function. 109 | 110 | ERREXIT(cinfo, JERR_BUFFER_SIZE); 111 | } 112 | 113 | JpegReadContext* ctx = reinterpret_cast(cinfo->src); 114 | 115 | ctx->mgr.init_source = init_source; 116 | ctx->mgr.fill_input_buffer = fill_input_buffer; 117 | ctx->mgr.skip_input_data = skip_input_data; 118 | ctx->mgr.resync_to_restart = jpeg_resync_to_restart; 119 | ctx->mgr.term_source = term_source; 120 | ctx->read = readCallbacks->read; 121 | ctx->skipBytes = readCallbacks->skipBytes; 122 | 123 | ctx->mgr.next_input_byte = nullptr; 124 | ctx->mgr.bytes_in_buffer = 0; 125 | } 126 | -------------------------------------------------------------------------------- /src/Exif/TagDataTypeUtil.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | namespace MozJpegFileType.Exif 14 | { 15 | internal static class TagDataTypeUtil 16 | { 17 | /// 18 | /// Determines whether the is known to GDI+. 19 | /// 20 | /// The tag type. 21 | /// 22 | /// if the tag type is known to GDI+; otherwise, . 23 | /// 24 | public static bool IsKnownToGDIPlus(TagDataType type) 25 | { 26 | switch (type) 27 | { 28 | case TagDataType.Byte: 29 | case TagDataType.Ascii: 30 | case TagDataType.Short: 31 | case TagDataType.Long: 32 | case TagDataType.Rational: 33 | case TagDataType.Undefined: 34 | case TagDataType.SLong: 35 | case TagDataType.SRational: 36 | return true; 37 | default: 38 | return false; 39 | } 40 | } 41 | 42 | /// 43 | /// Gets the size in bytes of a value. 44 | /// 45 | /// The tag type. 46 | /// 47 | /// The size of the value in bytes. 48 | /// 49 | public static int GetSizeInBytes(TagDataType type) 50 | { 51 | switch (type) 52 | { 53 | case TagDataType.Byte: 54 | case TagDataType.Ascii: 55 | case TagDataType.Undefined: 56 | case TagDataType.SByte: 57 | return 1; 58 | case TagDataType.Short: 59 | case TagDataType.SShort: 60 | return 2; 61 | case TagDataType.Long: 62 | case TagDataType.SLong: 63 | case TagDataType.Float: 64 | case TagDataType.IFD: 65 | return 4; 66 | case TagDataType.Rational: 67 | case TagDataType.SRational: 68 | case TagDataType.Double: 69 | return 8; 70 | default: 71 | return 0; 72 | } 73 | } 74 | 75 | /// 76 | /// Determines whether the values fit in the offset field. 77 | /// 78 | /// The type. 79 | /// The count. 80 | /// 81 | /// if the values fit in the offset field; otherwise, . 82 | /// 83 | public static bool ValueFitsInOffsetField(TagDataType type, uint count) 84 | { 85 | switch (type) 86 | { 87 | case TagDataType.Byte: 88 | case TagDataType.Ascii: 89 | case TagDataType.Undefined: 90 | case TagDataType.SByte: 91 | return count <= 4; 92 | case TagDataType.Short: 93 | case TagDataType.SShort: 94 | return count <= 2; 95 | case TagDataType.Long: 96 | case TagDataType.SLong: 97 | case TagDataType.Float: 98 | case TagDataType.IFD: 99 | return count <= 1; 100 | case TagDataType.Rational: 101 | case TagDataType.SRational: 102 | case TagDataType.Double: 103 | default: 104 | return false; 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/ImageTransform.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | 15 | namespace MozJpegFileType 16 | { 17 | internal static class ImageTransform 18 | { 19 | internal static unsafe void FlipHorizontal(Surface surface) 20 | { 21 | int lastColumn = surface.Width - 1; 22 | int flipWidth = surface.Width / 2; 23 | 24 | for (int y = 0; y < surface.Height; y++) 25 | { 26 | for (int x = 0; x < flipWidth; x++) 27 | { 28 | int sampleColumn = lastColumn - x; 29 | 30 | ColorBgra temp = surface[x, y]; 31 | surface[x, y] = surface[sampleColumn, y]; 32 | surface[sampleColumn, y] = temp; 33 | } 34 | } 35 | } 36 | 37 | internal static unsafe void FlipVertical(Surface surface) 38 | { 39 | int lastRow = surface.Height - 1; 40 | int flipHeight = surface.Height / 2; 41 | 42 | for (int x = 0; x < surface.Width; x++) 43 | { 44 | for (int y = 0; y < flipHeight; y++) 45 | { 46 | int sampleRow = lastRow - y; 47 | 48 | ColorBgra temp = surface[x, y]; 49 | surface[x, y] = surface[x, sampleRow]; 50 | surface[x, sampleRow] = temp; 51 | } 52 | } 53 | } 54 | 55 | internal static unsafe void Rotate90CCW(ref Surface surface) 56 | { 57 | Surface temp = null; 58 | try 59 | { 60 | temp = new Surface(surface.Height, surface.Width); 61 | 62 | int lastColumn = surface.Width - 1; 63 | 64 | for (int y = 0; y < temp.Height; y++) 65 | { 66 | ColorBgra* dstPtr = temp.GetRowPointerUnchecked(y); 67 | 68 | for (int x = 0; x < temp.Width; x++) 69 | { 70 | ColorBgra pixel = surface[lastColumn - y, x]; 71 | 72 | dstPtr->Bgra = pixel.Bgra; 73 | dstPtr++; 74 | } 75 | } 76 | 77 | surface.Dispose(); 78 | surface = temp; 79 | temp = null; 80 | } 81 | finally 82 | { 83 | temp?.Dispose(); 84 | } 85 | } 86 | 87 | internal static unsafe void Rotate180(Surface surface) 88 | { 89 | int lastColumn = surface.Width - 1; 90 | int lastRow = surface.Height - 1; 91 | 92 | for (int y = 0; y < surface.Height; y++) 93 | { 94 | for (int x = 0; x < surface.Width; x++) 95 | { 96 | surface[x, y] = surface[lastColumn - x, lastRow - y]; 97 | } 98 | } 99 | } 100 | 101 | internal static unsafe void Rotate270CCW(ref Surface surface) 102 | { 103 | Surface temp = null; 104 | try 105 | { 106 | temp = new Surface(surface.Height, surface.Width); 107 | 108 | int lastRow = surface.Height - 1; 109 | 110 | for (int y = 0; y < temp.Height; y++) 111 | { 112 | ColorBgra* dstPtr = temp.GetRowPointerUnchecked(y); 113 | 114 | for (int x = 0; x < temp.Width; x++) 115 | { 116 | ColorBgra pixel = surface[y, lastRow - x]; 117 | 118 | dstPtr->Bgra = pixel.Bgra; 119 | dstPtr++; 120 | } 121 | } 122 | 123 | surface.Dispose(); 124 | surface = temp; 125 | temp = null; 126 | } 127 | finally 128 | { 129 | temp?.Dispose(); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Third Party Notices.txt: -------------------------------------------------------------------------------- 1 | This file lists the original copyright and license information for 2 | the third-party libraries or other resources included with this software. 3 | 4 | The attached notices are provided for information only. 5 | 6 | License notice for libjpeg 7 | -------------------------------------------------------------- 8 | 9 | The authors make NO WARRANTY or representation, either express or implied, 10 | with respect to this software, its quality, accuracy, merchantability, or 11 | fitness for a particular purpose. This software is provided "AS IS", and you, 12 | its user, assume the entire risk as to its quality and accuracy. 13 | 14 | This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. 15 | All Rights Reserved except as specified below. 16 | 17 | Permission is hereby granted to use, copy, modify, and distribute this 18 | software (or portions thereof) for any purpose, without fee, subject to these 19 | conditions: 20 | (1) If any part of the source code for this software is distributed, then this 21 | README file must be included, with this copyright and no-warranty notice 22 | unaltered; and any additions, deletions, or changes to the original files 23 | must be clearly indicated in accompanying documentation. 24 | (2) If only executable code is distributed, then the accompanying 25 | documentation must state that "this software is based in part on the work of 26 | the Independent JPEG Group". 27 | (3) Permission for use of this software is granted only if the user accepts 28 | full responsibility for any undesirable consequences; the authors accept 29 | NO LIABILITY for damages of any kind. 30 | 31 | These conditions apply to any software derived from or based on the IJG code, 32 | not just to the unmodified library. If you use our work, you ought to 33 | acknowledge us. 34 | 35 | Permission is NOT granted for the use of any IJG author's name or company name 36 | in advertising or publicity relating to this software or products derived from 37 | it. This software may be referred to only as "the Independent JPEG Group's 38 | software". 39 | 40 | We specifically permit and encourage the use of this software as the basis of 41 | commercial products, provided that all warranty or liability claims are 42 | assumed by the product vendor. 43 | 44 | License notice for libjpeg-turbo 45 | -------------------------------------------------------------- 46 | 47 | Copyright (C)2009-2021 D. R. Commander. All Rights Reserved. 48 | Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. 49 | 50 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 51 | 52 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 53 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 54 | documentation and/or other materials provided with the distribution. 55 | Neither the name of the libjpeg-turbo Project nor the names of its contributors may be used to endorse or promote products derived 56 | from this software without specific prior written permission. 57 | 58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 59 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS 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. 60 | 61 | License notice for libjpeg-turbo SIMD extensions 62 | -------------------------------------------------------------- 63 | 64 | Copyright (C) 1999-2006, MIYASAKA Masaru. 65 | 66 | This software is provided 'as-is', without any express or implied 67 | warranty. In no event will the authors be held liable for any damages 68 | arising from the use of this software. 69 | 70 | Permission is granted to anyone to use this software for any purpose, 71 | including commercial applications, and to alter it and redistribute it 72 | freely, subject to the following restrictions: 73 | 74 | 1. The origin of this software must not be misrepresented; you must not 75 | claim that you wrote the original software. If you use this software 76 | in a product, an acknowledgment in the product documentation would be 77 | appreciated but is not required. 78 | 2. Altered source versions must be plainly marked as such, and must not be 79 | misrepresented as being the original software. 80 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /src/Interop/MozJpegStreamIO.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | using System; 15 | using System.IO; 16 | using System.Runtime.ExceptionServices; 17 | using System.Runtime.InteropServices; 18 | 19 | namespace MozJpegFileType.Interop 20 | { 21 | internal sealed class MozJpegStreamIO : Disposable 22 | { 23 | private const int BufferSize = 4096; 24 | 25 | private IArrayPoolBuffer buffer; 26 | private readonly Stream stream; 27 | 28 | public MozJpegStreamIO(Stream stream, PaintDotNet.AppModel.IArrayPoolService arrayPool) 29 | { 30 | if (stream is null) 31 | { 32 | throw new ArgumentNullException(nameof(stream)); 33 | } 34 | 35 | if (arrayPool is null) 36 | { 37 | throw new ArgumentNullException(nameof(arrayPool)); 38 | } 39 | 40 | this.stream = stream; 41 | this.buffer = arrayPool.Rent(BufferSize); 42 | } 43 | 44 | public ExceptionDispatchInfo ExceptionInfo { get; private set; } 45 | 46 | public int Read(IntPtr data, int maxNumberOfBytesToRead) 47 | { 48 | int totalBytesRead = 0; 49 | 50 | if (maxNumberOfBytesToRead > 0) 51 | { 52 | try 53 | { 54 | while (totalBytesRead < maxNumberOfBytesToRead) 55 | { 56 | int bytesToRead = Math.Min(maxNumberOfBytesToRead - totalBytesRead, BufferSize); 57 | int bytesRead = this.stream.Read(this.buffer.Array, 0, bytesToRead); 58 | 59 | if (bytesRead == 0) 60 | { 61 | break; 62 | } 63 | 64 | Marshal.Copy(this.buffer.Array, 0, data, bytesRead); 65 | 66 | totalBytesRead += bytesRead; 67 | } 68 | } 69 | catch (Exception ex) 70 | { 71 | this.ExceptionInfo = ExceptionDispatchInfo.Capture(ex); 72 | return -1; 73 | } 74 | } 75 | 76 | return totalBytesRead; 77 | } 78 | 79 | public bool SkipBytes(int numberOfBytesToSkip) 80 | { 81 | if (numberOfBytesToSkip > 0) 82 | { 83 | try 84 | { 85 | this.stream.Position += numberOfBytesToSkip; 86 | } 87 | catch (Exception ex) 88 | { 89 | this.ExceptionInfo = ExceptionDispatchInfo.Capture(ex); 90 | return false; 91 | } 92 | } 93 | 94 | return true; 95 | } 96 | 97 | public bool Write(IntPtr data, UIntPtr dataLength) 98 | { 99 | ulong count = dataLength.ToUInt64(); 100 | 101 | if (count > 0) 102 | { 103 | try 104 | { 105 | ulong offset = 0; 106 | ulong remaining = count; 107 | 108 | unsafe 109 | { 110 | byte* src = (byte*)data; 111 | 112 | fixed (byte* dest = this.buffer.Array) 113 | { 114 | while (remaining > 0) 115 | { 116 | ulong bytesToCopy = Math.Min(remaining, BufferSize); 117 | 118 | Buffer.MemoryCopy(src + offset, dest, bytesToCopy, bytesToCopy); 119 | 120 | this.stream.Write(this.buffer.Array, 0, (int)bytesToCopy); 121 | 122 | offset += bytesToCopy; 123 | remaining -= bytesToCopy; 124 | } 125 | } 126 | } 127 | } 128 | catch (Exception ex) 129 | { 130 | this.ExceptionInfo = ExceptionDispatchInfo.Capture(ex); 131 | return false; 132 | } 133 | } 134 | 135 | return true; 136 | } 137 | 138 | protected override void Dispose(bool disposing) 139 | { 140 | if (disposing) 141 | { 142 | 143 | } 144 | 145 | base.Dispose(disposing); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Exif/MetadataEntry.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | using System.Collections.Generic; 15 | using System.Diagnostics; 16 | 17 | namespace MozJpegFileType.Exif 18 | { 19 | [DebuggerDisplay("{DebuggerDisplay, nq}")] 20 | internal sealed class MetadataEntry 21 | : IEquatable 22 | { 23 | private readonly byte[] data; 24 | 25 | public MetadataEntry(MetadataKey key, TagDataType type, byte[] data) 26 | : this(key.Section, key.TagId, type, data) 27 | { 28 | } 29 | 30 | public MetadataEntry(MetadataSection section, ushort tagId, TagDataType type, byte[] data) 31 | { 32 | if (data is null) 33 | { 34 | throw new ArgumentNullException(nameof(data)); 35 | } 36 | 37 | this.Section = section; 38 | this.TagId = tagId; 39 | this.Type = type; 40 | this.data = (byte[])data.Clone(); 41 | } 42 | 43 | public int LengthInBytes => this.data.Length; 44 | 45 | public MetadataSection Section { get; } 46 | 47 | public ushort TagId { get; } 48 | 49 | public TagDataType Type { get; } 50 | 51 | [DebuggerBrowsable(DebuggerBrowsableState.Never)] 52 | private string DebuggerDisplay 53 | { 54 | get 55 | { 56 | return string.Format("{0}, Tag# {1} (0x{1:X}), {2}", this.Section, this.TagId, this.Type); 57 | } 58 | } 59 | 60 | public PaintDotNet.Imaging.ExifPropertyItem CreateExifPropertyItem() 61 | { 62 | PaintDotNet.Imaging.ExifSection exifSection; 63 | switch (this.Section) 64 | { 65 | case MetadataSection.Image: 66 | exifSection = PaintDotNet.Imaging.ExifSection.Image; 67 | break; 68 | case MetadataSection.Exif: 69 | exifSection = PaintDotNet.Imaging.ExifSection.Photo; 70 | break; 71 | case MetadataSection.Gps: 72 | exifSection = PaintDotNet.Imaging.ExifSection.GpsInfo; 73 | break; 74 | case MetadataSection.Interop: 75 | exifSection = PaintDotNet.Imaging.ExifSection.Interop; 76 | break; 77 | default: 78 | throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.InvariantCulture, 79 | "Unexpected {0} type: {1}", 80 | nameof(MetadataSection), 81 | (int)this.Section)); 82 | } 83 | 84 | return new PaintDotNet.Imaging.ExifPropertyItem(exifSection, 85 | this.TagId, 86 | new PaintDotNet.Imaging.ExifValue((PaintDotNet.Imaging.ExifValueType)this.Type, 87 | (byte[])this.data.Clone())); 88 | } 89 | 90 | public override bool Equals(object obj) 91 | { 92 | return obj is MetadataEntry entry && Equals(entry); 93 | } 94 | 95 | public bool Equals(MetadataEntry other) 96 | { 97 | if (other is null) 98 | { 99 | return false; 100 | } 101 | 102 | return this.Section == other.Section && this.TagId == other.TagId; 103 | } 104 | 105 | public byte[] GetData() 106 | { 107 | return (byte[])this.data.Clone(); 108 | } 109 | 110 | public byte[] GetDataReadOnly() 111 | { 112 | return this.data; 113 | } 114 | 115 | public override int GetHashCode() 116 | { 117 | int hashCode = -2103575766; 118 | 119 | unchecked 120 | { 121 | hashCode = (hashCode * -1521134295) + this.Section.GetHashCode(); 122 | hashCode = (hashCode * -1521134295) + this.TagId.GetHashCode(); 123 | } 124 | 125 | return hashCode; 126 | } 127 | 128 | public static bool operator ==(MetadataEntry left, MetadataEntry right) 129 | { 130 | return EqualityComparer.Default.Equals(left, right); 131 | } 132 | 133 | public static bool operator !=(MetadataEntry left, MetadataEntry right) 134 | { 135 | return !(left == right); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/JpegMetadataReader.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #include "JpegMetadataReader.h" 14 | #include 15 | #include 16 | 17 | namespace 18 | { 19 | DecodeStatus ReadApp1Blocks(j_decompress_ptr cinfo, const ReadCallbacks* callbacks) 20 | { 21 | constexpr int App1Marker = JPEG_APP0 + 1; 22 | 23 | constexpr const char* MainExifSignature = "Exif\0\0"; 24 | constexpr const char* AlternateExifSignature = "Exif\0\xFF"; 25 | constexpr unsigned int ExifSignatureLength = 6; 26 | 27 | constexpr const char* StandardXmpSignature = "http://ns.adobe.com/xap/1.0/\0"; 28 | constexpr unsigned int StandardXmpSignatureLength = 29; 29 | 30 | constexpr const char* ExtendedXmpSignature = "http://ns.adobe.com/xmp/extension/\0"; 31 | constexpr unsigned int ExtendedXmpSignatureLength = 35; 32 | 33 | bool setExif = false; 34 | bool setStandardXmp = false; 35 | 36 | for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; marker = marker->next) 37 | { 38 | if (marker->marker == App1Marker) 39 | { 40 | if (marker->data_length > ExifSignatureLength && 41 | memcmp(marker->data, MainExifSignature, ExifSignatureLength) == 0 || 42 | memcmp(marker->data, AlternateExifSignature, ExifSignatureLength) == 0) 43 | { 44 | if (setExif) 45 | { 46 | continue; 47 | } 48 | 49 | unsigned int exifLength = marker->data_length - ExifSignatureLength; 50 | 51 | if (exifLength > 0 && 52 | exifLength <= static_cast(std::numeric_limits::max())) 53 | { 54 | if (!callbacks->setMetadata( 55 | marker->data + ExifSignatureLength, 56 | static_cast(exifLength), 57 | MetadataType::Exif)) 58 | { 59 | return DecodeStatus::CallbackError; 60 | } 61 | setExif = true; 62 | } 63 | } 64 | else if (marker->data_length > StandardXmpSignatureLength && 65 | memcmp(marker->data, StandardXmpSignature, StandardXmpSignatureLength) == 0) 66 | { 67 | if (setStandardXmp) 68 | { 69 | continue; 70 | } 71 | 72 | unsigned int xmpLength = marker->data_length - StandardXmpSignatureLength; 73 | 74 | if (xmpLength > 0 && 75 | xmpLength <= static_cast(std::numeric_limits::max())) 76 | { 77 | if (!callbacks->setMetadata( 78 | marker->data + StandardXmpSignatureLength, 79 | static_cast(xmpLength), 80 | MetadataType::StandardXmp)) 81 | { 82 | return DecodeStatus::CallbackError; 83 | } 84 | setStandardXmp = true; 85 | } 86 | } 87 | else if (marker->data_length > ExtendedXmpSignatureLength && 88 | memcmp(marker->data, ExtendedXmpSignature, ExtendedXmpSignatureLength) == 0) 89 | { 90 | unsigned int extendedXmpLength = marker->data_length - ExtendedXmpSignatureLength; 91 | 92 | if (extendedXmpLength > 0 && 93 | extendedXmpLength <= static_cast(std::numeric_limits::max())) 94 | { 95 | if (!callbacks->setMetadata( 96 | marker->data + ExtendedXmpSignatureLength, 97 | static_cast(extendedXmpLength), 98 | MetadataType::ExtendedXmp)) 99 | { 100 | return DecodeStatus::CallbackError; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | return DecodeStatus::Ok; 108 | } 109 | 110 | DecodeStatus ReadIccProfile(j_decompress_ptr cinfo, const ReadCallbacks* callbacks) 111 | { 112 | JOCTET* iccProfile; 113 | unsigned int iccProfileSize; 114 | 115 | DecodeStatus status = DecodeStatus::Ok; 116 | 117 | if (jpeg_read_icc_profile(cinfo, &iccProfile, &iccProfileSize)) 118 | { 119 | if (iccProfileSize > 0 && 120 | iccProfileSize <= static_cast(std::numeric_limits::max())) 121 | { 122 | if (!callbacks->setMetadata(iccProfile, static_cast(iccProfileSize), MetadataType::Icc)) 123 | { 124 | status = DecodeStatus::CallbackError; 125 | } 126 | } 127 | 128 | free(iccProfile); 129 | } 130 | 131 | return status; 132 | } 133 | } 134 | 135 | DecodeStatus ReadMetadata(j_decompress_ptr cinfo, const ReadCallbacks* callbacks) 136 | { 137 | DecodeStatus status = ReadApp1Blocks(cinfo, callbacks); 138 | 139 | if (status == DecodeStatus::Ok) 140 | { 141 | status = ReadIccProfile(cinfo, callbacks); 142 | } 143 | 144 | return status; 145 | } 146 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # Benchmark Results 46 | BenchmarkDotNet.Artifacts/ 47 | 48 | # .NET Core 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | **/Properties/launchSettings.json 53 | 54 | *_i.c 55 | *_p.c 56 | *_i.h 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.svclog 77 | *.scc 78 | 79 | # Chutzpah Test files 80 | _Chutzpah* 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opendb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | *.VC.db 91 | *.VC.VC.opendb 92 | 93 | # Visual Studio profiler 94 | *.psess 95 | *.vsp 96 | *.vspx 97 | *.sap 98 | 99 | # TFS 2012 Local Workspace 100 | $tf/ 101 | 102 | # Guidance Automation Toolkit 103 | *.gpState 104 | 105 | # ReSharper is a .NET coding add-in 106 | _ReSharper*/ 107 | *.[Rr]e[Ss]harper 108 | *.DotSettings.user 109 | 110 | # JustCode is a .NET coding add-in 111 | .JustCode 112 | 113 | # TeamCity is a build add-in 114 | _TeamCity* 115 | 116 | # DotCover is a Code Coverage Tool 117 | *.dotCover 118 | 119 | # Visual Studio code coverage results 120 | *.coverage 121 | *.coveragexml 122 | 123 | # NCrunch 124 | _NCrunch_* 125 | .*crunch*.local.xml 126 | nCrunchTemp_* 127 | 128 | # MightyMoose 129 | *.mm.* 130 | AutoTest.Net/ 131 | 132 | # Web workbench (sass) 133 | .sass-cache/ 134 | 135 | # Installshield output folder 136 | [Ee]xpress/ 137 | 138 | # DocProject is a documentation generator add-in 139 | DocProject/buildhelp/ 140 | DocProject/Help/*.HxT 141 | DocProject/Help/*.HxC 142 | DocProject/Help/*.hhc 143 | DocProject/Help/*.hhk 144 | DocProject/Help/*.hhp 145 | DocProject/Help/Html2 146 | DocProject/Help/html 147 | 148 | # Click-Once directory 149 | publish/ 150 | 151 | # Publish Web Output 152 | *.[Pp]ublish.xml 153 | *.azurePubxml 154 | # TODO: Comment the next line if you want to checkin your web deploy settings 155 | # but database connection strings (with potential passwords) will be unencrypted 156 | *.pubxml 157 | *.publishproj 158 | 159 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 160 | # checkin your Azure Web App publish settings, but sensitive information contained 161 | # in these scripts will be unencrypted 162 | PublishScripts/ 163 | 164 | # NuGet Packages 165 | *.nupkg 166 | # The packages folder can be ignored because of Package Restore 167 | **/packages/* 168 | # except build/, which is used as an MSBuild target. 169 | !**/packages/build/ 170 | # Uncomment if necessary however generally it will be regenerated when needed 171 | #!**/packages/repositories.config 172 | # NuGet v3's project.json files produces more ignorable files 173 | *.nuget.props 174 | *.nuget.targets 175 | 176 | # Microsoft Azure Build Output 177 | csx/ 178 | *.build.csdef 179 | 180 | # Microsoft Azure Emulator 181 | ecf/ 182 | rcf/ 183 | 184 | # Windows Store app package directories and files 185 | AppPackages/ 186 | BundleArtifacts/ 187 | Package.StoreAssociation.xml 188 | _pkginfo.txt 189 | *.appx 190 | 191 | # Visual Studio cache files 192 | # files ending in .cache can be ignored 193 | *.[Cc]ache 194 | # but keep track of directories ending in .cache 195 | !*.[Cc]ache/ 196 | 197 | # Others 198 | ClientBin/ 199 | ~$* 200 | *~ 201 | *.dbmdl 202 | *.dbproj.schemaview 203 | *.jfm 204 | *.pfx 205 | *.publishsettings 206 | orleans.codegen.cs 207 | 208 | # Since there are multiple workflows, uncomment next line to ignore bower_components 209 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 210 | #bower_components/ 211 | 212 | # RIA/Silverlight projects 213 | Generated_Code/ 214 | 215 | # Backup & report files from converting an old project file 216 | # to a newer Visual Studio version. Backup files are not needed, 217 | # because we have git ;-) 218 | _UpgradeReport_Files/ 219 | Backup*/ 220 | UpgradeLog*.XML 221 | UpgradeLog*.htm 222 | 223 | # SQL Server files 224 | *.mdf 225 | *.ldf 226 | *.ndf 227 | 228 | # Business Intelligence projects 229 | *.rdl.data 230 | *.bim.layout 231 | *.bim_*.settings 232 | 233 | # Microsoft Fakes 234 | FakesAssemblies/ 235 | 236 | # GhostDoc plugin setting file 237 | *.GhostDoc.xml 238 | 239 | # Node.js Tools for Visual Studio 240 | .ntvs_analysis.dat 241 | node_modules/ 242 | 243 | # Typescript v1 declaration files 244 | typings/ 245 | 246 | # Visual Studio 6 build log 247 | *.plg 248 | 249 | # Visual Studio 6 workspace options file 250 | *.opt 251 | 252 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 253 | *.vbw 254 | 255 | # Visual Studio LightSwitch build output 256 | **/*.HTMLClient/GeneratedArtifacts 257 | **/*.DesktopClient/GeneratedArtifacts 258 | **/*.DesktopClient/ModelManifest.xml 259 | **/*.Server/GeneratedArtifacts 260 | **/*.Server/ModelManifest.xml 261 | _Pvt_Extensions 262 | 263 | # Paket dependency manager 264 | .paket/paket.exe 265 | paket-files/ 266 | 267 | # FAKE - F# Make 268 | .fake/ 269 | 270 | # JetBrains Rider 271 | .idea/ 272 | *.sln.iml 273 | 274 | # CodeRush 275 | .cr/ 276 | 277 | # Python Tools for Visual Studio (PTVS) 278 | __pycache__/ 279 | *.pyc 280 | 281 | # Cake - Uncomment if you are using it 282 | # tools/** 283 | # !tools/packages.config 284 | 285 | # Tabs Studio 286 | *.tss 287 | 288 | # Telerik's JustMock configuration file 289 | *.jmconfig 290 | 291 | # BizTalk build output 292 | *.btp.cs 293 | *.btm.cs 294 | *.odx.cs 295 | *.xsd.cs 296 | 297 | # Local repository 298 | 299 | *.diagsession 300 | 301 | vendor/ 302 | 303 | # Include the compiled lib files 304 | !/3rd-party/mozjpeg/libs/**/ 305 | -------------------------------------------------------------------------------- /src/MozJpegFileType.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | using PaintDotNet.AppModel; 15 | using PaintDotNet.IndirectUI; 16 | using PaintDotNet.PropertySystem; 17 | using System; 18 | using System.IO; 19 | 20 | namespace MozJpegFileType 21 | { 22 | 23 | [PluginSupportInfo(typeof(PluginSupportInfo))] 24 | internal sealed class MozJpegFileTypePlugin : PropertyBasedFileType 25 | { 26 | private readonly IArrayPoolService arrayPoolService; 27 | 28 | /// 29 | /// Constructs a ExamplePropertyBasedFileType instance 30 | /// 31 | internal MozJpegFileTypePlugin(IFileTypeHost host) 32 | : base( 33 | "MozJpeg", 34 | new FileTypeOptions 35 | { 36 | LoadExtensions = new string[] { ".jpg", ".jpeg", ".jpe", ".jfif" }, 37 | SaveExtensions = new string[] { ".jpg", ".jpeg", ".jpe", ".jfif" }, 38 | SupportsCancellation = true, 39 | SupportsLayers = false 40 | }) 41 | { 42 | this.arrayPoolService = host?.Services.GetService(); 43 | } 44 | 45 | // Names of the properties 46 | private enum PropertyNames 47 | { 48 | Quality, 49 | ChromaSubsampling, 50 | Progressive 51 | } 52 | 53 | /// 54 | /// Add properties to the dialog 55 | /// 56 | public override PropertyCollection OnCreateSavePropertyCollection() 57 | { 58 | Property[] props = new Property[] 59 | { 60 | new Int32Property(PropertyNames.Quality, 75, 0, 100, false), 61 | CreateChromaSubsampling(), 62 | new BooleanProperty(PropertyNames.Progressive, false, false) 63 | }; 64 | 65 | return new PropertyCollection(props); 66 | 67 | StaticListChoiceProperty CreateChromaSubsampling() 68 | { 69 | // The list is created manually because some of the YUVChromaSubsampling enumeration values 70 | // are used for internal signaling. 71 | 72 | object[] choiceValues = new object[] 73 | { 74 | ChromaSubsampling.Subsampling420, 75 | ChromaSubsampling.Subsampling422, 76 | ChromaSubsampling.Subsampling444 77 | }; 78 | 79 | int defaultChoiceIndex = Array.IndexOf(choiceValues, ChromaSubsampling.Subsampling422); 80 | 81 | return new StaticListChoiceProperty(PropertyNames.ChromaSubsampling, choiceValues, defaultChoiceIndex); 82 | } 83 | } 84 | 85 | /// 86 | /// Adapt properties in the dialog (DisplayName, ...) 87 | /// 88 | public override ControlInfo OnCreateSaveConfigUI(PropertyCollection props) 89 | { 90 | ControlInfo configUI = CreateDefaultSaveConfigUI(props); 91 | 92 | PropertyControlInfo qualityPCI = configUI.FindControlForPropertyName(PropertyNames.Quality); 93 | qualityPCI.ControlProperties[ControlInfoPropertyNames.DisplayName].Value = string.Empty; 94 | qualityPCI.ControlProperties[ControlInfoPropertyNames.Description].Value = "Quality"; 95 | 96 | configUI.SetPropertyControlValue(PropertyNames.Quality, ControlInfoPropertyNames.DisplayName, "Quality"); 97 | configUI.SetPropertyControlValue(PropertyNames.Quality, ControlInfoPropertyNames.Description, string.Empty); 98 | 99 | PropertyControlInfo chromaSubsamplingPCI = configUI.FindControlForPropertyName(PropertyNames.ChromaSubsampling); 100 | chromaSubsamplingPCI.ControlProperties[ControlInfoPropertyNames.DisplayName].Value = "Chroma Subsampling"; 101 | chromaSubsamplingPCI.ControlProperties[ControlInfoPropertyNames.Description].Value = string.Empty; 102 | chromaSubsamplingPCI.SetValueDisplayName(ChromaSubsampling.Subsampling420, "4:2:0 (Best Compression)"); 103 | chromaSubsamplingPCI.SetValueDisplayName(ChromaSubsampling.Subsampling422, "4:2:2"); 104 | chromaSubsamplingPCI.SetValueDisplayName(ChromaSubsampling.Subsampling444, "4:4:4 (Best Quality)"); 105 | 106 | PropertyControlInfo progressivePCI = configUI.FindControlForPropertyName(PropertyNames.Progressive); 107 | progressivePCI.ControlProperties[ControlInfoPropertyNames.DisplayName].Value = string.Empty; 108 | progressivePCI.ControlProperties[ControlInfoPropertyNames.Description].Value = "Progressive"; 109 | 110 | return configUI; 111 | } 112 | 113 | /// 114 | /// Creates a document from a stream 115 | /// 116 | protected override Document OnLoad(Stream input) 117 | { 118 | return MozJpegFile.Load(input, this.arrayPoolService); 119 | } 120 | 121 | /// 122 | /// Saves a document to a stream respecting the properties 123 | /// 124 | protected override void OnSaveT(Document input, 125 | Stream output, 126 | PropertyBasedSaveConfigToken token, 127 | Surface scratchSurface, 128 | ProgressEventHandler progressCallback) 129 | { 130 | int quality = token.GetProperty(PropertyNames.Quality).Value; 131 | ChromaSubsampling chromaSubsampling = (ChromaSubsampling)token.GetProperty(PropertyNames.ChromaSubsampling).Value; 132 | bool progressive = token.GetProperty(PropertyNames.Progressive).Value; 133 | 134 | MozJpegFile.Save(input, 135 | output, 136 | scratchSurface, 137 | quality, 138 | chromaSubsampling, 139 | progressive, 140 | progressCallback, 141 | this.arrayPoolService); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Interop/MozJpegLoadState.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using MozJpegFileType.Xmp; 14 | using PaintDotNet; 15 | using PaintDotNet.AppModel; 16 | using PaintDotNet.Imaging; 17 | using System; 18 | using System.Collections.Generic; 19 | using System.Runtime.ExceptionServices; 20 | using System.Runtime.InteropServices; 21 | using System.Xml.Linq; 22 | 23 | namespace MozJpegFileType.Interop 24 | { 25 | internal sealed class MozJpegLoadState 26 | { 27 | private byte[] exifBytes; 28 | private byte[] iccProfileBytes; 29 | private byte[] standardXmpBytes; 30 | private List extendedXmpBytes; 31 | 32 | private readonly IArrayPoolService arrayPool; 33 | 34 | public MozJpegLoadState(IArrayPoolService arrayPool) 35 | { 36 | this.exifBytes = null; 37 | this.iccProfileBytes = null; 38 | this.standardXmpBytes = null; 39 | this.extendedXmpBytes = new List(); 40 | this.ExceptionInfo = null; 41 | this.Surface = null; 42 | this.arrayPool = arrayPool; 43 | } 44 | 45 | public ExceptionDispatchInfo ExceptionInfo { get; private set; } 46 | 47 | public Surface Surface { get; private set; } 48 | 49 | public IntPtr AllocateSurface(int width, int height, out int outStride) 50 | { 51 | try 52 | { 53 | this.Surface = new Surface(width, height); 54 | outStride = this.Surface.Stride; 55 | 56 | return this.Surface.Scan0.Pointer; 57 | } 58 | catch (Exception ex) 59 | { 60 | this.ExceptionInfo = ExceptionDispatchInfo.Capture(ex); 61 | outStride = 0; 62 | return IntPtr.Zero; 63 | } 64 | } 65 | 66 | public byte[] GetExifBytes() => this.exifBytes; 67 | 68 | public byte[] GetIccProfileBytes() => this.iccProfileBytes; 69 | 70 | public XmpPacket GetXmpPacket() 71 | { 72 | if (this.standardXmpBytes is null) 73 | { 74 | return null; 75 | } 76 | 77 | XDocument standardXmp = XmpUtils.TryParseXmpBytes(this.standardXmpBytes); 78 | 79 | if (standardXmp is null) 80 | { 81 | return null; 82 | } 83 | 84 | XmpPacket xmpPacket; 85 | 86 | if (XmpUtils.TryGetExtendedXmpGuid(standardXmp, out string extendedXmpGuid)) 87 | { 88 | xmpPacket = TryMergeExtendedXmp(standardXmp, extendedXmpGuid); 89 | } 90 | else 91 | { 92 | xmpPacket = XmpPacket.TryLoad(standardXmp); 93 | } 94 | 95 | return xmpPacket; 96 | } 97 | 98 | public bool SetMetadata(IntPtr data, int size, MetadataType type) 99 | { 100 | try 101 | { 102 | byte[] bytes = new byte[size]; 103 | 104 | Marshal.Copy(data, bytes, 0, size); 105 | 106 | switch (type) 107 | { 108 | case MetadataType.Exif: 109 | this.exifBytes = bytes; 110 | break; 111 | case MetadataType.Icc: 112 | this.iccProfileBytes = bytes; 113 | break; 114 | case MetadataType.StandardXmp: 115 | this.standardXmpBytes = bytes; 116 | break; 117 | case MetadataType.ExtendedXmp: 118 | this.extendedXmpBytes.Add(bytes); 119 | break; 120 | default: 121 | break; 122 | } 123 | } 124 | catch (Exception ex) 125 | { 126 | this.ExceptionInfo = ExceptionDispatchInfo.Capture(ex); 127 | return false; 128 | } 129 | 130 | return true; 131 | } 132 | 133 | private XmpPacket TryMergeExtendedXmp(XDocument standardXmp, string extendedXmpGuid) 134 | { 135 | byte[] mergedPacketBytes = RecombineExtendedXmpChunks(extendedXmpGuid); 136 | 137 | if (mergedPacketBytes is null) 138 | { 139 | return null; 140 | } 141 | 142 | XDocument extendedXmp = XmpUtils.TryParseXmpBytes(mergedPacketBytes); 143 | 144 | if (extendedXmp is null) 145 | { 146 | return null; 147 | } 148 | 149 | XDocument mergedXmp = XmpUtils.MergeXmpPackets(standardXmp, extendedXmp); 150 | 151 | return XmpPacket.TryLoad(mergedXmp); 152 | } 153 | 154 | private byte[] RecombineExtendedXmpChunks(string extendedXmpGuid) 155 | { 156 | byte[] packetBytes = null; 157 | 158 | for (int i = 0; i < this.extendedXmpBytes.Count; i++) 159 | { 160 | byte[] extendedXmpData = this.extendedXmpBytes[i]; 161 | ExtendedXMPChunk chunk = XmpUtils.TryParseExtendedXmpData(extendedXmpData, this.arrayPool); 162 | 163 | if (chunk is null) 164 | { 165 | return null; 166 | } 167 | 168 | try 169 | { 170 | if (!extendedXmpGuid.Equals(chunk.MD5Guid, StringComparison.OrdinalIgnoreCase)) 171 | { 172 | return null; 173 | } 174 | 175 | if (packetBytes is null) 176 | { 177 | packetBytes = new byte[chunk.TotalLength]; 178 | } 179 | else if (packetBytes.LongLength != chunk.TotalLength) 180 | { 181 | // Invalid ExtendedXMP chunk. Abort. 182 | return null; 183 | } 184 | 185 | Array.Copy(chunk.Data.Array, 0, packetBytes, chunk.ChunkOffset, chunk.Data.RequestedLength); 186 | } 187 | finally 188 | { 189 | chunk.Dispose(); 190 | } 191 | } 192 | 193 | return packetBytes; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Interop/MetadataCustomMarshaler.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using System; 14 | using System.Runtime.InteropServices; 15 | 16 | namespace MozJpegFileType.Interop 17 | { 18 | internal sealed class MetadataCustomMarshaler : ICustomMarshaler 19 | { 20 | // This must be kept in sync with the ExtendedXmpBlock structure in MozJpegFileTypeIO.h. 21 | [StructLayout(LayoutKind.Sequential)] 22 | private unsafe struct NativeExtendedXmpBlock 23 | { 24 | public void* data; 25 | public nuint length; 26 | } 27 | 28 | // This must be kept in sync with the MetadataParams structure in MozJpegFileTypeIO.h. 29 | [StructLayout(LayoutKind.Sequential)] 30 | private unsafe struct NativeMetadataParams 31 | { 32 | public void* exif; 33 | public nuint exifSize; 34 | public void* iccProfile; 35 | public nuint iccProfileSize; 36 | public void* xmp; 37 | public nuint xmpSize; 38 | public NativeExtendedXmpBlock* extendedXmpBlocks; 39 | public nuint extendedXmpBlockCount; 40 | } 41 | 42 | private static readonly int NativeExtendedXmpBlockSize = Marshal.SizeOf(); 43 | private static readonly int NativeMetadataParamsSize = Marshal.SizeOf(); 44 | private static readonly MetadataCustomMarshaler instance = new MetadataCustomMarshaler(); 45 | 46 | [System.Diagnostics.CodeAnalysis.SuppressMessage( 47 | "Style", 48 | "IDE0060:Remove unused parameter", 49 | Justification = "The cookie parameter is required by the ICustomMarshaler API.")] 50 | public static ICustomMarshaler GetInstance(string cookie) 51 | { 52 | return instance; 53 | } 54 | 55 | private MetadataCustomMarshaler() 56 | { 57 | } 58 | 59 | public void CleanUpManagedData(object ManagedObj) 60 | { 61 | } 62 | 63 | public unsafe void CleanUpNativeData(IntPtr pNativeData) 64 | { 65 | if (pNativeData != IntPtr.Zero) 66 | { 67 | NativeMetadataParams* metadata = (NativeMetadataParams*)pNativeData; 68 | 69 | if (metadata->iccProfile != null) 70 | { 71 | NativeMemory.Free(metadata->iccProfile); 72 | } 73 | 74 | if (metadata->exif != null) 75 | { 76 | NativeMemory.Free(metadata->exif); 77 | } 78 | 79 | if (metadata->xmp != null) 80 | { 81 | NativeMemory.Free(metadata->xmp); 82 | 83 | if (metadata->extendedXmpBlocks != null) 84 | { 85 | NativeExtendedXmpBlock* blocks = metadata->extendedXmpBlocks; 86 | nuint extendedXmpBlockCount = metadata->extendedXmpBlockCount; 87 | 88 | for (nuint i = 0; i < extendedXmpBlockCount; i++) 89 | { 90 | NativeMemory.Free(blocks[i].data); 91 | } 92 | 93 | NativeMemory.Free(blocks); 94 | } 95 | } 96 | 97 | NativeMemory.Free(metadata); 98 | } 99 | } 100 | 101 | public int GetNativeDataSize() 102 | { 103 | return NativeMetadataParamsSize; 104 | } 105 | 106 | public unsafe IntPtr MarshalManagedToNative(object ManagedObj) 107 | { 108 | if (ManagedObj == null) 109 | { 110 | return IntPtr.Zero; 111 | } 112 | 113 | MetadataParams metadata = (MetadataParams)ManagedObj; 114 | 115 | NativeMetadataParams* nativeMetadata = (NativeMetadataParams*)NativeMemory.Alloc((uint)NativeMetadataParamsSize); 116 | 117 | if (metadata.exif != null && metadata.exif.Length > 0) 118 | { 119 | nativeMetadata->exif = NativeMemory.Alloc((uint)metadata.exif.Length); 120 | metadata.exif.AsSpan().CopyTo(new Span(nativeMetadata->exif, metadata.exif.Length)); 121 | nativeMetadata->exifSize = (uint)metadata.exif.Length; 122 | } 123 | else 124 | { 125 | nativeMetadata->exif = null; 126 | nativeMetadata->exifSize = 0; 127 | } 128 | 129 | if (metadata.iccProfile != null && metadata.iccProfile.Length > 0) 130 | { 131 | nativeMetadata->iccProfile = NativeMemory.Alloc((uint)metadata.iccProfile.Length); 132 | metadata.iccProfile.AsSpan().CopyTo(new Span(nativeMetadata->iccProfile, metadata.iccProfile.Length)); 133 | nativeMetadata->iccProfileSize = (uint)metadata.iccProfile.Length; 134 | } 135 | else 136 | { 137 | nativeMetadata->iccProfile = null; 138 | nativeMetadata->iccProfileSize = 0; 139 | } 140 | 141 | if (metadata.standardXmp != null && metadata.standardXmp.Length > 0) 142 | { 143 | nativeMetadata->xmp = NativeMemory.Alloc((uint)metadata.standardXmp.Length); 144 | metadata.standardXmp.AsSpan().CopyTo(new Span(nativeMetadata->xmp, metadata.standardXmp.Length)); 145 | nativeMetadata->xmpSize = (uint)metadata.standardXmp.Length; 146 | 147 | int extendedXmpBlockCount = metadata.extendedXmpChunks.Count; 148 | 149 | if (extendedXmpBlockCount > 0) 150 | { 151 | nativeMetadata->extendedXmpBlocks = (NativeExtendedXmpBlock*)NativeMemory.Alloc((uint)extendedXmpBlockCount, 152 | (uint)NativeExtendedXmpBlockSize); 153 | nativeMetadata->extendedXmpBlockCount = (uint)extendedXmpBlockCount; 154 | 155 | NativeExtendedXmpBlock* blocks = nativeMetadata->extendedXmpBlocks; 156 | 157 | for (int i = 0; i < extendedXmpBlockCount; i++) 158 | { 159 | ReadOnlySpan chunk = metadata.extendedXmpChunks[i]; 160 | 161 | NativeExtendedXmpBlock* block = &blocks[i]; 162 | 163 | block->data = NativeMemory.Alloc((uint)chunk.Length); 164 | chunk.CopyTo(new Span(block->data, chunk.Length)); 165 | block->length = (uint)chunk.Length; 166 | } 167 | } 168 | else 169 | { 170 | nativeMetadata->extendedXmpBlocks = null; 171 | nativeMetadata->extendedXmpBlockCount = 0; 172 | } 173 | } 174 | else 175 | { 176 | nativeMetadata->xmp = null; 177 | nativeMetadata->xmpSize = 0; 178 | nativeMetadata->extendedXmpBlocks = null; 179 | nativeMetadata->extendedXmpBlockCount = 0; 180 | } 181 | 182 | return (IntPtr)nativeMetadata; 183 | } 184 | 185 | public object MarshalNativeToManaged(IntPtr pNativeData) 186 | { 187 | return null; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/MozJpegFileTypeIO.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | #include "MozJpegFileTypeIO.h" 14 | #include "JpegDestiniationManager.h" 15 | #include "JpegMetadataReader.h" 16 | #include "JpegMetadataWriter.h" 17 | #include "JpegSourceManager.h" 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | namespace 27 | { 28 | struct JpegErrorContext 29 | { 30 | jpeg_error_mgr mgr; 31 | 32 | char messageBuffer[JMSG_LENGTH_MAX]; 33 | jmp_buf setjmpBuffer; 34 | }; 35 | 36 | void error_exit(j_common_ptr cinfo) 37 | { 38 | JpegErrorContext* ctx = reinterpret_cast(cinfo->err); 39 | 40 | switch (ctx->mgr.msg_code) 41 | { 42 | case JERR_FILE_READ: 43 | strcpy_s(ctx->messageBuffer, "File read error."); 44 | break; 45 | case JERR_FILE_WRITE: 46 | strcpy_s(ctx->messageBuffer, "File write error."); 47 | break; 48 | default: 49 | ctx->mgr.format_message(reinterpret_cast(cinfo), ctx->messageBuffer); 50 | break; 51 | } 52 | 53 | longjmp(ctx->setjmpBuffer, 1); 54 | } 55 | 56 | void HandleErrorMessage(const JpegErrorContext& ctx, JpegLibraryErrorInfo* info) 57 | { 58 | const size_t errorMessageLength = strlen(ctx.messageBuffer); 59 | 60 | if (errorMessageLength > 0 && errorMessageLength <= JpegLibraryErrorInfo::maxErrorMessageLength) 61 | { 62 | strncpy_s(info->errorMessage, ctx.messageBuffer, errorMessageLength); 63 | } 64 | } 65 | 66 | struct ColorBgra 67 | { 68 | uint8_t b; 69 | uint8_t g; 70 | uint8_t r; 71 | uint8_t a; 72 | }; 73 | } 74 | 75 | DecodeStatus ReadImage( 76 | const ReadCallbacks* callbacks, 77 | JpegLibraryErrorInfo* errorInfo) 78 | { 79 | if (callbacks == nullptr || errorInfo == nullptr) 80 | { 81 | return DecodeStatus::NullParameter; 82 | } 83 | 84 | JpegErrorContext errorContext{}; 85 | jpeg_decompress_struct dinfo{}; 86 | 87 | dinfo.err = jpeg_std_error(&errorContext.mgr); 88 | dinfo.err->error_exit = error_exit; 89 | memset(errorContext.messageBuffer, 0, _countof(errorContext.messageBuffer)); 90 | 91 | if (setjmp(errorContext.setjmpBuffer)) 92 | { 93 | // This block will be jumped to if the JPEG error_exit method is called. 94 | jpeg_destroy_decompress(&dinfo); 95 | 96 | HandleErrorMessage(errorContext, errorInfo); 97 | return DecodeStatus::JpegLibraryError; 98 | } 99 | 100 | jpeg_create_decompress(&dinfo); 101 | 102 | InitializeSourceManager(&dinfo, callbacks); 103 | 104 | // Save the EXIF and/or XMP data. 105 | jpeg_save_markers(&dinfo, JPEG_APP0 + 1, 0xFFFF); 106 | // Save the ICC profile. 107 | jpeg_save_markers(&dinfo, JPEG_APP0 + 2, 0xFFFF); 108 | 109 | jpeg_read_header(&dinfo, true); 110 | 111 | dinfo.out_color_space = JCS_EXT_BGRA; 112 | 113 | jpeg_calc_output_dimensions(&dinfo); 114 | 115 | if (dinfo.output_width > static_cast(std::numeric_limits::max()) || 116 | dinfo.output_height > static_cast(std::numeric_limits::max())) 117 | { 118 | jpeg_destroy_decompress(&dinfo); 119 | 120 | return DecodeStatus::OutOfMemory; 121 | } 122 | 123 | int32_t outputImageStride = 0; 124 | 125 | uint8_t* outputImageScan0 = callbacks->allocateSurface(dinfo.output_width, dinfo.output_height, &outputImageStride); 126 | 127 | if (outputImageScan0 == nullptr) 128 | { 129 | jpeg_destroy_decompress(&dinfo); 130 | 131 | return DecodeStatus::CallbackError; 132 | } 133 | 134 | jpeg_start_decompress(&dinfo); 135 | 136 | while (dinfo.output_scanline < dinfo.output_height) 137 | { 138 | uint8_t* dest = outputImageScan0 + (dinfo.output_scanline * outputImageStride); 139 | 140 | jpeg_read_scanlines(&dinfo, &dest, 1); 141 | } 142 | 143 | DecodeStatus status = ReadMetadata(&dinfo, callbacks); 144 | 145 | jpeg_finish_decompress(&dinfo); 146 | jpeg_destroy_decompress(&dinfo); 147 | 148 | return status; 149 | } 150 | 151 | EncodeStatus WriteImage( 152 | const BitmapData* bgraImage, 153 | const EncodeOptions* options, 154 | const MetadataParams* metadata, 155 | JpegLibraryErrorInfo* errorInfo, 156 | ProgressCallback progressCallback, 157 | WriteCallback writeCallback) 158 | { 159 | if (bgraImage == nullptr || options == nullptr || errorInfo == nullptr|| writeCallback == nullptr) 160 | { 161 | return EncodeStatus::NullParameter; 162 | } 163 | 164 | JpegErrorContext errorContext{}; 165 | jpeg_compress_struct cinfo{}; 166 | 167 | cinfo.err = jpeg_std_error(&errorContext.mgr); 168 | cinfo.err->error_exit = error_exit; 169 | memset(errorContext.messageBuffer, 0, _countof(errorContext.messageBuffer)); 170 | 171 | if (setjmp(errorContext.setjmpBuffer)) 172 | { 173 | // This block will be jumped to if the JPEG error_exit method is called. 174 | jpeg_destroy_compress(&cinfo); 175 | 176 | HandleErrorMessage(errorContext, errorInfo); 177 | return EncodeStatus::JpegLibraryError; 178 | } 179 | 180 | jpeg_create_compress(&cinfo); 181 | 182 | InitializeDestinationManager(&cinfo, writeCallback); 183 | 184 | const bool isGrayscale = options->chromaSubsampling == ChromaSubsampling::Subsampling400; 185 | 186 | cinfo.image_width = bgraImage->width; 187 | cinfo.image_height = bgraImage->height; 188 | cinfo.input_components = isGrayscale ? 1 : 3; 189 | #pragma warning(suppress: 26812) // Suppress C26812: Prefer 'enum class' over 'enum'. 190 | cinfo.in_color_space = JCS_EXT_BGRX; 191 | 192 | jpeg_set_defaults(&cinfo); 193 | jpeg_set_colorspace(&cinfo, isGrayscale ? JCS_GRAYSCALE : JCS_YCbCr); 194 | 195 | jpeg_set_quality(&cinfo, options->quality, !options->progressive); 196 | cinfo.optimize_coding = true; 197 | 198 | if (options->progressive) 199 | { 200 | jpeg_simple_progression(&cinfo); 201 | } 202 | 203 | if (isGrayscale) 204 | { 205 | cinfo.comp_info[0].h_samp_factor = 1; 206 | cinfo.comp_info[0].v_samp_factor = 1; 207 | } 208 | else 209 | { 210 | switch (options->chromaSubsampling) 211 | { 212 | 213 | case ChromaSubsampling::Subsampling420: 214 | cinfo.comp_info[0].h_samp_factor = 2; 215 | cinfo.comp_info[0].v_samp_factor = 2; 216 | cinfo.comp_info[1].h_samp_factor = 1; 217 | cinfo.comp_info[1].v_samp_factor = 1; 218 | cinfo.comp_info[2].h_samp_factor = 1; 219 | cinfo.comp_info[2].v_samp_factor = 1; 220 | break; 221 | case ChromaSubsampling::Subsampling422: 222 | cinfo.comp_info[0].h_samp_factor = 2; 223 | cinfo.comp_info[0].v_samp_factor = 1; 224 | cinfo.comp_info[1].h_samp_factor = 1; 225 | cinfo.comp_info[1].v_samp_factor = 1; 226 | cinfo.comp_info[2].h_samp_factor = 1; 227 | cinfo.comp_info[2].v_samp_factor = 1; 228 | break; 229 | case ChromaSubsampling::Subsampling444: 230 | cinfo.comp_info[0].h_samp_factor = 1; 231 | cinfo.comp_info[0].v_samp_factor = 1; 232 | cinfo.comp_info[1].h_samp_factor = 1; 233 | cinfo.comp_info[1].v_samp_factor = 1; 234 | cinfo.comp_info[2].h_samp_factor = 1; 235 | cinfo.comp_info[2].v_samp_factor = 1; 236 | break; 237 | } 238 | } 239 | 240 | jpeg_start_compress(&cinfo, true); 241 | 242 | WriteMetadata(&cinfo, metadata); 243 | 244 | int32_t currentProgressPercentage = -1; 245 | 246 | while (cinfo.next_scanline < cinfo.image_height) 247 | { 248 | if (progressCallback != nullptr) 249 | { 250 | double progressPercentage = (static_cast(cinfo.next_scanline) / static_cast(cinfo.image_height)) * 100.0; 251 | int32_t roundedPercentage = static_cast(round(progressPercentage)); 252 | 253 | if (currentProgressPercentage != roundedPercentage) 254 | { 255 | currentProgressPercentage = roundedPercentage; 256 | 257 | if (!progressCallback(currentProgressPercentage)) 258 | { 259 | jpeg_destroy_compress(&cinfo); 260 | 261 | return EncodeStatus::UserCanceled; 262 | } 263 | } 264 | } 265 | 266 | uint8_t* srcRow = bgraImage->scan0 + (static_cast(cinfo.next_scanline) * bgraImage->stride); 267 | 268 | jpeg_write_scanlines(&cinfo, &srcRow, 1); 269 | } 270 | 271 | jpeg_finish_compress(&cinfo); 272 | jpeg_destroy_compress(&cinfo); 273 | 274 | return EncodeStatus::Ok; 275 | } 276 | -------------------------------------------------------------------------------- /src/MozJpegNative.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using MozJpegFileType.Interop; 14 | using PaintDotNet; 15 | using PaintDotNet.AppModel; 16 | using System; 17 | using System.IO; 18 | using System.Runtime.InteropServices; 19 | 20 | namespace MozJpegFileType 21 | { 22 | internal static class MozJpegNative 23 | { 24 | public static unsafe MozJpegLoadState Load(Stream input, IArrayPoolService arrayPool) 25 | { 26 | MozJpegLoadState loadState = new MozJpegLoadState(arrayPool); 27 | 28 | using (MozJpegStreamIO streamIO = new MozJpegStreamIO(input, arrayPool)) 29 | { 30 | ReadCallbacks callbacks = new ReadCallbacks 31 | { 32 | read = streamIO.Read, 33 | skipBytes = streamIO.SkipBytes, 34 | allocateSurface = loadState.AllocateSurface, 35 | setIccProfile = loadState.SetMetadata 36 | }; 37 | 38 | JpegLibraryErrorInfo errorInfo = new JpegLibraryErrorInfo(); 39 | DecodeStatus status = DecodeStatus.Ok; 40 | 41 | if (RuntimeInformation.ProcessArchitecture == Architecture.X64) 42 | { 43 | status = MozJpeg_X64.ReadImage(callbacks, ref errorInfo); 44 | } 45 | else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) 46 | { 47 | status = MozJpeg_Arm64.ReadImage(callbacks, ref errorInfo); 48 | } 49 | else if (RuntimeInformation.ProcessArchitecture == Architecture.X86) 50 | { 51 | status = MozJpeg_X86.ReadImage(callbacks, ref errorInfo); 52 | } 53 | else 54 | { 55 | throw new PlatformNotSupportedException(); 56 | } 57 | 58 | GC.KeepAlive(callbacks); 59 | 60 | if (status != DecodeStatus.Ok) 61 | { 62 | if (status == DecodeStatus.JpegLibraryError) 63 | { 64 | if (streamIO.ExceptionInfo != null) 65 | { 66 | streamIO.ExceptionInfo.Throw(); 67 | } 68 | else 69 | { 70 | string libraryError = new string(errorInfo.errorMessage); 71 | 72 | if (string.IsNullOrWhiteSpace(libraryError)) 73 | { 74 | throw new FormatException("An unknown error occurred when reading the image."); 75 | } 76 | else 77 | { 78 | throw new FormatException(libraryError); 79 | } 80 | } 81 | } 82 | else if (status == DecodeStatus.CallbackError) 83 | { 84 | if (loadState.ExceptionInfo != null) 85 | { 86 | loadState.ExceptionInfo.Throw(); 87 | } 88 | else 89 | { 90 | throw new FormatException("An unknown error occurred when reading the image."); 91 | } 92 | } 93 | else 94 | { 95 | switch (status) 96 | { 97 | case DecodeStatus.NullParameter: 98 | throw new ArgumentException("A required ReadImage parameter was null."); 99 | case DecodeStatus.OutOfMemory: 100 | throw new OutOfMemoryException(); 101 | case DecodeStatus.UserCanceled: 102 | throw new OperationCanceledException(); 103 | default: 104 | throw new FormatException("An unknown error occurred when reading the image."); 105 | } 106 | } 107 | } 108 | } 109 | 110 | return loadState; 111 | } 112 | 113 | public static unsafe void Save( 114 | Surface input, 115 | Stream output, 116 | int quality, 117 | ChromaSubsampling chromaSubsampling, 118 | bool progressive, 119 | MetadataParams metadata, 120 | ProgressEventHandler progressEventHandler, 121 | IArrayPoolService arrayPool) 122 | { 123 | BitmapData bitmap = new BitmapData 124 | { 125 | scan0 = (byte*)input.Scan0.VoidStar, 126 | width = (uint)input.Width, 127 | height = (uint)input.Height, 128 | stride = (uint)input.Stride 129 | }; 130 | 131 | EncodeOptions encodeOptions = new EncodeOptions 132 | { 133 | quality = quality, 134 | chromaSubsampling = chromaSubsampling, 135 | progressive = progressive 136 | }; 137 | 138 | using (MozJpegStreamIO streamIO = new MozJpegStreamIO(output, arrayPool)) 139 | { 140 | WriteCallback writeCallback = streamIO.Write; 141 | 142 | ProgressCallback progressCallback = null; 143 | 144 | if (progressEventHandler != null) 145 | { 146 | progressCallback = new ProgressCallback(delegate (int progress) 147 | { 148 | try 149 | { 150 | progressEventHandler.Invoke(null, new ProgressEventArgs(progress, true)); 151 | return true; 152 | } 153 | catch (OperationCanceledException) 154 | { 155 | return false; 156 | } 157 | }); 158 | } 159 | 160 | EncodeStatus status = EncodeStatus.Ok; 161 | JpegLibraryErrorInfo errorInfo = new JpegLibraryErrorInfo(); 162 | 163 | if (RuntimeInformation.ProcessArchitecture == Architecture.X64) 164 | { 165 | status = MozJpeg_X64.WriteImage(ref bitmap, 166 | ref encodeOptions, 167 | metadata, 168 | ref errorInfo, 169 | progressCallback, 170 | writeCallback); 171 | } 172 | else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) 173 | { 174 | status = MozJpeg_Arm64.WriteImage(ref bitmap, 175 | ref encodeOptions, 176 | metadata, 177 | ref errorInfo, 178 | progressCallback, 179 | writeCallback); 180 | } 181 | else if (RuntimeInformation.ProcessArchitecture == Architecture.X86) 182 | { 183 | status = MozJpeg_X86.WriteImage(ref bitmap, 184 | ref encodeOptions, 185 | metadata, 186 | ref errorInfo, 187 | progressCallback, 188 | writeCallback); 189 | } 190 | else 191 | { 192 | throw new PlatformNotSupportedException(); 193 | } 194 | 195 | 196 | GC.KeepAlive(progressCallback); 197 | GC.KeepAlive(writeCallback); 198 | GC.KeepAlive(metadata); 199 | 200 | if (status != EncodeStatus.Ok) 201 | { 202 | if (status == EncodeStatus.JpegLibraryError) 203 | { 204 | if (streamIO.ExceptionInfo != null) 205 | { 206 | streamIO.ExceptionInfo.Throw(); 207 | } 208 | else 209 | { 210 | string libraryError = new string(errorInfo.errorMessage); 211 | 212 | if (string.IsNullOrWhiteSpace(libraryError)) 213 | { 214 | throw new FormatException("An unknown error occurred when writing the image."); 215 | } 216 | else 217 | { 218 | throw new FormatException(libraryError); 219 | } 220 | } 221 | } 222 | else 223 | { 224 | switch (status) 225 | { 226 | case EncodeStatus.NullParameter: 227 | throw new ArgumentException("A required WriteImage parameter was null."); 228 | case EncodeStatus.OutOfMemory: 229 | throw new OutOfMemoryException(); 230 | case EncodeStatus.UserCanceled: 231 | throw new OperationCanceledException(); 232 | default: 233 | throw new FormatException("An unknown error occurred when writing the image."); 234 | } 235 | } 236 | 237 | } 238 | } 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/MozJpegFile.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using MozJpegFileType.Exif; 14 | using MozJpegFileType.Interop; 15 | using MozJpegFileType.Xmp; 16 | using PaintDotNet; 17 | using PaintDotNet.AppModel; 18 | using PaintDotNet.Collections; 19 | using PaintDotNet.Imaging; 20 | using PaintDotNet.Rendering; 21 | using System; 22 | using System.Collections.Generic; 23 | using System.IO; 24 | using System.Text; 25 | 26 | namespace MozJpegFileType 27 | { 28 | internal static class MozJpegFile 29 | { 30 | public static Document Load(Stream input, IArrayPoolService arrayPool) 31 | { 32 | MozJpegLoadState loadState = MozJpegNative.Load(input, arrayPool); 33 | 34 | Surface surface = loadState.Surface; 35 | 36 | ExifValueCollection exifValues = GetExifValues(loadState, arrayPool); 37 | 38 | if (exifValues != null) 39 | { 40 | MetadataEntry orientation = exifValues.GetAndRemoveValue(MetadataKeys.Image.Orientation); 41 | 42 | if (orientation != null) 43 | { 44 | ApplyExifOrientationTransform(orientation, ref surface); 45 | } 46 | } 47 | 48 | Document doc = new Document(surface.Width, surface.Height); 49 | 50 | doc.Layers.Add(Layer.CreateBackgroundLayer(surface, takeOwnership: true)); 51 | 52 | AddMetadataToDocument(doc, loadState, exifValues); 53 | 54 | return doc; 55 | } 56 | 57 | public static unsafe void Save( 58 | Document input, 59 | Stream output, 60 | Surface scratchSurface, 61 | int quality, 62 | ChromaSubsampling chromaSubsampling, 63 | bool progressive, 64 | ProgressEventHandler progressCallback, 65 | IArrayPoolService arrayPool) 66 | { 67 | scratchSurface.Clear(); 68 | input.CreateRenderer().Render(scratchSurface); 69 | 70 | if (IsGrayscale(scratchSurface)) 71 | { 72 | // Chroma sub-sampling 4:0:0 is always used for gray-scale images because it 73 | // produces the smallest file size with no quality loss. 74 | chromaSubsampling = ChromaSubsampling.Subsampling400; 75 | } 76 | 77 | MetadataParams metadata = CreateMozJpegMetadata(input); 78 | 79 | MozJpegNative.Save(scratchSurface, 80 | output, 81 | quality, 82 | chromaSubsampling, 83 | progressive, 84 | metadata, 85 | progressCallback, 86 | arrayPool); 87 | } 88 | 89 | private static void AddMetadataToDocument(Document document, 90 | MozJpegLoadState loadState, 91 | ExifValueCollection exifValues) 92 | { 93 | if (exifValues != null) 94 | { 95 | exifValues.Remove(MetadataKeys.Image.InterColorProfile); 96 | 97 | foreach (MetadataEntry entry in exifValues) 98 | { 99 | document.Metadata.AddExifPropertyItem(entry.CreateExifPropertyItem()); 100 | } 101 | } 102 | 103 | byte[] iccProfileBytes = loadState.GetIccProfileBytes(); 104 | 105 | if (iccProfileBytes != null) 106 | { 107 | ExifPropertyKey interColorProfile = ExifPropertyKeys.Image.InterColorProfile; 108 | 109 | document.Metadata.AddExifPropertyItem(interColorProfile.Path.Section, 110 | interColorProfile.Path.TagID, 111 | new ExifValue(ExifValueType.Undefined, 112 | iccProfileBytes)); 113 | } 114 | 115 | XmpPacket xmpPacket = loadState.GetXmpPacket(); 116 | 117 | if (xmpPacket != null) 118 | { 119 | document.Metadata.SetXmpPacket(xmpPacket); 120 | } 121 | } 122 | 123 | private static void ApplyExifOrientationTransform(MetadataEntry orientation, ref Surface surface) 124 | { 125 | if (MetadataHelpers.TryDecodeShort(orientation, out ushort exifValue)) 126 | { 127 | if (exifValue >= TiffConstants.Orientation.TopLeft && exifValue <= TiffConstants.Orientation.LeftBottom) 128 | { 129 | switch (exifValue) 130 | { 131 | case TiffConstants.Orientation.TopLeft: 132 | // Do nothing 133 | break; 134 | case TiffConstants.Orientation.TopRight: 135 | // Flip horizontally. 136 | ImageTransform.FlipHorizontal(surface); 137 | break; 138 | case TiffConstants.Orientation.BottomRight: 139 | // Rotate 180 degrees. 140 | ImageTransform.Rotate180(surface); 141 | break; 142 | case TiffConstants.Orientation.BottomLeft: 143 | // Flip vertically. 144 | ImageTransform.FlipVertical(surface); 145 | break; 146 | case TiffConstants.Orientation.LeftTop: 147 | // Rotate 90 degrees clockwise and flip horizontally. 148 | ImageTransform.Rotate90CCW(ref surface); 149 | ImageTransform.FlipHorizontal(surface); 150 | break; 151 | case TiffConstants.Orientation.RightTop: 152 | // Rotate 90 degrees clockwise. 153 | ImageTransform.Rotate90CCW(ref surface); 154 | break; 155 | case TiffConstants.Orientation.RightBottom: 156 | // Rotate 270 degrees clockwise and flip horizontally. 157 | ImageTransform.Rotate270CCW(ref surface); 158 | ImageTransform.FlipHorizontal(surface); 159 | break; 160 | case TiffConstants.Orientation.LeftBottom: 161 | // Rotate 270 degrees clockwise. 162 | ImageTransform.Rotate270CCW(ref surface); 163 | break; 164 | } 165 | } 166 | } 167 | } 168 | 169 | private static MetadataParams CreateMozJpegMetadata(Document doc) 170 | { 171 | byte[] exifBytes = null; 172 | byte[] iccProfileBytes = null; 173 | byte[] standardXmpBytes = null; 174 | List extendedXmpChunks = new List(); 175 | 176 | Dictionary exifMetadata = GetExifMetadataFromDocument(doc); 177 | 178 | if (exifMetadata != null) 179 | { 180 | Exif.ExifColorSpace exifColorSpace = Exif.ExifColorSpace.Srgb; 181 | 182 | MetadataKey iccProfileKey = MetadataKeys.Image.InterColorProfile; 183 | 184 | if (exifMetadata.TryGetValue(iccProfileKey, out MetadataEntry iccProfileItem)) 185 | { 186 | iccProfileBytes = iccProfileItem.GetData(); 187 | exifMetadata.Remove(iccProfileKey); 188 | exifColorSpace = Exif.ExifColorSpace.Uncalibrated; 189 | } 190 | 191 | exifBytes = new ExifWriter(doc, exifMetadata, exifColorSpace).CreateExifApp1Payload(); 192 | } 193 | 194 | XmpPacket xmpPacket = doc.Metadata.TryGetXmpPacket(); 195 | if (xmpPacket != null) 196 | { 197 | const int MaxStandardXmpPacketLength = 65504; 198 | 199 | string packetAsString = xmpPacket.ToString(XmpPacketWrapperType.ReadOnly); 200 | 201 | byte[] xmpPacketXmlUtf8 = Encoding.UTF8.GetBytes(packetAsString); 202 | 203 | if (xmpPacketXmlUtf8.Length <= MaxStandardXmpPacketLength) 204 | { 205 | standardXmpBytes = XmpUtils.AddSignatureToStandardXmpPacket(xmpPacketXmlUtf8); 206 | } 207 | else 208 | { 209 | ExtendedXmpData data = XmpUtils.SplitXmpPacketIntoExtendedXmp(xmpPacketXmlUtf8); 210 | 211 | standardXmpBytes = data.StandardXmpBytes; 212 | extendedXmpChunks = data.ExtendedXmpChunks; 213 | } 214 | } 215 | 216 | return new MetadataParams(exifBytes, iccProfileBytes, standardXmpBytes, extendedXmpChunks); 217 | } 218 | 219 | private static Dictionary GetExifMetadataFromDocument(Document doc) 220 | { 221 | Dictionary items = null; 222 | 223 | Metadata metadata = doc.Metadata; 224 | 225 | ExifPropertyItem[] exifProperties = metadata.GetExifPropertyItems(); 226 | 227 | if (exifProperties.Length > 0) 228 | { 229 | items = new Dictionary(exifProperties.Length); 230 | 231 | foreach (ExifPropertyItem property in exifProperties) 232 | { 233 | MetadataSection section; 234 | switch (property.Path.Section) 235 | { 236 | case ExifSection.Image: 237 | section = MetadataSection.Image; 238 | break; 239 | case ExifSection.Photo: 240 | section = MetadataSection.Exif; 241 | break; 242 | case ExifSection.Interop: 243 | section = MetadataSection.Interop; 244 | break; 245 | case ExifSection.GpsInfo: 246 | section = MetadataSection.Gps; 247 | break; 248 | default: 249 | throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.InvariantCulture, 250 | "Unexpected {0} type: {1}", 251 | nameof(ExifSection), 252 | (int)property.Path.Section)); 253 | } 254 | 255 | MetadataKey metadataKey = new MetadataKey(section, property.Path.TagID); 256 | 257 | if (!items.ContainsKey(metadataKey)) 258 | { 259 | byte[] clonedData = property.Value.Data.ToArrayEx(); 260 | 261 | items.Add(metadataKey, new MetadataEntry(metadataKey, (TagDataType)property.Value.Type, clonedData)); 262 | } 263 | } 264 | } 265 | 266 | return items; 267 | } 268 | 269 | private static ExifValueCollection GetExifValues(MozJpegLoadState loadState, IArrayPoolService arrayPool) 270 | { 271 | ExifValueCollection exifValues = null; 272 | 273 | byte[] exifBytes = loadState.GetExifBytes(); 274 | 275 | if (exifBytes != null) 276 | { 277 | exifValues = ExifParser.Parse(exifBytes, arrayPool); 278 | } 279 | 280 | return exifValues; 281 | } 282 | 283 | private static unsafe bool IsGrayscale(Surface surface) 284 | { 285 | for (int y = 0; y < surface.Height; y++) 286 | { 287 | ColorBgra* ptr = surface.GetRowPointerUnchecked(y); 288 | ColorBgra* ptrEnd = ptr + surface.Width; 289 | 290 | while (ptr < ptrEnd) 291 | { 292 | if (!(ptr->B == ptr->G && ptr->G == ptr->R)) 293 | { 294 | return false; 295 | } 296 | 297 | ptr++; 298 | } 299 | } 300 | 301 | return true; 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/Xmp/XmpUtils.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | using PaintDotNet.Collections; 15 | using PaintDotNet.Imaging; 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Diagnostics; 19 | using System.Globalization; 20 | using System.IO; 21 | using System.Linq; 22 | using System.Security.Cryptography; 23 | using System.Text; 24 | using System.Xml.Linq; 25 | 26 | namespace MozJpegFileType.Xmp 27 | { 28 | internal static partial class XmpUtils 29 | { 30 | private static readonly byte[] StandardXmpSignatureAscii; 31 | private static readonly int StandardXmpSignatureLengthWithTerminator; 32 | 33 | static XmpUtils() 34 | { 35 | StandardXmpSignatureAscii = Encoding.ASCII.GetBytes(XmpConstants.StandardXmpSignature); 36 | StandardXmpSignatureLengthWithTerminator = StandardXmpSignatureAscii.Length + 1; 37 | } 38 | 39 | public static byte[] AddSignatureToStandardXmpPacket(byte[] xmpPacketXmlUtf8) 40 | { 41 | byte[] xmpPacketWithSignature = new byte[StandardXmpSignatureLengthWithTerminator + xmpPacketXmlUtf8.Length]; 42 | 43 | Array.Copy(StandardXmpSignatureAscii, xmpPacketWithSignature, StandardXmpSignatureAscii.Length); 44 | // Ensure the null terminator is present. 45 | xmpPacketWithSignature[StandardXmpSignatureLengthWithTerminator] = 0; 46 | 47 | Array.Copy(xmpPacketXmlUtf8, 0, xmpPacketWithSignature, StandardXmpSignatureLengthWithTerminator, xmpPacketXmlUtf8.Length); 48 | 49 | return xmpPacketWithSignature; 50 | } 51 | 52 | public static ExtendedXmpData SplitXmpPacketIntoExtendedXmp(byte[] xmpPacketXmlUtf8) 53 | { 54 | byte[] standardXmpPacketXmlUtf8; 55 | List extendedXmpChunks = new List(); 56 | 57 | // Calculate MD5 hash string 58 | string md5HashString; 59 | { 60 | MD5 md5Hasher = MD5.Create(); 61 | byte[] md5HashBytes = md5Hasher.ComputeHash(xmpPacketXmlUtf8); 62 | md5HashString = md5HashBytes.Select(b => b.ToString("X2", CultureInfo.InvariantCulture)).Join(string.Empty); 63 | Debug.Assert(md5HashString.Length == 32); 64 | } 65 | 66 | standardXmpPacketXmlUtf8 = CreateStandardPacketForExtenededXmp(md5HashString); 67 | 68 | // Prepare some constants 69 | byte[] nsAdobeComXmpExtensionAscii = Encoding.ASCII.GetBytes(XmpConstants.ExtendedXmpChunkSignature); 70 | Debug.Assert(nsAdobeComXmpExtensionAscii.Length == 34); 71 | 72 | byte[] md5HashTextAscii = Encoding.ASCII.GetBytes(md5HashString); 73 | Debug.Assert(md5HashTextAscii.Length == 32); 74 | 75 | List chunkBuilder = new List(65535); 76 | 77 | // Serialize the real XMP packet data and then blast it out into APP1 chunks that are 64K each 78 | int chunkStartIndex = 0; 79 | while (chunkStartIndex < xmpPacketXmlUtf8.Length) 80 | { 81 | const int maxChunkSize = 65400; 82 | int chunkEndIndex = Math.Min(xmpPacketXmlUtf8.Length, chunkStartIndex + maxChunkSize); 83 | int chunkLength = chunkEndIndex - chunkStartIndex; 84 | chunkBuilder.Clear(); 85 | 86 | // From section 1.1.3.1: 87 | // Each chunk is written into the JPEG file within a separate APP1 marker segment. 88 | // Each ExtendedXMP marker segment contains: ... 89 | 90 | // 1) A null-terminated signature string 91 | chunkBuilder.AddRange(nsAdobeComXmpExtensionAscii); 92 | chunkBuilder.Add(0); 93 | 94 | // 2) A 128-bit GUID stored as a 32-byte ASCII hex string, capital A-F, no null termination. 95 | // The GUID is a 128-bit MD5 digest of the full ExtendedXMP serialization. 96 | chunkBuilder.AddRange(md5HashTextAscii); // not null terminated 97 | 98 | // 3) The full length of the ExtendedXMP serialization as a 32-bit unsigned integer 99 | AddBytes(chunkBuilder, UInt32Util.GetBytesBigEndian(checked((uint)xmpPacketXmlUtf8.Length))); 100 | 101 | // 4) The offset of this portion as a 32-bit unsigned integer. 102 | AddBytes(chunkBuilder, UInt32Util.GetBytesBigEndian(checked((uint)chunkStartIndex))); 103 | 104 | // 5) The ExtendedXMP chunk itself 105 | ArraySegment extXmpUtf8Segment = new ArraySegment(xmpPacketXmlUtf8, chunkStartIndex, chunkLength); 106 | chunkBuilder.AddRange(extXmpUtf8Segment); 107 | 108 | // Give data to caller 109 | extendedXmpChunks.Add(chunkBuilder.ToArrayEx()); 110 | 111 | chunkStartIndex = chunkEndIndex; 112 | } 113 | 114 | return new ExtendedXmpData(standardXmpPacketXmlUtf8, extendedXmpChunks); 115 | } 116 | 117 | public static bool TryGetExtendedXmpGuid(XDocument document, out string extendedXmpGuid) 118 | { 119 | extendedXmpGuid = null; 120 | 121 | if (document is null) 122 | { 123 | return false; 124 | } 125 | 126 | XElement rdfElement = document.Document.Descendants(XmpConstants.RdfElementXName).First(); 127 | 128 | extendedXmpGuid = TryGetExtendedXmpGuid(rdfElement); 129 | 130 | return !string.IsNullOrWhiteSpace(extendedXmpGuid); 131 | } 132 | 133 | public static ExtendedXMPChunk TryParseExtendedXmpData(byte[] extendedXmp, PaintDotNet.AppModel.IArrayPoolService arrayPool) 134 | { 135 | const int MD5LengthInBytes = 32; 136 | const int ExtendedXmpHeaderLength = MD5LengthInBytes + sizeof(uint) + sizeof(uint); 137 | 138 | if (extendedXmp.Length <= ExtendedXmpHeaderLength) 139 | { 140 | return null; 141 | } 142 | 143 | string md5 = Encoding.ASCII.GetString(extendedXmp, 0, MD5LengthInBytes); 144 | 145 | uint totalLength = ParseUInt32BigEndian(extendedXmp, MD5LengthInBytes); 146 | uint chunkOffset = ParseUInt32BigEndian(extendedXmp, MD5LengthInBytes + sizeof(uint)); 147 | 148 | int dataLength = extendedXmp.Length - ExtendedXmpHeaderLength; 149 | 150 | IArrayPoolBuffer data = arrayPool.Rent(dataLength); 151 | 152 | Array.Copy(extendedXmp, ExtendedXmpHeaderLength, data.Array, 0, dataLength); 153 | 154 | return new ExtendedXMPChunk(md5, totalLength, chunkOffset, data); 155 | } 156 | 157 | public static XDocument TryParseXmpBytes(byte[] xmpBytes) 158 | { 159 | XDocument document = null; 160 | 161 | try 162 | { 163 | using (MemoryStream stream = new MemoryStream(xmpBytes)) 164 | { 165 | document = XDocument.Load(stream); 166 | } 167 | } 168 | catch (Exception ex) when (!(ex is OutOfMemoryException)) 169 | { 170 | // Ignore it. 171 | } 172 | 173 | return document; 174 | } 175 | 176 | public static XDocument MergeXmpPackets(XDocument standardXmpPacket, XDocument extendedXmpPacket) 177 | { 178 | XDocument mergedDocument = new XDocument(standardXmpPacket); 179 | XElement mergedXmpMetaElement = mergedDocument.Element(XmpConstants.XmpMetaElementXName); 180 | XElement mergedRdfElement = mergedXmpMetaElement.Element(XmpConstants.RdfElementXName); 181 | 182 | XDocument extendedDocument = extendedXmpPacket; 183 | XElement extendedXmpMetaElement = extendedDocument.Element(XmpConstants.XmpMetaElementXName); 184 | XElement extendedRdfElement = extendedXmpMetaElement.Element(XmpConstants.RdfElementXName); 185 | 186 | foreach (XElement element in extendedRdfElement.Elements(XmpConstants.DescriptionElementXName)) 187 | { 188 | mergedRdfElement.Add(element); 189 | } 190 | 191 | XElement descriptionElement0 = mergedRdfElement.Elements(XmpConstants.DescriptionElementXName).FirstOrDefault(); 192 | if (descriptionElement0 != null) 193 | { 194 | foreach (XElement descriptionElementN in mergedRdfElement.Elements(XmpConstants.DescriptionElementXName).Skip(1)) 195 | { 196 | if (CanMergeDescriptionElements(descriptionElement0, descriptionElementN)) 197 | { 198 | BestFaithMergeElements(descriptionElement0, descriptionElementN); 199 | descriptionElementN.Remove(); 200 | } 201 | } 202 | } 203 | 204 | foreach (XElement element in mergedRdfElement.Elements(XmpConstants.DescriptionElementXName)) 205 | { 206 | TryRemoveHasExtendedXMPObject(element); 207 | } 208 | 209 | return mergedDocument; 210 | } 211 | 212 | private static void AddBytes(ICollection output, (byte b0, byte b1, byte b2, byte b3) bytes) 213 | { 214 | output.Add(bytes.b0); 215 | output.Add(bytes.b1); 216 | output.Add(bytes.b2); 217 | output.Add(bytes.b3); 218 | } 219 | 220 | private static void BestFaithMergeElements(XElement targetElement, XElement sourceElement) 221 | { 222 | foreach (XAttribute sourceAttribute in sourceElement.Attributes()) 223 | { 224 | TryAddAttribute(targetElement, sourceAttribute); 225 | } 226 | 227 | foreach (XElement childSourceElement in sourceElement.Elements()) 228 | { 229 | TryAddElement(targetElement, childSourceElement); 230 | } 231 | } 232 | 233 | private static bool CanMergeDescriptionElements(XElement element1, XElement element2) 234 | { 235 | XAttribute aboutUri1 = element1.Attribute(XmpConstants.AboutUriXName); 236 | XAttribute aboutUri2 = element2.Attribute(XmpConstants.AboutUriXName); 237 | 238 | if (aboutUri1 == null || aboutUri2 == null) 239 | { 240 | return true; 241 | } 242 | 243 | if (aboutUri1.Value.Equals(aboutUri2.Value, StringComparison.InvariantCulture)) 244 | { 245 | return true; 246 | } 247 | 248 | return false; 249 | } 250 | 251 | private static byte[] CreateStandardPacketForExtenededXmp(string md5HashString) 252 | { 253 | string standardXmpPacketXmlBegin = 254 | "" + Environment.NewLine + 255 | " " + Environment.NewLine + 256 | " " + Environment.NewLine + 257 | $" " + Environment.NewLine + 258 | " " + Environment.NewLine + 259 | " "; 260 | 261 | string standardXmpPacketXmlEnd = Environment.NewLine + ""; 262 | 263 | // Aim for having 1024 bytes total. Put padding spaces before the xpacket end. This allows modifying XMP without reencoding. 264 | int padding0 = 1024 - Encoding.UTF8.GetByteCount(standardXmpPacketXmlBegin) - Encoding.UTF8.GetByteCount(standardXmpPacketXmlEnd); 265 | int padding = Math.Max(0, padding0); 266 | 267 | string standardXmpPacketXml = standardXmpPacketXmlBegin + new string(' ', padding) + standardXmpPacketXmlEnd; 268 | 269 | return AddSignatureToStandardXmpPacket(Encoding.UTF8.GetBytes(standardXmpPacketXml)); 270 | } 271 | 272 | private static uint ParseUInt32BigEndian(byte[] bytes, int startIndex) 273 | { 274 | return (uint)((bytes[startIndex] << 24) | 275 | (bytes[startIndex + 1] << 16) | 276 | (bytes[startIndex + 2] << 8) | 277 | bytes[startIndex + 3]); 278 | } 279 | 280 | private static bool TryAddAttribute(XElement targetElement, XAttribute sourceAttribute) 281 | { 282 | XAttribute targetAttribute = targetElement.Attribute(sourceAttribute.Name); 283 | 284 | if (targetAttribute == null) 285 | { 286 | // Attribute does not exist in target -- add and we're done 287 | targetElement.Add(sourceAttribute); 288 | return true; 289 | } 290 | 291 | if (targetAttribute.Value.Equals(sourceAttribute.Value, StringComparison.InvariantCulture)) 292 | { 293 | // Attribute exists in target and has the same value. No problem. 294 | return true; 295 | } 296 | 297 | return false; 298 | } 299 | 300 | private static bool TryAddElement(XElement targetElement, XElement sourceChildElement) 301 | { 302 | XElement targetChildElement = targetElement.Element(sourceChildElement.Name); 303 | 304 | if (targetChildElement == null) 305 | { 306 | // Element does not exist in target -- add and we're done 307 | targetElement.Add(sourceChildElement); 308 | return true; 309 | } 310 | 311 | if (targetChildElement.Value.Equals(sourceChildElement.Value, StringComparison.InvariantCultureIgnoreCase)) 312 | { 313 | // Element exists in target and has the same value. No problem. 314 | return true; 315 | } 316 | 317 | Debug.Assert(false); 318 | return false; 319 | } 320 | 321 | private static string TryGetExtendedXmpGuid(XElement rdfElement) 322 | { 323 | // The xmpNote:HasExtendedXMP element may be located in any top-level rdf:Description element, not just the first one. 324 | 325 | foreach (XElement descriptionElement in rdfElement.Elements(XmpConstants.DescriptionElementXName)) 326 | { 327 | XAttribute hasExtXmpAttr = descriptionElement.Attribute(XmpConstants.HasExtendedXmpObjectXName); 328 | if (hasExtXmpAttr != null) 329 | { 330 | return hasExtXmpAttr.Value; 331 | } 332 | 333 | XElement hasExtXmpElement = descriptionElement.Element(XmpConstants.HasExtendedXmpObjectXName); 334 | if (hasExtXmpElement != null) 335 | { 336 | return hasExtXmpElement.Value; 337 | } 338 | } 339 | 340 | return null; 341 | } 342 | 343 | private static bool TryRemoveHasExtendedXMPObject(XElement descriptionElement) 344 | { 345 | XAttribute hasExtXmpAttr = descriptionElement.Attribute(XmpConstants.HasExtendedXmpObjectXName); 346 | if (hasExtXmpAttr != null) 347 | { 348 | hasExtXmpAttr.Remove(); 349 | return true; 350 | } 351 | 352 | XElement hasExtXmpElement = descriptionElement.Element(XmpConstants.HasExtendedXmpObjectXName); 353 | if (hasExtXmpElement != null) 354 | { 355 | hasExtXmpElement.Remove(); 356 | return true; 357 | } 358 | 359 | return false; 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/MozJpegFileTypeIO/MozJpegFileTypeIO.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | ARM64 7 | 8 | 9 | Debug 10 | Win32 11 | 12 | 13 | Release 14 | ARM64 15 | 16 | 17 | Release 18 | Win32 19 | 20 | 21 | Debug 22 | x64 23 | 24 | 25 | Release 26 | x64 27 | 28 | 29 | 30 | 16.0 31 | Win32Proj 32 | {599e9bce-0b46-40b8-aa1c-ce0ce67bc4be} 33 | MozJpegFileTypeIO 34 | 10.0 35 | 36 | 37 | 38 | DynamicLibrary 39 | true 40 | v143 41 | Unicode 42 | 43 | 44 | DynamicLibrary 45 | false 46 | v143 47 | true 48 | Unicode 49 | 50 | 51 | DynamicLibrary 52 | true 53 | v143 54 | Unicode 55 | 56 | 57 | DynamicLibrary 58 | true 59 | v143 60 | Unicode 61 | 62 | 63 | DynamicLibrary 64 | false 65 | v143 66 | true 67 | Unicode 68 | 69 | 70 | DynamicLibrary 71 | false 72 | v143 73 | true 74 | Unicode 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | true 102 | MozJpegFileTypeIO_x86 103 | 104 | 105 | false 106 | MozJpegFileTypeIO_x86 107 | 108 | 109 | true 110 | MozJpegFileTypeIO_x64 111 | 112 | 113 | true 114 | MozJpegFileTypeIO_ARM64 115 | 116 | 117 | false 118 | MozJpegFileTypeIO_x64 119 | 120 | 121 | false 122 | MozJpegFileTypeIO_ARM64 123 | 124 | 125 | true 126 | 127 | 128 | true 129 | 130 | 131 | true 132 | 133 | 134 | true 135 | 136 | 137 | true 138 | 139 | 140 | true 141 | 142 | 143 | 144 | Level3 145 | true 146 | WIN32;_DEBUG;MOZJPEGFILETYPEIO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 147 | true 148 | %(AdditionalIncludeDirectories) 149 | 150 | 151 | Windows 152 | true 153 | false 154 | 155 | 156 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 157 | 158 | 159 | 160 | 161 | Level3 162 | true 163 | true 164 | true 165 | WIN32;NDEBUG;MOZJPEGFILETYPEIO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 166 | true 167 | %(AdditionalIncludeDirectories) 168 | 169 | 170 | Windows 171 | true 172 | true 173 | true 174 | false 175 | 176 | 177 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 178 | 179 | 180 | 181 | 182 | Level3 183 | true 184 | _DEBUG;MOZJPEGFILETYPEIO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 185 | true 186 | MultiThreadedDebug 187 | %(AdditionalIncludeDirectories) 188 | 189 | 190 | Windows 191 | true 192 | false 193 | 194 | 195 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 196 | 197 | 198 | copy "$(TargetPath)" "D:\Program Files\paint.net\FileTypes" /y 199 | 200 | 201 | 202 | 203 | Level3 204 | true 205 | _DEBUG;MOZJPEGFILETYPEIO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 206 | true 207 | MultiThreadedDebug 208 | %(AdditionalIncludeDirectories) 209 | 210 | 211 | Windows 212 | true 213 | false 214 | 215 | 216 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | Level3 226 | true 227 | true 228 | true 229 | NDEBUG;MOZJPEGFILETYPEIO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 230 | true 231 | MultiThreaded 232 | %(AdditionalIncludeDirectories) 233 | 234 | 235 | Windows 236 | true 237 | true 238 | true 239 | false 240 | 241 | 242 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 243 | 244 | 245 | 246 | 247 | Level3 248 | true 249 | true 250 | true 251 | NDEBUG;MOZJPEGFILETYPEIO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 252 | true 253 | MultiThreaded 254 | %(AdditionalIncludeDirectories) 255 | 256 | 257 | Windows 258 | true 259 | true 260 | true 261 | false 262 | 263 | 264 | kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /src/Exif/ExifWriter.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-mozjpeg, a FileType plugin for Paint.NET 4 | // that saves JPEG images using the mozjpeg encoder. 5 | // 6 | // Copyright (c) 2021, 2022 Nicholas Hayes 7 | // 8 | // This file is licensed under the MIT License. 9 | // See LICENSE.txt for complete licensing and attribution information. 10 | // 11 | //////////////////////////////////////////////////////////////////////// 12 | 13 | using PaintDotNet; 14 | using System; 15 | using System.Collections.Generic; 16 | using System.IO; 17 | using System.Linq; 18 | using System.Text; 19 | 20 | namespace MozJpegFileType.Exif 21 | { 22 | internal sealed class ExifWriter 23 | { 24 | private const int FirstIFDOffset = 8; 25 | private const int MaxJpegApp1PayloadLength = 65535 - sizeof(ushort); 26 | 27 | private readonly Dictionary> metadata; 28 | 29 | private static readonly byte[] ExifMarkerAscii = Encoding.ASCII.GetBytes("Exif\0\0"); 30 | 31 | public ExifWriter(Document doc, IDictionary entries, ExifColorSpace exifColorSpace) 32 | { 33 | this.metadata = CreateTagDictionary(doc, entries, exifColorSpace); 34 | } 35 | 36 | public byte[] CreateExifApp1Payload() 37 | { 38 | IFDInfo ifdInfo = BuildIFDEntries(); 39 | Dictionary ifdEntries = ifdInfo.IFDEntries; 40 | 41 | long exifApp1PayloadLength = ExifMarkerAscii.LongLength + ifdInfo.EXIFDataLength; 42 | 43 | if (exifApp1PayloadLength > MaxJpegApp1PayloadLength) 44 | { 45 | return null; 46 | } 47 | 48 | byte[] exifBytes = new byte[checked((int)exifApp1PayloadLength)]; 49 | 50 | using (MemoryStream stream = new MemoryStream(exifBytes)) 51 | using (BinaryWriter writer = new BinaryWriter(stream)) 52 | { 53 | writer.Write(ExifMarkerAscii); 54 | 55 | IFDEntryInfo imageInfo = ifdEntries[MetadataSection.Image]; 56 | IFDEntryInfo exifInfo = ifdEntries[MetadataSection.Exif]; 57 | 58 | writer.Write(TiffConstants.LittleEndianByteOrderMarker); 59 | writer.Write(TiffConstants.Signature); 60 | writer.Write((uint)imageInfo.StartOffset); 61 | 62 | WriteDirectory(writer, this.metadata[MetadataSection.Image], imageInfo.IFDEntries, imageInfo.StartOffset); 63 | WriteDirectory(writer, this.metadata[MetadataSection.Exif], exifInfo.IFDEntries, exifInfo.StartOffset); 64 | 65 | if (ifdEntries.TryGetValue(MetadataSection.Interop, out IFDEntryInfo interopInfo)) 66 | { 67 | WriteDirectory(writer, this.metadata[MetadataSection.Interop], interopInfo.IFDEntries, interopInfo.StartOffset); 68 | } 69 | 70 | if (ifdEntries.TryGetValue(MetadataSection.Gps, out IFDEntryInfo gpsInfo)) 71 | { 72 | WriteDirectory(writer, this.metadata[MetadataSection.Gps], gpsInfo.IFDEntries, gpsInfo.StartOffset); 73 | } 74 | } 75 | 76 | return exifBytes; 77 | } 78 | 79 | private void WriteDirectory(BinaryWriter writer, Dictionary tags, List entries, long ifdOffset) 80 | { 81 | writer.BaseStream.Position = ifdOffset; 82 | 83 | long nextIFDPointerOffset = ifdOffset + sizeof(ushort) + ((long)entries.Count * IFDEntry.SizeOf); 84 | 85 | writer.Write((ushort)entries.Count); 86 | 87 | foreach (IFDEntry entry in entries.OrderBy(e => e.Tag)) 88 | { 89 | entry.Write(writer); 90 | 91 | if (!TagDataTypeUtil.ValueFitsInOffsetField(entry.Type, entry.Count)) 92 | { 93 | long oldPosition = writer.BaseStream.Position; 94 | 95 | writer.BaseStream.Position = entry.Offset; 96 | 97 | writer.Write(tags[entry.Tag].GetDataReadOnly()); 98 | 99 | writer.BaseStream.Position = oldPosition; 100 | } 101 | } 102 | 103 | writer.BaseStream.Position = nextIFDPointerOffset; 104 | // There is only one IFD in this directory. 105 | writer.Write(0); 106 | } 107 | 108 | private IFDInfo BuildIFDEntries() 109 | { 110 | Dictionary imageMetadata = this.metadata[MetadataSection.Image]; 111 | Dictionary exifMetadata = this.metadata[MetadataSection.Exif]; 112 | 113 | // Add placeholders for the sub-IFD tags. 114 | imageMetadata.Add( 115 | MetadataKeys.Image.ExifTag.TagId, 116 | new MetadataEntry(MetadataKeys.Image.ExifTag, 117 | TagDataType.Long, 118 | new byte[sizeof(uint)])); 119 | 120 | if (this.metadata.ContainsKey(MetadataSection.Gps)) 121 | { 122 | imageMetadata.Add( 123 | MetadataKeys.Image.GPSTag.TagId, 124 | new MetadataEntry(MetadataKeys.Image.GPSTag, 125 | TagDataType.Long, 126 | new byte[sizeof(uint)])); 127 | } 128 | 129 | if (this.metadata.ContainsKey(MetadataSection.Interop)) 130 | { 131 | exifMetadata.Add( 132 | MetadataKeys.Exif.InteroperabilityTag.TagId, 133 | new MetadataEntry(MetadataKeys.Exif.InteroperabilityTag, 134 | TagDataType.Long, 135 | new byte[sizeof(uint)])); 136 | } 137 | 138 | return CalculateSectionOffsets(); 139 | } 140 | 141 | private IFDInfo CalculateSectionOffsets() 142 | { 143 | IFDEntryInfo imageIFDInfo = CreateIFDList(this.metadata[MetadataSection.Image], FirstIFDOffset); 144 | IFDEntryInfo exifIFDInfo = CreateIFDList(this.metadata[MetadataSection.Exif], imageIFDInfo.NextAvailableOffset); 145 | IFDEntryInfo interopIFDInfo = null; 146 | IFDEntryInfo gpsIFDInfo = null; 147 | 148 | UpdateSubIFDOffset(ref imageIFDInfo, MetadataKeys.Image.ExifTag.TagId, (uint)exifIFDInfo.StartOffset); 149 | 150 | if (this.metadata.TryGetValue(MetadataSection.Interop, out Dictionary interopSection)) 151 | { 152 | interopIFDInfo = CreateIFDList(interopSection, exifIFDInfo.NextAvailableOffset); 153 | 154 | UpdateSubIFDOffset(ref exifIFDInfo, MetadataKeys.Exif.InteroperabilityTag.TagId, (uint)interopIFDInfo.StartOffset); 155 | } 156 | 157 | if (this.metadata.TryGetValue(MetadataSection.Gps, out Dictionary gpsSection)) 158 | { 159 | long startOffset = interopIFDInfo?.NextAvailableOffset ?? exifIFDInfo.NextAvailableOffset; 160 | gpsIFDInfo = CreateIFDList(gpsSection, startOffset); 161 | 162 | UpdateSubIFDOffset(ref imageIFDInfo, MetadataKeys.Image.GPSTag.TagId, (uint)gpsIFDInfo.StartOffset); 163 | } 164 | 165 | return CreateIFDInfo(imageIFDInfo, exifIFDInfo, interopIFDInfo, gpsIFDInfo); 166 | } 167 | 168 | private static void UpdateSubIFDOffset(ref IFDEntryInfo ifdInfo, ushort tagId, uint newOffset) 169 | { 170 | int index = ifdInfo.IFDEntries.FindIndex(i => i.Tag == tagId); 171 | 172 | if (index != -1) 173 | { 174 | ifdInfo.IFDEntries[index] = new IFDEntry(tagId, TagDataType.Long, 1, newOffset); 175 | } 176 | } 177 | 178 | private IFDInfo CreateIFDInfo( 179 | IFDEntryInfo imageIFDInfo, 180 | IFDEntryInfo exifIFDInfo, 181 | IFDEntryInfo interopIFDInfo, 182 | IFDEntryInfo gpsIFDInfo) 183 | { 184 | Dictionary entries = new Dictionary 185 | { 186 | { MetadataSection.Image, imageIFDInfo }, 187 | { MetadataSection.Exif, exifIFDInfo } 188 | }; 189 | 190 | long dataLength = exifIFDInfo.NextAvailableOffset; 191 | 192 | if (interopIFDInfo != null) 193 | { 194 | entries.Add(MetadataSection.Interop, interopIFDInfo); 195 | dataLength = interopIFDInfo.NextAvailableOffset; 196 | } 197 | 198 | if (gpsIFDInfo != null) 199 | { 200 | entries.Add(MetadataSection.Gps, gpsIFDInfo); 201 | dataLength = gpsIFDInfo.NextAvailableOffset; 202 | } 203 | 204 | return new IFDInfo(entries, dataLength); 205 | } 206 | 207 | private static IFDEntryInfo CreateIFDList(Dictionary tags, long startOffset) 208 | { 209 | List ifdEntries = new List(tags.Count); 210 | 211 | // Leave room for the tag count, tags and next IFD offset. 212 | long ifdDataOffset = startOffset + sizeof(ushort) + ((long)tags.Count * IFDEntry.SizeOf) + sizeof(uint); 213 | 214 | foreach (KeyValuePair item in tags.OrderBy(i => i.Key)) 215 | { 216 | MetadataEntry entry = item.Value; 217 | 218 | uint count; 219 | switch (entry.Type) 220 | { 221 | case TagDataType.Byte: 222 | case TagDataType.Ascii: 223 | case TagDataType.SByte: 224 | case TagDataType.Undefined: 225 | count = (uint)entry.LengthInBytes; 226 | break; 227 | case TagDataType.Short: 228 | case TagDataType.SShort: 229 | count = (uint)entry.LengthInBytes / 2; 230 | break; 231 | case TagDataType.Long: 232 | case TagDataType.SLong: 233 | case TagDataType.Float: 234 | count = (uint)entry.LengthInBytes / 4; 235 | break; 236 | case TagDataType.Rational: 237 | case TagDataType.SRational: 238 | case TagDataType.Double: 239 | count = (uint)entry.LengthInBytes / 8; 240 | break; 241 | default: 242 | throw new InvalidOperationException("Unexpected tag type."); 243 | } 244 | 245 | if (TagDataTypeUtil.ValueFitsInOffsetField(entry.Type, count)) 246 | { 247 | uint packedOffset = 0; 248 | 249 | // Some applications may write EXIF fields with a count of zero. 250 | // See https://github.com/0xC0000054/pdn-webp/issues/6. 251 | if (count > 0) 252 | { 253 | byte[] data = entry.GetDataReadOnly(); 254 | 255 | // The data is always in little-endian byte order. 256 | switch (data.Length) 257 | { 258 | case 1: 259 | packedOffset |= data[0]; 260 | break; 261 | case 2: 262 | packedOffset |= data[0]; 263 | packedOffset |= (uint)data[1] << 8; 264 | break; 265 | case 3: 266 | packedOffset |= data[0]; 267 | packedOffset |= (uint)data[1] << 8; 268 | packedOffset |= (uint)data[2] << 16; 269 | break; 270 | case 4: 271 | packedOffset |= data[0]; 272 | packedOffset |= (uint)data[1] << 8; 273 | packedOffset |= (uint)data[2] << 16; 274 | packedOffset |= (uint)data[3] << 24; 275 | break; 276 | default: 277 | throw new InvalidOperationException("data.Length must be in the range of [1-4]."); 278 | } 279 | } 280 | 281 | ifdEntries.Add(new IFDEntry(entry.TagId, entry.Type, count, packedOffset)); 282 | } 283 | else 284 | { 285 | ifdEntries.Add(new IFDEntry(entry.TagId, entry.Type, count, (uint)ifdDataOffset)); 286 | ifdDataOffset += entry.LengthInBytes; 287 | 288 | // The IFD offsets must begin on a WORD boundary. 289 | if ((ifdDataOffset & 1) == 1) 290 | { 291 | ifdDataOffset++; 292 | } 293 | } 294 | } 295 | 296 | return new IFDEntryInfo(ifdEntries, startOffset, ifdDataOffset); 297 | } 298 | 299 | private static Dictionary> CreateTagDictionary( 300 | Document doc, 301 | IDictionary entries, 302 | ExifColorSpace exifColorSpace) 303 | { 304 | Dictionary> metadataEntries = new Dictionary> 305 | { 306 | { 307 | MetadataSection.Image, 308 | new Dictionary 309 | { 310 | { 311 | MetadataKeys.Image.Orientation.TagId, 312 | new MetadataEntry(MetadataKeys.Image.Orientation, 313 | TagDataType.Short, 314 | MetadataHelpers.EncodeShort(TiffConstants.Orientation.TopLeft)) 315 | } 316 | } 317 | }, 318 | { 319 | MetadataSection.Exif, 320 | new Dictionary() 321 | } 322 | }; 323 | 324 | // Add the image size tags. 325 | if (IsUncompressedImage(entries)) 326 | { 327 | Dictionary imageSection = metadataEntries[MetadataSection.Image]; 328 | imageSection.Add(MetadataKeys.Image.ImageWidth.TagId, 329 | new MetadataEntry(MetadataKeys.Image.ImageWidth, 330 | TagDataType.Long, 331 | MetadataHelpers.EncodeLong((uint)doc.Width))); 332 | imageSection.Add(MetadataKeys.Image.ImageLength.TagId, 333 | new MetadataEntry(MetadataKeys.Image.ImageLength, 334 | TagDataType.Long, 335 | MetadataHelpers.EncodeLong((uint)doc.Height))); 336 | 337 | entries.Remove(MetadataKeys.Image.ImageWidth); 338 | entries.Remove(MetadataKeys.Image.ImageLength); 339 | // These tags should not be included in uncompressed images. 340 | entries.Remove(MetadataKeys.Exif.PixelXDimension); 341 | entries.Remove(MetadataKeys.Exif.PixelYDimension); 342 | } 343 | else 344 | { 345 | Dictionary exifSection = metadataEntries[MetadataSection.Exif]; 346 | exifSection.Add(MetadataKeys.Exif.PixelXDimension.TagId, 347 | new MetadataEntry(MetadataKeys.Exif.PixelXDimension, 348 | TagDataType.Long, 349 | MetadataHelpers.EncodeLong((uint)doc.Width))); 350 | exifSection.Add(MetadataKeys.Exif.PixelYDimension.TagId, 351 | new MetadataEntry(MetadataKeys.Exif.PixelYDimension, 352 | TagDataType.Long, 353 | MetadataHelpers.EncodeLong((uint)doc.Height))); 354 | 355 | entries.Remove(MetadataKeys.Exif.PixelXDimension); 356 | entries.Remove(MetadataKeys.Exif.PixelYDimension); 357 | // These tags should not be included in compressed images. 358 | entries.Remove(MetadataKeys.Image.ImageWidth); 359 | entries.Remove(MetadataKeys.Image.ImageLength); 360 | } 361 | 362 | // Add the EXIF color space tag. 363 | if (!entries.ContainsKey(MetadataKeys.Exif.ColorSpace)) 364 | { 365 | metadataEntries[MetadataSection.Exif].Add(MetadataKeys.Exif.ColorSpace.TagId, 366 | new MetadataEntry(MetadataKeys.Exif.ColorSpace, 367 | TagDataType.Short, 368 | MetadataHelpers.EncodeShort((ushort)exifColorSpace))); 369 | } 370 | 371 | foreach (KeyValuePair kvp in entries) 372 | { 373 | MetadataEntry entry = kvp.Value; 374 | 375 | MetadataSection section = entry.Section; 376 | 377 | if (section == MetadataSection.Image && !ExifTagHelper.CanWriteImageSectionTag(entry.TagId)) 378 | { 379 | continue; 380 | } 381 | 382 | if (metadataEntries.TryGetValue(section, out Dictionary values)) 383 | { 384 | if (!values.ContainsKey(entry.TagId)) 385 | { 386 | values.Add(entry.TagId, entry); 387 | } 388 | } 389 | else 390 | { 391 | metadataEntries.Add(section, new Dictionary 392 | { 393 | { entry.TagId, entry } 394 | }); 395 | } 396 | } 397 | 398 | AddVersionEntries(ref metadataEntries); 399 | 400 | return metadataEntries; 401 | } 402 | 403 | private static bool IsUncompressedImage(IDictionary entries) 404 | { 405 | return entries.ContainsKey(MetadataKeys.Image.ImageWidth); 406 | } 407 | 408 | private static void AddVersionEntries(ref Dictionary> metadataEntries) 409 | { 410 | if (metadataEntries.TryGetValue(MetadataSection.Exif, out Dictionary exifItems)) 411 | { 412 | if (!exifItems.ContainsKey(MetadataKeys.Exif.ExifVersion.TagId)) 413 | { 414 | exifItems.Add( 415 | MetadataKeys.Exif.ExifVersion.TagId, 416 | new MetadataEntry(MetadataKeys.Exif.ExifVersion, 417 | TagDataType.Undefined, 418 | new byte[] { (byte)'0', (byte)'2', (byte)'3', (byte)'0' })); 419 | } 420 | } 421 | 422 | if (metadataEntries.TryGetValue(MetadataSection.Gps, out Dictionary gpsItems)) 423 | { 424 | if (!gpsItems.ContainsKey(MetadataKeys.Gps.GPSVersionID.TagId)) 425 | { 426 | gpsItems.Add( 427 | MetadataKeys.Gps.GPSVersionID.TagId, 428 | new MetadataEntry(MetadataKeys.Gps.GPSVersionID, 429 | TagDataType.Byte, 430 | new byte[] { 2, 3, 0, 0 })); 431 | } 432 | } 433 | } 434 | 435 | private sealed class IFDEntryInfo 436 | { 437 | public IFDEntryInfo(List ifdEntries, long startOffset, long nextAvailableOffset) 438 | { 439 | if (ifdEntries is null) 440 | { 441 | throw new ArgumentNullException(nameof(ifdEntries)); 442 | } 443 | 444 | this.IFDEntries = ifdEntries; 445 | this.StartOffset = startOffset; 446 | this.NextAvailableOffset = nextAvailableOffset; 447 | } 448 | 449 | public List IFDEntries { get; } 450 | 451 | public long StartOffset { get; } 452 | 453 | public long NextAvailableOffset { get; } 454 | } 455 | 456 | private sealed class IFDInfo 457 | { 458 | public IFDInfo(Dictionary entries, long exifDataLength) 459 | { 460 | if (entries is null) 461 | { 462 | throw new ArgumentNullException(nameof(entries)); 463 | } 464 | 465 | this.IFDEntries = entries; 466 | this.EXIFDataLength = exifDataLength; 467 | } 468 | 469 | public Dictionary IFDEntries { get; } 470 | 471 | public long EXIFDataLength { get; } 472 | } 473 | } 474 | } 475 | --------------------------------------------------------------------------------