Вступление

Социальные сети и мессенджеры последнее время завоевали большую популярность среди пользователей интернета. Большинство пользователей (обычные пользователи) просто переписываются и обмениваются различным контентом с друзьями. Более продвинутые пользователи используют социальные сети для других целей, применяя элементы программирования. Лично я в настоящее время не сильно увлечён общением в соцсетях, по сравнению с прошлыми годами.

Одной из таких соцсетей (или мессенджера, – не знаю) было приложение «ДругВокруг». Точнее, было в моём обиходе. Стояло оно у меня на компьютере с Windows XP во времена, когда на данной ОС ещё можно было как-то сосуществовать в интернете. Это было примерно в 2016 году. Тогда как раз только начинали появляться смартфоны у моих знакомых, но у меня смартфона ещё не было.

Окно авторизации в «ДругВокруг» при первом запуске (старая версия).
Окно авторизации в «ДругВокруг» при первом запуске (старая версия).

В приложении «ДругВокруг» меня заинтересовала функция «Поиск пользователя по номеру телефона». В данный момент хотел прикрепить к статье иллюстрации с окнами, но приложение перестало работать, и я не смог в него войти: нет связи с сервером. А устанавливать новую версию или на компьютер со свежей ОС мне неохота. Тем более там, скорее всего, всё будет по-другому, не как раньше. Благодаря данной функции имелась (или имеется до сих пор) возможность, перебирая разные номера телефонов, посмотреть информацию о пользователе, если он зарегистрирован. Никаких ограничений на количество запросов программа не накладывала.

Автокликер

Одним зимним вечером, когда я сидел дома на больничном, и мне было скучно, я захотел написать такую программу, которая бы автоматизировала процесс перебора телефонных номеров из определённого списка или диапазона и забивала их в поиск, сохраняя результат в виде скриншота. Я не знал, существовали ли подобные программы по автоматизации действий. Слышал, что бывают похожие программы, которые симулируют активность пользователя на определённой веб-странице. Но для меня стояла более сложная задача: не только двигать курсором мыши и нажимать кнопки, но и анализировать изображение на экране. В качестве анализа изображения пришла идея анализировать цвета пикселей по конкретным координатам конкретного окна. Смысл простой. Нужно вбивать номер в поле поиска и нажимать кнопку «Найти». Если пользователь не найден – один вид окна, а если найден – другой. Виды окон можно анализировать с информационной точки зрения по цвету определённого пикселя. В зависимости от вида окна нужно принимать дальнейшие действия по алгоритму.

Для начала я предварительно подготовил алгоритм, проанализировав через Paint цвета и координаты пикселей. Алгоритм получился такой. Размещу я его под спойлером в текстовом виде, в каком я его подготовил изначально. Рисовать блок-схему мне неохота.

Алгоритм
  1. Поместить курсор в поле ввода номера телефона:

    1.1. Переместить указатель мыши в точку X312Y246.

    1.2. Нажать и отпустить левую клавишу мыши.

1'. Очистить поле ввода, нажав комбинацию клавиш Ctrl+Backspase.

  1. Поместить в буфер обмена необходимый номер телефона.

  2. Произвести вставку, нажав комбинацию клавиш Ctrl+V.

  3. Нажмать клавишу ENTER.

  4. Подождать 500 мс.

  5. Сдедать скриншот и проанализировать цвет пикселя X40Y288:
    a) если R228G233B237, значит поиск завершён. Перейти к пункту 7;
    b) если R212G208B200, значит поиск ещё не завершён. Перейти к пункту 5.

  6. Проанализировать цвет пикселя X200Y410:
    a) если R225G225B225, значит аккаунт найден. Перейти к пункту 8;
    b) если R243G243B243, значит аккаунт не найден. Перейти к пункту 1;

  7. Кликнуть по аватару найденного аккаунта:
    8.1. Переместить указатель мыши в точку X67Y380.
    8.2. Нажать и отпустить левую клавишу мыши.

  8. Сделать скриншот от X336Y30 размером X490Y851.

  9. Сохранить скриншот в BMP файл с именем номера телефона.

  10. Закрыть вкладку с информацией о найденном аккаунте:
    11.1. Переместить указатель мыши в точку X487Y41.
    11.2. Нажать и отпустить левую клавишу мыши.

  11. Перейти к пункту 1.

