如何使用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 EXIF标记来旋转图像,而无需更改文件中的任何其他内容。我知道这是可能的,因为如果你在Windows资源管理器中右键单击文件并选择"向左/向右旋转",那么就会发生这种情况——一个字节被修改以匹配新的方向值。我特别不想修改图片本身。
然而,我所尝试的一切要么没有效果,要么对文件进行了重大更改(例如,可能通过重新编码将其减少了14k字节(。我在几个网站上读过很多帖子,似乎没有人对我的具体问题有答案——他们大多谈论添加额外的标签,以及添加填充的必要性,但如果我只是试图修改一个现有的字节,我肯定不需要添加填充(尤其是我知道Windows资源管理器可以做到这一点(。
我使用的是一个C#Windows窗体应用程序,在Windows 10 Pro下运行Framework 4.5.2。还试着用C++来做。感谢所有的贡献者,我以他们为榜样。
以下是5个基本控制台应用程序示例:
-
使用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) { }
-
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标签,文件显示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资源管理器可以做到!
用程序实现这一点的方法是读取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;
}
- 在其他文件中创建类时在 c++ 项目中不起作用
- 我应该将除 .cpp 以外的其他文件添加到 git 中吗?
- 如何在其他文件中使用函数
- C++ - 忽略并从其他文件获取数据
- CMake:在其他目录中找不到我的头文件
- 无法使用 CMake 从其他文件夹添加源文件
- 如何使用 C/C++ 和 system() 系统调用以外的其他方法在 Linux 中获取文件功能?
- 从文件中查找单词并替换到其他文件中
- 在命名空间中声明变量,在 main 中定义它,使其对所有其他文件可见
- 没有头文件如何使用c ++调用其他模块中的函数?
- 访问从 CPP 文件到其他头文件的静态变量
- 不能在其他文件中包含结构
- 无法打开包含文件'Graphics.hpp'没有这样的文件或目录,Visual Studio的其他包含不起作用
- 如何在 Linux 下使用 c++ 知道文件是否被其他进程使用?
- 使用函数打开文件,然后让其他函数利用该文件?
- 如何使用Winforms C#或C++.Net在不更改文件中其他内容的情况下修改jpg文件中的Orientation e
- 如何调试visual studio 2017生成的C++代码.android中的SO文件和其他第三方库
- 如何使用QXmlStreamReader解析包含对其他XML文件的引用的XML文件
- 如何在不受其他文件影响的情况下"by itself" Visual Studio 项目中运行C++文件?
- 如何将 cpp 文件中的静态函数公开给其他文件