Generování náhledu fotografií

Tomáš Holan       11. 8. 2014       I/O operace, Grafika, .NET       5365 zobrazení

Před nedávnem jsem implementoval vlastní fotogalerii, která byla součástí jednoho webu v ASP.NET. Jeden z dílčích problémů, který bylo potřeba řešit je generování náhledů (thumbnails) fotografií.

Aby výsledné zobrazení fotogalerie k něčemu vypadalo, řeší se to obvykle tak, že mají všechny náhledy fotografií stejné – pevně “natvrdo” zvolené rozměry - bez ohledu na to, že originální fotografie mají rozměry třeba i zcela různé (a to včetně různého poměru stran tj. například některé fotografie jsou orientované “na výšku” a jiné “na šířku”). Rozměry náhledů se přitom volí podle poměru stran nejčastěji vkládaných fotografií tj. pro typické použití fotogalerie je to dnes obvykle nějaký širokoúhlý rozměr v rozumné velikosti. Například to může být 211 x 141 nebo 220 x 147 (v pořadí šířka x výška) apod.

Při generování náhledů je pak potřeba provést dvě operace:

  • Originální fotografii oříznout (crop) na poměr stran náhledu. K tomu je potřeba zvolit, zda bude oříznutá část nahoře a dole se zachováním celé původní šířky nebo zda bude oříznutá část vlevo a vpravo se zachováním celé původní výšky. Oříznutí se provádí z obou stran stejně tj. “centrovaně”.
  • Zmenšit (nebo případně zvětšit) oříznutý originál na velikost náhledu (oříznutý originál i náhled bude mít nyní již stejný poměr stran).

To čeho chceme dosáhnout v praxi může tedy vypadat nějak takto:

Originál

Náhled

Sample01Org Sample01
                Sample02Org

Sample02

(Klikněte pro zobrazení originálů v původní velikosti)

V prvním případě je zachována původní výška, ve druhém případě je zachována původní šířka.

Úkolem je tedy vytvořit metodu ResizeAndSaveImage, která na vstupu dostane název souboru originální fotografie a rozměry náhledu. Výstupem této metody pak bude buď přímo uložení náhledu do výstupního souboru nebo zapsání náhledu do výstupního streamu (uděláme obě varianty):

public static void ResizeAndSaveImage(string filename, int newWidth, int newHeight, string outputFilename)
{
    if (filename == null)
    {
        throw new ArgumentNullException("filename");
    }
    if (outputFilename == null)
    {
        throw new ArgumentNullException("outputFilename");
    }

    using (var image = LoadImage(filename))
    {
        using (var temp = ResizeImage(image, newWidth, newHeight))
        {
            temp.Save(outputFilename, ImageFormat.Png);
        }
    }
}

public static void ResizeAndSaveImage(string filename, int newWidth, int newHeight, Stream output)
{
    if (filename == null)
    {
        throw new ArgumentNullException("filename");
    }
    if (output == null)
    {
        throw new ArgumentNullException("output");
    }

    using (var image = LoadImage(filename))
    {
        using (var temp = ResizeImage(image, newWidth, newHeight))
        {
            temp.Save(output, ImageFormat.Png);
        }
    }
}

private static Image LoadImage(string filename)
{
    using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        return Image.FromStream(fs);
    }
}

private static Image ResizeImage(Image image, int newWidth, int newHeight)
{
    //TODO: Resize image and return new image
}

Obě varianty využívají pomocné metody LoadImage a ResizeImage. Metoda LoadImage je jednoduchá a pouze načítá objekt Image pomoci objektu FileStream a volání statické metody Image.FromStream.

Metoda ResizeImage pak dostane na vstupu již načtený originální obrázek jako objekt typu Image a má za úkol vrátit nový objekt Image s vytvořeným náhledem fotografie.

Základní idea je taková, že v této metodě vytvoříme nový objekt Bitmap požadovaných cílových rozměrů (pomoci konstruktoru Bitmap(int, int)), a pomoci příslušné varianty metody DrawImage objektu Graphics do něho vykreslíme odpovídající výsek originálního obrázku. Tímto postupem budou obě grafické operace (crop a resize) provedeny najednou.

Předtím tato metoda ale ještě provede nejprve kontrolu resp. dopočtení hodnot chybějících argumentů výšky nebo šířky nového obrázku tj. metoda bude obecnější a půjde jí volat i při určení pouze šířky nebo naopak pouze výšky náhledu – v takovém případě bude chybějící rozměr dopočítán tak, aby byl zachován poměr stran původního obrázku (a operace crop se tak nebude provádět protože pro výsek budou použity přímo rozměry celého originálu).

