11#include < windows.h>
2+ #include < wincodec.h>
3+ #include < wrl/client.h>
24
5+ #include < algorithm>
36#include < array>
7+ #include < cmath>
8+ #include < cstdint>
49#include < cstdlib>
510#include < filesystem>
611#include < fstream>
712#include < iostream>
13+ #include < limits>
814#include < string>
915#include < vector>
1016
@@ -110,6 +116,167 @@ void CheckPng(const std::filesystem::path& path)
110116 " real renderer writes nontrivial preview pixels" );
111117}
112118
119+ struct PixelBounds
120+ {
121+ int left = std::numeric_limits<int >::max();
122+ int top = std::numeric_limits<int >::max();
123+ int right = -1 ;
124+ int bottom = -1 ;
125+ std::size_t count = 0 ;
126+
127+ void Include (int x, int y)
128+ {
129+ left = std::min (left, x);
130+ top = std::min (top, y);
131+ right = std::max (right, x);
132+ bottom = std::max (bottom, y);
133+ ++count;
134+ }
135+
136+ bool Empty () const noexcept { return count == 0 ; }
137+ };
138+
139+ struct RgbaBitmap
140+ {
141+ UINT width = 0 ;
142+ UINT height = 0 ;
143+ std::vector<std::uint8_t > pixels;
144+ };
145+
146+ RgbaBitmap ReadPng (const std::filesystem::path& path)
147+ {
148+ using Microsoft::WRL ::ComPtr;
149+ ComPtr<IWICImagingFactory> factory;
150+ Check (SUCCEEDED (CoCreateInstance (CLSID_WICImagingFactory, nullptr ,
151+ CLSCTX_INPROC_SERVER , IID_PPV_ARGS (&factory))),
152+ " WIC factory is created" );
153+ ComPtr<IWICBitmapDecoder> decoder;
154+ Check (SUCCEEDED (factory->CreateDecoderFromFilename (path.c_str (), nullptr ,
155+ GENERIC_READ , WICDecodeMetadataCacheOnLoad, &decoder)),
156+ " preview PNG decoder is created" );
157+ ComPtr<IWICBitmapFrameDecode> frame;
158+ Check (SUCCEEDED (decoder->GetFrame (0 , &frame)),
159+ " preview PNG frame is available" );
160+ ComPtr<IWICFormatConverter> converter;
161+ Check (SUCCEEDED (factory->CreateFormatConverter (&converter)) &&
162+ SUCCEEDED (converter->Initialize (frame.Get (),
163+ GUID_WICPixelFormat32bppRGBA, WICBitmapDitherTypeNone,
164+ nullptr , 0.0 , WICBitmapPaletteTypeCustom)),
165+ " preview PNG converts to RGBA pixels" );
166+
167+ RgbaBitmap bitmap;
168+ Check (SUCCEEDED (converter->GetSize (&bitmap.width , &bitmap.height )) &&
169+ bitmap.width > 0 && bitmap.height > 0 ,
170+ " preview PNG has nonzero dimensions" );
171+ const UINT stride = bitmap.width * 4 ;
172+ bitmap.pixels .resize (static_cast <std::size_t >(stride) * bitmap.height );
173+ Check (SUCCEEDED (converter->CopyPixels (nullptr , stride,
174+ static_cast <UINT >(bitmap.pixels .size ()),
175+ bitmap.pixels .data ())),
176+ " preview PNG pixels are decoded" );
177+ return bitmap;
178+ }
179+
180+ void CheckPomodoroPlayGlyphCentered (const std::filesystem::path& path)
181+ {
182+ const RgbaBitmap bitmap = ReadPng (path);
183+ const auto isTomato = [&](UINT x, UINT y) {
184+ const std::size_t offset =
185+ (static_cast <std::size_t >(y) * bitmap.width + x) * 4 ;
186+ const int red = bitmap.pixels [offset];
187+ const int green = bitmap.pixels [offset + 1 ];
188+ const int blue = bitmap.pixels [offset + 2 ];
189+ const int alpha = bitmap.pixels [offset + 3 ];
190+ return alpha > 0 && red > 140 && red > green + 35 &&
191+ red > blue + 30 ;
192+ };
193+
194+ std::vector<std::uint8_t > candidates (
195+ static_cast <std::size_t >(bitmap.width ) * bitmap.height , 0 );
196+ for (UINT y = 0 ; y < bitmap.height ; ++y)
197+ {
198+ for (UINT x = 0 ; x < bitmap.width ; ++x)
199+ {
200+ if (isTomato (x, y))
201+ candidates[static_cast <std::size_t >(y) * bitmap.width + x] = 1 ;
202+ }
203+ }
204+
205+ PixelBounds playButton;
206+ std::vector<std::size_t > pending;
207+ for (UINT y = 0 ; y < bitmap.height ; ++y)
208+ {
209+ for (UINT x = 0 ; x < bitmap.width ; ++x)
210+ {
211+ const std::size_t start =
212+ static_cast <std::size_t >(y) * bitmap.width + x;
213+ if (!candidates[start]) continue ;
214+ candidates[start] = 0 ;
215+ pending.assign (1 , start);
216+ PixelBounds component;
217+ while (!pending.empty ())
218+ {
219+ const std::size_t index = pending.back ();
220+ pending.pop_back ();
221+ const UINT currentX =
222+ static_cast <UINT >(index % bitmap.width );
223+ const UINT currentY =
224+ static_cast <UINT >(index / bitmap.width );
225+ component.Include (static_cast <int >(currentX),
226+ static_cast <int >(currentY));
227+ const auto Visit = [&](UINT nextX, UINT nextY) {
228+ const std::size_t next =
229+ static_cast <std::size_t >(nextY) * bitmap.width + nextX;
230+ if (!candidates[next]) return ;
231+ candidates[next] = 0 ;
232+ pending.push_back (next);
233+ };
234+ if (currentX > 0 ) Visit (currentX - 1 , currentY);
235+ if (currentX + 1 < bitmap.width )
236+ Visit (currentX + 1 , currentY);
237+ if (currentY > 0 ) Visit (currentX, currentY - 1 );
238+ if (currentY + 1 < bitmap.height )
239+ Visit (currentX, currentY + 1 );
240+ }
241+ if (component.count > playButton.count &&
242+ component.bottom > static_cast <int >(bitmap.height * 3 / 4 ) &&
243+ component.right < static_cast <int >(bitmap.width / 2 ))
244+ {
245+ playButton = component;
246+ }
247+ }
248+ }
249+ Check (playButton.count > 100 ,
250+ " Pomodoro play button surface is found in the preview" );
251+
252+ PixelBounds glyph;
253+ for (int y = playButton.top ; y <= playButton.bottom ; ++y)
254+ {
255+ for (int x = playButton.left ; x <= playButton.right ; ++x)
256+ {
257+ const std::size_t offset =
258+ (static_cast <std::size_t >(y) * bitmap.width + x) * 4 ;
259+ const int red = bitmap.pixels [offset];
260+ const int green = bitmap.pixels [offset + 1 ];
261+ const int blue = bitmap.pixels [offset + 2 ];
262+ const int alpha = bitmap.pixels [offset + 3 ];
263+ const int darkest = std::min ({ red, green, blue });
264+ const int lightest = std::max ({ red, green, blue });
265+ if (alpha > 0 && darkest >= 200 && lightest - darkest <= 35 )
266+ glyph.Include (x, y);
267+ }
268+ }
269+ Check (glyph.count > 20 ,
270+ " Pomodoro Font Awesome play glyph is found in the preview" );
271+ const double buttonCenterX = (playButton.left + playButton.right ) * 0.5 ;
272+ const double buttonCenterY = (playButton.top + playButton.bottom ) * 0.5 ;
273+ const double glyphCenterX = (glyph.left + glyph.right ) * 0.5 ;
274+ const double glyphCenterY = (glyph.top + glyph.bottom ) * 0.5 ;
275+ Check (std::abs (glyphCenterX - buttonCenterX) <= 2.0 &&
276+ std::abs (glyphCenterY - buttonCenterY) <= 2.0 ,
277+ " Pomodoro Font Awesome play glyph is visually centered" );
278+ }
279+
113280void Write (const std::filesystem::path& path, std::string_view text)
114281{
115282 std::ofstream output (path, std::ios::binary | std::ios::trunc);
@@ -194,6 +361,8 @@ return widget.define({
194361
195362int wmain (int argc, wchar_t ** argv)
196363{
364+ Check (SUCCEEDED (CoInitializeEx (nullptr , COINIT_MULTITHREADED )),
365+ " COM initializes for preview pixel decoding" );
197366 Check (argc == 4 ,
198367 " test receives snowwidget, SnowDesktop, and repository root" );
199368 const std::filesystem::path snowwidget = argv[1 ];
@@ -220,6 +389,18 @@ int wmain(int argc, wchar_t** argv)
220389 " snowwidget preview reports a completed real API v2 render" );
221390 CheckPng (output);
222391
392+ const auto pomodoroOutput = temporary.path / L" pomodoro.png" ;
393+ const auto pomodoroSource = repository / L" widgets" / L" pomodoro" ;
394+ const auto [pomodoroExit, pomodoroJson] = Run (snowwidget, {
395+ L" preview" , pomodoroSource.wstring (), pomodoroOutput.wstring (),
396+ L" --dpi" , L" 96" , L" --locale" , L" zh-CN" , L" --theme" , L" dark" ,
397+ L" --host" , host.wstring () });
398+ Check (pomodoroExit == 0 &&
399+ pomodoroJson.find (" \" ok\" :true" ) != std::string::npos,
400+ " Pomodoro paused-state preview renders successfully" );
401+ CheckPng (pomodoroOutput);
402+ CheckPomodoroPlayGlyphCentered (pomodoroOutput);
403+
223404 const auto environmentSource =
224405 CreateEnvironmentFixture (temporary.path );
225406 const auto environmentOutput = temporary.path / L" environment.png" ;
@@ -260,5 +441,6 @@ int wmain(int argc, wchar_t** argv)
260441 " preview rejects an unknown deterministic data state" );
261442
262443 std::cout << " widget author preview CLI tests passed\n " ;
444+ CoUninitialize ();
263445 return 0 ;
264446}
0 commit comments