如何使用Winforms C#或C ++ .Net

时间:2018-07-31 08:51:27

标签: c# c++ jpeg orientation exif

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

但是,我尝试过的所有操作都没有效果,或者对文件进行了重大更改(例如,将文件减少了14k字节,大概是通过重新编码)。我已经在几个网站上阅读了很多帖子,似乎没有人对我的特定问题有任何答案-大多数情况下,他们都在谈论添加额外的标签以及添加填充的必要性,但是,如果我只是试图修改一个现有的字节(特别是因为我知道Windows资源管理器可以做到)。

我正在使用Windows 10 Pro下运行Framework 4.5.2的C#Windows Forms应用程序。还尝试从C ++做到这一点。感谢所有我以其为榜样的贡献者。

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

  1. 使用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)
        {
        }
    
  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. 上述内容的演变,用于显式写出文件。这样可以设置“方向”标签,文件会显示“确定”,但会减小尺寸,即重新编码图片。

    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 +尝试一些替代技术。这样可以将“方向”标签设置为“正常”,但可以减小尺寸,即重新编码图片。

    // 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. 这是一个骗子,相当低级。这样可以将“方向”标签设置为“正常”,但可以减小尺寸,即重新编码图片。

    // 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资源管理器可以做到!

谢谢。

蒂姆

2 个答案:

答案 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