После алгоритма я приступил к разработке программы. Внимательные читатели моих статей знают, что я программировал тогда (и иногда это делаю и ныне) в Dev-C++. Мне когда-то давно подкинули эту программу, и она мне приглянулась. Пишу я программы для командной строки, создавая просто файл Си. А здесь мне предстояло сделать полноценный проект, хоть даже и консольный. Это разные функции в Dev-C++. И я потратил много времени на гугл-поиск, получая справочную информацию по всем этим делам. Предстояло освоить WinAPI функции работы с курсором мыши, кнопками мыши и клавиатуры, буфером обмена, графическими элементами окон. Никакого опыта по работе с этими функциями у меня не было. Получая теоретические знания, я поэтапно применял их на практике методом проб и ошибок.

Подробности работы программы я уже вряд ли вспомню. Просматривая свой код, я понимаю его только поверхностно. Окно, с которым я работал, имело название «ДругВокруг - Поиск». Это не просто название окна, а аргумент функции FindWindow. Затем была структура RECT. Это структура прямоугольника (как я это понимаю), в которую передаётся окно приложения функцией GetWindowRect. Также в программе встречаются другие типы данных, связанные с графикой, и их связи с вышеперечисленными структурами. Перед анализом пикселя в программном коде создаётся «виртуальное» изображение маленького участка окна в требуемом месте. Этим же способом создаётся большое изображение с результатом, но оно уже сохраняется в BMP-файл.

В результате программа с горем пополам заработала. На этапе её тестирования я исправлял некие косяки и дополнял различными мелочами. Программа перебирает 10000 номеров по последним четырём цифрам от 0000 до 9999. При этом я предварительно задаю вручную те или иные префиксы на своё усмотрение. Время выполнения программы занимало около 10 минут (учитывая задержки Sleep() в тексте программы перед некоторыми операциями). Тогда я обработал 4 префикса: 8953617____, 8953478____, 8953617____ и на 1000 номеров 89536293___. Все префиксы соответствуют номерам Орловской области мобильного оператора Теле2. На каждый такой префикс я создал одноимённую папку, куда разместил результаты работы программы – скриншоты с информацией о пользователе. По статистике получилось в среднем 185 зарегистрированных номеров из 10000 (1.85%). То есть, социальная сеть не особо популярная, хотя о популярности судить сложно – нужно куда больше критериев. Ниже я приведу несколько иллюстраций с результатами.

Результат работы программы - папка со скриншотами (вид в Total Commander).
Результат работы программы - папка со скриншотами (вид в Total Commander).
Образец одной картинки 9536172187.bmp.
Образец одной картинки 9536172187.bmp.

Спустя два года, в 2018 году, я опять вернулся к своему проекту. Неудивительно, но после обновления «Друга» моя программа перестала корректно работать: поменялись координаты и цвета требуемых элементов окна поиска. Пришлось корректировать программу. Старые участки кода я местами закомментировал. Да и в целом, процедура развёртывания информации с подробными данными о найденных пользователях стала занимать больше времени. А с увеличением числа поисковых запросов и вовсе приводило к зависанию. Поэтому я отказался от развёртывания информации, сохраняя только миниатюры – аватарки пользователей с ником. Для этого я скорректировал код ещё раз, зарезервировав старый. В новой программе я просканировал ещё несколько диапазонов на 10000 номеров – процедура сканирования сократилась до 5 минут. Ниже будет приведён под спойлером текст старого кода (где результатом служат не миниатюры, а скрины подробной информацией о пользователях) в том виде, как есть. А прямо сейчас – иллюстрации с результатами работы упрощённой версии программы.

Результат работы новой программы - папка со скриншотами (вид в Total Commander).
Результат работы новой программы - папка со скриншотами (вид в Total Commander).
Образец одной картинки 89536150868_s.bmp.
Образец одной картинки 89536150868_s.bmp.
Текст программы (файл main.cpp).
#include <windows.h>
#include <stdio.h>
#include <iostream>

using namespace std;

/*#ifndef CAPTUREBLT
#define CAPTUREBLT (DWORD)0x40000000 /* Include layered windows */

