Ticket #269: ImageSearch.cpp

File ImageSearch.cpp, 21.7 KB (added by dexto, 15 years ago)

This is the code for that particular function described above

Line 
1ResultType Line::ImageSearch(int aLeft, int aTop, int aRight, int aBottom, char *aImageFile)
2// Author: ImageSearch was created by Aurelian Maga.
3{
4        // Many of the following sections are similar to those in PixelSearch(), so they should be
5        // maintained together.
6        Var *output_var_x = ARGVAR1;  // Ok if NULL. RAW wouldn't be safe because load-time validation actually
7        Var *output_var_y = ARGVAR2;  // requires a minimum of zero parameters so that the output-vars can be optional. Also:
8        // Load-time validation has ensured that these are valid output variables (e.g. not built-in vars).
9
10        // Set default results, both ErrorLevel and output variables, in case of early return:
11        g_ErrorLevel->Assign(ERRORLEVEL_ERROR2);  // 2 means error other than "image not found".
12        if (output_var_x)
13                output_var_x->Assign();  // Init to empty string regardless of whether we succeed here.
14        if (output_var_y)
15                output_var_y->Assign(); // Same.
16
17        RECT rect = {0}; // Set default (for CoordMode == "screen").
18        if (!(g->CoordMode & COORD_MODE_PIXEL)) // Using relative vs. screen coordinates.
19        {
20                if (!GetWindowRect(GetForegroundWindow(), &rect))
21                        return OK; // Let ErrorLevel tell the story.
22                aLeft   += rect.left;
23                aTop    += rect.top;
24                aRight  += rect.left;  // Add left vs. right because we're adjusting based on the position of the window.
25                aBottom += rect.top;   // Same.
26        }
27
28        // Options are done as asterisk+option to permit future expansion.
29        // Set defaults to be possibly overridden by any specified options:
30        int aVariation = 0;  // This is named aVariation vs. variation for use with the SET_COLOR_RANGE macro.
31        COLORREF trans_color = CLR_NONE; // The default must be a value that can't occur naturally in an image.
32        int icon_number = 0; // Zero means "load icon or bitmap (doesn't matter)".
33        int width = 0, height = 0;
34        // For icons, override the default to be 16x16 because that is what is sought 99% of the time.
35        // This new default can be overridden by explicitly specifying w0 h0:
36        char *cp = strrchr(aImageFile, '.');
37        if (cp)
38        {
39                ++cp;
40                if (!(stricmp(cp, "ico") && stricmp(cp, "exe") && stricmp(cp, "dll")))
41                        width = GetSystemMetrics(SM_CXSMICON), height = GetSystemMetrics(SM_CYSMICON);
42        }
43
44        char color_name[32], *dp;
45        cp = omit_leading_whitespace(aImageFile); // But don't alter aImageFile yet in case it contains literal whitespace we want to retain.
46        while (*cp == '*')
47        {
48                ++cp;
49                switch (toupper(*cp))
50                {
51                case 'W': width = ATOI(cp + 1); break;
52                case 'H': height = ATOI(cp + 1); break;
53                default:
54                        if (!strnicmp(cp, "Icon", 4))
55                        {
56                                cp += 4;  // Now it's the character after the word.
57                                icon_number = ATOI(cp); // LoadPicture() correctly handles any negative value.
58                        }
59                        else if (!strnicmp(cp, "Trans", 5))
60                        {
61                                cp += 5;  // Now it's the character after the word.
62                                // Isolate the color name/number for ColorNameToBGR():
63                                strlcpy(color_name, cp, sizeof(color_name));
64                                if (dp = StrChrAny(color_name, " \t")) // Find space or tab, if any.
65                                        *dp = '\0';
66                                // Fix for v1.0.44.10: Treat trans_color as containing an RGB value (not BGR) so that it matches
67                                // the documented behavior.  In older versions, a specified color like "TransYellow" was wrong in
68                                // every way (inverted) and a specified numeric color like "Trans0xFFFFAA" was treated as BGR vs. RGB.
69                                trans_color = ColorNameToBGR(color_name);
70                                if (trans_color == CLR_NONE) // A matching color name was not found, so assume it's in hex format.
71                                        // It seems strtol() automatically handles the optional leading "0x" if present:
72                                        trans_color = strtol(color_name, NULL, 16);
73                                        // if color_name did not contain something hex-numeric, black (0x00) will be assumed,
74                                        // which seems okay given how rare such a problem would be.
75                                else
76                                        trans_color = bgr_to_rgb(trans_color); // v1.0.44.10: See fix/comment above.
77
78                        }
79                        else // Assume it's a number since that's the only other asterisk-option.
80                        {
81                                aVariation = ATOI(cp); // Seems okay to support hex via ATOI because the space after the number is documented as being mandatory.
82                                if (aVariation < 0)
83                                        aVariation = 0;
84                                if (aVariation > 255)
85                                        aVariation = 255;
86                                // Note: because it's possible for filenames to start with a space (even though Explorer itself
87                                // won't let you create them that way), allow exactly one space between end of option and the
88                                // filename itself:
89                        }
90                } // switch()
91                if (   !(cp = StrChrAny(cp, " \t"))   ) // Find the first space or tab after the option.
92                        return OK; // Bad option/format.  Let ErrorLevel tell the story.
93                // Now it's the space or tab (if there is one) after the option letter.  Advance by exactly one character
94                // because only one space or tab is considered the delimiter.  Any others are considered to be part of the
95                // filename (though some or all OSes might simply ignore them or tolerate them as first-try match criteria).
96                aImageFile = ++cp; // This should now point to another asterisk or the filename itself.
97                // Above also serves to reset the filename to omit the option string whenever at least one asterisk-option is present.
98                cp = omit_leading_whitespace(cp); // This is done to make it more tolerant of having more than one space/tab between options.
99        }
100
101        // Update: Transparency is now supported in icons by using the icon's mask.  In addition, an attempt
102        // is made to support transparency in GIF, PNG, and possibly TIF files via the *Trans option, which
103        // assumes that one color in the image is transparent.  In GIFs not loaded via GDIPlus, the transparent
104        // color might always been seen as pure white, but when GDIPlus is used, it's probably always black
105        // like it is in PNG -- however, this will not relied upon, at least not until confirmed.
106        // OLDER/OBSOLETE comment kept for background:
107        // For now, images that can't be loaded as bitmaps (icons and cursors) are not supported because most
108        // icons have a transparent background or color present, which the image search routine here is
109        // probably not equipped to handle (since the transparent color, when shown, typically reveals the
110        // color of whatever is behind it; thus screen pixel color won't match image's pixel color).
111        // So currently, only BMP and GIF seem to work reliably, though some of the other GDIPlus-supported
112        // formats might work too.
113        int image_type;
114        HBITMAP hbitmap_image = LoadPicture(aImageFile, width, height, image_type, icon_number, false);
115        // The comment marked OBSOLETE below is no longer true because the elimination of the high-byte via
116        // 0x00FFFFFF seems to have fixed it.  But "true" is still not passed because that should increase
117        // consistency when GIF/BMP/ICO files are used by a script on both Win9x and other OSs (since the
118        // same loading method would be used via "false" for these formats across all OSes).
119        // OBSOLETE: Must not pass "true" with the above because that causes bitmaps and gifs to be not found
120        // by the search.  In other words, nothing works.  Obsolete comment: Pass "true" so that an attempt
121        // will be made to load icons as bitmaps if GDIPlus is available.
122        if (!hbitmap_image)
123                return OK; // Let ErrorLevel tell the story.
124
125        HDC hdc = GetDC(NULL);
126        if (!hdc)
127        {
128                DeleteObject(hbitmap_image);
129                return OK; // Let ErrorLevel tell the story.
130        }
131
132        // From this point on, "goto end" will assume hdc and hbitmap_image are non-NULL, but that the below
133        // might still be NULL.  Therefore, all of the following must be initialized so that the "end"
134        // label can detect them:
135        HDC sdc = NULL;
136        HBITMAP hbitmap_screen = NULL;
137        LPCOLORREF image_pixel = NULL, screen_pixel = NULL, image_mask = NULL;
138        HGDIOBJ sdc_orig_select = NULL;
139        bool found = false; // Must init here for use by "goto end".
140   
141        bool image_is_16bit;
142        LONG image_width, image_height;
143
144        if (image_type == IMAGE_ICON)
145        {
146                // Must be done prior to IconToBitmap() since it deletes (HICON)hbitmap_image:
147                ICONINFO ii;
148                if (GetIconInfo((HICON)hbitmap_image, &ii))
149                {
150                        // If the icon is monochrome (black and white), ii.hbmMask will contain twice as many pixels as
151                        // are actually in the icon.  But since the top half of the pixels are the AND-mask, it seems
152                        // okay to get all the pixels given the rarity of monochrome icons.  This scenario should be
153                        // handled properly because: 1) the variables image_height and image_width will be overridden
154                        // further below with the correct icon dimensions; 2) Only the first half of the pixels within
155                        // the image_mask array will actually be referenced by the transparency checker in the loops,
156                        // and that first half is the AND-mask, which is the transparency part that is needed.  The
157                        // second half, the XOR part, is not needed and thus ignored.  Also note that if width/height
158                        // required the icon to be scaled, LoadPicture() has already done that directly to the icon,
159                        // so ii.hbmMask should already be scaled to match the size of the bitmap created later below.
160                        image_mask = getbits(ii.hbmMask, hdc, image_width, image_height, image_is_16bit, 1);
161                        DeleteObject(ii.hbmColor); // DeleteObject() probably handles NULL okay since few MSDN/other examples ever check for NULL.
162                        DeleteObject(ii.hbmMask);
163                }
164                if (   !(hbitmap_image = IconToBitmap((HICON)hbitmap_image, true))   )
165                        return OK; // Let ErrorLevel tell the story.
166        }
167
168        if (   !(image_pixel = getbits(hbitmap_image, hdc, image_width, image_height, image_is_16bit))   )
169                goto end;
170
171        // Create an empty bitmap to hold all the pixels currently visible on the screen that lie within the search area:
172        int search_width = aRight - aLeft + 1;
173        int search_height = aBottom - aTop + 1;
174        if (   !(sdc = CreateCompatibleDC(hdc)) || !(hbitmap_screen = CreateCompatibleBitmap(hdc, search_width, search_height))   )
175                goto end;
176
177        if (   !(sdc_orig_select = SelectObject(sdc, hbitmap_screen))   )
178                goto end;
179
180        // Copy the pixels in the search-area of the screen into the DC to be searched:
181        if (   !(BitBlt(sdc, 0, 0, search_width, search_height, hdc, aLeft, aTop, SRCCOPY))   )
182                goto end;
183
184        LONG screen_width, screen_height;
185        bool screen_is_16bit;
186        if (   !(screen_pixel = getbits(hbitmap_screen, sdc, screen_width, screen_height, screen_is_16bit))   )
187                goto end;
188
189        LONG image_pixel_count = image_width * image_height;
190        LONG screen_pixel_count = screen_width * screen_height;
191        int i, j, k, x, y; // Declaring as "register" makes no performance difference with current compiler, so let the compiler choose which should be registers.
192
193        // If either is 16-bit, convert *both* to the 16-bit-compatible 32-bit format:
194        if (image_is_16bit || screen_is_16bit)
195        {
196                if (trans_color != CLR_NONE)
197                        trans_color &= 0x00F8F8F8; // Convert indicated trans-color to be compatible with the conversion below.
198                for (i = 0; i < screen_pixel_count; ++i)
199                        screen_pixel[i] &= 0x00F8F8F8; // Highest order byte must be masked to zero for consistency with use of 0x00FFFFFF below.
200                for (i = 0; i < image_pixel_count; ++i)
201                        image_pixel[i] &= 0x00F8F8F8;  // Same.
202        }
203
204        // v1.0.44.03: The below is now done even for variation>0 mode so its results are consistent with those of
205        // non-variation mode.  This is relied upon by variation=0 mode but now also by the following line in the
206        // variation>0 section:
207        //     || image_pixel[j] == trans_color
208        // Without this change, there are cases where variation=0 would find a match but a higher variation
209        // (for the same search) wouldn't.
210        for (i = 0; i < image_pixel_count; ++i)
211                image_pixel[i] &= 0x00FFFFFF;
212
213        // Search the specified region for the first occurrence of the image:
214        if (aVariation < 1) // Caller wants an exact match.
215        {
216                // Concerning the following use of 0x00FFFFFF, the use of 0x00F8F8F8 above is related (both have high order byte 00).
217                // The following needs to be done only when shades-of-variation mode isn't in effect because
218                // shades-of-variation mode ignores the high-order byte due to its use of macros such as GetRValue().
219                // This transformation incurs about a 15% performance decrease (percentage is fairly constant since
220                // it is proportional to the search-region size, which tends to be much larger than the search-image and
221                // is therefore the primary determination of how long the loops take). But it definitely helps find images
222                // more successfully in some cases.  For example, if a PNG file is displayed in a GUI window, this
223                // transformation allows certain bitmap search-images to be found via variation==0 when they otherwise
224                // would require variation==1 (possibly the variation==1 success is just a side-effect of it
225                // ignoring the high-order byte -- maybe a much higher variation would be needed if the high
226                // order byte were also subject to the same shades-of-variation analysis as the other three bytes [RGB]).
227                for (i = 0; i < screen_pixel_count; ++i)
228                        screen_pixel[i] &= 0x00FFFFFF;
229
230                for (i = 0; i < screen_pixel_count; ++i)
231                {
232                        // Unlike the variation-loop, the following one uses a first-pixel optimization to boost performance
233                        // by about 10% because it's only 3 extra comparisons and exact-match mode is probably used more often.
234                        // Before even checking whether the other adjacent pixels in the region match the image, ensure
235                        // the image does not extend past the right or bottom edges of the current part of the search region.
236                        // This is done for performance but more importantly to prevent partial matches at the edges of the
237                        // search region from being considered complete matches.
238                        // The following check is ordered for short-circuit performance.  In addition, image_mask, if
239                        // non-NULL, is used to determine which pixels are transparent within the image and thus should
240                        // match any color on the screen.
241                        if ((screen_pixel[i] == image_pixel[0] // A screen pixel has been found that matches the image's first pixel.
242                                || image_mask && image_mask[0]     // Or: It's an icon's transparent pixel, which matches any color.
243                                || image_pixel[0] == trans_color)  // This should be okay even if trans_color==CLR_NONE, since CLR_NONE should never occur naturally in the image.
244                                && image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
245                                && image_width <= screen_width - i%screen_width)  // Image is narrow enough not to exceed the right-side boundary of the search region.
246                        {
247                                // Check if this candidate region -- which is a subset of the search region whose height and width
248                                // matches that of the image -- is a pixel-for-pixel match of the image.
249                                for (found = true, x = 0, y = 0, j = 0, k = i; j < image_pixel_count; ++j)
250                                {
251                                        if (!(found = (screen_pixel[k] == image_pixel[j] // At least one pixel doesn't match, so this candidate is discarded.
252                                                || image_mask && image_mask[j]      // Or: It's an icon's transparent pixel, which matches any color.
253                                                || image_pixel[j] == trans_color))) // This should be okay even if trans_color==CLR_NONE, since CLR none should never occur naturally in the image.
254                                                break;
255                                        if (++x < image_width) // We're still within the same row of the image, so just move on to the next screen pixel.
256                                                ++k;
257                                        else // We're starting a new row of the image.
258                                        {
259                                                x = 0; // Return to the leftmost column of the image.
260                                                ++y;   // Move one row downward in the image.
261                                                // Move to the next row within the current-candiate region (not the entire search region).
262                                                // This is done by moving vertically downward from "i" (which is the upper-left pixel of the
263                                                // current-candidate region) by "y" rows.
264                                                k = i + y*screen_width; // Verified correct.
265                                        }
266                                }
267                                if (found) // Complete match found.
268                                        break;
269                        }
270                }
271        }
272        else // Allow colors to vary by aVariation shades; i.e. approximate match is okay.
273        {
274                // The following section is part of the first-pixel-check optimization that improves performance by
275                // 15% or more depending on where and whether a match is found.  This section and one the follows
276                // later is commented out to reduce code size.
277                // Set high/low range for the first pixel of the image since it is the pixel most often checked
278                // (i.e. for performance).
279                //BYTE search_red1 = GetBValue(image_pixel[0]);  // Because it's RGB vs. BGR, the B value is fetched, not R (though it doesn't matter as long as everything is internally consistent here).
280                //BYTE search_green1 = GetGValue(image_pixel[0]);
281                //BYTE search_blue1 = GetRValue(image_pixel[0]); // Same comment as above.
282                //BYTE red_low1 = (aVariation > search_red1) ? 0 : search_red1 - aVariation;
283                //BYTE green_low1 = (aVariation > search_green1) ? 0 : search_green1 - aVariation;
284                //BYTE blue_low1 = (aVariation > search_blue1) ? 0 : search_blue1 - aVariation;
285                //BYTE red_high1 = (aVariation > 0xFF - search_red1) ? 0xFF : search_red1 + aVariation;
286                //BYTE green_high1 = (aVariation > 0xFF - search_green1) ? 0xFF : search_green1 + aVariation;
287                //BYTE blue_high1 = (aVariation > 0xFF - search_blue1) ? 0xFF : search_blue1 + aVariation;
288                // Above relies on the fact that the 16-bit conversion higher above was already done because like
289                // in PixelSearch, it seems more appropriate to do the 16-bit conversion prior to setting the range
290                // of high and low colors (vs. than applying 0xF8 to each of the high/low values individually).
291
292                BYTE red, green, blue;
293                BYTE search_red, search_green, search_blue;
294                BYTE red_low, green_low, blue_low, red_high, green_high, blue_high;
295
296                // The following loop is very similar to its counterpart above that finds an exact match, so maintain
297                // them together and see above for more detailed comments about it.
298                for (i = 0; i < screen_pixel_count; ++i)
299                {
300                        // The following is commented out to trade code size reduction for performance (see comment above).
301                        //red = GetBValue(screen_pixel[i]);   // Because it's RGB vs. BGR, the B value is fetched, not R (though it doesn't matter as long as everything is internally consistent here).
302                        //green = GetGValue(screen_pixel[i]);
303                        //blue = GetRValue(screen_pixel[i]);
304                        //if ((red >= red_low1 && red <= red_high1
305                        //      && green >= green_low1 && green <= green_high1
306                        //      && blue >= blue_low1 && blue <= blue_high1 // All three color components are a match, so this screen pixel matches the image's first pixel.
307                        //              || image_mask && image_mask[0]         // Or: It's an icon's transparent pixel, which matches any color.
308                        //              || image_pixel[0] == trans_color)      // This should be okay even if trans_color==CLR_NONE, since CLR none should never occur naturally in the image.
309                        //      && image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
310                        //      && image_width <= screen_width - i%screen_width)  // Image is narrow enough not to exceed the right-side boundary of the search region.
311                       
312                        // Instead of the above, only this abbreviated check is done:
313                        if (image_height <= screen_height - i/screen_width    // Image is short enough to fit in the remaining rows of the search region.
314                                && image_width <= screen_width - i%screen_width)  // Image is narrow enough not to exceed the right-side boundary of the search region.
315                        {
316                                // Since the first pixel is a match, check the other pixels.
317                                for (found = true, x = 0, y = 0, j = 0, k = i; j < image_pixel_count; ++j)
318                                {
319                                        search_red = GetBValue(image_pixel[j]);
320                                        search_green = GetGValue(image_pixel[j]);
321                                        search_blue = GetRValue(image_pixel[j]);
322                                        SET_COLOR_RANGE
323                                        red = GetBValue(screen_pixel[k]);
324                                        green = GetGValue(screen_pixel[k]);
325                                        blue = GetRValue(screen_pixel[k]);
326
327                                        if (!(found = red >= red_low && red <= red_high
328                                                && green >= green_low && green <= green_high
329                        && blue >= blue_low && blue <= blue_high
330                                                        || image_mask && image_mask[j]     // Or: It's an icon's transparent pixel, which matches any color.
331                                                        || image_pixel[j] == trans_color)) // This should be okay even if trans_color==CLR_NONE, since CLR_NONE should never occur naturally in the image.
332                                                break; // At least one pixel doesn't match, so this candidate is discarded.
333                                        if (++x < image_width) // We're still within the same row of the image, so just move on to the next screen pixel.
334                                                ++k;
335                                        else // We're starting a new row of the image.
336                                        {
337                                                x = 0; // Return to the leftmost column of the image.
338                                                ++y;   // Move one row downward in the image.
339                                                k = i + y*screen_width; // Verified correct.
340                                        }
341                                }
342                                if (found) // Complete match found.
343                                        break;
344                        }
345                }
346        }
347
348        if (!found) // Must override ErrorLevel to its new value prior to the label below.
349                g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // "1" indicates search completed okay, but didn't find it.
350
351end:
352        // If found==false when execution reaches here, ErrorLevel is already set to the right value, so just
353        // clean up then return.
354        ReleaseDC(NULL, hdc);
355        DeleteObject(hbitmap_image);
356        if (sdc)
357        {
358                if (sdc_orig_select) // i.e. the original call to SelectObject() didn't fail.
359                        SelectObject(sdc, sdc_orig_select); // Probably necessary to prevent memory leak.
360                DeleteDC(sdc);
361        }
362        if (hbitmap_screen)
363                DeleteObject(hbitmap_screen);
364        if (image_pixel)
365                free(image_pixel);
366        if (image_mask)
367                free(image_mask);
368        if (screen_pixel)
369                free(screen_pixel);
370
371        if (!found) // Let ErrorLevel, which is either "1" or "2" as set earlier, tell the story.
372                return OK;
373
374        // Otherwise, success.  Calculate xpos and ypos of where the match was found and adjust
375        // coords to make them relative to the position of the target window (rect will contain
376        // zeroes if this doesn't need to be done):
377        if (output_var_x && !output_var_x->Assign((aLeft + i%screen_width) - rect.left))
378                return FAIL;
379        if (output_var_y && !output_var_y->Assign((aTop + i/screen_width) - rect.top))
380                return FAIL;
381
382        return g_ErrorLevel->Assign(ERRORLEVEL_NONE); // Indicate success.
383}