I need to add a metadata tag (description) to uploaded images.
I have found out this answer: https://stackoverflow.com/a/1764913/6776 which works great for JPG files, but not for PNG.
private string Tag = "test meta data";
private static Stream TagImage(Stream input, string type)
{
bool isJpg = type.EndsWith("jpg", StringComparison.InvariantCultureIgnoreCase) || type.EndsWith("jpeg", StringComparison.InvariantCultureIgnoreCase);
bool isPng = type.EndsWith("png", StringComparison.InvariantCultureIgnoreCase);
BitmapDecoder decoder = null;
if (isJpg)
{
decoder = new JpegBitmapDecoder(input, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
}
else if (isPng)
{
decoder = new PngBitmapDecoder(input, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
}
else
{
return input;
}
// modify the metadata
BitmapFrame bitmapFrame = decoder.Frames[0];
BitmapMetadata metaData = (BitmapMetadata)bitmapFrame.Metadata.Clone();
metaData.Subject = Tag;
metaData.Comment = Tag;
metaData.Title = Tag;
// get an encoder to create a new jpg file with the new metadata.
BitmapEncoder encoder = null;
if (isJpg)
{
encoder = new JpegBitmapEncoder();
}
else if (isPng)
{
encoder = new PngBitmapEncoder();
}
encoder.Frames.Add(BitmapFrame.Create(bitmapFrame, bitmapFrame.Thumbnail, metaData, bitmapFrame.ColorContexts));
// Save the new image
Stream output = new MemoryStream();
encoder.Save(output);
output.Seek(0, SeekOrigin.Begin);
return output;
}
It works great when I upload a jpg, but with a png, at the metaData.Subject = Tag line, it throws a System.NotSupportedException (this codec does not support the specified property).
Update
It seems I have to use a different method based on the image format:
if (isJpg)
{
metaData.SetQuery("/app1/ifd/exif:{uint=270}", Tag);
}
else
{
metaData.SetQuery("/tEXt/{str=Description}", Tag);
}
Based on the available formats' queries the first should work for both formats. The second doesn't really work either (it creates the metadata in the image but does not save its value).
If I try to use the first method (/app1/ifd/exif) for PNG, at the encoder.Save line I get a not supported exception, "no imaging component suitable".
I solved it using pngcs library (you need to rename the downloaded dll to "pngcs.dll")
Here is how I implemented it:
using Hjg.Pngcs; // https://code.google.com/p/pngcs/
using Hjg.Pngcs.Chunks;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MarkerGenerator.Utils
{
class PngUtils
{
public string getMetadata(string file, string key)
{
PngReader pngr = FileHelper.CreatePngReader(file);
//pngr.MaxTotalBytesRead = 1024 * 1024 * 1024L * 3; // 3Gb!
//pngr.ReadSkippingAllRows();
string data = pngr.GetMetadata().GetTxtForKey(key);
pngr.End();
return data; ;
}
public static void addMetadata(String origFilename, Dictionary<string, string> data)
{
String destFilename = "tmp.png";
PngReader pngr = FileHelper.CreatePngReader(origFilename); // or you can use the constructor
PngWriter pngw = FileHelper.CreatePngWriter(destFilename, pngr.ImgInfo, true); // idem
//Console.WriteLine(pngr.ToString()); // just information
int chunkBehav = ChunkCopyBehaviour.COPY_ALL_SAFE; // tell to copy all 'safe' chunks
pngw.CopyChunksFirst(pngr, chunkBehav); // copy some metadata from reader
foreach (string key in data.Keys)
{
PngChunk chunk = pngw.GetMetadata().SetText(key, data[key]);
chunk.Priority = true;
}
int channels = pngr.ImgInfo.Channels;
if (channels < 3)
throw new Exception("This example works only with RGB/RGBA images");
for (int row = 0; row < pngr.ImgInfo.Rows; row++)
{
ImageLine l1 = pngr.ReadRowInt(row); // format: RGBRGB... or RGBARGBA...
pngw.WriteRow(l1, row);
}
pngw.CopyChunksLast(pngr, chunkBehav); // metadata after the image pixels? can happen
pngw.End(); // dont forget this
pngr.End();
File.Delete(origFilename);
File.Move(destFilename, origFilename);
}
public static void addMetadata(String origFilename,string key,string value)
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add(key, value);
addMetadata(origFilename, data);
}
}
}
The library CompactExifLib can write EXIF tags in JPEG, TIFF and PNG files:
https://www.codeproject.com/Articles/5251929/CompactExifLib-Access-to-EXIF-Tags-in-JPEG-TIFF-an
It is written purely in C# and can be used for free.
Note: I am the author of this library.
The PNG-format does not support metadata :(
But XMP does, which might be of help when converting between JPEGs, with EXIF-metadata, and PNG.
Related
I'm working with Xamarin in the part of Xamarin.Forms i need to convert a file ("image.png") to a Bitmap because when project run its enter in "break mode" and show me this message "Java.Lang.IllegalArgumentException: 'Failed to decode image. The provided image must be a Bitmap.'". So i tried to convert the file in many ways like:
1_ Using methods from System.Drawing.Bitmap but it's show this exception "This operation is not supported in this platform"
2_ Cannot use Android.Graphics because i'm working in xamarin.forms not in xamarin.android.
3_ I tried to convert the "image.png" into a base64 or byte[] array and then into a bitmap but the problem is the same like in first problem because to convert from byte[] array or base64 to a Bitmap i have use methods from System.Drawing.Bitmap.
4_ I tried using the library SkiaSharp but i don't have success because i don't found so much information about how to convert .png to SKBitmap and then convert SKBitmap to Bitmap (even i don't know if this is possible).
5_ Finally i converted "image.png" to a "image.bmp" with an app and use the file .bmp in my project but it doesn't work too.
the "image.png" i have to save in a string variable, well that's the idea.
If you have a solution with SkiaSharp o whatever i will glad
EDIT
here is a part of my code, i just save in a variable the image
Icon = "pin_blue.png";
//i can't use a path because in xamarin you have many size from the same
//image for the different size of the screen
EDIT 2 This is my method to show the pins in google maps
private void ShowPins(List<PointsOfInterest> resultPointsOfInterest)
{
if (resultPointsOfInterest != null && resultPointsOfInterest.Any())
{
var location = Geolocation.GetLastKnownLocationAsync();
PointsOfInterest position = new PointsOfInterest();
if (location != null)
{
position.ccsm0166latitud = location.Result.Latitude;
position.ccsm0166longitud = location.Result.Longitude;
}
else {
position = resultPointsOfInterest.First();
}
//Distance = Distance.FromKilometers(new Random().Next(23,35));
Distance = Distance.FromKilometers(3);
Position = new Position(position.ccsm0166latitud, position.ccsm0166longitud);
PinsFiltered= Pins = new List<PinCustom>(resultPointsOfInterest.Select(
x => new PinCustom()
{
Position =
new Position(x.ccsm0166latitud, x.ccsm0166longitud),
Address = x.ccsm0166direccion,
Label = x.ccsm0166nombre,
Type = PinType.Place,
TypePointOfInterest = x.ccsm0166tipositio,
IconBM = Int32.Parse(x.ccsm0166tipositio) == (int)PointOfInterestType.branch ? PinCustom.ConvertToBitmap("pin_blue.png") :
Int32.Parse(x.ccsm0166tipositio) == (int)PointOfInterestType.branch ? "pin_blue.png" :
Int32.Parse(x.ccsm0166tipositio) == (int)PointOfInterestType.branchWithExtendedHours ? "pin_black.png" :
Int32.Parse(x.ccsm0166tipositio) == (int)PointOfInterestType.branchWithExtendedHours2 ? "pin_black.png" :
Int32.Parse(x.ccsm0166tipositio) == (int)PointOfInterestType.branchWithExtendedHours3 ? "pin_black.png" :
Int32.Parse(x.ccsm0166tipositio) == (int)PointOfInterestType.selfServiceTerminal ? "pin_green.png" :
Int32.Parse(x.ccsm0166tipositio) == (int)PointOfInterestType.atmServBox ? "pin_yellow.png" : string.Empty
}).ToList());
}
else
{
Pins = new List<PinCustom>();
}
}
This is the class Pin where i save the image
public class PinCustom : Pin
{
public string TypePointOfInterest { get; set; }
public string Icon { get; set; }
public Bitmap { get; set; }
//Here i create this variable to save a bitmap but i don't know if i do the things well
}
this is the icon .png i want to show in googlemaps
Pin Blue Image
To open your file and convert it to byte array:
string directory = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, Android.OS.Environment.DirectoryDownloads);
// or your image directory, just replace it
var stream = File.OpenWrite(Path.Combine(directory, "image.png"));
byte[] buff = ConvertStreamToByteArray(stream);
stream.Close();
public static byte[] ConvertStreamToByteArray(Stream stream)
{
byte[] byteArray = new byte[16 * 1024];
using (MemoryStream mStream = new MemoryStream())
{
int bit;
while ((bit = stream.Read(byteArray, 0, byteArray.Length)) > 0)
{
mStream.Write(byteArray, 0, bit);
}
return mStream.ToArray();
}
}
then, to pass this byte array to SKBitmap:
SKBitmap bmp = SKBitmap.Decode(buff);
EDIT:
If you want to try without ConvertStreamToByteArray():
byte[] buff = null;
stream.Write(buff, 0, buff.Length);
This Write will depend of the type of your stream. In this example, I'm using FileStream.
EDIT 2:
string resourceID = "image.png";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
resourceBitmap = SKBitmap.Decode(stream);
SKBitmap bmp = SKImage.FromBitmap(resourceBitmap);
}
and then you can use this bitmap to draw and fill your SKCanvas:
canvas.DrawBitmap(bmp, 0, 0);
Use ffmpeg
command for this: ffmpeg -i image.png image.bmp
i have 5 types of pins (pins are the image .png) when i put the pins
in format .png
if you want to custom the pins in your map, you can simply use Custom Renderers to achieve this.
The icon used to represent a marker can be customized by calling the MarkerOptions.SetIcon method. This can be accomplished by overriding the CreateMarker method, which is invoked for each Pin that's added to the map:
protected override MarkerOptions CreateMarker(Pin pin)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
marker.SetIcon(BitmapDescriptorFactory.FromResource(Resource.Drawable.pin));
return marker;
}
If you want to display different icons, you can refer to the following code:
protected override MarkerOptions CreateMarker(Pin pin)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
var customPin = (Element as CustomMap).CustomPins.FirstOrDefault(p => p.Position == pin.Position);
if (customPin != null)
{
if (customPin.IconType == "corporate")
{
marker.SetIcon(BitmapDescriptorFactory.FromResource(Resource.Drawable.corporate));
}
else if (customPin.IconType == "pickup")
{
marker.SetIcon(BitmapDescriptorFactory.FromResource(Resource.Drawable.bus));
}
}
return marker;
}
For more, check:Customizing the Marker.
I have a batch of PDFs that I want to convert to Text. It's easy to get text with something like this from iTextSharp:
PdfTextExtractor.GetTextFromPage(reader, pageNumber);
It's easy to get Images using this answer (or similar answers in the thread).
What I can't figure out easily... is how to interleave image placeholders in the text.
Given a PDF, a page # and GetTextFromPage I expect the output to be:
line 1
line 2
line 3
When I'd like it to be (Where 1.1 means page 1, image 1... Page 1, image 2):
line 1
[1.1]
line 2
[1.2]
line 3
Is there a way to get an "image placeholder" for iTextSharp, PdfSharp or anything similar? I'd like a GetTextAndPlaceHoldersFromPage method (or similar).
PS: Hrm... it's not letting me tag iTextSHARP - not iText. C# not Java.
C# Pdf to Text with image placeholder
https://stackoverflow.com/a/28087521/
https://stackoverflow.com/a/33697745/
Although this doesn't have the exact layout mentioned in my question (Since that was a simplified version of what I really wanted anyways), it does have the starting parts as listed by the second note (translated from iText Java)... with extra information pulled from the third note (Some of the reflection used in Java didn't seem to work in C#, so that info came from #3).
Working from this, I'm able to get a List of Strings representing lines in the PDF (all pages, instead of just page 1)... with text added where images should be (Huzzah!). ByteArrayToFile extension method added for flavor (Although I didn't include other parts/extensions that may break a copy/paste usages of this code).
I've also been able to greatly simplify other parts of my process and gut half of the garbage I had working before. Huzzah!!! Thanks #Mkl
internal class Program
{
public static void Main(string[] args)
{
var dir = Settings.TestDirectory;
var file = Settings.TestFile;
Log.Info($"File to Process: {file.FullName}");
using (var reader = new PdfReader(file.FullName))
{
var parser = new PdfReaderContentParser(reader);
var listener = new SimpleMixedExtractionStrategy(file, dir);
parser.ProcessContent(1, listener);
var x = listener.GetResultantText().Split('\n');
}
}
}
public class SimpleMixedExtractionStrategy : LocationTextExtractionStrategy
{
public static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public DirectoryInfo OutputPath { get; }
public FileInfo OutputFile { get; }
private static readonly LineSegment UNIT_LINE = new LineSegment(new Vector(0, 0, 1), new Vector(1, 0, 1));
private int _counter;
public SimpleMixedExtractionStrategy(FileInfo outputFile, DirectoryInfo outputPath)
{
OutputPath = outputPath;
OutputFile = outputFile;
}
public override void RenderImage(ImageRenderInfo renderInfo)
{
try
{
var image = renderInfo.GetImage();
if (image == null) return;
var number = _counter++;
var imageFile = new FileInfo($"{OutputFile.FullName}-{number}.{image.GetFileType()}");
imageFile.ByteArrayToFile(image.GetImageAsBytes());
var segment = UNIT_LINE.TransformBy(renderInfo.GetImageCTM());
var location = new TextChunk("[" + imageFile + "]", segment.GetStartPoint(), segment.GetEndPoint(), 0f);
var locationalResultField = typeof(LocationTextExtractionStrategy).GetField("locationalResult", BindingFlags.NonPublic | BindingFlags.Instance);
var LocationalResults = (List<TextChunk>)locationalResultField.GetValue(this);
LocationalResults.Add(location);
}
catch (Exception ex)
{
Log.Debug($"{ex.Message}");
Log.Verbose($"{ex.StackTrace}");
}
}
}
public static class ByteArrayExtensions
{
public static bool ByteArrayToFile(this FileInfo fileName, byte[] byteArray)
{
try
{
// Open file for reading
var fileStream = new FileStream(fileName.FullName, FileMode.Create, FileAccess.Write);
// Writes a block of bytes to this stream using data from a byte array.
fileStream.Write(byteArray, 0, byteArray.Length);
// close file stream
fileStream.Close();
return true;
}
catch (Exception exception)
{
// Error
Log.Error($"Exception caught in process: {exception.Message}", exception);
}
// error occured, return false
return false;
}
}
I am trying to fill a word document with data from an XML. I am using openXML to fill the document, which works great and save it as .docx. The thing is I have to open Word and save the document as an .odt and then use the OpenOffice SDK to open the .docx and save it as a pdf. When I don't save the .docx as .odt, the formatting is off.
What I need to be able to do is be able to either convert the .docx to .odt or save it originally as .odt.
Here is what I have right now:
static void Main()
{
string documentText;
XmlDocument xmlDoc = new XmlDocument(); // Create an XML document object
xmlDoc.Load("C:\\Cache\\MMcache.xml"); // Load the XML document from the specified file
XmlNodeList PatientFirst = xmlDoc.GetElementsByTagName("PatientFirst");
XmlNodeList PatientSignatureImg = xmlDoc.GetElementsByTagName("PatientSignatureImg");
byte[] byteArray = File.ReadAllBytes("C:\\Cache\\TransportationRunReporttemplate.docx");
using (MemoryStream stream = new MemoryStream())
{
stream.Write(byteArray, 0, (int)byteArray.Length);
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(stream, true))
{
using (StreamReader reader = new StreamReader(wordDoc.MainDocumentPart.GetStream()))
{
documentText = reader.ReadToEnd();
}
using (StreamWriter writer = new StreamWriter(wordDoc.MainDocumentPart.GetStream(FileMode.Create)))
{
writer.Write(documentText);
}
}
// Save the file with the new name
File.WriteAllBytes("C:\\Cache\\MYFINISHEDTEMPLATE.docx", stream.ToArray());
}
}
private static void AddPicture(Bitmap bitmap)
{
using (WordprocessingDocument doc = WordprocessingDocument.Open("C:\\Cache\\MYFINISHEDTEMPLATE.docx", true))
{
//Bitmap image = new Bitmap("C:\\Cache\\scribus.jpg");
SdtElement controlBlock = doc.MainDocumentPart.Document.Body
.Descendants<SdtElement>()
.Where
(r =>
r.SdtProperties.GetFirstChild<Tag>().Val == "Signature"
).SingleOrDefault();
// Find the Blip element of the content control.
A.Blip blip = controlBlock.Descendants<A.Blip>().FirstOrDefault();
ImagePart imagePart = doc.MainDocumentPart
.AddImagePart(ImagePartType.Jpeg);
using (MemoryStream stream = new MemoryStream())
{
bitmap.Save(stream, ImageFormat.Jpeg);
stream.Position = 0;
imagePart.FeedData(stream);
}
blip.Embed = doc.MainDocumentPart.GetIdOfPart(imagePart);
/* DW.Inline inline = controlBlock
.Descendants<DW.Inline>().FirstOrDefault();
// 9525 = pixels to points
inline.Extent.Cy = image.Size.Height * 9525;
inline.Extent.Cx = image.Size.Width * 9525;
PIC.Picture pic = inline
.Descendants<PIC.Picture>().FirstOrDefault();
pic.ShapeProperties.Transform2D.Extents.Cy
= image.Size.Height * 9525;
pic.ShapeProperties.Transform2D.Extents.Cx
= image.Size.Width * 9525;*/
}
ConvertToPDF(#"C:\Cache\MYFINISHEDTEMPLATE2.docx",#"C:\Cache\OpenPdf.pdf");
}
public static Bitmap Base64StringToBitmap(string base64String)
{
Bitmap bmpReturn = null;
byte[] byteBuffer = Convert.FromBase64String(base64String);
MemoryStream memoryStream = new MemoryStream(byteBuffer);
memoryStream.Position = 0;
bmpReturn = (Bitmap)Bitmap.FromStream(memoryStream);
memoryStream.Close();
memoryStream = null;
byteBuffer = null;
return bmpReturn;
}
public static void ConvertToPDF(string inputFile, string outputFile)
{
if (ConvertExtensionToFilterType(System.IO.Path.GetExtension(inputFile)) == null)
throw new InvalidProgramException("Unknown file type for OpenOffice. File = " + inputFile);
StartOpenOffice();
//Get a ComponentContext
var xLocalContext =
Bootstrap.bootstrap();
//Get MultiServiceFactory
var xRemoteFactory =
(XMultiServiceFactory)
xLocalContext.getServiceManager();
//Get a CompontLoader
var aLoader =
(XComponentLoader)xRemoteFactory.createInstance("com.sun.star.frame.Desktop");
//Load the sourcefile
XComponent xComponent = null;
try
{
xComponent = InitDocument(aLoader,
PathConverter(inputFile), "_blank");
//Wait for loading
while (xComponent == null)
{
Thread.Sleep(1000);
}
// save/export the document
SaveDocument(xComponent, inputFile, PathConverter(outputFile));
}
finally
{
if (xComponent != null) xComponent.dispose();
}
}
private static void StartOpenOffice()
{
var ps = Process.GetProcessesByName("soffice.exe");
if (ps.Length != 0)
throw new InvalidProgramException("OpenOffice not found. Is OpenOffice installed?");
if (ps.Length > 0)
return;
var p = new Process
{
StartInfo =
{
Arguments = "-headless -nofirststartwizard",
FileName = "soffice.exe",
CreateNoWindow = true
}
};
var result = p.Start();
if (result == false)
throw new InvalidProgramException("OpenOffice failed to start.");
}
private static XComponent InitDocument(XComponentLoader aLoader, string file, string target)
{
var openProps = new PropertyValue[1];
openProps[0] = new PropertyValue { Name = "Hidden", Value = new Any(true) };
XComponent xComponent = aLoader.loadComponentFromURL(
file, target, 0,
openProps);
return xComponent;
}
private static void SaveDocument(XComponent xComponent, string sourceFile, string destinationFile)
{
var propertyValues = new PropertyValue[2];
// Setting the flag for overwriting
propertyValues[1] = new PropertyValue { Name = "Overwrite", Value = new Any(true) };
//// Setting the filter name
propertyValues[0] = new PropertyValue
{
Name = "FilterName",
Value = new Any(ConvertExtensionToFilterType(System.IO.Path.GetExtension(sourceFile)))
};
((XStorable)xComponent).storeToURL(destinationFile, propertyValues);
}
private static string PathConverter(string file)
{
if (file == null || file.Length == 0)
throw new NullReferenceException("Null or empty path passed to OpenOffice");
return String.Format("file:///{0}", file.Replace(#"\", "/"));
}
public static string ConvertExtensionToFilterType(string extension)
{
switch (extension)
{
case ".odt":
case ".doc":
case ".docx":
case ".txt":
case ".rtf":
case ".html":
case ".htm":
case ".xml":
case ".wps":
case ".wpd":
return "writer_pdf_Export";
case ".xls":
case ".xlsb":
case ".ods":
return "calc_pdf_Export";
case ".ppt":
case ".pptx":
case ".odp":
return "impress_pdf_Export";
default: return null;
}
}
}
}
Just for information I cannot use anything that uses Interop because word will not be installed the machine. I am using OpenXML and OpenOffice
This is what I would try (details below):
1) try Doc format instead of DocX
2) switch to Libre Office and try DocX again
2) use the odf-converter to get a better DocX -> ODT conversion.
More details...
There's something called the odf-conveter (opensource) that can convert DocX->ODT which gives you (typically) more accurate DocX->ODT than Open Office. Take a look at odf-conveter-integrator by OONinja for pre-packaged versions.
Also, Libre Office has DocX support ahead of OpenOffice so you might get a better result simply by switching to Libre Office.
A further option is to start from Doc format rather than DocX. In the OpenOffice world that translates much better to ODT and then to PDF.
Hope that helps.
You may try to use Docxpresso to generate your .odt directly from HTML + CSS code and avoid any conversion issue.
Docxpresso is free for non-commercial use.
Within the property window of a JPEG image, there is a tab called 'Summary'. Within this tab, there is a field called 'Comments' I would like to write some c# code which will add a given string to this field e.g "This is a photo".
Does some kind soul out there know how to do this?
Many thanks.
Based on other answers I wrote the following class which allows various metadata manipulations. You use it like this:
var jpeg = new JpegMetadataAdapter(pathToJpeg);
jpeg.Metadata.Comment = "Some comments";
jpeg.Metadata.Title = "A title";
jpeg.Save(); // Saves the jpeg in-place
jpeg.SaveAs(someNewPath); // Saves with a new path
The differences between my solution and the others are not large. Principally I have refactored this to be cleaner. I also use the higher level properties of BitmapMetadata, rather than the SetQuery method.
Here is the full code, which is licensed under the MIT licence. You will need to add references to PresentationCore, WindowsBase, and System.Xaml.
public class JpegMetadataAdapter
{
private readonly string path;
private BitmapFrame frame;
public readonly BitmapMetadata Metadata;
public JpegMetadataAdapter(string path)
{
this.path = path;
frame = getBitmapFrame(path);
Metadata = (BitmapMetadata)frame.Metadata.Clone();
}
public void Save()
{
SaveAs(path);
}
public void SaveAs(string path)
{
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(frame, frame.Thumbnail, Metadata, frame.ColorContexts));
using (Stream stream = File.Open(path, FileMode.Create, FileAccess.ReadWrite))
{
encoder.Save(stream);
}
}
private BitmapFrame getBitmapFrame(string path)
{
BitmapDecoder decoder = null;
using (Stream stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
decoder = new JpegBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
}
return decoder.Frames[0];
}
}
The following code solves my problem and adds comments to a given JPEG image:
public void addImageComment(string imageFlePath, string comments)
{
string jpegDirectory = Path.GetDirectoryName(imageFlePath);
string jpegFileName = Path.GetFileNameWithoutExtension(imageFlePath);
BitmapDecoder decoder = null;
BitmapFrame bitmapFrame = null;
BitmapMetadata metadata = null;
FileInfo originalImage = new FileInfo(imageFlePath);
if (File.Exists(imageFlePath))
{
// load the jpg file with a JpegBitmapDecoder
using (Stream jpegStreamIn = File.Open(imageFlePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
decoder = new JpegBitmapDecoder(jpegStreamIn, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
}
bitmapFrame = decoder.Frames[0];
metadata = (BitmapMetadata)bitmapFrame.Metadata;
if (bitmapFrame != null)
{
BitmapMetadata metaData = (BitmapMetadata)bitmapFrame.Metadata.Clone();
if (metaData != null)
{
// modify the metadata
metaData.SetQuery("/app1/ifd/exif:{uint=40092}", comments);
// get an encoder to create a new jpg file with the new metadata.
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapFrame, bitmapFrame.Thumbnail, metaData, bitmapFrame.ColorContexts));
//string jpegNewFileName = Path.Combine(jpegDirectory, "JpegTemp.jpg");
// Delete the original
originalImage.Delete();
// Save the new image
using (Stream jpegStreamOut = File.Open(imageFlePath, FileMode.CreateNew, FileAccess.ReadWrite))
{
encoder.Save(jpegStreamOut);
}
}
}
}
}
This is essentially a lightly modified version of the code found under the link which Konamiman kindly supplied.
Please be aware that to make this work you will need to add .NET references to PresentationCore and WindowsBase. If using Visual Studio 2008, this can be achieved via the following:
Right click on your project in the Solution Explorer
From the drop down list, select Add 'Reference...'
From the new box which opens, select the '.NET' tab
Scroll to the two references mentioned above and on each, click ok
Many thanks to both danbystrom and Konamiman for your help in this matter. I really appreciate the quick response.
The easy part:
Add this property item:
var data = System.Text.Encoding.UTF8.GetBytes( "Some comments" );
PropertyItem pi;
*** create an empty PropertyItem here
pi.Type = 2;
pi.Id = 37510;
pi.Len = data.Length;
pi.Value = data;
To the Image's PropertItems collection.
The somewhat more cumbersome part:
How do you create a new PropertyItem, since it has no public constructor?
The common "trick" is to have an empty image lying around from which you can steal a PropertyItem. sigh
Thanks to the answers here, I've coded a solution to set a comment using memory only:
public static Image SetImageComment(Image image, string comment) {
using (var memStream = new MemoryStream()) {
image.Save(memStream, ImageFormat.Jpeg);
memStream.Position = 0;
var decoder = new JpegBitmapDecoder(memStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
BitmapMetadata metadata;
if (decoder.Metadata == null) {
metadata = new BitmapMetadata("jpg");
} else {
metadata = decoder.Metadata;
}
metadata.Comment = comment;
var bitmapFrame = decoder.Frames[0];
BitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapFrame, bitmapFrame.Thumbnail, metadata, bitmapFrame.ColorContexts));
var imageStream = new MemoryStream();
encoder.Save(imageStream);
imageStream.Position = 0;
image.Dispose();
image = null;
return Image.FromStream(imageStream);
}
}
Don't forget to dispose the image that is returned by this method. (For example after saving the image to a file)
Thanks to the previous tips I was able to put the following together. I've tested it and it seems to work. One of the biggest stumbling blocks was determining the Id needed for the field you want to assign.
string fileName = "c:/SomeImage.jpg";
// Retrieve the Image
System.Drawing.Image originalImage = System.Drawing.Image.FromFile(fileName);
// Get the list of existing PropertyItems. i.e. the metadata
PropertyItem[] properties = originalImage.PropertyItems;
// Create a bitmap image to assign attributes and do whatever else..
Bitmap bmpImage = new Bitmap((Bitmap)originalImage);
// Don't need this anymore
originalImage.Dispose();
// Get / setup a PropertyItem
PropertyItem item = properties[0]; // We have to copy an existing one since no constructor exists
// This will assign "Joe Doe" to the "Authors" metadata field
string sTmp = "Joe DoeX"; // The X will be replaced with a null. String must be null terminated.
var itemData = System.Text.Encoding.UTF8.GetBytes(sTmp);
itemData[itemData.Length - 1] = 0;// Strings must be null terminated or they will run together
item.Type = 2; //String (ASCII)
item.Id = 315; // Author(s), 315 is mapped to the "Authors" field
item.Len = itemData.Length; // Number of items in the byte array
item.Value = itemData; // The byte array
bmpImage.SetPropertyItem(item); // Assign / add to the bitmap
// This will assign "MyApplication" to the "Program Name" field
sTmp = "MyApplicationX";
itemData = System.Text.Encoding.UTF8.GetBytes(sTmp);
itemData[itemData.Length - 1] = 0; // Strings must be null terminated or they will run together
item.Type = 2; //String (ASCII)
item.Id = 305; // Program Name, 305 is mapped to the "Program Name" field
item.Len = itemData.Length;
item.Value = itemData;
bmpImage.SetPropertyItem(item);
// Save the image
bmpImage.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);
//Clean up
bmpImage.Dispose();
A variant on Peter Kistler's solution to set Title, Subject and Comment. I found I had to create items as Unicode byte array (type 1) and the IDs for Title, Subject and Comment are the same as for EXIF XPTitle, XPSubject and XP Comment. sFileOut can be the same as sFile.
public static void SetWindowsTags2(string sFile, string sFileOut, string Title = "", string Subject = "", string Comment = "", bool bShowError = false)
{
try
{
// Retrieve the Image
System.Drawing.Image originalImage = System.Drawing.Image.FromFile(sFile);
// Get the list of existing PropertyItems. i.e. the metadata
PropertyItem[] properties = originalImage.PropertyItems;
/*foreach (PropertyItem propItem in properties)
{
string sTag = System.Text.Encoding.UTF8.GetString(propItem.Value);
string sItem = sTag.Replace("\0", string.Empty);
Debug.Print(propItem.Id.ToString() + ", " + propItem.Type + ", " + sItem);
}*/
// Create a bitmap image to assign attributes and do whatever else..
Bitmap bmpImage = new Bitmap((Bitmap)originalImage);
// Don't need this anymore
originalImage.Dispose();
// Get / setup a PropertyItem
PropertyItem item = properties[0]; // We have to copy an existing one since no constructor exists
var itemData = System.Text.Encoding.Unicode.GetBytes(Title);
itemData[itemData.Length - 1] = 0;// Strings must be null terminated or they will run together
item.Type = 1; //Unicode Byte Array
item.Id = 40091; // Title ID
item.Len = itemData.Length; // Number of items in the byte array
item.Value = itemData; // The byte array
bmpImage.SetPropertyItem(item); // Assign / add to the bitmap
itemData = System.Text.Encoding.Unicode.GetBytes(Subject);
itemData[itemData.Length - 1] = 0; // Strings must be null terminated or they will run together
item.Type = 1; //Unicode Byte Array
item.Id = 40095; // subject
item.Len = itemData.Length;
item.Value = itemData;
bmpImage.SetPropertyItem(item); // Assign / add to the bitmap
itemData = System.Text.Encoding.Unicode.GetBytes(Comment);
itemData[itemData.Length - 1] = 0; // Strings must be null terminated or they will run together
item.Type = 1; ////Unicode Byte Array
item.Id = 40092; // comment
item.Len = itemData.Length;
item.Value = itemData;
bmpImage.SetPropertyItem(item); // Assign / add to the bitmap
// Save the image
bmpImage.Save(sFileOut, System.Drawing.Imaging.ImageFormat.Jpeg);
//Clean up
bmpImage.Dispose();
}
catch (Exception Ex)
{
}
}
I am looping through a directory and copying all files. Right now I am doing string.EndsWith checks for ".jpg" or ".png", etc . .
Is there any more elegant way of determining if a file is an image (any image type) without the hacky check like above?
Check the file for a known header. (Info from link also mentioned in this answer)
The first eight bytes of a PNG file always contain the following (decimal) values: 137 80 78 71 13 10 26 10
Check out System.IO.Path.GetExtension
Here is a quick sample.
public static readonly List<string> ImageExtensions = new List<string> { ".JPG", ".JPEG", ".JPE", ".BMP", ".GIF", ".PNG" };
private void button_Click(object sender, RoutedEventArgs e)
{
var folder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
var files = Directory.GetFiles(folder);
foreach(var f in files)
{
if (ImageExtensions.Contains(Path.GetExtension(f).ToUpperInvariant()))
{
// process image
}
}
}
System.Web.MimeMapping.GetMimeMapping(filename).StartsWith("image/");
MimeMapping.GetMimeMapping produces these results:
file.jpg: image/jpeg
file.gif: image/gif
file.jpeg: image/jpeg
file.png: image/png
file.bmp: image/bmp
file.tiff: image/tiff
file.svg: application/octet-stream
file.svg not returning an image/ MIME type works out in most cases because you're probably not going to process a vector image like you would a scalar image. When checking MIME type, do be aware that SVG does have the standard MIME type of image/svg+xml, even if GetMimeMapping doesn't return it.
This will look at the first few bytes of a file and determine whether it is an image.
using System.Collections.Generic;
using System.IO;
using System.Linq;
public static class Extension
{
public static bool IsImage(this Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
List<string> jpg = new List<string> { "FF", "D8" };
List<string> bmp = new List<string> { "42", "4D" };
List<string> gif = new List<string> { "47", "49", "46" };
List<string> png = new List<string> { "89", "50", "4E", "47", "0D", "0A", "1A", "0A" };
List<List<string>> imgTypes = new List<List<string>> { jpg, bmp, gif, png };
List<string> bytesIterated = new List<string>();
for (int i = 0; i < 8; i++)
{
string bit = stream.ReadByte().ToString("X2");
bytesIterated.Add(bit);
bool isImage = imgTypes.Any(img => !img.Except(bytesIterated).Any());
if (isImage)
{
return true;
}
}
return false;
}
}
Edit
I've made a few changes to the above to allow you to add your own images if you needed to, also removed collections which weren't necessary to begin with. I also added an overload accepting an out parameter of type string, setting the value to the type of image the stream is composed of.
public static class Extension
{
static Extension()
{
ImageTypes = new Dictionary<string, string>();
ImageTypes.Add("FFD8","jpg");
ImageTypes.Add("424D","bmp");
ImageTypes.Add("474946","gif");
ImageTypes.Add("89504E470D0A1A0A","png");
}
/// <summary>
/// <para> Registers a hexadecimal value used for a given image type </para>
/// <param name="imageType"> The type of image, example: "png" </param>
/// <param name="uniqueHeaderAsHex"> The type of image, example: "89504E470D0A1A0A" </param>
/// </summary>
public static void RegisterImageHeaderSignature(string imageType, string uniqueHeaderAsHex)
{
Regex validator = new Regex(#"^[A-F0-9]+$", RegexOptions.CultureInvariant);
uniqueHeaderAsHex = uniqueHeaderAsHex.Replace(" ", "");
if (string.IsNullOrWhiteSpace(imageType)) throw new ArgumentNullException("imageType");
if (string.IsNullOrWhiteSpace(uniqueHeaderAsHex)) throw new ArgumentNullException("uniqueHeaderAsHex");
if (uniqueHeaderAsHex.Length % 2 != 0) throw new ArgumentException ("Hexadecimal value is invalid");
if (!validator.IsMatch(uniqueHeaderAsHex)) throw new ArgumentException ("Hexadecimal value is invalid");
ImageTypes.Add(uniqueHeaderAsHex, imageType);
}
private static Dictionary<string, string> ImageTypes;
public static bool IsImage(this Stream stream)
{
string imageType;
return stream.IsImage(out imageType);
}
public static bool IsImage(this Stream stream, out string imageType)
{
stream.Seek(0, SeekOrigin.Begin);
StringBuilder builder = new StringBuilder();
int largestByteHeader = ImageTypes.Max(img => img.Value.Length);
for (int i = 0; i < largestByteHeader; i++)
{
string bit = stream.ReadByte().ToString("X2");
builder.Append(bit);
string builtHex = builder.ToString();
bool isImage = ImageTypes.Keys.Any(img => img == builtHex);
if (isImage)
{
imageType = ImageTypes[builder.ToString()];
return true;
}
}
imageType = null;
return false;
}
}
If you want a quick way to validate an image file before it is fully read from the file, besides comparing the file extension, you can just check its header looking for file signature (the following code IsValidImageFile() checks for BMP, GIF87a, GIF89a, PNG, TIFF, JPEG)
/// <summary>
/// Reads the header of different image formats
/// </summary>
/// <param name="file">Image file</param>
/// <returns>true if valid file signature (magic number/header marker) is found</returns>
private bool IsValidImageFile(string file)
{
byte[] buffer = new byte[8];
byte[] bufferEnd = new byte[2];
var bmp = new byte[] { 0x42, 0x4D }; // BMP "BM"
var gif87a = new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }; // "GIF87a"
var gif89a = new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }; // "GIF89a"
var png = new byte[] { 0x89, 0x50, 0x4e, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; // PNG "\x89PNG\x0D\0xA\0x1A\0x0A"
var tiffI = new byte[] { 0x49, 0x49, 0x2A, 0x00 }; // TIFF II "II\x2A\x00"
var tiffM = new byte[] { 0x4D, 0x4D, 0x00, 0x2A }; // TIFF MM "MM\x00\x2A"
var jpeg = new byte[] { 0xFF, 0xD8, 0xFF }; // JPEG JFIF (SOI "\xFF\xD8" and half next marker xFF)
var jpegEnd = new byte[] { 0xFF, 0xD9 }; // JPEG EOI "\xFF\xD9"
try
{
using (System.IO.FileStream fs = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
if (fs.Length > buffer.Length)
{
fs.Read(buffer, 0, buffer.Length);
fs.Position = (int)fs.Length - bufferEnd.Length;
fs.Read(bufferEnd, 0, bufferEnd.Length);
}
fs.Close();
}
if (this.ByteArrayStartsWith(buffer, bmp) ||
this.ByteArrayStartsWith(buffer, gif87a) ||
this.ByteArrayStartsWith(buffer, gif89a) ||
this.ByteArrayStartsWith(buffer, png) ||
this.ByteArrayStartsWith(buffer, tiffI) ||
this.ByteArrayStartsWith(buffer, tiffM))
{
return true;
}
if (this.ByteArrayStartsWith(buffer, jpeg))
{
// Offset 0 (Two Bytes): JPEG SOI marker (FFD8 hex)
// Offest 1 (Two Bytes): Application segment (FF?? normally ??=E0)
// Trailer (Last Two Bytes): EOI marker FFD9 hex
if (this.ByteArrayStartsWith(bufferEnd, jpegEnd))
{
return true;
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Lang.Lang.ErrorTitle + " IsValidImageFile()", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return false;
}
/// <summary>
/// Returns a value indicating whether a specified subarray occurs within array
/// </summary>
/// <param name="a">Main array</param>
/// <param name="b">Subarray to seek within main array</param>
/// <returns>true if a array starts with b subarray or if b is empty; otherwise false</returns>
private bool ByteArrayStartsWith(byte[] a, byte[] b)
{
if (a.Length < b.Length)
{
return false;
}
for (int i = 0; i < b.Length; i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
Checking the header signature can be fast, since it does not load the whole file or create large objects, specially when processing several files. But it does not check if the rest of the data is well formed. To do so, a second step to try to load the file to an Image object can be done (and this way be certain that the file can be displayed and handled by your program).
bool IsValidImage(string filename)
{
try
{
using(Image newImage = Image.FromFile(filename))
{}
}
catch (OutOfMemoryException ex)
{
//The file does not have a valid image format.
//-or- GDI+ does not support the pixel format of the file
return false;
}
return true;
}
We can use Image and graphics classes from namespace System.Drawing; to do our job. If the code works without error it is an image, else it is not. That is let the DotNet framework do the job for us.
The code -
public string CheckFile(file)
{
string result="";
try
{
System.Drawing.Image imgInput = System.Drawing.Image.FromFile(file);
System.Drawing.Graphics gInput = System.Drawing.Graphics.fromimage(imgInput);
Imaging.ImageFormat thisFormat = imgInput.RawFormat;
result="It is image";
}
catch(Exception ex)
{
result="It is not image";
}
return result;
}
I use the following method. It uses the built in Image decoder to retrieve a list of extensions that the system recognises as image files, then compares those extensions to the extension of the file name you pass in. Returns a simple TRUE/FALSE.
public static bool IsRecognisedImageFile(string fileName)
{
string targetExtension = System.IO.Path.GetExtension(fileName);
if (String.IsNullOrEmpty(targetExtension))
return false;
else
targetExtension = "*" + targetExtension.ToLowerInvariant();
List<string> recognisedImageExtensions = new List<string>();
foreach (System.Drawing.Imaging.ImageCodecInfo imageCodec in System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders())
recognisedImageExtensions.AddRange(imageCodec.FilenameExtension.ToLowerInvariant().Split(";".ToCharArray()));
foreach (string extension in recognisedImageExtensions)
{
if (extension.Equals(targetExtension))
{
return true;
}
}
return false;
}
See if this helps.
EDIT: Also, Image.FromFile(....).RawFormat might help. It could throw an exception if the file is not an image.
Not exactly the answer you need. But if it’s the Internet then MIME type.
I'm not sure what the performance drawback would be for this solution but couldn't you perform some image function on the file in a try block that would fail and fall to a catch block if it is not an image?
This strategy may not be the best solution in all situations but in the case that I am currently working with it has one major advantage:
You can use whatever function that you plan to use to process the image (if it is an image) for the test function. In that way you can test all current image types but it would also extend to future image types without adding that new image extension to your supported image type list.
Does anyone see any drawbacks to this strategy?
This is a tricky one. If file is not an Image an exception will thrown. From that we can check the file is image or not.
using (Stream stream = File.OpenRead(file))
{
try
{
using (Image sourceImage = Image.FromStream(stream, false, false))
{
}
}
catch (Exception x)
{
if (x.Message.Contains("not valid"))
{
Console.Write("This is not a Image.");
}
}
}
This is what I use - it is just a tweak of #dylmcc's answer to make it a bit more readable.
public static bool IsRecognisedImageFile(string fileName)
{
string targetExtension = System.IO.Path.GetExtension(fileName);
if (String.IsNullOrEmpty(targetExtension))
{
return false;
}
var recognisedImageExtensions = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders().SelectMany(codec => codec.FilenameExtension.ToLowerInvariant().Split(';'));
targetExtension = "*" + targetExtension.ToLowerInvariant();
return recognisedImageExtensions.Contains(targetExtension);
}
My simple code
public static List<string> GetAllPhotosExtensions()
{
var list = new List<string>();
list.Add(".jpg");
list.Add(".png");
list.Add(".bmp");
list.Add(".gif");
list.Add(".jpeg");
list.Add(".tiff");
return list;
}
Check if image file
public static bool IsPhoto(string fileName)
{
var list = FileListExtensions.GetAllPhotosExtensions();
var filename= fileName.ToLower();
bool isThere = false;
foreach(var item in list)
{
if (filename.EndsWith(item))
{
isThere = true;
break;
}
}
return isThere;
}