我正在编写一个程序来帮助我整理多年来拍摄的数千张数码照片。我想要的一个功能是能够通过修改 Orientation EXIF 标签来旋转图像,而无需更改文件中的任何其他内容。我知道这是可能的,因为如果您在 Windows 资源管理器中右键单击该文件并选择向左/向右旋转,那么就会发生这种情况 - 一个字节被修改以匹配新的方向值。我特别不想修改图片本身。
然而,我尝试过的所有方法要么没有效果,要么显着改变了文件(例如,将其减少 14k 字节,大概是通过重新编码)。我在几个网站上阅读了很多帖子,似乎没有人对我的具体问题有答案——他们大多谈论添加额外的标签,以及添加填充的需要,但如果我只是试图修改一个现有字节(尤其是我知道 Windows 资源管理器可以做到这一点)。
我在 Windows 10 专业版下使用运行 Framework 4.5.2 的 C# Windows 窗体应用程序。还尝试从 C++ 中进行。感谢所有贡献者,我以他们的例子为基础。
这里有 5 个简单的控制台应用程序示例:
使用 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) { }
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) { }
上述内容的演变,明确写出文件。这会设置 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); } } }
尝试改用 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; }
这是一个弥天大谎而且水平相当低。这会将 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/