#define CAPTUREBLT 0x40000000
//#define CLICK mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);Sleep(5);mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0)

/*inline void init(){
   HWND dvkr = FindWindow(NULL,"ДругВокруг");
	RECT crd;
	GetWindowRect(dvkr,&crd);
}*/

inline void moveclick(int x, int y){
	SetCursorPos(x,y);
	mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
	Sleep(5);
	mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
}

inline void telclear(){
	keybd_event(VK_CONTROL, 0, 0, 0);
	keybd_event(VK_BACK, 0, 0, 0);
	Sleep(5);
	keybd_event(VK_BACK, 0, KEYEVENTF_KEYUP, 0);
	keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
}

void txtpaste(char* text, RECT crd){
    if(OpenClipboard(0)){
        EmptyClipboard();
        char* clip_data = (char*)(GlobalAlloc(GMEM_FIXED, MAX_PATH));
        lstrcpy(clip_data, text);
        SetClipboardData(CF_TEXT, (HANDLE)(clip_data));
        LCID* lcid = (DWORD*)(GlobalAlloc(GMEM_FIXED, sizeof(DWORD)));
        *lcid = MAKELCID(MAKELANGID(LANG_RUSSIAN, SUBLANG_NEUTRAL), SORT_DEFAULT);
        SetClipboardData(CF_LOCALE, (HANDLE)(lcid));
        CloseClipboard();
    }
    //В новом интерфейсе запретили комбинацию "CTRL+V";
    //Отказывается путём работать "SHIFT+INSERT";
    //Но работает вставка через контекстное меню;
	//keybd_event(VK_CONTROL, 0, 0, 0);
	/*keybd_event(VK_LSHIFT, 0, 0, 0);
	//keybd_event(0x56, 0, 0, 0);
	keybd_event(VK_INSERT, 0, 0, 0);
	Sleep(5);
	//keybd_event(0x56, 0, KEYEVENTF_KEYUP, 0);
	keybd_event(VK_INSERT, 0, KEYEVENTF_KEYUP, 0);
	//keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
	keybd_event(VK_LSHIFT, 0, KEYEVENTF_KEYUP, 0);*/
	SetCursorPos(crd.left+455,crd.top+118);
	mouse_event(MOUSEEVENTF_RIGHTDOWN,0,0,0,0);
	Sleep(5);
	mouse_event(MOUSEEVENTF_RIGHTUP,0,0,0,0);
	SetCursorPos(crd.left+483,crd.top+220);
	mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
	Sleep(5);
	mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
}

inline void find(RECT crd){
	//Не работает поиск по нажатию виртуальной "ENTER" в новом интерфейсе;
	//Придётся жмать мышью;
	/*keybd_event(VK_RETURN, 0, 0, 0);
	Sleep(5);
	keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);*/
	SetCursorPos(crd.left+420,crd.top+177);
	mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
	Sleep(5);
	mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
}

inline int GetFilePointer(HANDLE FileHandle){
    return SetFilePointer(FileHandle, 0, 0, FILE_CURRENT);
}
 
