úterý 17. února 2015

Poznámky z Code Review - vlastnosti obrázků

Programátor dostal za úkol umožnit upload obrázků, součástí podmínek byly požadavky na velikost obrázku - tedy omezení šířky a výšky v pixelech, typ obrázku a maximální velikost souboru. Obrázek byl následně pro další zpracování přejmenován.

 V kódu se objevilo toto:

var im = System.Drawing.Image.FromStream(image.First().InputStream);
if (im.Height > 90 || im.Width > 920)
    return Content("over limits");

var extension = Path.GetExtension(img.FileName).Substring(1);

První problém spočívá v použití objektu s rozhraním IDisposable (třída  System.Drawing.Image), který  není nikde korektně  uzavřen.
Druhý problém pak v tom, že se uživateli věří, že je soubor správně pojmenován, včetně přípony. To ale nemusí být nic závažného. Dobré je se spíše zamyslet nad tím, jaké jsou možnosti.

Pro zjištění rozměrů obrázku a typu obrázku existují tyto čtyři možnosti (tedy možná je jich více, ale tohle jsou ty, které znám):
  • Image from System.Drawning
  • WebImage z System.Web.Helpers
  • BitmapDecoder z System..Windows.Media.Imaging
  • vlastní zpracování

Pokud bych je měl ve stručnosti popsat, tak Image vyžaduje od programátora správné použití vzhledem k tomu, že implementuje IDisposable rozhraní. Dle zdrojů na internetu také dokáže překvapit při použití streamů a neodpovídající úrovni práv - viz třeba http://www.roelvanlisdonk.nl/?p=2511

WebImage je pak vlastně jen obalený Image - programátor je tak zbaven nutnosti dodržet správné použití a  jsou mu nabízeny některé grafické funkce vhodné pro weby dostupné jako jednoduchá volání metod.

BitmapdDecoder pak vyžaduje knihovny WPF (System.Core, System.Xaml, PresentationCore).

Asi nejzajímavější je pak vlastní zpracování. Níže uvedený příklad je kód zkombinovaný z různých zdrojů:

http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
http://stackoverflow.com/questions/552467/how-do-i-reliably-get-an-image-dimensions-in-net-without-loading-the-image
http://www.codeproject.com/Articles/35978/Reading-Image-Headers-to-Get-Width-and-Height
http://stackoverflow.com/questions/1397512/find-image-format-using-bitmap-object-in-c-sharp

z nich si lze napsat takovýto kód:

