c# - 如何使用 Winforms C# 或 C++ .Net 修改 jpg 文件中的方向 exif 标记而不更改文件中的任何其他内容

标签 c# c++ jpeg orientation exif

我正在编写一个程序来帮助我整理多年来拍摄的数千张数码照片。我想要的一个功能是能够通过修改 Orientation EXIF 标签来旋转图像,而无需更改文件中的任何其他内容。我知道这是可能的,因为如果您在 Windows 资源管理器中右键单击该文件并选择向左/向右旋转,那么就会发生这种情况 - 一个字节被修改以匹配新的方向值。我特别不想修改图片本身。

然而,我尝试过的所有方法要么没有效果,要么显着改变了文件(例如,将其减少 14k 字节,大概是通过重新编码)。我在几个网站上阅读了很多帖子,似乎没有人对我的具体问题有答案——他们大多谈论添加额外的标签,以及添加填充的需要,但如果我只是试图修改一个现有字节(尤其是我知道 Windows 资源管理器可以做到这一点)。

我在 Windows 10 专业版下使用运行 Framework 4.5.2 的 C# Windows 窗体应用程序。还尝试从 C++ 中进行。感谢所有贡献者,我以他们的例子为基础。

这里有 5 个简单的控制台应用程序示例:

  1. 使用 System.Drawing.Image 类的基本 C#。这会将 Orientation 标签设置为 OK,但会减小尺寸,即重新编码图片。

     static void Main(string[] args)
     {
         const int EXIF_ORIENTATION = 0x0112;
    
         try
         {
             using (Image image = Image.FromFile("Test.jpg"))
             {
                 System.Drawing.Imaging.PropertyItem orientation = image.GetPropertyItem(EXIF_ORIENTATION);
    
                 byte o = 6; // Rotate 90 degrees clockwise
    
                 orientation.Value[0] = o;
    
                 image.SetPropertyItem(orientation);
    
                 image.Save("Test2.jpg");
             }
         }
         catch (Exception ex)
         {
         }
    
  2. InPlaceBitMapEditor 类看起来正是我需要的,调试行表明这是在修改 EXIF 标签,但文件未修改,即更改未写出。

     static void Main(string[] args)
     {
         try
         {
             Stream stream = new System.IO.FileStream("Test.JPG", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
    
             JpegBitmapDecoder pngDecoder = new JpegBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    
             BitmapFrame frame = pngDecoder.Frames[0];
    
             InPlaceBitmapMetadataWriter inplace = frame.CreateInPlaceBitmapMetadataWriter();
    
             ushort u = 6; // Rotate 90 degrees clockwise
    
             object i1 = inplace.GetQuery("/app1/ifd/{ushort=274}"); // DEBUG - this is what it was before - 1
    
             if (inplace.TrySave() == true)
             {
                 inplace.SetQuery("/app1/ifd/{ushort=274}", u);
             }
    
             object i2 = inplace.GetQuery("/app1/ifd/{ushort=274}"); // DEBUG - this is what it is after - 6
    
             stream.Close();
         }
         catch (Exception ex)
         {
         }
    
  3. 上述内容的演变,明确写出文件。这会设置 Orientation 标签,文件显示正常但会减小尺寸,即重新编码图片。

     static void Main(string[] args)
     {
         BitmapCreateOptions createOptions = BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile;
    
         using (Stream originalFile = File.Open("Test.JPG", FileMode.Open, FileAccess.ReadWrite))
         {
             BitmapDecoder original = BitmapDecoder.Create(originalFile, createOptions, BitmapCacheOption.None);
    
             if (!original.CodecInfo.FileExtensions.Contains("jpg"))
             {
                 Console.WriteLine("The file you passed in is not a JPEG.");
                 return;
             }
    
             JpegBitmapEncoder output = new JpegBitmapEncoder();
    
             BitmapFrame frame = original.Frames[0];
    
             BitmapMetadata metadata = frame.Metadata.Clone() as BitmapMetadata;
    
             ushort u = 6;
    
             object i1 = metadata.GetQuery("/app1/ifd/{ushort=274}"); // DEBUG - this is what it was before - 1
    
             metadata.SetQuery("/app1/ifd/{ushort=274}", u);
    
             object i2 = metadata.GetQuery("/app1/ifd/{ushort=274}"); // DEBUG - this is what it was after - 6
    
             output.Frames.Add(BitmapFrame.Create(original.Frames[0], original.Frames[0].Thumbnail, metadata, original.Frames[0].ColorContexts));
    
    
             using (Stream outputFile = File.Open("Test2.JPG", FileMode.Create, FileAccess.ReadWrite))
             {
                 output.Save(outputFile);
             }
         }
     }
    
  4. 尝试改用 C++,并使用 GDI+ 的一些替代技术。这会将 Orientation 标签设置为 OK,但会减小尺寸,即重新编码图片。

     // ConsoleApplication4.cpp : Defines the entry point for the console application.
     //
    
     #include "stdafx.h"
    
     #include <windows.h>
     #include <gdiplus.h>
     #include <stdio.h>
     using namespace Gdiplus;
    
     /*
     This rotates the file and saves under a different name, but the file size has been shrunk by 18 KB from 3446 KB to 3428 KB
     */
    
    
     int GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
     {
         UINT  num = 0;          // number of image encoders
         UINT  size = 0;         // size of the image encoder array in bytes
    
         ImageCodecInfo* pImageCodecInfo = NULL;
    
         GetImageEncodersSize(&num, &size);
         if (size == 0)
             return -1;  // Failure
    
         pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
         if (pImageCodecInfo == NULL)
             return -1;  // Failure
    
         GetImageEncoders(num, size, pImageCodecInfo);
    
         for (UINT j = 0; j < num; ++j)
         {
             if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0)
             {
                 *pClsid = pImageCodecInfo[j].Clsid;
                 free(pImageCodecInfo);
                 return j;  // Success
             }
         }
    
         free(pImageCodecInfo);
         return -1;  // Failure
     }
    
     int RotateImage()
     {
         // Initialize <tla rid="tla_gdiplus"/>.
         GdiplusStartupInput gdiplusStartupInput;
         ULONG_PTR gdiplusToken;
         GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
         Status stat;
         CLSID  clsid;
         unsigned short v;
    
         Bitmap* bitmap = new Bitmap(L"Test.JPG");
         PropertyItem* propertyItem = new PropertyItem;
    
         // Get the CLSID of the JPEG encoder.
         GetEncoderClsid(L"image/jpeg", &clsid);
         propertyItem->id = PropertyTagOrientation;
         propertyItem->length = 2;  // string length including NULL terminator
         propertyItem->type = PropertyTagTypeShort;
    
         v = 6; // Rotate 90 degrees clockwise
         propertyItem->value = &v;
         bitmap->SetPropertyItem(propertyItem);
         stat = bitmap->Save(L"Test2.JPG", &clsid, NULL);
    
         if (stat != Ok) printf("Error saving.\n");
    
         delete propertyItem;
         delete bitmap;
         GdiplusShutdown(gdiplusToken);
         return 0;
     }
    
     int main()
     {
         RotateImage();
    
         return 0;
     }
    
  5. 这是一个弥天大谎而且水平相当低。这会将 Orientation 标签设置为 OK,但会减小尺寸,即重新编码图片。

     // ConsoleApplication5.cpp : Defines the entry point for the console application.
     //
    
     #include "stdafx.h"
    
     #include <Windows.h>
     #include <wincodecsdk.h>
    
     /*
     This rotates the file and saves under a different name, but the file size has been shrunk by 18 KB from 3446 KB to 3428 KB
     */
    
     int RotateImage()
     {
         // Initialize COM.
         HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    
         IWICImagingFactory *piFactory = NULL;
         IWICBitmapDecoder *piDecoder = NULL;
    
         // Create the COM imaging factory.
         if (SUCCEEDED(hr))
         {
             hr = CoCreateInstance(CLSID_WICImagingFactory,
                 NULL, CLSCTX_INPROC_SERVER,
                 IID_PPV_ARGS(&piFactory));
         }
    
         // Create the decoder.
         if (SUCCEEDED(hr))
         {
             hr = piFactory->CreateDecoderFromFilename(L"Test.JPG", NULL, GENERIC_READ,
                 WICDecodeMetadataCacheOnDemand, //For JPEG lossless decoding/encoding.
                 &piDecoder);
         }
    
         // Variables used for encoding.
         IWICStream *piFileStream = NULL;
         IWICBitmapEncoder *piEncoder = NULL;
         IWICMetadataBlockWriter *piBlockWriter = NULL;
         IWICMetadataBlockReader *piBlockReader = NULL;
    
         WICPixelFormatGUID pixelFormat = { 0 };
         UINT count = 0;
         double dpiX, dpiY = 0.0;
         UINT width, height = 0;
    
         // Create a file stream.
         if (SUCCEEDED(hr))
         {
             hr = piFactory->CreateStream(&piFileStream);
         }
    
         // Initialize our new file stream.
         if (SUCCEEDED(hr))
         {
             hr = piFileStream->InitializeFromFilename(L"Test2.jpg", GENERIC_WRITE);
         }
    
         // Create the encoder.
         if (SUCCEEDED(hr))
         {
             hr = piFactory->CreateEncoder(GUID_ContainerFormatJpeg, NULL, &piEncoder);
         }
         // Initialize the encoder
         if (SUCCEEDED(hr))
         {
             hr = piEncoder->Initialize(piFileStream, WICBitmapEncoderNoCache);
         }
    
         if (SUCCEEDED(hr))
         {
             hr = piDecoder->GetFrameCount(&count);
         }
    
         if (SUCCEEDED(hr))
         {
             // Process each frame of the image.
             for (UINT i = 0; i < count &&SUCCEEDED(hr); i++)
             {
                 // Frame variables.
                 IWICBitmapFrameDecode *piFrameDecode = NULL;
                 IWICBitmapFrameEncode *piFrameEncode = NULL;
                 IWICMetadataQueryReader *piFrameQReader = NULL;
                 IWICMetadataQueryWriter *piFrameQWriter = NULL;
    
                 // Get and create the image frame.
                 if (SUCCEEDED(hr))
                 {
                     hr = piDecoder->GetFrame(i, &piFrameDecode);
                 }
                 if (SUCCEEDED(hr))
                 {
                     hr = piEncoder->CreateNewFrame(&piFrameEncode, NULL);
                 }
    
                 // Initialize the encoder.
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameEncode->Initialize(NULL);
                 }
                 // Get and set the size.
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameDecode->GetSize(&width, &height);
                 }
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameEncode->SetSize(width, height);
                 }
                 // Get and set the resolution.
                 if (SUCCEEDED(hr))
                 {
                     piFrameDecode->GetResolution(&dpiX, &dpiY);
                 }
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameEncode->SetResolution(dpiX, dpiY);
                 }
                 // Set the pixel format.
                 if (SUCCEEDED(hr))
                 {
                     piFrameDecode->GetPixelFormat(&pixelFormat);
                 }
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameEncode->SetPixelFormat(&pixelFormat);
                 }
    
                 // Check that the destination format and source formats are the same.
                 bool formatsEqual = FALSE;
                 if (SUCCEEDED(hr))
                 {
                     GUID srcFormat;
                     GUID destFormat;
    
                     hr = piDecoder->GetContainerFormat(&srcFormat);
                     if (SUCCEEDED(hr))
                     {
                         hr = piEncoder->GetContainerFormat(&destFormat);
                     }
                     if (SUCCEEDED(hr))
                     {
                         if (srcFormat == destFormat)
                             formatsEqual = true;
                         else
                             formatsEqual = false;
                     }
                 }
    
                 if (SUCCEEDED(hr) && formatsEqual)
                 {
                     // Copy metadata using metadata block reader/writer.
                     if (SUCCEEDED(hr))
                     {
                         piFrameDecode->QueryInterface(IID_PPV_ARGS(&piBlockReader));
                     }
                     if (SUCCEEDED(hr))
                     {
                         piFrameEncode->QueryInterface(IID_PPV_ARGS(&piBlockWriter));
                     }
                     if (SUCCEEDED(hr))
                     {
                         piBlockWriter->InitializeFromBlockReader(piBlockReader);
                     }
                 }
    
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameEncode->GetMetadataQueryWriter(&piFrameQWriter);
                 }
    
                 if (SUCCEEDED(hr))
                 {
                     // Set Orientation.
                     PROPVARIANT    value;
                     value.vt = VT_UI2;
                     value.uiVal = 6; // Rotate 90 degrees clockwise
                     hr = piFrameQWriter->SetMetadataByName(L"/app1/ifd/{ushort=274}", &value);
                 }
    
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameEncode->WriteSource(
                         static_cast<IWICBitmapSource*> (piFrameDecode),
                         NULL); // Using NULL enables JPEG loss-less encoding.
                 }
    
                 // Commit the frame.
                 if (SUCCEEDED(hr))
                 {
                     hr = piFrameEncode->Commit();
                 }
    
                 if (piFrameDecode)
                 {
                     piFrameDecode->Release();
                 }
    
                 if (piFrameEncode)
                 {
                     piFrameEncode->Release();
                 }
    
                 if (piFrameQReader)
                 {
                     piFrameQReader->Release();
                 }
    
                 if (piFrameQWriter)
                 {
                     piFrameQWriter->Release();
                 }
             }
         }
    
         if (SUCCEEDED(hr))
         {
             piEncoder->Commit();
         }
    
         if (SUCCEEDED(hr))
         {
             piFileStream->Commit(STGC_DEFAULT);
         }
    
         if (piFileStream)
         {
             piFileStream->Release();
         }
         if (piEncoder)
         {
             piEncoder->Release();
         }
         if (piBlockWriter)
         {
             piBlockWriter->Release();
         }
         if (piBlockReader)
         {
             piBlockReader->Release();
         }
         return 0;
     }
    
     int main()
     {
         RotateImage();
    
         return 0;
     }
    