bool SaveBMPFile(char *filename, HDC bitmapDC, int width, int height){
    bool Success=0;
    HDC SurfDC=NULL;
    HBITMAP OffscrBmp=NULL;
    HDC OffscrDC=NULL;
    LPBITMAPINFO lpbi=NULL;
    LPVOID lpvBits=NULL;
    HANDLE BmpFile=INVALID_HANDLE_VALUE;
    BITMAPFILEHEADER bmfh;
    if ((OffscrBmp = CreateCompatibleBitmap(bitmapDC, width, height)) == NULL)
        return 0;
    if ((OffscrDC = CreateCompatibleDC(bitmapDC)) == NULL)
        return 0;
    HBITMAP OldBmp = (HBITMAP)SelectObject(OffscrDC, OffscrBmp);
    BitBlt(OffscrDC, 0, 0, width, height, bitmapDC, 0, 0, SRCCOPY);
    if ((lpbi = (LPBITMAPINFO)(new char[sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)])) == NULL) 
        return 0;
    ZeroMemory(&lpbi->bmiHeader, sizeof(BITMAPINFOHEADER));
    lpbi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    SelectObject(OffscrDC, OldBmp);
    if (!GetDIBits(OffscrDC, OffscrBmp, 0, height, NULL, lpbi, DIB_RGB_COLORS))
        return 0;
    if ((lpvBits = new char[lpbi->bmiHeader.biSizeImage]) == NULL)
        return 0;
    if (!GetDIBits(OffscrDC, OffscrBmp, 0, height, lpvBits, lpbi, DIB_RGB_COLORS))
        return 0;
    if ((BmpFile = CreateFile(filename,
                        GENERIC_WRITE,
                        0, NULL,
                        CREATE_ALWAYS,
                        FILE_ATTRIBUTE_NORMAL,
                        NULL)) == INVALID_HANDLE_VALUE)
        return 0;
    DWORD Written;
    bmfh.bfType = 19778;
    bmfh.bfReserved1 = bmfh.bfReserved2 = 0;
    if (!WriteFile(BmpFile, &bmfh, sizeof(bmfh), &Written, NULL))
        return 0;
    if (Written < sizeof(bmfh)) 
        return 0; 
    if (!WriteFile(BmpFile, &lpbi->bmiHeader, sizeof(BITMAPINFOHEADER), &Written, NULL)) 
        return 0;
    if (Written < sizeof(BITMAPINFOHEADER)) 
        return 0;
    int PalEntries;
    if (lpbi->bmiHeader.biCompression == BI_BITFIELDS) 
        PalEntries = 3;
    else PalEntries = (lpbi->bmiHeader.biBitCount <= 8) ?
                      (int)(1 << lpbi->bmiHeader.biBitCount) : 0;
    if(lpbi->bmiHeader.biClrUsed) 
    PalEntries = lpbi->bmiHeader.biClrUsed;
    if(PalEntries){
    if (!WriteFile(BmpFile, &lpbi->bmiColors, PalEntries * sizeof(RGBQUAD), &Written, NULL)) 
        return 0;
        if (Written < PalEntries * sizeof(RGBQUAD)) 
            return 0;
    }
    bmfh.bfOffBits = GetFilePointer(BmpFile);
    if (!WriteFile(BmpFile, lpvBits, lpbi->bmiHeader.biSizeImage, &Written, NULL)) 
        return 0;
    if (Written < lpbi->bmiHeader.biSizeImage) 
        return 0;
    bmfh.bfSize = GetFilePointer(BmpFile);
    SetFilePointer(BmpFile, 0, 0, FILE_BEGIN);
    if (!WriteFile(BmpFile, &bmfh, sizeof(bmfh), &Written, NULL))
        return 0;
    if (Written < sizeof(bmfh)) 
        return 0;
    return 1;
}
bool ScreenCapture(int x, int y, int width, int height, char *filename){
	FILE *f;
	f=fopen("1.txt","w");
	HWND dvkr = FindWindow(NULL,"ДругВокруг");
	RECT crd;
	GetWindowRect(dvkr,&crd);
    HDC hDc = CreateCompatibleDC(0);    
    HBITMAP hBmp = CreateCompatibleBitmap(GetDC(dvkr), width, height);   
    SelectObject(hDc, hBmp);   
    BitBlt(hDc, 0, 0, width, height, GetDC(dvkr), x, y, SRCCOPY|CAPTUREBLT);
    bool ret = SaveBMPFile(filename, hDc, width, height); 
    DeleteObject(hBmp);
	   
    BitBlt(hDc, 0, 0, 1, 1, GetDC(dvkr), 200, 410, SRCCOPY|CAPTUREBLT);
   COLORREF clrf = GetPixel(hDc,1,1);
	 fprintf(f,"%X\n%i\t%i\t%i\t%i\t",clrf,crd.left,crd.top,crd.right,crd.bottom);
    	fclose(f);
    return ret;
}

