Skip to content

Commit 20bd9fe

Browse files
committed
save as png again
1 parent dde6a64 commit 20bd9fe

File tree

2 files changed

+87
-102
lines changed

2 files changed

+87
-102
lines changed

index.d.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
/**
2-
* Extracts the icon from a file (like `.exe` or `.lnk`) as a raw BGRA bitmap buffer.
3-
* The returned buffer contains 32-bit pixel data (BGRA, top-down row order).
4-
*
5-
* @param inputPath Absolute path to the target file.
6-
* @param size Desired width and height of the icon.
7-
* @returns Buffer with raw image data.
8-
*/
9-
export function getIconBuffer(inputPath: string, size: number): Buffer;
1+
/**
2+
* Extracts the associated windows icon from a file and saves it as a PNG.
3+
* @param inputPath Absolute path to the file (e.g., `.exe`, `.lnk`)
4+
* @param outputPath Absolute path to save the PNG
5+
* @param size Size of the icon (e.g., 256)
6+
*/
7+
export function getIcon(inputPath: string, outputPath: string, size: number): void;
108

11-
/**
12-
* Extracts a thumbnail from a file using IThumbnailProvider, as a raw BGRA bitmap buffer.
13-
* The returned buffer contains 32-bit pixel data (BGRA, top-down row order).
14-
*
15-
* @param inputPath Absolute path to the target file.
16-
* @param size Desired width and height of the thumbnail.
17-
* @returns Buffer with raw image data.
18-
*/
19-
export function getThumbnailBuffer(inputPath: string, size: number): Buffer;
9+
/**
10+
* Extracts a thumbnail provided by applications using IThumbnailProvider and saves it as a PNG.
11+
* @param inputPath Absolute path to the file (e.g., `.pdf`, `.docx`)
12+
* @param outputPath Absolute path to save the PNG
13+
* @param size Size of the thumbnail
14+
*/
15+
export function getThumbnail(inputPath: string, outputPath: string, size: number): void;

src/winicon.cc

Lines changed: 73 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
#include <shlwapi.h>
88
#include <gdiplus.h>
99
#include <thumbcache.h>
10-
#include <memory>
11-
#include <exception>
12-
#include <fstream>
13-
#include <algorithm>
14-
#include <iostream>
1510

1611
#pragma comment(lib, "Ole32.lib")
1712
#pragma comment(lib, "Shlwapi.lib")
@@ -67,98 +62,92 @@ HRESULT GetThumbnailImage(const std::wstring& filePath, int size, HBITMAP& hBitm
6762
return hr;
6863
}
6964

