Skalowanie ikony do dowolnego rozmiaru

0

Jest taka sytuacja.
Mam ikonkę TIcon o rozmiarze 256x256 px.

Chciałbym móc zmienić dowolnie jej rozmiar, żeby móc wyświetlić ją w komponencie TImage, w określonym rozmiarze, np. 96px.
Jak to zrobić?
Chodzi o to, żeby docelowy obraz nie utracił jakości ani pzezroczystości.

O ile się orientuję, z ikonką nie da się tak zrobić, i trzeba skorzystać z bitmapy...
Czy ktoś z was zajmował się takim przypadkiem?

-Pawel

7

Używam czegoś takiego w obu swoich edytorach — fontów i kursorów — zarówno do renderowania podglądu zaznaczonego pliku w drzewie, jak i do renderowania komórek grida. Tyle że u mnie wszystko ma tylne bufory w postaci 32-bitowych PNG, więc takie miniaturki mogę spokojnie renderować bez SIMD — po prostu zwykłe pętle śmigające po pikselach obrazków, do których dostęp uzyskuję za pomocą ScanLine.

Kod metody kopiującej stempel fontu/kursora (czyli sprajt) na bitmapę tylnego bufora:

type
  PPngPixel = ^TPngPixel;
  TPngPixel = record B, G, R, A: UInt8 end;

type
  PPngLine = ^TPngLine;
  TPngLine = array [UInt32] of TPngPixel;

procedure DrawStampStretched(ABuffer, AStamp: TPortableNetworkGraphic; ADestRect: TRect; AAlpha: UInt8);
var
  PixelBuffer:      PPngPixel;
  PixelStamp:       PPngPixel;
  LineStamp:        PPngLine;
  LineIndexBuffer:  Integer;
  LineIndexStamp:   Integer;
  PixelIndexBuffer: Integer;
  PixelIndexStamp:  Integer;
var
  MulX:  Single;
  MulY:  Single;
  Alpha: UInt8;
begin
  MulX := AStamp.Width  / ADestRect.Width;
  MulY := AStamp.Height / ADestRect.Height;

  ABuffer.BeginUpdate();

  LineIndexBuffer := ADestRect.Top;

  while LineIndexBuffer < ADestRect.Bottom do
  begin
    PixelBuffer := ABuffer.ScanLine[LineIndexBuffer];
    PixelBuffer += ADestRect.Left;

    LineIndexStamp := Round((LineIndexBuffer - ADestRect.Top) * MulY - 0.35);
    LineStamp      := AStamp.ScanLine[LineIndexStamp];

    for PixelIndexBuffer := 0 to ADestRect.Width - 1 do
    begin
      PixelIndexStamp := Round(PixelIndexBuffer * MulX - 0.35);
      PixelStamp      := @LineStamp^[PixelIndexStamp];

      Alpha := Round(PixelStamp^.A * (AAlpha / 255));

      PixelBuffer^.B := Round(PixelBuffer^.B + (PixelStamp^.B - PixelBuffer^.B) / 255 * Alpha);
      PixelBuffer^.G := Round(PixelBuffer^.G + (PixelStamp^.G - PixelBuffer^.G) / 255 * Alpha);
      PixelBuffer^.R := Round(PixelBuffer^.R + (PixelStamp^.R - PixelBuffer^.R) / 255 * Alpha);

      PixelBuffer += 1;
    end;

    LineIndexBuffer += 1;
  end;

  ABuffer.EndUpdate();
end;
  • ABuffer — bitmapa tła,
  • AStamp — bitmapa do namalowania na tle, w zadanym obszarze,
  • ADestRect — obszar docelowy, musi znajdować się wewnątrz bitmapy bufora (a jak nie to glitche lub SIGSEGV),
  • AAlpha — dodatkowa przezroczystość stempla (aka modulacja źródłowego kanału alpha).