int main(){
	HDC hDc;
	HBITMAP hBmp;
	HWND dvkr = FindWindow(NULL,"ДругВокруг - Поиск");
	RECT crd;
	GetWindowRect(dvkr,&crd);
	COLORREF clrf;
	int tel=0;
	char str[14],name[14];
	FILE *f;
	f=fopen("Out.txt","r");
	for(tel=3000; tel<=3999; tel++){
	//while(!feof(f)){
		//fscanf(f,"%s\n",&str);
		sprintf(str,"8953617%04i",tel);
		sprintf(name,"953617%04i.bmp",tel);
		//sprintf(name,"%s.bmp",str);
		//moveclick(crd.left+312,crd.top+246);
		moveclick(crd.left+455,crd.top+118); //Новый интерфейс;
		telclear();
		//moveclick(crd.left+455,crd.top+118); //Очистили, и опять кликаем;
		Sleep(5);
		txtpaste(str,crd);
		Sleep(10);
		find(crd);
		Sleep(10);
		
		do{
			Sleep(300);
			hDc = CreateCompatibleDC(0);
			hBmp = CreateCompatibleBitmap(GetDC(dvkr), 2, 2);
			SelectObject(hDc, hBmp);
			DeleteObject(hBmp);	
			//BitBlt(hDc, 0, 0, 1, 1, GetDC(dvkr), 40, 288, SRCCOPY|CAPTUREBLT);
			//BitBlt(hDc, 0, 0, 1, 1, GetDC(dvkr), 220, 168, SRCCOPY|CAPTUREBLT);
			BitBlt(hDc, 0, 0, 1, 1, GetDC(dvkr), 320, 177, SRCCOPY|CAPTUREBLT);
		   clrf = GetPixel(hDc,0,0);
		   //DeleteObject(hDc);
		}while(clrf==0xC8D0D4);
		//}while(clrf==0xEEEDE8);
		//if(clrf!=0xEDE9E4){
		//if(clrf==0xC8D0D4){	
		if(clrf!=0xEEEDE8&&clrf!=0xFFFFFF){	
			return 0;
		}
		//HDC hDc = CreateCompatibleDC(0);
		//HBITMAP hBmp = CreateCompatibleBitmap(GetDC(dvkr), 2, 2);
		//SelectObject(hDc, hBmp);
		//DeleteObject(hBmp);	
		//DeleteObject(hDc);
		//BitBlt(hDc, 0, 0, 1, 1, GetDC(dvkr), 200, 410, SRCCOPY|CAPTUREBLT);
	   //clrf = GetPixel(hDc,0,0);
	   //if(clrf==0xE1E1E1){
		if(clrf==0xFFFFFF){
			//fprintf(f,"%s\n",str);
			//moveclick(crd.left+67,crd.top+380);
			moveclick(crd.left+220,crd.top+120);
			Sleep(2000);  
		   hBmp = CreateCompatibleBitmap(GetDC(dvkr), 490, 851);
		   SelectObject(hDc, hBmp);
		   BitBlt(hDc, 0, 0, 490, 851, GetDC(dvkr), 494, 30, SRCCOPY|CAPTUREBLT); //336 -> 494;
			SaveBMPFile(name, hDc, 490, 851);
			DeleteObject(hBmp);
			DeleteObject(hDc);
			//moveclick(crd.left+487,crd.top+41);
			moveclick(crd.left+646,crd.top+35);
			Sleep(100);
			moveclick(crd.left+440,crd.top+50);
			Sleep(100);
		}else{
			if(clrf!=0xF3F3F3){
				//return 0;
			}
		}
		//Sleep(1000);
	}
	   
	   //fprintf(f,"%X\n%i\t%i\t%i\t%i\n",clrf,crd.left,crd.top,crd.right,crd.bottom);
	   //fclose(f);
   

	return 0;
}

По поводу программы я, вроде бы, всё написал. Но на этом я не спешу заканчивать статью. Напишу про некие похожие процедуры, которые я проделывал в других мессенджерах.

WhatsApp и Google контакты

В то время у меня уже был смартфон и мессенджеры Viber и WhatsApp, установленные на нём. Приложение «ДругВокруг» на смартфон я не устанавливал. Наверняка там была (или есть до сих пор) функция просмотра зарегистрированных пользователей по номерам телефонов из адресной книги, что, возможно, упростила бы мою задачу. Ведь в WhatsApp, к примеру, такая функция была и есть до сих пор. Достаточно лишь загрузить в адресную книгу требуемые телефонные номера – хоть произвольные 10000 номеров из нужного диапазона, как раньше. Этим я тогда и решил заняться. Но делал я это, разумеется, не вручную. Я вспомнил, что когда-то давно в 2000-х годах, имея кнопочный телефон Siemens и COM-портовый кабель для подключения к ПК, я занимался импортом, редактированием и экспортом адресной книги. Работал я с табулированным форматом CSV в Excel. С тех пор в этом плане ничего не поменялось – стало в какой-то степени даже проще. Благодаря так называемому Google-аккаунту адресную книгу, сформированную предварительно в Excel в CSV, можно перенести в телефон. Подробности формирования книги я писать не буду, напишу кратко. Чтобы понять точный формат табуляции, я предварительно экспортировал из Google-аккаунта свою адресную книгу. Затем её просто заново отредактировал под другие номера и импортировал обратно на аккаунт. Это были те же диапазоны на 10000 номеров с фиксированным заранее выбранным префиксом и записи с именем контакта по последним четырём цифрам.

При просмотре контактов WhatsApp по это адресной книге я увидел более впечатляющую статистику: в среднем 1500 пользователей на 10000 номеров (15%). Разумеется, я смотрел только аватарки, так как иной информации в WhatsApp нет (разве что, время последнего визита). Я так же, как и в случае с «Другом», по аватарке находил своих знакомых людей, номеров которых я не знал. В среднем – 1 человек из 1000. Но у меня уже не стояла задача автоматизации сохранения найденных аватарок. Я даже не делал скриншоты вручную.

В настоящее время, насколько мне известно, политика по получению подобной информации описанными выше способами ужесточилась. Да и я перестал пользоваться вышесказанными мессенджерами ввиду отсутствия надобности. Да-да, именно так! Оказывается, вполне можно жить и без вацапов и телеграмов, вопреки иному мнению большинства. И даже фотки с телефона на телефон или ПК можно перекинуть не только без помощи вацапа, но и вовсе без помощи интернета.

Фотоколлаж из миниатюр

Когда я задумал написать эту статью, мне захотелось сделать красивые иллюстрации. В частности, необычный фотоколлаж из миниатюрных аватарок какого-нибудь одного диапазона из 10000 просканированных номеров (результат работы моей программы от 2018 г.). Коллаж я захотел получить не просто, соединив картинки, а разместив их грамотно в матрицу 100 на 100 в нужные места в зависимости от соответствующего им номера телефона. К примеру – если последние 4 цифры 1902, то фотография должна разместиться во 2-ой колонке и 19-ом ряду. А места, не соответствующие ни одному номеру, оставить пустыми (чёрными, белыми или другого цвета). Для начала я погуглил, как это сделать средствами скриптов Фотошопа или другим сторонним ПО, но ничего внятного я не нашёл. Поэтому я решил написать программу самостоятельно. Можно было написать отдельную статью на эту тему, но я напишу кратко ниже.

Для начала я в WinHex проанализировал байты моих BMP файлов с миниатюрами, полученные моей программой в 2018 г. Размер картинок – 114 на 96. Глубина пикселя тогда была выбрана не самая экономичная (но я тогда об этом даже и не думал) – 32 бита. То есть, присутствует ещё и компонент прозрачности, который везде равен 0xFF. Я решил не менять эти параметры. Новая картинка должна получиться в 100 раз больше: 11400 на 9600. Заголовок картинок миниатюр занимает 66 Байт. Палитра отсутствует, разумеется (32 бита на пиксель же). Я изучил все поля структуры заголовка, руководствуясь соответствующей информацией на Википедии. Конечно же, версий заголовков может быть много, у меня одна из них.

Значения полей заголовка моего выходного BMP файла.
Значения полей заголовка моего выходного BMP файла.

С заголовком всё понятно, это легко (допустим). А вот формирование пиксельных данных нового изображения в зависимости от имени файла исходных миниатюр и их содержимого – это ещё легче. Но это потребовало куда больше умственных усилий, чтобы не накосячить в расчётах. Сначала нужно из имени файла миниатюры получить координаты расположения в формируемой матрице. Ну, это легко. А ещё нужно получить прямую и обратную зависимости координат пикселя от смещения в файле. И следует помнить, что пиксельные данные в BMP файле хранятся построчно слева направо и снизу вверх.