private static Image ResizeImage(Image image, int newWidth, int newHeight)
{
    if (newWidth == 0 && newHeight == 0)
    {
        throw new InvalidOperationException("newWidth or newHeight must be specified.");
    }

    if (newWidth == 0)
    {
        newWidth = Convert.ToUInt16(image.Width * (newHeight / (double)image.Height));
    }
    else if (newHeight == 0)
    {
        newHeight = Convert.ToUInt16(image.Height * (newWidth / (double)image.Width));
    }

    var temp = new Bitmap(newWidth, newHeight);
    try
    {
        temp.SetResolution(image.HorizontalResolution, image.VerticalResolution);

        Graphics g = Graphics.FromImage(temp);
        g.SmoothingMode = SmoothingMode.HighQuality;
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;

        //g.DrawImage(image, 
        //    new Rectangle(0, 0, newWidth, newHeight /*cílové rozměry*/), 
        //    new Rectangle(/*TODO: rozměry výseku z originálu*/), GraphicsUnit.Pixel);
    }
    catch
    {
        temp.Dispose();
        throw;
    }

    return temp;
}

Nyní nám tedy ještě zbývá dopočítat rozměry oblasti, která se z originálu vysekne. Označme jí následovně:

new Rectangle(srcX, srcY, srcWidth, srcHeight)

Šírka a výška budou vycházet z rozměrů původního obrázku s tím, že jeden z nich se ponechá a druhý se pak upraví v poměru stran náhledu. To která se ponechá bude určeno z poměrů nové a původní šířky a výšky:

int srcWidth = image.Width;
int srcHeight = image.Height;
double scaleX = newWidth / (double)image.Width;
double scaleY = newHeight / (double)image.Height;

if (scaleY < scaleX)
{
    double ratio = newWidth / (double)newHeight;
    srcHeight = Convert.ToInt32(Math.Round(image.Width / ratio));
}
else
{
    double ratio = newHeight / (double)newWidth;
    srcWidth = Convert.ToInt32(Math.Round(image.Height / ratio));
}

X a Y levého horního rohu oblasti se pak určí dle rozdílu původní a nově určené šířky resp. výšky tak, aby byl výsek centrovaný.

int srcX = (image.Width - srcWidth) / 2;
int srcY = (image.Height - srcHeight) / 2;

A to je vše.

Pro úplnost ještě uvedu finální podobu celé metody ResizeImage:

private static Image ResizeImageIfNeeded(Image image, int newWidth, int newHeight)
{
    if (newWidth == 0 && newHeight == 0)
    {
        throw new InvalidOperationException("newWidth or newHeight must be specified.");
    }

    if (newWidth == 0)
    {
        newWidth = Convert.ToUInt16(image.Width * (newHeight / (double)image.Height));
    }
    else if (newHeight == 0)
    {
        newHeight = Convert.ToUInt16(image.Height * (newWidth / (double)image.Width));
    }

    var temp = new Bitmap(newWidth, newHeight);
    try
    {
        temp.SetResolution(image.HorizontalResolution, image.VerticalResolution);

        int srcWidth = image.Width;
        int srcHeight = image.Height;
        double scaleX = newWidth / (double)image.Width;
        double scaleY = newHeight / (double)image.Height;

        if (scaleY < scaleX)
        {
            double ratio = newWidth / (double)newHeight;
            srcHeight = Convert.ToInt32(Math.Round(image.Width / ratio));
        }
        else
        {
            double ratio = newHeight / (double)newWidth;
            srcWidth = Convert.ToInt32(Math.Round(image.Height / ratio));
        }

        int srcX = (image.Width - srcWidth) / 2;
        int srcY = (image.Height - srcHeight) / 2;

        Graphics g = Graphics.FromImage(temp);
        g.SmoothingMode = SmoothingMode.HighQuality;
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;

        g.DrawImage(image, new Rectangle(0, 0, newWidth, newHeight), new Rectangle(srcX, srcY, srcWidth, srcHeight), GraphicsUnit.Pixel);
    }
    catch
    {
        temp.Dispose();
        throw;
    }

    return temp;
}

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

okraje v náhledu

Existuje ještě jedna možnost a to fotografii zmenšit tak aby se do náhledu celá vešla a vycentrovat ji v náhledu. Ve výsledku bude sice celá fotka, ale zároveň okraj.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Samozřejmě popisovaná metoda není jediná, ale pro obecně scénáře je asi nejčastěji používaná. Použití pad & resize (místo crop & resize) může ale v některých scénářích dávat smysl.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback