【发布时间】:2019-01-07 14:34:09
【问题描述】:
我正在编写一个程序来帮助我整理多年来拍摄的数千张数码照片。我想要的一个功能是能够通过修改 Orientation EXIF 标签来旋转图像,而无需更改文件中的任何其他内容。我知道这是可能的,因为如果您在 Windows 资源管理器中右键单击该文件并选择向左/向右旋转,那么就会发生这种情况 - 修改一个字节以匹配新的方向值。我特别不想修改图片本身。
但是,我尝试过的所有操作要么没有效果,要么显着改变了文件(例如,减少了 14k 字节,大概是通过重新编码)。我在几个网站上阅读了很多帖子,但似乎没有人对我的具体问题有答案——他们大多谈论添加额外的标签,以及添加填充的需要,但如果我只是,我肯定不需要添加填充试图修改一个现有字节(尤其是我知道 Windows Explorer 可以做到这一点)。
我正在使用在 Windows 10 Pro 下运行 Framework 4.5.2 的 C# Windows 窗体应用程序。还尝试从 C++ 中进行操作。感谢所有贡献者,我以他们的例子为基础。
这里有 5 个简单的控制台应用示例:
-
使用 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) { } -
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) { } -
上述的演变,它明确地写出文件。这会设置方向标签,文件显示 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); } } } -
尝试改用 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; } -
这是一个巨大且相当低级的东西。这会将方向标记设置为 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 tour 和read about how to ask good questions。最后请学习如何创建Minimal, Complete, and Verifiable Example。
-
另外,请阅读this question checklist 和idownvotedbecau.se 的所有内容,了解您的问题可能被否决的一些原因。最后请learn how to debug your programs.
-
我建议不要混淆“更改文件大小”和“重新编码”。也可以通过编写较小版本的 exif 或其他元数据来减小文件大小。建议对图像数据进行像素精确比较。
-
@Some:这是一个写得很好的问题,经过很好的研究。您的任何观点都不适用,至少暗示调试器..!
-
@TaW:是的,我怀疑你是对的,实际上并没有重新编码图像。所以我写了一个快速的程序来从最后比较文件,实际上最后 99% 左右是相同的。所以也许它只是删除一些填充或什么?这意味着我可以忍受它,因为它不会降低实际的图片质量,但我仍然很想知道为什么就地编辑类不起作用(上面的第 2 点)。哦,感谢您的信任投票 - 我花了几个小时将我的尝试提炼成简单的例子,很高兴知道它被注意到了:)
标签: c# c++ jpeg orientation exif