【问题标题】:How to modify the Orientation exif tag in a jpg file without changing anything else in the file, using Winforms C# or C++ .Net如何使用 Winforms C# 或 C++ .Net 修改 jpg 文件中的方向 exif 标签而不更改文件中的任何其他内容
【发布时间】:2019-01-07 14:34:09
【问题描述】:

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

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

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

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

  1. 使用 System.Drawing.Image 类的基本 C#。这会将方向标记设置为 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. 上述的演变,它明确地写出文件。这会设置方向标签,文件显示 OK,但会减小大小,即重新编码图片。

     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+ 的一些替代技术。这会将方向标记设置为 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. 这是一个巨大且相当低级的东西。这会将方向标记设置为 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 资源管理器可以做到!

【问题讨论】:

  • 欢迎来到 stackoverflow.com。请花点时间阅读the help pages,尤其是名为"What topics can I ask about here?""What types of questions should I avoid asking?" 的部分。也请take the tourread about how to ask good questions。最后请学习如何创建Minimal, Complete, and Verifiable Example
  • 另外,请阅读this question checklistidownvotedbecau.se 的所有内容,了解您的问题可能被否决的一些原因。最后请learn how to debug your programs.
  • 我建议不要混淆“更改文件大小”和“重新编码”。也可以通过编写较小版本的 exif 或其他元数据来减小文件大小。建议对图像数据进行像素精确比较。
  • @Some:这是一个写得很好的问题,经过很好的研究。您的任何观点都不适用,至少暗示调试器..!
  • @TaW:是的,我怀疑你是对的,实际上并没有重新编码图像。所以我写了一个快速的程序来从最后比较文件,实际上最后 99% 左右是相同的。所以也许它只是删除一些填充或什么?这意味着我可以忍受它,因为它不会降低实际的图片质量,但我仍然很想知道为什么就地编辑类不起作用(上面的第 2 点)。哦,感谢您的信任投票 - 我花了几个小时将我的尝试提炼成简单的例子,很高兴知道它被注意到了:)

标签: c# c++ jpeg orientation exif


【解决方案1】:

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

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

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

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

【讨论】:

  • 感谢您的建议:)
【解决方案2】:

除非jpeg的宽度和高度都是16的倍数,否则无法做到这一点。如果在GDI+中进行此操作,并且宽度和高度不是16的倍数,GDI+会尽力保持压缩质量相同的。在.net中也是一样的

另请参阅
Transforming a JPEG Image Without Loss of Information

请注意,您的 GDI+ 代码只会旋转缩略图。要旋转图像,请使用以下代码:

void RotateImage()
{
    //new/delete operator is not necessary, unless 
    //Gdiplus startup/shutdown is in the same scope
    Gdiplus::Image image(L"source.jpg");
    if((image.GetWidth() % 16) != 0 || (image.GetHeight() % 16) != 0)
        wprintf(L"Lossless compression is not possible\n");

    Gdiplus::EncoderParameters encoder_params;
    encoder_params.Count = 1;
    encoder_params.Parameter[0].Guid = Gdiplus::EncoderTransformation;
    encoder_params.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
    encoder_params.Parameter[0].NumberOfValues = 1;

    //rotate
    ULONG transformation = Gdiplus::EncoderValueTransformRotate90;
    encoder_params.Parameter[0].Value = &transformation;

    CLSID clsid;
    GetEncoderClsid(L"image/jpeg", &clsid);
    auto stat = image.Save(L"destination.jpg", &clsid, &encoder_params);

    wprintf(L"Save %s\n", (stat == Gdiplus::Ok) ? L"succeeded" : L"failed");
}

int main()
{
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    RotateImage();
    Gdiplus::GdiplusShutdown(gdiplusToken);
    return 0;
}

【讨论】:

  • 谢谢 - 这是我需要考虑的事情
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-29
  • 2018-07-19
  • 2023-03-05
  • 2021-06-23
  • 2014-03-24
  • 2013-12-28
相关资源
最近更新 更多