Konvertierung von PNG in HBRUSH



  • Ich habe ein PNG-Bild, 100x100, mit 10 Paletteneinträgen und 4 Bits pro Pixel. Das will ich nun als Hintergrundbild eines Fensters zuweisen, und zwar nicht über WM_PAINT, sondern als Zuweisung von WNDCLASSEX::hbrBackground, damit sich da mal schön das System drum kümmern darf. Für hbrBackground brauche ich einen HBRUSH, und nach meinem Verständnis kann ich einen solchen mit CreatePatternBrush auf Basis eines HBITMAPs erzeugen. So weit, so uninteressant.

    Bleibt also die Konvertierung von PNG nach BMP, und die bricht mir seit Tagen die Beine. Ich habe eine eigene PNG Bibliothek geschrieben, und meine Tests mit Direct2D zeigen, dass die's auch ordentlich tut. Aus Gründen der Flexibilität habe ich das Decodieren des zlib-Streams und die Pixeltransformierung in ein gewünschtes Zielformat gesplittet für den Fall, dass es Schnittstellen gibt, bei denen ich auch einfach die Palette und die Palettenindizes reinwerfen kann, und daraus wird mir dann ein BMP gemacht.

    Erster Versuch war also:

    int test
    (
    	mapping_mem*mm,
    	WNDCLASSEX*wc
    )
    {
    	int my_errno;
    	png_file pf;
    	png_buffer_infos pbi;
    
    	uint32_t width;
    	uint32_t height;
    
    	HDC hDC;
    	HBITMAP hBitmap;
    
    	/*Genug Speicher für BITMAPINFO und seinen Paletteninformationen reservieren.*/
    	BYTE bitmap_info_raw[4096] = {0};
    	BITMAPINFO*bitmap_info = (BITMAPINFO*)(bitmap_info_raw);
    
    	/*Öffne PNG Mapping, und überspringe nicht die Formatprüfung, bei der interne
    	**Zeiger auf wichtige Chunks gesetzt werden (PLTE und so).*/
    	if(0 != (my_errno = png_file_init(&pf,"E:\\mono.png",FALSE)))
    		goto LABEL_END_NO_PNG;
    
    	/*Dekodiere, reserviere im Memory Mapping (mm) den Pixelbuffer und den
    	**Transformationszielbuffer getrennt, richte jede Scanline nach sizeof(DWORD)
    	**aus, und speichere mir die reservierten Bufferindizes in pbi.*/
    	if(0 != (my_errno = png_file_decode(&pf,FALSE,sizeof(DWORD),mm,&pbi)))
    		goto LABEL_END_NO_DC;
    
    	/*Erstelle einen Memory DC, der das gleiche Pixelformat hat wie der primäre**
    	**Grafikadapter. Wir wollen uns potentielles Hin- und Herkonvertieren von
    	**DIB nach DDB sparen, da das System das Rendern übernimmt.*/
    	if(NULL == (hDC = CreateCompatibleDC(NULL)))
    	{
    		my_errno = GetLastError();
    		goto LABEL_END_NO_DC;
    	}
    
    	/*PNG ist ein Big-Endian Format. Konvertiere die Breite und Höhe des Bildes.*/
    	width  = htonl(pf.pf_pcIHDR->pcIHDR_width);
    	height = htonl(pf.pf_pcIHDR->pcIHDR_height);
    
    	/*Erzeuge ein BMP mit dem gleichen Pixelformat wie der DC.*/
    	if(NULL == (hBitmap = CreateCompatibleBitmap
    	(
    		hDC,
    		width,
    		height
    	)))
    	{
    		my_errno = GetLastError();
    		goto LABEL_END_NO_BITMAP;
    	}
    
    	/*Initialisiere die Bitmap-Struktur*/
    	bitmap_info->bmiHeader.biSize		= sizeof(bitmap_info->bmiHeader);
    	bitmap_info->bmiHeader.biWidth		= width;
    	bitmap_info->bmiHeader.biHeight		-= (int32_t)height;
    	bitmap_info->bmiHeader.biPlanes		= 1;
    	bitmap_info->bmiHeader.biBitCount	= pf.pf_pcIHDR->pcIHDR_bipc;
    	bitmap_info->bmiHeader.biCompression	= BI_RGB;
    
    	/*PNG's PLTE-Chuck speichert drei Bytes pro Eintrag; GDI verwendet RGBQUAD,
    	**welches 4 Bytes benötigt. Konvertiere die Einträge und speichere sie in der
    	**BITMAPINFO-Struktur.*/
    	if(0 != (my_errno = png_chunk_PLTE_2_RGBQUAD(pf.pf_pcPLTE,bitmap_info->bmiColors)))
    		goto LABEL_END;
    
    	/*BMP setzen.*/
    	if(!SetDIBits
    	(
    		hDC,
    		hBitmap,
    		0,
    		height,
    		mm->mm_mapping + pbi.pbi_begin,
    		bitmap_info,
    		DIB_RGB_COLORS
    	))
    	{
    		my_errno = GetLastError();
    		goto LABEL_END;
    	}
    
    	wc->hbrBackground = CreatePatternBrush(hBitmap);
    
    LABEL_END:
    	DeleteObject(hBitmap);
    LABEL_END_NO_BITMAP:
    	DeleteDC(hDC);
    LABEL_END_NO_DC:
    	png_file_free(&pf);
    LABEL_END_NO_PNG:
    	return my_errno;
    }
    

    Dieser Code funktioniert leidlich: das System rendert definitiv ein schwarz-weißes (1-Bit) Bild, definitiv in 100x100-Bildgröße, mit einigen Pixeln auf weiß und dem überwiegenen Rest (>90%) schwarz gesetzt. pf.pf_pcIHDR->pcIHDR_bipc ist definitiv 4; tatsächlich kann ich die Bittiefe auch hartkodiert auf 4 stellen, und es macht keinen Unterschied. Stelle ich die Bittiefe auf 8, bekomme ich nur schwarz, während 2 RegisterClassEx fehlschlagen lässt.

    Ich könnte an dieser Stelle noch sehr viel mehr aufschreiben - schließlich teste ich den Mist schon seit Tagen - aber erstmal möchte ich einfach so in den Raum gefragt haben, ob an dem Code oder dem Denkansatz offensichtlicherweise was kaputt ist? Bitte noch nicht darauf verweisen, dass es CreateDIBitmap gibt - das weiß ich, das habe ich auch schon ausprobiert, diesmal mit einem transformierten (32-bit) Bild, und auf seine Weise funktioniert das noch schlechter als SetDIBits.

    EDIT: ich sollte noch erwähnen, dass ich sowohl den Pixelbuffer als auch die Paletteninformationen in Hex auf der Konsole habe ausgeben lassen. Allein die Tatsache, dass der Buffer mit jeder Menge 0x6 gefüllt ist (RGB: 808080), zeigt, dass hier was nicht stimmen kann.

    EDIT 2: Ich habe mir auch mal einige Capabilities des DCs zurückgeben lassen, den ich mir da generieren lasse. Sieht nicht aus, als ob's da irgendwelche Probleme geben könnte:

    DRIVERVERSION: 16384
    TECHNOLOGY:    1
    HORZSIZE:      531
    VERTSIZE:      298
    HORZRES:       1920
    VERTRES:       1080
    BITSPIXEL:     32
    PLANES:        1
    NUMCOLORS:     -1
    COLORRES:      24
    VREFRESH:      144
    BLTALIGNMENT:  1
    RASTERCAPS:    32409
    

    Der DC kann also RC_OP_DX_OUTPUT, RC_STRETCHDIB, RC_FLOODFILL, RC_STRETCHBLT, RC_BIGFONT, RC_DIBTODEV, RC_DI_BITMAP, RC_GDI20_OUTPUT, RC_BITMAP64 und RC_BITBLT - ich seh' da jetzt nicht unbedingt einen Grund, warum das nicht funktionieren sollte, es sei denn natürlich, dass SetDIBits nicht schlau genug ist, um den Buffer mithilfe der Palette in ein 32-Bit-Bild umzuwandeln, sondern stattdessen mit der Systempalette rumzuspielen versucht.



  • @Cerrseien
    Ich denke nicht, dass es an dem DC liegt. CreatePatternBrush nimmt außerdem nur DDBs und DIBSECTIONS entgegen, daher würde CreateDIBitmap auch nicht funktionieren.

    Ich habe es gerade getestet und kann bestätigen, dass die Funktion auch mit 4bpp Bitmaps funktioniert. Da es nicht anders dokumentiert ist, hatte ich allerdings auch keine Zweifel.
    Was das konkrete Problem zwischen Zeile 17 und Zeile 86 ist, kann hier niemand beantworten, da der Code fehlt.
    Falls du alles selber machen möchtest, würde ich empfehlen, alles in einem Objekt zu kapseln, das eine DIBSECTION erzeugt (es ist allerdings recht viel Arbeit). Dort könnte und müsste jeder einzelne Schritt von der Erzeugung bis zum Einlesen und der Herausgabe genau getestet werden. Charles Petzold hat so etwas in C entwickelt (allerdings mit sehr vielen Einschränkungen, nur BMP, keine TopDown-Bitmaps, keine Unterstützung von RLE4 oder RLE8, ...).
    Sehr viel weniger Arbeit wäre es, das Bild mit einem externen Tool als bmp zu speichern, mit LoadImage zu laden und dieses HBITMAP an CreatePatternBrush zu übergeben.
    Ebenfalls mit viel weniger Arbeit verbunden wäre (allerdings mit C++) GDIplus und daraus Gdiplus::Bitmap zu verwenden, das ziemlich viele Formate unterstützt und das interne Bitmaphandle auch herausgibt.

    Edit: CreatePatternBrush erstellt eine Kopie (hat vermutlich damit zu tun, dass von Win95-Me* nur 8x8 Bildpunkte unterstützt wurden und bei größeren Bildern die obere linke Ecke kopiert wurde). Daher kann und sollte das Handle auch direkt gelöscht werden. Edit2: Okay, den letzten Punkt kannst du vergessen, das Handle wird ja gelöscht. Das habe ich übersehen, sorry.

    *Auch wenn es eigentlich egal sein sollte: MSDN schreibt, dass das nur für Win95 gilt, Petzold schreibt hingegen, dass dies für alle außer den NT-Versionen gilt. Wie auch immer, ich kann es ohnehin nicht überprüfen.


  • Gesperrt

    Hi,

    habe mich heute - bevor ... - auch mit Ethanol mit png's ruhmgeschlagen.

    Also, ich habe png_write_info(png_ptr, info_ptr); übersehen, die genaue Fehlermeldung kam erst, als ich

    png_write_image(png_ptr, row_pointers);
    	
    

    durch

    for (unsigned int index = 0; index < height; ++index)
    	{
    		printf("write row %d of %d\n", index, height);
    		png_write_rows(png_ptr, &row_pointers[index], 1);
    	}
    

    ersetzte.

    Übersehen habe ich auch, dass

    auto setjmp_value = setjmp(png_jmpbuf(png_ptr));
    if (setjmp_value)
    {
    	fclose(fp);
    	png_destroy_write_struct(&png_ptr, &info_ptr);
    
    }
    

    beim ersten Durchlauf 0 ist (nicht true).

    Zudem wird je nach dem eine andere Funktion angesprungen lodepng, wobei if(lodepng_get_raw_size_lct(w, h, colortype, bitdepth) > in.size()) return 84; keine Garantie war bei A0 und rund 1200 dpi.



  • @yahendrik sagte in Konvertierung von PNG in HBRUSH:

    Ich denke nicht, dass es an dem DC liegt. CreatePatternBrush nimmt außerdem nur DDBs und DIBSECTIONS entgegen, daher würde CreateDIBitmap auch nicht funktionieren.

    Die Dokumentation sagt was anderes ...:

    The CreateDIBitmap function creates a compatible bitmap (DDB) from a DIB and, optionally, sets the bitmap bits.

    Ich wär auch im Eck gesprungen, wenn's anders gewesen wäre. Ich bin aber gerade anderweitig beschäftigt und kann's nicht mit CreateDIBSection testen - ich würde dich aber mal bitten, deinen Code zu posten, damit ich später nachschauen kann.

    @yahendrik sagte in Konvertierung von PNG in HBRUSH:

    Charles Petzold hat so etwas in C entwickelt (allerdings mit sehr vielen Einschränkungen, nur BMP, keine TopDown-Bitmaps, keine Unterstützung von RLE4 oder RLE8, ...).

    Dass Bitmap kein gut durchdachtes Format ist, weiß ich selbst. Deswegen will ich's ja mit PNG machen; wie ich bereits sagte, funktioniert die Lib schon herrlich mit Direct2D. CreateDIBSection habe ich bisher gemieden, weil sich mir der Sinn dahinter noch nicht ganz erschließt - ich will keine Sektion eines Bitmaps haben, sondern ein Bitmap.



  • Also gut, ich hätte besser schreiben sollen, dass es nicht direkt mit device independent bitmaps geht.
    Wie auch immer, der empfohlene Weg ist eigentlich seit „jeher“, DIBSECTIONs zu verwenden, wenn man einen direkten Zugriff auf die Bilddaten benötigt.

    Die Kapitel sind auch online zu finden, zum Beispiel hier:
    https://www-user.tu-chemnitz.de/~heha/petzold/ch16e.htm



  • Ich habe gerade den ganz leisen Verdacht, dass CreateDIBSection schon vom Ansatz her eine Totgeburt ist. Rührt aus zwei Absätzen her:

    The CreateDIBSection function creates a DIB

    und

    ICM: No color is done at brush creation. However, color management is performed when the brush is selected into an ICM-enabled device context.

    Ein DIB, welches "no color done at brush creation" kriegt, müsste nach der Logik jedes Mal in ein hardware-natives Format konvertiert werden, wenn das System den HBRUSH für's Rendern selektiert. Das sauge ich mir auch nicht aus den Fingern, sondern das sagen die mit Schmackes auf der Dokumentationsseite für CreateBitmap an:

    However, for performance reasons applications should use CreateBitmap to create monochrome bitmaps and CreateCompatibleBitmap to create color bitmaps. Whenever a color bitmap returned from CreateBitmap is selected into a device context, the system checks that the bitmap matches the format of the device context it is being selected into. Because CreateCompatibleBitmap takes a device context, it returns a bitmap that has the same format as the specified device context. Thus, subsequent calls to SelectObject are faster with a color bitmap from CreateCompatibleBitmap than with a color bitmap returned from CreateBitmap.

    Ich will hoffen, dass ich nur Gespenster sehe - denn wenn nicht, dann lasse ich das einfach mit CreateDIBSection und bleibe bei SetDIBits/CreateDIBitmap. Andere mögen das als empfohlenen Weg sehen; ich sehe das höchstens als Pfusch am Bau.

    Zum Dekodier-Code: der ist an sich kaum interessant; was wichtig ist, ist dass ich bereits geprüft habe, ob und was ich dekodiert zurückbekomme.
    Den Paletten-Konvertiercode kann ich (modifiziert) gerne posten, da dieser relativ neu dazugekommen ist und es halt sein kann, dass ich die Struktur falsch befülle:

    int png_chunk_PLTE_get_count
    (
    	const png_chunk_PLTE*pcPLTE,
    	uint32_t*count
    )
    {
    	int my_errno = 0;
    	uint32_t chunk_size;
    
    	if(!pcPLTE)
    	{
    		my_errno = EFAULT;
    		goto LABEL_END;
    	}
    
    	chunk_size = htonl(pcPLTE->pcPLTE_base.pcb_size);
    	if(chunk_size % 3)
    	{
    		my_errno = ERANGE;
    		goto LABEL_END;
    	}
    
    	if(count)(*count) = chunk_size / 3;
    
    LABEL_END:
    	return my_errno;
    }
    
    int png_chunk_PLTE_2_RGBQUAD
    (
    	const png_chunk_PLTE*pcPLTE,
    	RGBQUAD*palette
    )
    {
    	int my_errno;
    	uint32_t i,count;
    
    	if(0 != (my_errno = png_chunk_PLTE_get_count(pcPLTE,&count)))
    		goto LABEL_END;
    
    	for(i = 0;i < count;i++)
    	{
    		palette[i].rgbRed	= pcPLTE->pcPLTE_colors[i].red;
    		palette[i].rgbGreen	= pcPLTE->pcPLTE_colors[i].green;
    		palette[i].rgbBlue	= pcPLTE->pcPLTE_colors[i].blue;
    		palette[i].rgbReserved	= 0;
    	}
    
    LABEL_END:
    	return my_errno;
    }
    

    Nur wie gesagt, habe ich die Konvertierung von Palette zu 32-bit-True-Color auch schon für CreateDIBitmap gemacht - und hier sehe ich dann buchstäblich nur noch schwarz, und nicht mal mehr noch weiße Punkte dazwischen:

    int test2
    (
    	mapping_mem*mm,
    	WNDCLASSEX*wc
    )
    {
    	int my_errno;
    	png_file pf;
            png_buffer_infos pbi;
    
    	uint32_t width;
    	uint32_t height;
    
    	HDC hDC;
    	HBITMAP hBitmap;
    
    	BYTE bitmap_info_raw[4096] = {0};
    	BITMAPINFO*bitmap_info = (BITMAPINFO*)(bitmap_info_raw);
    	BITMAPV5HEADER header = {0};
    
    	if(0 != (my_errno = png_file_init(&pf,"E:\\mono.png",FALSE)))
                    goto LABEL_END_NO_PNG;
    
    	/*Diesmal sage ich dem Dekoder, dass er Quell- und Zielbuffer
    	**als eine Einheitr reservieren soll. Die Transformationsfunktion
    	**macht die Konvertierung dann im gleichen Buffer, was die
    	**Originaldaten zerstört - aber die brauche ich nicht mehr.
    	**
    	**Scanline-Alignment muss ich aber dennoch drinhaben, damit
    	**der Anfang des Bildes korrekt ausgerichtet ist.*/
    	if(0 != (my_errno = png_file_decode(&pf,TRUE,sizeof(DWORD),mm,&pbi))
    	|| 0 != (my_errno = png_buffer_transform
    	(
    		pf.pf_pcIHDR,
    		pf.pf_pcPLTE,
    		mm,
    		&pbi,
    		NULL,
    		PNG_COLORMODE_NORMAL
    	)))
    		goto LABEL_END_NO_DC;
    
    	/*In mm->mm_mapping + pbi.pbi_begin sind jetzt 32-bit Farbdaten drin.*/
    	if(NULL == (hDC = CreateCompatibleDC(NULL)))
    	{
    		my_errno = GetLastError();
    		goto LABEL_END_NO_DC;
    	}
    
    	width  = htonl(pf.pf_pcIHDR->pcIHDR_width);
    	height = htonl(pf.pf_pcIHDR->pcIHDR_height);
    
    	bitmap_info->bmiHeader.biSize		= sizeof(bitmap_info->bmiHeader);
    	bitmap_info->bmiHeader.biWidth		= width;
    	bitmap_info->bmiHeader.biHeight		-= (int32_t)height;
    	bitmap_info->bmiHeader.biPlanes		= 1;
    	bitmap_info->bmiHeader.biBitCount	= 32;
    	bitmap_info->bmiHeader.biCompression	= BI_RGB;
    
    	bitmap_info->bmiColors[0].rgbRed	= 0;
    	bitmap_info->bmiColors[0].rgbGreen	= 0;
    	bitmap_info->bmiColors[0].rgbBlue	= 0;
    	bitmap_info->bmiColors[0].rgbReserved	= 0;
    
    	header.bV5Size		= sizeof(header);
    	header.bV5Width		= width;
    	header.bV5Height	= -(int32_t)height;
    	header.bV5BitCount	= 32;
    	header.bV5Planes	= 1;
    	header.bV5Compression	= BI_RGB;
    
    	if(NULL == (hBitmap = CreateDIBitmap
    	(
    		hDC,
    		&header,
    		CBM_INIT,
    		mm->mm_mapping + pbi.pbi_begin,
    		bitmap_info,
    		DIB_RGB_COLORS
    	)))
    	{
    		my_errno = GetLastError();
    		goto LABEL_END;
    	}
    
    	wc->hbrBackground = CreatePatternBrush(hBitmap);
    
    LABEL_END:
    	DeleteObject(hBitmap);
    LABEL_END_NO_BITMAP:
    	DeleteDC(hDC);
    LABEL_END_NO_DC:
    	png_file_free(&pf);
    LABEL_END_NO_PNG:
    	return my_errno;
    }
    

    EDIT: den Petzold habe ich dazu auch schon gelesen, aber er konzentriert sich - zumindest in den Kapiteln 14 und 15 - in der Regel darauf, Bitmaps von der Platte einzulesen, die die Headerstrukturen schon haben. Ich habe keine Bitmaps auf der Platte, ich habe PNGs, die ich im Speicher dekodiere.

    Ich habe gerade erst mit Kapitel 16 angefangen, und was mir direkt auffällt, ist dieser Satz am Anfang:

    In practice, however, this method has serious performance problems when it comes to "get pixel" and "set pixel" routines.

    Kurz: er möchte in Software Änderungen machen dürfen, aber weil das Holen der Daten aus dem VRAM langsam ist, hält er sich lieber eine Schattenkopie im RAM, manipuliert diese, und lädt diese dann hoch. CreateDIBSection ergibt dann natürlich Sinn. Passt aber nicht auf meinen Anwendungszweck, wo ich das Bitmap nicht mehr anfassen werde.



  • @Cerrseien Es ist doch vollkommen egal, ob bmps, pngs, jpgs oder webps eingelesen werden. Entsprechende Decoder verlangen in der Regel entweder nur den Datenzeiger, die Adresse der n-ten Rasterzeile oder ein Array auf die Zeilenadressen (png_read_image aus libpng z.B.). All das wird von HDIB direkt bereitgestellt.

    Tatsächlich kann es auch sehr leicht so programmiert werden, dass das GDI-Objekt optional ist. Der Datenzeiger zeigt dann entweder auf die von Windows bereitgestellten Daten oder auf den eigenen Speicher. Sinnvoll ist letzteres dann, wenn das Ergebnis ohnehin nicht oder selten angezeigt wird. Oder eben nur einmalig zum Erzeugen eines Bitmapshandles. Und angesichts der Größe einer typischen Bitmap fällt der zusätzlich benötigte Speicher kaum ins Gewicht.


Anmelden zum Antworten