Parametr AAlpha możesz wywalić, bo pewnie nie będzie Ci potrzebny — zamiast tego zmiksuj kanał alpha ikonki i bufora, w ten sam sposób co kanały RGB (zwykła interpolacja liniowa). W razie czego, odejmowanie wartości 0.35 od mnożników kompensuje niedoskonałości (nie wiem czy floatów czy moje, ale bez tego są małe glitche).

Sposób ten jest zgodny z działaniem nearest pixel sampling, więc zachowuje pikselozę po zeskalowaniu w górę.

0

O, jaki piękny kod!

Dzięki... tylko, że ja naprawdę nie jestem ekspertem od grafiki.
Proszę o rozjaśnienie, jak tego cuda użyć - napisałeś to pięknie, ale dla ludzi, którzy wiedzą o co chodzi.

procedure DrawStampStretched(ABuffer, AStamp: TPortableNetworkGraphic; ADestRect: TRect; AAlpha: UInt8);
  1. Jak wspomniałem, mam na wejściu TIcon...
  • ABuffer to PNG - mam rozumieć, że TIcon muszę zmienić na PNG?
  • AStamp -> czy to mój obraz wynikowy?
  • ADestRect -> tu jak rozumiem określam rozmiar docelowy?

AAlpha rzeczywiście mnie nie interesuje.

Czy byłbyś tak uprzejmy i zademonstrował jak użyć tego kodu?

Wejście: TIcon (256x256) -> Wyjście: TIcon (cokolwiek z przezroczystością), np. 96x96
-Pawel

4

Ja w Lazarus korzystam z BGRABitmap - jest też wersja dla Delphi: https://github.com/bgrabitmap/BGRABitmapDelphi
Kod działający w Lazarus-ie:

uses
  BGRABitmap, BGRABitmapTypes, BGRAIconCursor;

procedure TForm1.FormCreate(Sender: TObject);
var
  bmp: TBGRABitmap;
  ico: TBGRAIconCursor;
  ms: TMemoryStream;
begin
  ms := TMemoryStream.Create;
  Application.Icon.SaveToStream(ms); //zapisz ticon to stream
  ms.Position := 0;
  ico := TBGRAIconCursor.Create(ifIco);
  ico.LoadFromStream(ms);              //załaduj ikonę ze stream
  bmp := TBGRABitmap.Create;
  bmp.Assign(ico.GetBitmap(0));        //pobierz do bgrabitmap pierwszą ikonę (w pliku ICO może być kilka ikon, można pobrać wg indeksu lub rozmiaru)
  BGRAReplace(bmp, bmp.Resample(128, 128));   //zmień rozmiar na 128
  ms.Clear;
  bmp.SaveToStreamAsPng(ms);                 //zapisz obraz jako png do stream
  ms.Position := 0;
  Image1.Picture.LoadFromStream(ms);         //załaduj ze stream do image
  ms.Free;
  ico.Free;
  bmp.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  bmp: TBGRABitmap;
  ico: TBGRAIconCursor;
  ms: TMemoryStream;
begin
  bmp := TBGRABitmap.Create('D:\test ikona 1.ico');     //załaduj ikonę z pliku do bgrabitmap
  BGRAReplace(bmp, bmp.Resample(16, 16));               //zmień rozmiar na 16px
  ico := TBGRAIconCursor.Create(ifIco);
  ico.Add(bmp, 32);                                     //dodaj obraz do ikony
  ms := TMemoryStream.Create;
  ico.SaveToStream(ms);                                 //zapisz- ikonę do stream
  ms.Position := 0;
  Application.Icon.LoadFromStream(ms);                  //podmień ikonę aplikacji tą nowo utworzoną
  ico.Free;
  ms.Clear;
  bmp.SaveToStreamAsPng(ms);                            //zapisz obraz do stream jako png
  ms.Position := 0;            
  Image2.Picture.LoadFromStream(ms);                    //pokaż png w image2
  bmp.Free;
  ms.Free;
end;         
4
Pepe napisał(a):

O, jaki piękny kod!