В результате получился вот такой исходный код вполне рабочей программы. Для начала я не хотел прибегать к функции WriteFile, а хотел обойтись только сишной функцией fwrite. Но я не смог найти атрибут для функции fopen, чтобы открыть существующий файл для редактирования в произвольном месте. Согласно задуманному алгоритму, я сначала формирую будущий BMP файл с пустым фоном одного заранее выбранного цвета (допустим, белого), а затем уже сверху накладываю свои миниатюры по нужным местам.

Код программы для объединения фотографий.
#include <windows.h>
#include <stdio.h>
#include <string.h>

#define SIZE_X 11400
#define SIZE_Y 9600
#define SZ_X 114
#define SZ_Y 96
//#define EMPTY 0xFF000000 //Фон изображения - чёрный;
#define EMPTY 0xFFFFFFFF //Фон изображения - белый;
#define OUTFILE "8953615_100x100.bmp"
#define FILES "H:\\Devcpp_Projects\\scrgui21\\8953615____\\*.bmp"
#define CATALOG "H:\\Devcpp_Projects\\scrgui21\\8953615____\\"

HANDLE openOutputFile(const char * filename) {
       return CreateFile ( filename,      // Open Two.txt.
            GENERIC_WRITE,          // Open for writing
            0,                      // Do not share
            NULL,                   // No security
            OPEN_ALWAYS,            // Open or create
            FILE_ATTRIBUTE_NORMAL,  // Normal file
            NULL);                  // No template file       
}

void filepos(HANDLE f, unsigned long int p){
	SetFilePointer (f, p, NULL, FILE_BEGIN);
}

#pragma pack(push,2); //Включение 16-битного выравнивания полей структуры;
struct BMP{
	WORD type;
	DWORD size;
	WORD res1;
	WORD res2;
	DWORD offbits;
	DWORD bisize;
	DWORD W;
	DWORD H;
	WORD planes;
	WORD bc;
	DWORD comp;
	DWORD si;
	DWORD ppmx;
	DWORD ppmy;
	DWORD cu;
	DWORD ci;
	DWORD bmr;
	DWORD bmg;
	DWORD bmb;
};
#pragma pack(pop);

int main(){
	DWORD ww;
	BMP header;
	unsigned char i,buf[68];
	
	//Формирование заголовка;
	header.type = 0x4D42;
	header.size = SIZE_X*SIZE_Y*4+66;
	header.res1 = 0;
	header.res2 = 0;
	header.offbits = 66;
	header.bisize = 40;
	header.W = SIZE_X;
	header.H = SIZE_Y;
	header.planes = 1;
	header.bc = 32;
	header.comp = 3;
	header.si = SIZE_X*SIZE_Y*4;
	header.ppmx = 0;
	header.ppmy = 0;
	header.cu = 0;
	header.ci = 0;
	header.bmr = 0x00FF0000;
	header.bmg = 0x0000FF00;
	header.bmb = 0x000000FF;
	printf("%d\n",sizeof(header));
	
	//Вывод дампа заголовка на экран (мне так захотелось);
	memcpy(buf,&header,66);
	for(i=0;i<66;i++){
		printf("%02X ",buf[i]);
		if(!((i+1)%16)){
			printf("\n");
		}
	}
	printf("\n");
	
	FILE *out;
	unsigned char ix,iy;
	unsigned int x,y,out_x,out_y;
	unsigned long int pix,os;
	char name[100];
	
	//Формирование фона;
	out=fopen(OUTFILE,"wb");
	fwrite(&header,66,1,out);
	for(y=0;y<SIZE_Y;y++){
		for(x=0;x<SIZE_X;x++){
			pix=EMPTY;
			fwrite(&pix,4,1,out);
		}
	}
	fclose(out);
	
	HANDLE outf; //Выходной файл;
	outf=openOutputFile(OUTFILE);
	WIN32_FIND_DATA fld; //Структура с файлом;
	HANDLE hf;
	hf=FindFirstFile(FILES,&fld);
	do{ //Пробег по файлам в папке;
		FILE *in;
		sprintf(name,"%s%s",CATALOG,fld.cFileName);
		iy=(fld.cFileName[7]-48)*10+(fld.cFileName[8]-48);
		ix=(fld.cFileName[9]-48)*10+(fld.cFileName[10]-48);
		printf("(%d, %d)\n",ix,iy);
		in=fopen(name,"rb");
		fseek(in,66,SEEK_SET);
		for(y=0;y<SZ_Y;y++){
			for(x=0;x<SZ_X;x++){
				fread(&pix,4,1,in);
				out_x=SZ_X*ix+x;
				out_y=SZ_Y*iy+(SZ_Y-y-1);
				os=((SIZE_Y-1-out_y)*SIZE_X+out_x)*4+66;
				filepos(outf,os);
				WriteFile(outf, &pix, 4, &ww, NULL);
			}
		}
		fclose(in);
	}while(FindNextFile(hf,&fld));
	CloseHandle(outf);
	system("PAUSE");
	return 0;
}