70-
Napi::Value getImageBuffer(const Napi::CallbackInfo& info, bool useThumbnail) {
71-
Napi::Env env = info.Env();
72-
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsNumber()) {
73-
Napi::TypeError::New(env, "Expected (inputPath: string, size: number)").ThrowAsJavaScriptException();
74-
return env.Null();
65+
void SaveBitmapAsPNG(HBITMAP hBitmap, const std::wstring& outputPath) {
66+
GdiplusStartupInput gdiplusStartupInput;
67+
ULONG_PTR gdiplusToken;
68+
if (GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr) != Ok) return;
69+
70+
BITMAP bm;
71+
if (GetObject(hBitmap, sizeof(BITMAP), &bm) && bm.bmBits) {
72+
Bitmap bitmap(bm.bmWidth, bm.bmHeight, PixelFormat32bppARGB);
73+
74+
Gdiplus::BitmapData bmpData;
75+
Rect rect(0, 0, bm.bmWidth, bm.bmHeight);
76+
77+
if (bitmap.LockBits(&rect, ImageLockModeWrite, PixelFormat32bppARGB, &bmpData) == Ok) {
78+
int bytesPerPixel = 4;
79+
BYTE* srcData = (BYTE*)bm.bmBits;
80+
BYTE* destData = (BYTE*)bmpData.Scan0;
81+
82+
for (int y = 0; y < bm.bmHeight; y++) {
83+
BYTE* srcRow = srcData + (bm.bmHeight - 1 - y) * bm.bmWidth * bytesPerPixel;
84+
BYTE* destRow = destData + y * bmpData.Stride;
85+
memcpy(destRow, srcRow, bm.bmWidth * bytesPerPixel);
86+
}
87+
88+
bitmap.UnlockBits(&bmpData);
89+
}
90+
91+
CLSID clsidPng;
92+
CLSIDFromString(L"{557CF406-1A04-11D3-9A73-0000F81EF32E}", &clsidPng);
93+
bitmap.Save(outputPath.c_str(), &clsidPng, nullptr);
7594
}
7695

77-
std::wstring inputPath = std::wstring(info[0].As<Napi::String>().Utf8Value().begin(), info[0].As<Napi::String>().Utf8Value().end());
78-
int size = info[1].As<Napi::Number>().Int32Value();
96+
GdiplusShutdown(gdiplusToken);
97+
}
98+
99+
void getImage(const Napi::CallbackInfo& info, bool useThumbnail) {
100+
Napi::Env env = info.Env();
101+
if (info.Length() != 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsNumber()) {
102+
Napi::TypeError::New(env, "Expected (inputPath: string, outputPath: string, size: number)").ThrowAsJavaScriptException();
103+
return;
104+
}
79105

80-
if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) {
81-
Napi::Error::New(env, "Failed to initialize COM").ThrowAsJavaScriptException();
82-
return env.Null();
106+
std::u16string inputUtf16 = info[0].As<Napi::String>().Utf16Value();
107+
std::u16string outputUtf16 = info[1].As<Napi::String>().Utf16Value();
108+
std::wstring inputPath(inputUtf16.begin(), inputUtf16.end());
109+
std::wstring outputPath(outputUtf16.begin(), outputUtf16.end());
110+
int size = info[2].As<Napi::Number>().Int32Value();
111+
112+
GdiplusStartupInput gdiplusStartupInput;
113+
ULONG_PTR gdiplusToken;
114+
if (GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr) != Ok) {
115+
Napi::Error::New(env, "Failed to initialize GDI+").ThrowAsJavaScriptException();
116+
return;
83117
}
84118

119+
HRESULT coHr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
120+
bool comInitialized = SUCCEEDED(coHr);
121+
85122
HBITMAP hBitmap = nullptr;
86-
Napi::Value result = env.Null();
87-
try {
88-
HRESULT hr = useThumbnail
89-
? GetThumbnailImage(inputPath, size, hBitmap)
90-
: GetIconImage(inputPath, size, hBitmap);
91-
92-
if (SUCCEEDED(hr) && hBitmap) {
93-
BITMAP bmp;
94-
if (GetObject(hBitmap, sizeof(BITMAP), &bmp) && bmp.bmWidth > 0 && bmp.bmHeight > 0) {
95-
std::wcerr << L"Bitmap: " << bmp.bmWidth << L"x" << bmp.bmHeight << std::endl;
96-
97-
int width = bmp.bmWidth;
98-
int height = bmp.bmHeight;
99-
int bmpSize = width * 4 * height;
100-
101-
BITMAPINFO bmi = {};
102-
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
103-
bmi.bmiHeader.biWidth = width;
104-
bmi.bmiHeader.biHeight = -height; // top-down
105-
bmi.bmiHeader.biPlanes = 1;
106-
bmi.bmiHeader.biBitCount = 32;
107-
bmi.bmiHeader.biCompression = BI_RGB;
108-
109-
BYTE* bits = nullptr;
110-
HDC hdc = GetDC(nullptr);
111-
HBITMAP hDib = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, reinterpret_cast<void**>(&bits), nullptr, 0);
112-
HDC hMemDC = CreateCompatibleDC(hdc);
113-
ReleaseDC(nullptr, hdc);
114-
115-
if (hMemDC && hDib && bits) {
116-
HGDIOBJ old = SelectObject(hMemDC, hDib);
117-
HDC hSrcDC = CreateCompatibleDC(nullptr);
118-
if (hSrcDC) {
119-
SelectObject(hSrcDC, hBitmap);
120-
BitBlt(hMemDC, 0, 0, width, height, hSrcDC, 0, 0, SRCCOPY);
121-
DeleteDC(hSrcDC);
122-
123-
result = Napi::Buffer<BYTE>::Copy(env, bits, bmpSize);
124-
} else {
125-
Napi::Error::New(env, "Failed to create source DC").ThrowAsJavaScriptException();
126-
}
127-
SelectObject(hMemDC, old);
128-
DeleteDC(hMemDC);
129-
DeleteObject(hDib);
130-
} else {
131-
Napi::Error::New(env, "Failed to create DIB section").ThrowAsJavaScriptException();
132-
}
133-
} else {
134-
std::wcerr << L"Invalid bitmap dimensions." << std::endl;
135-
Napi::Error::New(env, "Invalid bitmap").ThrowAsJavaScriptException();
136-
}
137-
DeleteObject(hBitmap);
138-
} else {
139-
std::wcerr << L"Failed to get icon/thumbnail. HRESULT=" << hr << std::endl;
140-
Napi::Error::New(env, "Failed to retrieve image").ThrowAsJavaScriptException();
141-
}
142-
} catch (...) {
143-
if (hBitmap) DeleteObject(hBitmap);
144-
Napi::Error::New(env, "Native exception in image processing").ThrowAsJavaScriptException();
123+
HRESULT hr = useThumbnail
124+
? GetThumbnailImage(inputPath, size, hBitmap)
125+
: GetIconImage(inputPath, size, hBitmap);
126+
127+
if (SUCCEEDED(hr) && hBitmap) {
128+
SaveBitmapAsPNG(hBitmap, outputPath);
129+
DeleteObject(hBitmap);
130+
} else {
131+
Napi::Error::New(env, "Failed to retrieve image.").ThrowAsJavaScriptException();
145132
}
146133

147-
CoUninitialize();
148-
return result;
134+
if (comInitialized) CoUninitialize();
135+
GdiplusShutdown(gdiplusToken);
149136
}
150137

151-
Napi::Value getIconBuffer(const Napi::CallbackInfo& info) {
152-
return getImageBuffer(info, false);
138+
Napi::Value getIcon(const Napi::CallbackInfo& info) {
139+
getImage(info, false);
140+
return info.Env().Undefined();
153141
}
154142

155-
Napi::Value getThumbnailBuffer(const Napi::CallbackInfo& info) {
156-
return getImageBuffer(info, true);
143+
Napi::Value getThumbnail(const Napi::CallbackInfo& info) {
144+
getImage(info, true);
145+
return info.Env().Undefined();
157146
}
158147

159148
Napi::Object Init(Napi::Env env, Napi::Object exports) {
160-
exports.Set("getIconBuffer", Napi::Function::New(env, getIconBuffer));
161-
exports.Set("getThumbnailBuffer", Napi::Function::New(env, getThumbnailBuffer));
149+
exports.Set("getIcon", Napi::Function::New(env, getIcon));
150+
exports.Set("getThumbnail", Napi::Function::New(env, getThumbnail));
162151
return exports;
163152
}
164153

@@ -172,8 +161,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
172161
Napi::TypeError::New(env, "winicon only works on Windows.").ThrowAsJavaScriptException();
173162
});
174163

175-
exports.Set("getIconBuffer", stub);
176-
exports.Set("getThumbnailBuffer", stub);
164+
exports.Set("getIcon", stub);
165+
exports.Set("getThumbnail", stub);
177166
return exports;
178167
}
179168

0 commit comments

Comments
 (0)