我正在编写一个程序,以帮助整理多年来我拍摄的数千张数码照片。我想要的一个功能是能够通过修改Orientation EXIF标签来旋转图像,而无需更改文件中的任何其他内容。我知道这是可能的,因为如果您在Windows资源管理器中右键单击该文件,然后选择“向左/向右旋转”,就会发生这种情况-修改了一个字节以匹配新的方向值。我特别不想修改图片本身。
但是,我尝试过的所有操作都没有效果,或者对文件进行了重大更改(例如,将文件减少了14k字节,大概是通过重新编码)。我已经在几个网站上阅读了很多帖子,似乎没有人对我的特定问题有任何答案-大多数情况下,他们都在谈论添加额外的标签以及添加填充的必要性,但是,如果我只是试图修改一个现有的字节(特别是因为我知道Windows资源管理器可以做到)。
我正在使用Windows 10 Pro下运行Framework 4.5.2的C#Windows Forms应用程序。还尝试从C ++做到这一点。感谢所有我以其为榜样的贡献者。
以下是5个基本控制台应用程序示例:
使用System.Drawing.Image类的基本C#。这样可以将“方向”标签设置为“正常”,但可以减小尺寸,即重新编码图片。
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)
{
}
上述内容的演变,用于显式写出文件。这样可以设置“方向”标签,文件会显示“确定”,但会减小尺寸,即重新编码图片。
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 +尝试一些替代技术。这样可以将“方向”标签设置为“正常”,但可以减小尺寸,即重新编码图片。
// 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;
}
这是一个骗子,相当低级。这样可以将“方向”标签设置为“正常”,但可以减小尺寸,即重新编码图片。
// 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资源管理器可以做到!
谢谢。
蒂姆
答案 0 :(得分:0)
以编程方式执行此操作的方法是读取应该在SOS市场之后出现的APP1标记。获取标记结构的JPEG文档。
有了APP1标记后,您需要根据需要更改方向。
然后将SOS标记,修改后的APP1标记以及APP1标记之后的其余JPEG流写入新文件。
这就是他们的全部。唯一的复杂性是浏览EXIF文档以进行方向设置。
答案 1 :(得分:0)
除非jpeg的宽度和高度均为16的倍数,否则无法执行此操作。如果此操作在GDI +中完成,并且宽度和高度不是16的倍数,则GDI +将尽最大努力保持压缩质量。相同。在.net中相同
另请参阅
Transforming a JPEG Image Without Loss of Information
请注意,您的GDI +代码只会旋转缩略图。要旋转图像,请使用以下代码:
php.ini