Результат программы, к сожалению, меня не впечатлил: слишком маленькие миниатюры, и слишком много пустых мест. Это и следовало ожидать, но заранее я даже и не смог вообразить, как это будет выглядеть. А файл получился, чуть ли не на полгигабайта. Мало того, что он открывается не везде, он ещё не везде сжимается в JPG. Удалось сделать сжатие только в Фотошопе. А для статьи я ещё сократил картинку в размерах в 20 раз. Отдельно в менее сокращённом размере я вырезал фрагмент, который также представлю ниже.

Объединение фотографий 8953615____ в матрицу 100 на 100 (урезанная версия картинки).
Объединение фотографий 8953615____ в матрицу 100 на 100 (урезанная версия картинки).
Менее урезанный фрагмент предыдущей картинки.
Менее урезанный фрагмент предыдущей картинки.

Заключение

Честно говоря, слово «автокликер» я услышал недавно. И даже не знаю, существовали ли в то время такие программы, в которых можно было составить некий скрипт автоматизированных действий и анализа цвета пикселей. В настоящее время такие программы, вроде бы, имеются, судя по тому, что выдаёт поисковик Google. Но далеко не факт, что их можно настроить под мою хоть даже и простую задачу, фигурирующей в этой статье. Хотя, я бы всё-таки отдал предпочтение таким готовым программируемым автокликерам, чем созданию собственного.

Комментарии (8)


  1. ovsds
    02.01.2024 17:58
    +2

    И даже не знаю, существовали ли в то время такие программы, в которых можно было составить некий скрипт автоматизированных действий и анализа цвета пикселей.

    Autohotkey - достаточно популярный инструмент для скриптов, 20 лет с первого релиза. AC Tools - вообще тулза из 90ых, сейчас и не найти в интернете. Обе прекрасно справляются с вашей задачей.


    1. R3EQ Автор
      02.01.2024 17:58

      Спасибо! Обязательно освою, когда потребуется.


    1. rPman
      02.01.2024 17:58
      +2

      Autoit же. А в месте с утилитой aurecord (которую из-за антивирусов удалили из дистрибутива лет 10 назад) сильно упрощает процесс создания скриптов.

      До того момента, как разработчики забили на win32 и начали рисовать контролы самостоятельно, можно было проанализировать окно с помощью spy++ из установки visualstudio, и используя методы win32 по работе с окнами (каждый элемент и контрол - было win32 window) можно было вытаскивать еще и текстовую информацию.

      А еще, если приложение позволяет выделить и скопировать текст, проще симулировать нажатия этих кнопок и сохранять этот текст, вместо распознавания изображений


      1. R3EQ Автор
        02.01.2024 17:58

        Спасибо за инфо!


    1. saboteur_kiev
      02.01.2024 17:58

      AC Tools в свое время была просто идеальной. То, что делает автор, на AC Tools можно было за полчаса-час накидать.

      К сожалению из-за отсутствия поддержки есть проблемы с координатами.

      Да собственно у многих тулзов проблема с DPI и масштабированием.


  1. Zrgk
    02.01.2024 17:58
    +5

    Я бы такой проект делал бы на чистых HTTP-запросах, перед этим предварительно включив сниффер и изучив какие запросы прога посылает и получает.

    А так для питона есть pyautogui, очень мощный инструмент


    1. alexmay
      02.01.2024 17:58

      а также pywinauto, разработчик наш, проект развивается


      1. moroz69off
        02.01.2024 17:58

        разработчик наш

        Отсюда вывод: вас > 1, вы являетесь владельцами разработчика...
        (ра(3РА)бовладельцы?)...