Nie wiem czy taki piękny, ale robi robotę i działa na tyle szybko, że renderowanie dziesiątków obrazków z rozciągnięciem i modulacją kanałów nie powoduje utraty responsywności nawet na moim, raczej słabym laptopie.

Dzięki... tylko, że ja naprawdę nie jestem ekspertem od grafiki.

Ja raczej też nie (chyba). Jeśli chodzi o zabawę z obrazami, to myśl o nich jak o wielkich blokach pamięci, które zawierają dane kanałów RGB(A), w formie UInt8. Możesz robić z danymi kanałów co tylko chcesz — w końcu to tylko ciągi liczb, na których można wykonywać dowolne operacje matematyczne.

Myślę, że jeśli nie za bardzo ogarniasz to jak bawić się grafikami, to lepiej będzie jeśli skorzystasz z propozycji od @Paweł Dmitruk — niewiele kodu, łatwe w użyciu klasy.

Proszę o rozjaśnienie, jak tego cuda użyć - napisałeś to pięknie, ale dla ludzi, którzy wiedzą o co chodzi.

Funkcja ta służy do namalowania jednego PNG na drugim. W parametrze ABuffer przekazuje się obraz-tło — może to być bufor tła komponentu, albo tła komórki grida (jak u mnie). Parametr AStamp to mała grafika, która zostanie wyrenderowana na obrazie ABuffer — u mnie to grafika sprajtu, u Ciebie grafika konkretnej ikonki. ADestRect to obszar docelowy — obszar w obrazie ABuffer, w którym zostanie wyrenderowany AStamp. Jego rozmiar możesz policzyć dowolnie, z zachowaniem proporcji lub bez, w zależności od tego jaki efekt chcesz uzyskać. Jeśli proporcje ADestRect będą inne niż proporcje obrazu AStamp, to zostanie on wyrenderowany z rozciągnięciem.

Parametr AAlpha u mnie służy do tego, aby można było sprawdzić jak będzie wyglądał sprajt np. w trakcie zanikania obiektu. Jeśli przyjmie on wartość 255, to AStamp zostanie namalowany na ABuffer z oryginalną przezroczystością. Natomiast jeśli będzie to mniej, to AStamp będzie półprzezroczysty w zadanym stopniu. Dzięki temu mogę renderować sprajty na tle w formie szachownicy, uwzględniając oryginalny kanał alpha sprajtów oraz dodatkową przezroczystość (określaną suwakiem w edytorze).

screenshot-20230715121910.gif

Jednak tutaj trzeba zaznaczyć, że ta funkcja przeznaczona jest do renderowania jednego obrazu na drugim. Nieważne jaki format, byle oba były 32-bitowe — w pamięci wszystkie obrazy istnieją w formie bitmap, czyli bloków z danymi kanałów pikseli. Ja korzystam obecnie z klas TPortableNetworkGraphic, ale bez problemu mogłyby to być klasy TBitmap.

Czy byłbyś tak uprzejmy i zademonstrował jak użyć tego kodu?

Z ikonami nie miałem do tej pory czynienia, a klasa TIcon to niestety smutny żart. Tak to jest, jak się wymusza ścieżkę dziedziczenia na potrzeby LCL/VCL i używa klasy bazowej pojedynczego obrazu do reprezentacji zbioru obrazów o różnych wymiarach i formatach (bo tym jest format .ico).

Z tego co widziałem, jakoś da się zmieniać aktywny format ikony (chyba ustawiając TIcon.Current na interesujący nas indeks grafiki) i wtedy ma się do niej dostęp z poziomu TIcon.BitmapHandle lub można przekopiować taką bitmapę do pomocniczej bitmapki, za pomocą TempBitmap.Assign(MyIcon) lub za pośrednictwem strumienia pomocniczego. Ogólnie dramat, ale raczej da się to zrobić.

Mając zgraną konkretną ikonkę do bitmapy pomocniczej, można tę bitmapkę namalować na innej (zwykłym Canvas.Draw lub używając ScanLine, aby mieć możliwość manipulowania pikselami) lub bezpośrednio na płótnie komponentu. Sam wszędzie używam tylnych buforów, bo renderowanie na płótnie nie dość, że nie daje możliwości szybkiego dostępu do danych jego pikseli, to w dodatku wymusza odmalowywanie płótna, co znacząco spowalnia proces renderowania. Dlatego stworzyłem własny grid, którego każda komórka ma bitmapkę jako tylny bufor, dzięki czemu odmalowanie komórki nie ma nic wspólnego z płótnem komponentu, a jeśli komponent musi zostać odmalowany, to wtedy tylko maluję tylne bufory na płótnie, co jest wysoce wydajne.

Mogę ci pokazać kod wyjęty z któregoś edytora, ale będzie go trochę. Jeśli potrzebujesz wyrenderować grafikę w formie podglądu w jakimś komponencie, to możesz użyć mojego kodu. Sam też korzystam z takiego podglądu — zaznaczony w drzewie plik jest wyświetlany w PaintBox pod drzewkiem:

screenshot-20230715123934.png

Jest to mniej więcej to czego potrzebujesz. Ale żeby można było użyć podanej wcześniej funkcji, PaintBox też posiada tylny bufor w formie bitmapki — najpierw renderowane jest tło (np. szachownica lub jednolity kolor), następnie na tym renderowany jest sprajt, z rozciągnięciem. W zdarzeniu PaintBox.OnPaint jedyne co robię to PaintBox.Canvas.Draw(Buffer, 0, 0) i to samo dzieje się w gridach.

Jeśli chcesz samodzielnie manipulować pikselami, czyli przede wszystkim renderować jeden obraz na drugim z rozciągnięciem, to bez używania tylnych buforów, całość będzie działać bardzo wolno. U mnie to szczególnie istotne, bo przesuwanie suwaków ma natychmiast kolorować zawartość komórek gridów i zmieniać przezroczystość, dlatego renderowanie musi działać błyskawicznie. Poza tym, jak widać wyżej na gifie, mam wsparcie animacji w aktywnej komórce grida oraz opcję renderowania linii hot spotu, więc wydajność ma kluczowe znaczenie. No i dzięki zastosowaniu wszędzie tylnych buforów, ruszanie suwakami jest płynne, komórki bardzo szybko reagują na zmiany, a animowana komórka grida nie gotuje procesora — zużycie czasu CPU utrzymuje się na poziomie 1-2%, dla całego procesu edytora.

0

Posprawdzam to sobie. Interesuje mnie jak najprostszy i najwydajniejszy kod.
To co mam działa, ale mam stały rozmiar 48x48px (o tym zaraz). A chciałbym móc wyświetlić większe obrazki.

@furious programming: Twoje rozwiązanie jest pod Twoje potrzeby, do gry. Tutaj sprawdza się to jak widzę znakomicie.
Ja nie potrzebuję takich cudów. Myślę, że bym przekombinował.

Może napiszę co ja w ogóle próbuje osiągnąć. Mój program ma za zadanie otwierać pliki w określonej aplikacji w zależności od jej przyporządkowania (dany typ pliku z określoną aplikacją) - program po prostu pozwala na zarządzanie plikami bez udziału rejestru (w sensie, pomijając istniejące powiązania plików).

Pliki, które zostały w programie powiązane z konkretnym programem, otwierają się w nim bezpośrednio. Inne nie - użytkownik musi zdecydować jak je otworzyć. W takiej sytuacji wyświetlam okno z listą programów, które mogą obsługiwać dany typ plików oraz wyświetlam informacje o danym typu plików.

Test.png

Czyli ikonka, nazwa pliku oraz opis typu pliku.
Korzystam tutaj z funkcji SHGetFileInfo(), SHGetImageList
Jeśli zapodam rozmiar SHIL_EXTRALARGE uzyskuje ikonkę w rozmiarze (zazwyczaj) 48x48.
Ale, mogę użyć SHIL_JUMBO, co daje mi 256x256 (vista+).

I teraz - nie zawsze chcę ikonkę 256x256, np. chciałbym wyświetlić ikonkę 96x96.

Próbowałem z bitmapami i rzeczywiście można zmienić rozmiar ikonki, ale widzę, że to jest strechowanie, nie za fajny efekt (w sensie widać, że obraz jest "ściśnięty").
Rysuję ikonkę TIcon na canvasie Bitmapy, następnie przerysowuje ją na innej bitmapie:
Ustawiam rozmiar bitmapy na rozmiar ikonki
BMP1.Canvas.Draw(0, 0, ICON);
Ustawiam rozmiar końcowy i przerysowuje BMP1 na BMP2
BMP2.Canvas.StretchDraw(DrawRect, BMP1);

i wrzucam do TImage
MyImage.Picture.Assign(BMP2);

Niby działa, ale to nie to... Poza tym, Ikonka nie zawsze jest zwracana w 256px...
Ogólnie, chciałem mieć możliwość dowolnej zmiany rozmiaru, patrząc też pod kątem skalowania interfejsu (np. dla 150% DPI)

Rozwiązanie @Paweł Dmitruk wygląda prosto w użyciu, ale plików jest od groma.
Przetestuję to, jak mi się uda to odpalić w projekcie.

0
Pepe napisał(a):

@furious programming: Twoje rozwiązanie jest pod Twoje potrzeby, do gry. Tutaj sprawdza się to jak widzę znakomicie.
Ja nie potrzebuję takich cudów. Myślę, że bym przekombinował.

W moim przypadku, zadaniem tej funkcje jest wyrenderowanie dowolnego obrazka na dowolnym innym, z odpowiednim rozciągnięciem i bez antialiasingu. To że dotyczy to edytora na potrzeby silnika gry, nie ma większego znaczenia. Tak samo obsługa modulacji wszystkich czterech kanałów nie ma znaczenia, bo użycie globalnego kanału alpha można z tego kodu usunąć.

Skup się na sprawie kluczowej, czyli na wyświetlaniu grafiki w jakimś komponencie. Najpierw określ czy jest Ci potrzeby antialiasing i jeśli tak, to skorzystaj z odpowiedniej biblioteki zapewniającej taką funkcję, jeśli VCL nie wspiera rozmywania krawędzi. Ewentualnie sam sobie taką napisz (tutaj artykuł, z którego dawno temu korzystałem). Natomiast jeśli nie potrzebujesz antialiasingu, to możesz użyć Canvas.StretchDraw lub samodzielnie napisać funkcję, która takie renderowanie wykona (coś podobnego do tego czego sam używam).

Jeśli chodzi o ikonki, to klasa TIcon (przynajmniej w LCL) daje możliwość załadowania ikony, a następnie daje dostęp do informacji o wszystkich formatach i rozmiarach zawartych w takiej ikonie. Bodaj TIcon.Count zwraca liczbę grafik ikony, a metoda TIcon.GetDescription pozwala pobrać informacje o każdej z nich (rozmiar oraz głębie bitów). Pewnie wiele rzeczy można przy użyciu tej klasy zrobić, w tym pobierać bitmapki konkretnych grafik ikonki, ale jeszcze się tym nie bawiłem jakoś konkretniej.

Tak więc zamiast pobierać jakąś konkretną, dużą grafikę z ikonki i ją później rozciągać, lepiej by było uzyskać dostęp do całej ikony (pliku lub ID zasobu z exe/dll), załadować ją do instancji TIcon, a następnie sprawdzić jakie ma formaty i wybrać grafikę o najlepszym rozmiarze (zależy od twoich wymagań). Ale nie wiem ile kodu trzeba będzie naklepać, aby faktycznie znaleźć ikonkę jakiegoś formatu i ją załadować do klasy TIcon.

1 użytkowników online, w tym zalogowanych: 0, gości: 1