public class ImageInfo
    {
        public ImageInfo(string extension, int width, int height)
        {
            this.Width = width;
            this.Height = height;
            this.Extension = extension;
        }

        public ImageInfo(int height, int width, string extension)
        {
            this.Width = width;
            this.Height = height;
            this.Extension = extension;
        }

        /// <summary>
        /// returns the type of the image, the values are: bmp, gif, jpg, png, emf, exif, ico, tiff, wmf
        /// </summary>
        public string Extension { get; private set; }

        /// <summary>
        /// returns the width of the image in px
        /// </summary>
        public int Width { get; private set; }
        
        /// <summary>
        /// returns the height of the image in px
        /// </summary>
        public int Height { get; private set; }
    }


    /// <summary>
    /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
    /// and from http://stackoverflow.com/questions/552467/how-do-i-reliably-get-an-image-dimensions-in-net-without-loading-the-image
    /// and form http://www.codeproject.com/Articles/35978/Reading-Image-Headers-to-Get-Width-and-Height
    /// and from http://stackoverflow.com/questions/1397512/find-image-format-using-bitmap-object-in-c-sharp
    /// </summary>
    public static class ImageInfoDecoder
    {
        const string errorMessage = "Coul    d not recognise image format.";

        private static Dictionary<byte[], Func<BinaryReader, ImageInfo>> imageFormatDecoders = new Dictionary<byte[], Func<BinaryReader, ImageInfo>>()
        {
            { new byte[] { 0x42, 0x4D }, DecodeBitmap },
            { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif },
            { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif },
            { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },
            { new byte[] { 0xff, 0xd8 }, DecodeJpg },
        };

        public static ImageInfo GetInfo(Stream stream)
        {
            if (stream == null)
                return null;

            try
            {
                using (BinaryReader binaryReader = new BinaryReader(stream, Encoding.Default, true))
                {
                    return ProcessWithDecoder(binaryReader);
                }
            }
            catch (ArgumentException)
            {
                //do it the old fashioned way
                using (Image image = Image.FromStream(stream))
                {
                    string extension = string.Empty;
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Jpeg))
                        extension = "jpg";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Bmp))
                        extension = "bmp";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png))
                        extension = "png";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Emf))
                        extension = "emf";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Exif))
                        extension = "exif";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Gif))
                        extension = "gif";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Icon))
                        extension = "ico";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.MemoryBmp))
                        extension = "bmp";
                    if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Tiff))
                        extension = "tiff";
                    else
                        extension = "wmf";
                    
                    return new ImageInfo(extension, image.Size.Width, image.Size.Height);
                }
            }
        }

        private static ImageInfo ProcessWithDecoder(BinaryReader binaryReader)
        {
            int maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;
            byte[] magicBytes = new byte[maxMagicBytesLength];
            for (int i = 0; i < maxMagicBytesLength; i += 1)
            {
                magicBytes[i] = binaryReader.ReadByte();
                foreach (var kvPair in imageFormatDecoders)
                {
                    if (StartsWith(magicBytes, kvPair.Key))
                    {
                        return kvPair.Value(binaryReader);
                    }
                }
            }

            throw new ArgumentException(errorMessage, "binaryReader");
        }

        private static bool StartsWith(byte[] thisBytes, byte[] thatBytes)
        {
            for (int i = 0; i < thatBytes.Length; i += 1)
            {
                if (thisBytes[i] != thatBytes[i])
                {
                    return false;
                }
            }

            return true;
        }
        
        private static ImageInfo DecodeBitmap(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(16);

            return new ImageInfo("bmp", binaryReader.ReadInt32(), binaryReader.ReadInt32());
        }

        private static ImageInfo DecodeGif(BinaryReader binaryReader)
        {
            return new ImageInfo("gif", binaryReader.ReadInt16(), binaryReader.ReadInt16());
        }

        private static ImageInfo DecodePng(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(8);
            return new ImageInfo("png", ReadLittleEndianInt32(binaryReader), ReadLittleEndianInt32(binaryReader));
        }
        
        public static ImageInfo DecodeJpg(BinaryReader binaryReader)
        {
            // keep reading packets until we find one that contains Size info
            for (; ; )
            {
                byte code = binaryReader.ReadByte();
                if (code != 0xFF) throw new ApplicationException(
                           "Unexpected value in file ");
                code = binaryReader.ReadByte();
                switch (code)
                {
                    // filler byte
                    case 0xFF:
                        binaryReader.BaseStream.Position--;
                        break;
                    // packets without data
                    case 0xD0:
                    case 0xD1:
                    case 0xD2:
                    case 0xD3:
                    case 0xD4:
                    case 0xD5:
                    case 0xD6:
                    case 0xD7:
                    case 0xD8:
                    case 0xD9:
                        break;
                    // packets with size information
                    case 0xC0:
                    case 0xC1:
                    case 0xC2:
                    case 0xC3:
                    case 0xC4:
                    case 0xC5:
                    case 0xC6:
                    case 0xC7:
                    case 0xC8:
                    case 0xC9:
                    case 0xCA:
                    case 0xCB:
                    case 0xCC:
                    case 0xCD:
                    case 0xCE:
                    case 0xCF:
                        ReadBEUshort(binaryReader);
                        binaryReader.ReadByte();

                        return new ImageInfo(ReadBEUshort(binaryReader), ReadBEUshort(binaryReader), "jpg");

                    // irrelevant variable-length packets
                    default:
                        int len = ReadBEUshort(binaryReader);
                        binaryReader.BaseStream.Position += len - 2;
                        break;
                }
            }
        }

        private static ushort ReadBEUshort(BinaryReader rdr)
        {
            ushort hi = rdr.ReadByte();
            hi <<= 8;
            ushort lo = rdr.ReadByte();
            return (ushort)(hi | lo);
        }

        private static int ReadLittleEndianInt32(BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(int)];
            for (int i = 0; i < sizeof(int); i += 1)
            {
                bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToInt32(bytes, 0);
        }
    }

Samozřejmě mne zajímalo, jak rychlé jsou jednotlivé možnost, takže jsem si napsal jednoduchou konzolovou aplikaci a spustil ji na vzorku obrázků:

class Program
    {
        static void Main(string[] args)
        {
            string sourceFolder = @"d:\Temp\Images";
            string[] sourceFiles = Directory.GetFiles(sourceFolder, "*");

            Console.WriteLine("{0} files to check:", sourceFiles.Length);

            foreach (string file in sourceFiles)
            {
                using (Stream stream = File.OpenRead(file))
                {
                    stream.Seek(0, SeekOrigin.Begin);
                    
                }

                Console.WriteLine(Path.GetFileNameWithoutExtension(file));
            }
            ImageInfoDecoder.GetInfo(null);
            Console.WriteLine("RESULTS:");


            SpeedAction(UsingImage, sourceFiles);
            SpeedAction(UsingWpfImage, sourceFiles);
            SpeedAction(UsingWebImage, sourceFiles);
            SpeedAction(UsingCustomReader, sourceFiles);

            SpeedAction(UsingImage, sourceFiles);
            SpeedAction(UsingWpfImage, sourceFiles);
            SpeedAction(UsingWebImage, sourceFiles);
            SpeedAction(UsingCustomReader, sourceFiles);
            Console.ReadLine();
        }
        public static string Truncate(string source, int length)
        {
            if (source.Length > length)
            {
                return source.Substring(0, length);
            }
            return source;
        }

        private static void SpeedAction(Action<string[]> action, string[] sourceFiles)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Reset();
            stopwatch.Start();
            action(sourceFiles);
            stopwatch.Stop();

            Console.WriteLine("FINAL SPEED OF {0}: {1}ms", action.Method.Name, stopwatch.ElapsedMilliseconds);
        }

        static void UsingImage(string[] sourceFiles)
        {
            foreach (string file in sourceFiles)
            {
                using (Stream stream = File.OpenRead(file))
                {
                    using (Image image = Image.FromStream(stream))
                    {
                        WriteImageInfo(file, image.Width, image.Height);
                    }
                }
            }
        }

        static void UsingWpfImage(string[] sourceFiles)
        {
            
            foreach (string file in sourceFiles)
            {
                
                using (Stream stream = File.OpenRead(file))
                {
                    var image = BitmapDecoder.Create(stream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.OnDemand);

                    WriteImageInfo(file, (int)image.Frames[0].Width, (int) image.Frames[0].Height);
                }
            }
        }

        static void UsingWebImage(string[] sourceFiles)
        {

            foreach (string file in sourceFiles)
            {
                using (Stream stream = File.OpenRead(file))
                {
                    WebImage image = new WebImage(stream);
                    WriteImageInfo(file, image.Width, image.Height);

                }
            }
        }


        static void UsingCustomReader(string[] sourceFiles)
        {
            foreach (string file in sourceFiles)
            {    
                using (Stream stream = File.OpenRead(file))
                {

                    try
                    {
                        var info = ImageInfoDecoder.GetInfo(stream);
                        WriteImageInfo(file, info.Width, info.Height);
                    }
                    catch { Console.WriteLine("NOT PICTURE"); }
                }
            }
        }

        static void WriteImageInfo(string name, int width, int height)
        {
            Console.WriteLine("{0}: {1} x {2}", Path.GetFileNameWithoutExtension(name), width, height);
        }

    }

A jak to dopadlo:

  • System.Drawning.Image: 607 ms
  • System.Web.Helpers.WebImage: 1128 ms 
  • System.Windows.Media.Imaging.BitmapDecoder: 342 ms
  • vlastní kód: 11 ms

Samozřejmě záleží na velikosti a počtu obrázků, ale pro představu to stačí. Neměřil jsem paměťovou ani CPU náročnost jednotlivých přístupů. Použití Image pak nepředstavuje rozhodně nejhorší volbu - navíc tyhle hotové třídy jsou prověřené a poměrně spolehlivé. Můžeme je použít i ke kontrole použití správného barevného profilu - obrázky pro web by vždy měly být RGB, měli bychom tedy zabránit vkládání CMYK obrázků.

Pokud je zase rozhodující rychlost, určitě není špatné zauvažovat nad vlastním kódem, neboť rychlostí zpracování bude vždy vynikat.

Na tomhle příkladě jsem chtěl jen ukázat, že i obyčejné code review může nakonec vyústit v malou prezentaci o možnostech získávání údajů z image souborů.

Žádné komentáře:

Okomentovat