同样,各种网站上有很多相似但不够接近的帖子,我尝试应用他们的建议但没有成功。如果这确实在其他地方得到了回答,请接受我的道歉。

我知道我可以接受对文件的轻微更改,并且一旦它被更改一次它似乎就不会再次更改 - 如果我将文件旋转 90 度 5 次然后它会生成相同的二进制文件好像我只旋转了一次,但我根本看不出它为什么会改变,如果我只想修改方向标签,我知道这是可能的,因为 Windows 资源管理器可以做到!

最佳答案

以编程方式执行此操作的方法是读取应该在 SOS 市场之后出现的 APP1 标记。获取标记结构的 JPEG 文档。

有了 APP1 标记后,您需要根据需要更改方向。

然后将 SOS 标记、修改后的 APP1 标记和 APP1 标记后的 JPEG 流的其余部分写入新文件。

这就是他们的全部。唯一复杂的是浏览 EXIF 文档以进行方向设置。

关于c# - 如何使用 Winforms C# 或 C++ .Net 修改 jpg 文件中的方向 exif 标记而不更改文件中的任何其他内容,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51609263/

相关文章:

R - 获取颜色值

jpeg - 如何在以特定品质因数保存 Jpeg 文件之前估计其大小?

c# - Monotouch UITextField 只有数字

c# - 如何读取一系列单元格(例如 A1 :G30) from Excel file to GridView

c# - 在主窗口和子窗口之间共享对象作为来回传递数据的方式 (C#)

c++ - 来自指向派生类的指针的 `static_cast<Base*>(static_cast<void*>(derived))` 何时有效?

c++ - 三字母仍然有效 C++ 吗?

javascript - 在 web worker 中解码 JPEG

c# - 如何在接收前不知道数据包大小的情况下通过 TCP 套接字接收数据包?

c++ - Qt中如何在窗体之间传递数据序列