如何使用Winforms C#或C++.Net在不更改文件中其他内容的情况下修改jpg文件中的Orientation e

How to modify the Orientation exif tag in a jpg file without changing anything else in the file, using Winforms C# or C++ .Net

本文关键字:文件 其他 情况下 Orientation jpg 修改 Winforms 何使用 C++ Net      更新时间:2023-10-16

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

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

我使用的是一个C#Windows窗体应用程序,在Windows 10 Pro下运行Framework 4.5.2。还试着用C++来做。感谢所有的贡献者,我以他们为榜样。

以下是5个基本控制台应用程序示例:

  1. 使用System.Drawing.Image类的Basic 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. 上面的一个演变,它明确地写出了文件。这设置了Orientation标签,文件显示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资源管理器可以做到!

用程序实现这一点的方法是读取SOS市场之后的APP1标记。获取标记结构的JPEG文档。

一旦你有了APP1标记,你需要改变你想要的方向。

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

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

除非jpeg的宽度和高度都是16的倍数,否则无法执行此操作。如果这个操作是在GDI+中完成的,并且宽度和高度不是16的倍数,那么GDI+将尽最大努力保持压缩质量不变。在.net 中也是如此

另请参阅
在不丢失信息的情况下转换JPEG图像

注意,你的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 possiblen");
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 %sn", (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;
}