From 3bb57278f231668e0520e55b35451ff9c40f1c88 Mon Sep 17 00:00:00 2001 From: David Bernard Date: Fri, 21 Jan 2022 16:15:31 -0500 Subject: [PATCH 1/6] Initial commit! :D --- .gitignore | 13 + composer/composer.json | 7 + composer/composer.lock | 368 ++++++++++++++++++++++ src/controllers/members.php | 69 ++++ src/controllers/post-screenshots.php | 306 ++++++++++++++++++ src/index.php | 12 + src/models/CMemberPointsModel.php | 92 ++++++ src/robots.txt | 2 + src/templates/index.html.twig | 27 ++ src/templates/index_member_days.html.twig | 30 ++ src/templates/upload_form.twig | 7 + src/tesseract.ini | 16 + src/writable/cache/twig/.htaccess | 6 + src/writable/database/.htaccess | 6 + src/writable/logs/.htaccess | 6 + src/writable/uploads/.htaccess | 6 + 16 files changed, 973 insertions(+) create mode 100644 .gitignore create mode 100644 composer/composer.json create mode 100644 composer/composer.lock create mode 100644 src/controllers/members.php create mode 100644 src/controllers/post-screenshots.php create mode 100644 src/index.php create mode 100644 src/models/CMemberPointsModel.php create mode 100644 src/robots.txt create mode 100644 src/templates/index.html.twig create mode 100644 src/templates/index_member_days.html.twig create mode 100644 src/templates/upload_form.twig create mode 100644 src/tesseract.ini create mode 100755 src/writable/cache/twig/.htaccess create mode 100755 src/writable/database/.htaccess create mode 100755 src/writable/logs/.htaccess create mode 100755 src/writable/uploads/.htaccess diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4446ae5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# composer.lock +composer/vendor/ +vagrant/.vagrant +src/writable/cache/twig/* +!src/writable/cache/twig/.htaccess +src/writable/database/* +!src/writable/database/.htaccess +src/writable/logs/* +!src/writable/logs/.htaccess +src/writable/uploads/* +!src/writable/uploads/.htaccess diff --git a/composer/composer.json b/composer/composer.json new file mode 100644 index 0000000..c8b532f --- /dev/null +++ b/composer/composer.json @@ -0,0 +1,7 @@ +{ + "require": { + "thiagoalessio/tesseract_ocr": "^2.12", + "joserick/png-metadata": "^0.2.7", + "twig/twig": "^3.0" + } +} diff --git a/composer/composer.lock b/composer/composer.lock new file mode 100644 index 0000000..57779f0 --- /dev/null +++ b/composer/composer.lock @@ -0,0 +1,368 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "df0e55ca68c17d6ff6d25c21a4d16558", + "packages": [ + { + "name": "joserick/png-metadata", + "version": "0.2.7", + "source": { + "type": "git", + "url": "https://github.com/joserick/PNGMetadata.git", + "reference": "c46f0c604818b678981e01a6d3261045145928a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joserick/PNGMetadata/zipball/c46f0c604818b678981e01a6d3261045145928a3", + "reference": "c46f0c604818b678981e01a6d3261045145928a3", + "shasum": "" + }, + "require": { + "ext-exif": "*", + "php": ">=7.4" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.18", + "phpstan/phpstan-nette": "^0.12.6", + "symplify/easy-coding-standard": "^7.2", + "tracy/tracy": "^2.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "PNGMetadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "authors": [ + { + "name": "Jóse Erick Carreón", + "email": "joserick.92@gmail.com", + "homepage": "https://github.com/joserick" + }, + { + "name": "Jan Barášek", + "homepage": "https://baraja.cz" + } + ], + "description": "A PHP library for extract the metadata (XMP, EXIF) within a PNG format image.", + "keywords": [ + "exif", + "exiftool", + "metadata", + "png", + "xmp" + ], + "support": { + "issues": "https://github.com/joserick/PNGMetadata/issues", + "source": "https://github.com/joserick/PNGMetadata/tree/0.2.7" + }, + "time": "2020-11-02T22:47:54+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "thiagoalessio/tesseract_ocr", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/thiagoalessio/tesseract-ocr-for-php.git", + "reference": "0f10bd7b02bdcba59c4fbd98fbd93a56f93b09b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thiagoalessio/tesseract-ocr-for-php/zipball/0f10bd7b02bdcba59c4fbd98fbd93a56f93b09b7", + "reference": "0f10bd7b02bdcba59c4fbd98fbd93a56f93b09b7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/php-code-coverage": "^2.2.4 || ^9.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "thiagoalessio\\TesseractOCR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "thiagoalessio", + "email": "thiagoalessio@me.com" + } + ], + "description": "A wrapper to work with Tesseract OCR inside PHP.", + "keywords": [ + "OCR", + "Tesseract", + "text recognition" + ], + "support": { + "irc": "irc://irc.freenode.net/tesseract-ocr-for-php", + "issues": "https://github.com/thiagoalessio/tesseract-ocr-for-php/issues", + "source": "https://github.com/thiagoalessio/tesseract-ocr-for-php" + }, + "time": "2021-06-04T21:21:33+00:00" + }, + { + "name": "twig/twig", + "version": "v3.3.7", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "8f168c6ffa3ce76d1786b3cd52275424a3fc675b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/8f168c6ffa3ce76d1786b3cd52275424a3fc675b", + "reference": "8f168c6ffa3ce76d1786b3cd52275424a3fc675b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.3.7" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2022-01-03T21:15:37+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/src/controllers/members.php b/src/controllers/members.php new file mode 100644 index 0000000..589632c --- /dev/null +++ b/src/controllers/members.php @@ -0,0 +1,69 @@ + './writable/cache/twig/', + 'debug' => TRUE +)); + +date_default_timezone_set('UTC'); + +// Get dates and members from database. +$aAll = (new CMemberPointsModel)->GetLatestDates('Québec Kingdóm'); +$AllDiff = ComputeMembersProgression($aAll); + +// Render HTML +echo $twig->render('index_member_days.html.twig', [ + 'days' => $AllDiff +]); + +/** + * Return associative array with points diff against previous day. For example: + * + * [1639173778] => Array + * ( + * [0] => Array + * ( + * [name] => Name1 + * [pts] => 6368 + * [diff] => 27 + * ) + * [1] => Array + * ( + * [name] => Name2 + * [pts] => 6107 + * [diff] => 30 + * ) + * ) + */ +function ComputeMembersProgression($aTimeGroups) +{ + $aAllProgress = array(); + $aTimeGroupKeys = array_keys($aTimeGroups); + for ($i = 0; $i < count($aTimeGroupKeys); $i++) { + $aMemberPoints = $aTimeGroups[$aTimeGroupKeys[$i]]; + $aMemberPointsOld = array(); + + // If still an older date to come + if ($i < count($aTimeGroupKeys) - 1) { + $aMemberPointsOld = $aTimeGroups[$aTimeGroupKeys[$i + 1]]; + } + + $aMemberProgress = array(); + foreach ($aMemberPoints as $szName => $uPoints) { + $uDiff = null; + if (array_key_exists($szName, $aMemberPointsOld)) { + $uDiff = $uPoints - $aMemberPointsOld[$szName]; + } + + $aMemberProgress[] = array('name' => $szName, 'pts' => $uPoints, 'diff' => $uDiff); + } + $aAllProgress[$aTimeGroupKeys[$i]] = $aMemberProgress; + } + return $aAllProgress; +} diff --git a/src/controllers/post-screenshots.php b/src/controllers/post-screenshots.php new file mode 100644 index 0000000..650f40a --- /dev/null +++ b/src/controllers/post-screenshots.php @@ -0,0 +1,306 @@ + E_ALL ? E_ALL : $uReportLevel)); +ini_set('display_errors', '1'); + +require_once __DIR__ . '/../../composer/vendor/autoload.php'; + +include 'models/CMemberPointsModel.php'; + +use thiagoalessio\TesseractOCR\TesseractOCR; +use PNGMetadata\PNGMetadata; + +// Config +$g_UPLOAD_DIR = 'writable/uploads'; +$g_CLEANUP_UPLOADS = false; +date_default_timezone_set('UTC'); + +$g_sqlModel = new CMemberPointsModel; + +if (count($_FILES) > 0) +{ + HandlePost(); +} + +echo "
Go Back"; + +function HandlePost() +{ + global $g_UPLOAD_DIR; + global $g_sqlModel; + + if (isset($g_UPLOAD_DIR)) { + $aFiles = GetUploadedFiles(); + } else { + // array_filter is used to remove empty entries, just in case. + $aFiles = array_filter($_FILES['upload']['tmp_name']); + } + + if (count($aFiles) > 0) { + $aAll = array(); + + foreach ($aFiles as $szFilename) { + CropImage($szFilename); + + $aPart = ExtractFromImage($szFilename); + + if ($aPart !== -1) { + // Append and magically ignore duplicates + // TODO: This does not support uploading screenshots from differente date (since array indexed by position). Need an index by date. + $aAll += $aPart; + } + } + + // Sort by array key (i.e. position) + ksort(/*INOUT*/$aAll, SORT_NUMERIC); + + // Insert in database; + $g_sqlModel->InsertMembers('Québec Kingdóm', $aAll); + + echo "
" . serialize($aAll) . "
"; + echo "
\nDone!"; + } +} + +/** + * Crop screenshot because OCR works a lot better. + * + * Note: Using UZN files didn't worked as good (even with exact same coordinates). + */ +function CropImage($szFilename) +{ + $type = exif_imagetype($szFilename); + if ($type == IMAGETYPE_PNG) { + $im = imagecreatefrompng($szFilename); + } elseif ($type == IMAGETYPE_JPEG) { + $im = imagecreatefromjpeg($szFilename); + } else { + echo "Unsupported format $type"; + exit; + } + + // Screen name + $im2 = imagecrop($im, ['x' => 14, 'y' => 27, 'width' => 89, 'height' => 19]); + // Convert to monochrome image. + imagefilter($im2, IMG_FILTER_GRAYSCALE); + imagefilter($im2, IMG_FILTER_BRIGHTNESS, 10); + imagefilter($im2, IMG_FILTER_CONTRAST, -255); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-screen.png")) { + unlink("$szFilename-screen.png"); + } + imagepng($im2, "$szFilename-screen.png"); + imagedestroy($im2); + } + + // Better rendering if we increase contrast, which increase text and reduce graphics. + imagefilter($im, IMG_FILTER_GRAYSCALE); + imagefilter($im, IMG_FILTER_BRIGHTNESS, 20); + imagefilter($im, IMG_FILTER_CONTRAST, -150); + + if (file_exists("$szFilename-filtered.png")) { + unlink("$szFilename-filtered.png"); + } + imagepng($im, "$szFilename-filtered.png"); + + // TODO: Coordinates are hardcoded for an iPhone 7 + + // Positions + $im2 = imagecrop($im, ['x' => 294, 'y' => 214, 'width' => 60, 'height' => 520]); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-pos.png")) { + unlink("$szFilename-pos.png"); + } + imagepng($im2, "$szFilename-pos.png"); + imagedestroy($im2); + } + + // Names + $im2 = imagecrop($im, ['x' => 442, 'y' => 214, 'width' => 200, 'height' => 520]); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-names.png")) { + unlink("$szFilename-names.png"); + } + imagepng($im2, "$szFilename-names.png"); + imagedestroy($im2); + } + + // Points + $im2 = imagecrop($im, ['x' => 778, 'y' => 214, 'width' => 80, 'height' => 520]); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-pts.png")) { + unlink("$szFilename-pts.png"); + } + imagepng($im2, "$szFilename-pts.png"); + imagedestroy($im2); + } + + imagedestroy($im); +} + +function ExtractFromImage($szFilename) +{ + $bufferToForceBrowserToDisplay = str_repeat(" ", 4096); // some browser wait to display despite having receiving it. + + echo "Processing $szFilename...\n
".$bufferToForceBrowserToDisplay; + flush(); + + // Extract date from PNG file exif. + $szCreatedDate = GetImageCreationDate($szFilename); + $createdDateGroup = GetReferenceDate($szCreatedDate); + $szScreenType = false; + + // First look if it's a supported screenshot type. + $aScreenName = OCR("$szFilename-screen.png", 6, false); + if (array_search("ALLIANCE", $aScreenName) !== false) { + $szScreenType = "Alliance Ranking"; + } elseif ( + array_search("HUNT", $aScreenName) !== false || + array_search("CLASSEMENT", $aScreenName) !== false + ) { + $szScreenType = "Treasure Hunt Ranking"; + } else { + echo "-> Error, cannot find Alliance or Treasure Hunt ranking.\n
"; + echo "--> RAW output is " . print_r($aScreenName, true) . "\n
"; + return -1; + } + + //TODO: Support TH. + if ($szScreenType == "Treasure Hunt Ranking") { + echo "-> Treasure Hunt ranking are not yet supported. Skipping.\n
"; + return -1; + } + + $aPositions = OCR("$szFilename-pos.png", 6, true); // positions work a bit better with psm(6) + $aNames = OCR("$szFilename-names.png", 4, false); + $aPoints = OCR("$szFilename-pts.png", 4, true); + + // echo "-> NAME RAW data: " . print_r($aNames, true) . "
\n"; + //TODO: Hardcoded for iPhone7 screenshots with 6 players visible. + if (count($aPoints) != 6 || count($aPositions) != 6 || count($aNames) < 11) { + echo "Error, did not found 6 players for " . $szFilename . "\n
"; + echo "-> RAW data: " . print_r($aNames, true) . "
\n"; + return -1; + } + + global $g_CLEANUP_UPLOADS; + if ($g_CLEANUP_UPLOADS) { + unlink($szFilename); + unlink("$szFilename-pos.png"); + unlink("$szFilename-names.png"); + unlink("$szFilename-pts.png"); + unlink("$szFilename-filtered.png"); + } + + // Build array indexed by position. + $aComplete = array(); + $uStartingIndexForNames = (count($aNames) == 12 ? 1 : 0); + for ($i = 0; $i < count($aPositions); $i++) { + $szName = $aNames[$i + (1 * $i) + $uStartingIndexForNames]; // complex algo because names have junks in-between. + $aComplete[$aPositions[$i]] = array( + 'fullname' => $szName, + 'name_id' => preg_replace('/[^A-Za-z0-9#]/', '', $szName), // keep only alpha-numeric chars, except the #. + 'pts' => $aPoints[$i], + 'date' => $szCreatedDate, + 'time_group' => $createdDateGroup + ); + } + return $aComplete; +} + +function OCR($szFilename, $uPsm, $bNumbersOnly) +{ + // $aAcceptedChars = array_merge(range('A','Z'), range('a','z'), range(0,9)); + // array_push($aAcceptedChars, '#', '*', '\'', ' ', '_', '-'); + $aNumbersOnly = array(); + if ($bNumbersOnly) { + $aNumbersOnly = range(0, 9); + } + + $blob = (new TesseractOCR($szFilename)) + ->psm($uPsm) + ->allowlist($aNumbersOnly) + ->configFile('tesseract.ini') + ->run(); + + // $blob = (new TesseractOCR($szFilename)) + // ->psm($uPsm) + // ->tessdataDir('./tessdata/') + // ->lang('fra') + // ->allowlist($aNumbersOnly) + // ->userWords('words.txt') + // ->configFile('config2.ini') + // ->run(); + + $aLines = array_values(array_filter(explode("\n", $blob))); + // print_r($aLines); + return $aLines; +} + +/** + * Return in the format: + * 2021:12:02 13:55:01 + */ +function GetImageCreationDate($szFilename) +{ + $dateTimeOriginal = ''; + + if (PNGMetadata::isPNG($szFilename)) { + $png_metadata = new PNGMetadata($szFilename); // return a 'ArrayObject' or 'Exception' + $dateTimeOriginal = $png_metadata->get('exif:DateTimeOriginal'); + } else { + $exif_metadata = exif_read_data($szFilename); + $dateTimeOriginal = $exif_metadata['DateTimeOriginal']; + } + + return $dateTimeOriginal; +} + +/** + * Return only the date (no time) in unix timestamp. + */ +function GetReferenceDate($szDateTime) +{ + $dateTime = new DateTime($szDateTime); + $dateOnly = new DateTime($dateTime->format('Y-m-d')); + return $dateOnly->format('U'); +} + +/** + * Copy the uploaded files, and return the filenames. Mostly useful for debugging. + */ +function GetUploadedFiles() +{ + global $g_UPLOAD_DIR; + + // print_r($_FILES); + $uNbFiles = count($_FILES['upload']['name']); + + $aFilenames = array(); + // Loop through each file + for ($i = 0; $i < $uNbFiles; $i++) { + // Get the temp file path + $tmpFilePath = $_FILES['upload']['tmp_name'][$i]; + + // Make sure we have a file path + if ($tmpFilePath != "") { + // Setup our new file path + $newFilePath = $g_UPLOAD_DIR . '/' . $_FILES['upload']['name'][$i]; + + // Remove old file, else moving file may fail. + if (file_exists($newFilePath)) { + unlink($newFilePath); + } + + // Move the uploaded file from tmpdir to the new destination. + if (move_uploaded_file($tmpFilePath, $newFilePath)) { + echo "Moved image to $newFilePath\n
"; + array_push($aFilenames, $newFilePath); + } + } + } + return $aFilenames; +} diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..a0dcca8 --- /dev/null +++ b/src/index.php @@ -0,0 +1,12 @@ + 0) +{ + include_once __DIR__ . '/controllers/post-screenshots.php'; +} +else +{ + include_once __DIR__ . '/controllers/members.php'; +} diff --git a/src/models/CMemberPointsModel.php b/src/models/CMemberPointsModel.php new file mode 100644 index 0000000..5ff79c5 --- /dev/null +++ b/src/models/CMemberPointsModel.php @@ -0,0 +1,92 @@ +m_pDb = new SQLite3($this->m_szPath, SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); + chmod($this->m_szPath, 0664); // make sure we can write inside afterwards. + + $this->m_pDb->exec('CREATE TABLE IF NOT EXISTS member_points( + id INTEGER PRIMARY KEY, alliance_id TEXT, name_id TEXT, fullname TEXT, mp_points INT, mp_real_datetime TEXT, mp_time_group INT, + UNIQUE(name_id, mp_time_group) ON CONFLICT REPLACE)'); + } + + public function __destruct() + { + // Close the DB, even if done automatically when script ends. + $this->m_pDb->close(); + } + + public function InsertMembers($szAllianceId, $aMembers) + { + // Prepare the query for faster batch insert. + $sqlStatement = $this->m_pDb->prepare('INSERT INTO member_points (alliance_id, name_id, fullname, mp_points, mp_real_datetime, mp_time_group) + VALUES (:alliance_id, :name_id, :fullname, :mp_points, :mp_real_datetime, :mp_time_group)'); + + $this->m_pDb->exec('BEGIN'); + foreach ($aMembers as $m) + { + $sqlStatement->bindValue(':alliance_id', $szAllianceId, SQLITE3_TEXT); + $sqlStatement->bindValue(':name_id', $m['name_id'], SQLITE3_TEXT); + $sqlStatement->bindValue(':fullname', $m['fullname'], SQLITE3_TEXT); + $sqlStatement->bindValue(':mp_points', $m['pts'], SQLITE3_INTEGER); + $sqlStatement->bindValue(':mp_real_datetime', $m['date'], SQLITE3_TEXT); + $sqlStatement->bindValue(':mp_time_group', $m['time_group'], SQLITE3_INTEGER); + $res = $sqlStatement->execute(); + $sqlStatement->reset(); + } + $this->m_pDb->exec('COMMIT'); + } + + /** + * Ordered by most recent first, and order by points. For example: + * + * [1639173778] => Array + * ( + * [Name1] => 4829 + * [Name2] => 4703 + * [Name3] => 3910 + * ) + */ + public function GetLatestDates($szAllianceId) + { + $aDates = array(); + $aFull = array(); + $u_NB_DATES_MAX = 5; + + // First get latest time groups. + $sqlStatement = $this->m_pDb->prepare('SELECT mp_time_group FROM member_points WHERE alliance_id = :alliance_id' . + " GROUP BY mp_time_group ORDER BY mp_time_group DESC LIMIT $u_NB_DATES_MAX"); + $sqlStatement->bindValue(':alliance_id', $szAllianceId, SQLITE3_TEXT); + $res = $sqlStatement->execute(); + + while ($row = $res->fetchArray(SQLITE3_NUM)) { + array_push($aDates, $row[0]); + } + + // Then members per time groups. + $sqlStatement = $this->m_pDb->prepare('SELECT name_id, mp_points FROM member_points ' . + ' WHERE alliance_id = :alliance_id AND mp_time_group = :mp_time_group ORDER BY mp_points DESC'); + + for ($i = 0; $i < count($aDates); $i++) { + $uDate = $aDates[$i]; + $sqlStatement->bindValue(':alliance_id', $szAllianceId, SQLITE3_TEXT); + $sqlStatement->bindValue(':mp_time_group', $uDate, SQLITE3_INTEGER); + $res = $sqlStatement->execute(); + $sqlStatement->reset(); + + while ($row = $res->fetchArray(SQLITE3_ASSOC)) { + $aFull[$uDate][$row['name_id']] = $row['mp_points']; + } + } + + return $aFull; + } +} diff --git a/src/robots.txt b/src/robots.txt new file mode 100644 index 0000000..9e60f97 --- /dev/null +++ b/src/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/src/templates/index.html.twig b/src/templates/index.html.twig new file mode 100644 index 0000000..7d88ab1 --- /dev/null +++ b/src/templates/index.html.twig @@ -0,0 +1,27 @@ + + + + + + + + RoC Alliance Management + {% block head %} + {% endblock head %} + + + +
+

Rise of Cultures - Log members points from screenshots

+ + {% block content %} + {% endblock content %} +
+ + {% block scripts %} + {% endblock scripts %} + + \ No newline at end of file diff --git a/src/templates/index_member_days.html.twig b/src/templates/index_member_days.html.twig new file mode 100644 index 0000000..84eba8f --- /dev/null +++ b/src/templates/index_member_days.html.twig @@ -0,0 +1,30 @@ +{% extends 'index.html.twig' %} + +{% block content %} + + {% include 'upload_form.twig' %} + + + + {% for day in days|keys %} + + {% endfor %} + + + {% for day in days %} + + {% endfor %} + +
{{day|date('M d Y')}}
+ {% for member in day %} + {% if member.diff is null %} + {{member.name}}: {{member.pts}}
+ {% elseif member.diff == 0 %} + {{member.name}}: {{member.pts}} (+0)
+ {% else %} + {{member.name}}: {{member.pts}} (+{{member.diff}})
+ {% endif %} + {% endfor %} +
+ +{% endblock content %} diff --git a/src/templates/upload_form.twig b/src/templates/upload_form.twig new file mode 100644 index 0000000..d6be57d --- /dev/null +++ b/src/templates/upload_form.twig @@ -0,0 +1,7 @@ + +
+

Alliance:

+

Screenshots:

+ + +
diff --git a/src/tesseract.ini b/src/tesseract.ini new file mode 100644 index 0000000..22083fb --- /dev/null +++ b/src/tesseract.ini @@ -0,0 +1,16 @@ +; config file for Tesseract OCR +load_system_dawg 0 +load_freq_dawg 0 +load_punc_dawg 0 +load_number_dawg 0 +load_unambig_dawg 0 +load_bigram_dawg 0 +load_fixed_length_dawgs 0 + +load_system_dawg false +load_freq_dawg false +load_punc_dawg false +load_number_dawg false +load_unambig_dawg false +load_bigram_dawg false +load_fixed_length_dawgs false diff --git a/src/writable/cache/twig/.htaccess b/src/writable/cache/twig/.htaccess new file mode 100755 index 0000000..f24db0a --- /dev/null +++ b/src/writable/cache/twig/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/src/writable/database/.htaccess b/src/writable/database/.htaccess new file mode 100755 index 0000000..f24db0a --- /dev/null +++ b/src/writable/database/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/src/writable/logs/.htaccess b/src/writable/logs/.htaccess new file mode 100755 index 0000000..f24db0a --- /dev/null +++ b/src/writable/logs/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/src/writable/uploads/.htaccess b/src/writable/uploads/.htaccess new file mode 100755 index 0000000..f24db0a --- /dev/null +++ b/src/writable/uploads/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + From 12911daf39fa09f45ce241f6f9cb6732a0fc7510 Mon Sep 17 00:00:00 2001 From: dbernard Date: Fri, 21 Jan 2022 16:16:59 -0500 Subject: [PATCH 2/6] Add Vagrant support for easy developement setup. --- vagrant/Vagrantfile | 60 +++++++++++++++++++++++++++++++++++++++++++++ vagrant/setup.sh | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 vagrant/Vagrantfile create mode 100644 vagrant/setup.sh diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile new file mode 100644 index 0000000..19b57b0 --- /dev/null +++ b/vagrant/Vagrantfile @@ -0,0 +1,60 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Synced folder inside the VM +LOCAL_PC_PATH = ".." +MOUNTED_VM_PATH = "/var/www/roc-alliance-mgmt" + +Vagrant.configure("2") do |config| + + config.vm.box = "geerlingguy/ubuntu2004" + + config.vm.hostname = "ubuntu-vagrant" + + # Forward some DGW network . + config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "0.0.0.0" #WEB + + # Prevent vagrant asking everytime by specifying the interface on you PC, for example: + config.vm.network "public_network", bridge: "en0: Wi-Fi (AirPort)" + + # This mounts LOCAL_PC_PATH directly from local files on your PC. + config.vm.synced_folder LOCAL_PC_PATH, MOUNTED_VM_PATH + + config.vm.provider "virtualbox" do |vb| + # Display the VirtualBox GUI when booting the machine (useful for troubleshooting). + #vb.gui = true + vb.memory = "768" + + # Disable Audio and USB in VirtualBox (may prevent having to install add-ons on your PC). + vb.customize ["modifyvm", :id, "--audio", "none"] + vb.customize ["modifyvm", :id, "--usb", "off"] + end + + # Set timezone + config.vm.provision "shell", inline: <<-END + sudo rm /etc/localtime && sudo ln -s /usr/share/zoneinfo/America/Toronto /etc/localtime + END + + # Faster SSH login during boot time. See https://unix.stackexchange.com/questions/487742/system-is-booting-up-unprivileged-users-are-not-permitted-to-log-in-yet + config.vm.provision "shell", inline: <<-END + echo "Commenting out nologin authentication..." + sed -i 's/^.*pam_nologin.so$/#\0/' /etc/pam.d/login + END + + # Setup Apache + config.vm.provision "shell", path: "setup.sh", args: MOUNTED_VM_PATH + + # Easier web development with everything under vagrant user + config.vm.provision "shell", inline: <<-END + sed -i 's/APACHE_RUN_USER=.*/APACHE_RUN_USER=vagrant/g' /etc/apache2/envvars + sed -i 's/APACHE_RUN_GROUP=.*/APACHE_RUN_GROUP=vagrant/g' /etc/apache2/envvars + systemctl restart apache2 + END + + config.vm.post_up_message = <<-END + --------------------------------------------------------------- + Web app available at: http://127.0.0.1:8080 + --------------------------------------------------------------- + END + +end diff --git a/vagrant/setup.sh b/vagrant/setup.sh new file mode 100644 index 0000000..1f9c427 --- /dev/null +++ b/vagrant/setup.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +PROJECT_PATH="$1" +if [ -z "$PROJECT_PATH" ]; then + PROJECT_PATH="/var/www/my_web_app/" + echo "Path not provided in arguments, falling back to default $PROJECT_PATH" +fi + +echo "Installing dependencies with apt-get..." +apt-get update +# Tesseract +apt-get -y install tesseract-ocr +# My web app +apt-get -y install vim git unzip curl apache2 php php-sqlite3 php-gd +# Not sure if needed anymore +apt-get -y install php-cli php-mbstring + +# Install composer +echo "Installing composer..." +curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php && php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer; rm /tmp/composer-setup.php + +# TODO +# php.ini max upload size (2 MB by default) +# php.ini enable libsqlite3 +# php.ini max_execution_time > 30 sec (eu un timeout) + +###################################### +# Configure apache +###################################### +echo "Configuring apache..." + +# Enable required apache modules +a2enmod rewrite +a2enmod vhost_alias + +CONFIG_NAME=`basename $PROJECT_PATH` + +# Write virtual host +cat << END >/etc/apache2/sites-available/$CONFIG_NAME + + DocumentRoot "$PROJECT_PATH/src" + + + # Allow .htaccess to control web access (allow/deny). + AllowOverride All + + + +END + +# Replace default site +a2dissite 000-default.conf +a2ensite $CONFIG_NAME + +# Apply config +systemctl restart apache2.service From adbae5f2dabdd6b5b75d71f1a2b10e95b8f082f0 Mon Sep 17 00:00:00 2001 From: dbernard Date: Fri, 21 Jan 2022 21:02:52 -0500 Subject: [PATCH 3/6] Fixed vagrant setup. --- composer/composer.lock | 2 +- vagrant/setup.sh | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/composer/composer.lock b/composer/composer.lock index 57779f0..f17904b 100644 --- a/composer/composer.lock +++ b/composer/composer.lock @@ -364,5 +364,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/vagrant/setup.sh b/vagrant/setup.sh index 1f9c427..b53efb7 100644 --- a/vagrant/setup.sh +++ b/vagrant/setup.sh @@ -11,14 +11,19 @@ apt-get update # Tesseract apt-get -y install tesseract-ocr # My web app -apt-get -y install vim git unzip curl apache2 php php-sqlite3 php-gd +apt-get -y install vim git unzip curl apache2 php php-sqlite3 php-gd php-xml php-curl # Not sure if needed anymore -apt-get -y install php-cli php-mbstring +apt-get -y install php-cli php-mbstring # Install composer echo "Installing composer..." curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php && php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer; rm /tmp/composer-setup.php +# Install dependencies with composer +pushd $PROJECT_PATH/composer +composer install +popd + # TODO # php.ini max upload size (2 MB by default) # php.ini enable libsqlite3 @@ -36,7 +41,7 @@ a2enmod vhost_alias CONFIG_NAME=`basename $PROJECT_PATH` # Write virtual host -cat << END >/etc/apache2/sites-available/$CONFIG_NAME +cat << END >/etc/apache2/sites-available/$CONFIG_NAME.conf DocumentRoot "$PROJECT_PATH/src" @@ -50,7 +55,7 @@ END # Replace default site a2dissite 000-default.conf -a2ensite $CONFIG_NAME +a2ensite $CONFIG_NAME.conf # Apply config systemctl restart apache2.service From a08c964afef6b6af8f161d0220043ace53092e07 Mon Sep 17 00:00:00 2001 From: dbernard Date: Fri, 21 Jan 2022 21:27:48 -0500 Subject: [PATCH 4/6] Fixed warning message about composer running as root. --- vagrant/Vagrantfile | 6 ++++++ vagrant/setup.sh | 8 ++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile index 19b57b0..1fbad1f 100644 --- a/vagrant/Vagrantfile +++ b/vagrant/Vagrantfile @@ -51,6 +51,12 @@ Vagrant.configure("2") do |config| systemctl restart apache2 END + # Install dependencies with composer (without being root) + config.vm.provision "shell", privileged: false, args: MOUNTED_VM_PATH, inline: <<-END + cd $1/composer + composer install 2>&1 + END + config.vm.post_up_message = <<-END --------------------------------------------------------------- Web app available at: http://127.0.0.1:8080 diff --git a/vagrant/setup.sh b/vagrant/setup.sh index b53efb7..067c2d5 100644 --- a/vagrant/setup.sh +++ b/vagrant/setup.sh @@ -17,12 +17,8 @@ apt-get -y install php-cli php-mbstring # Install composer echo "Installing composer..." -curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php && php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer; rm /tmp/composer-setup.php - -# Install dependencies with composer -pushd $PROJECT_PATH/composer -composer install -popd +curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php && php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer +rm /tmp/composer-setup.php # TODO # php.ini max upload size (2 MB by default) From 422e53727d283258ec7e5531aa064ac7c1ef271f Mon Sep 17 00:00:00 2001 From: dbernard Date: Mon, 24 Jan 2022 21:54:24 -0500 Subject: [PATCH 5/6] Create a separate class for the image processing and OCR, instead of being mixed up in the upload controller. --- src/controllers/post-screenshots.php | 224 ++------------------------- src/utils/CImageOcrHelper.php | 213 +++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 212 deletions(-) create mode 100644 src/utils/CImageOcrHelper.php diff --git a/src/controllers/post-screenshots.php b/src/controllers/post-screenshots.php index 650f40a..2367817 100644 --- a/src/controllers/post-screenshots.php +++ b/src/controllers/post-screenshots.php @@ -9,14 +9,10 @@ require_once __DIR__ . '/../../composer/vendor/autoload.php'; include 'models/CMemberPointsModel.php'; +include 'utils/CImageOcrHelper.php'; -use thiagoalessio\TesseractOCR\TesseractOCR; -use PNGMetadata\PNGMetadata; - -// Config +// Comment out to cleanup intermediate images. $g_UPLOAD_DIR = 'writable/uploads'; -$g_CLEANUP_UPLOADS = false; -date_default_timezone_set('UTC'); $g_sqlModel = new CMemberPointsModel; @@ -32,6 +28,8 @@ function HandlePost() global $g_UPLOAD_DIR; global $g_sqlModel; + $ocrHelper = new CImageOcrHelper(); + if (isset($g_UPLOAD_DIR)) { $aFiles = GetUploadedFiles(); } else { @@ -41,11 +39,17 @@ function HandlePost() if (count($aFiles) > 0) { $aAll = array(); + $bufferToForceBrowserToDisplay = str_repeat(" ", 4096); // some browser wait to display despite having receiving data. foreach ($aFiles as $szFilename) { - CropImage($szFilename); + echo "Processing $szFilename...\n
".$bufferToForceBrowserToDisplay; + flush(); - $aPart = ExtractFromImage($szFilename); + $res = $ocrHelper->CropImage($szFilename); + + if ($res != -1) { + $aPart = $ocrHelper->ExtractFromImage($szFilename, isset($g_UPLOAD_DIR)); + } if ($aPart !== -1) { // Append and magically ignore duplicates @@ -65,210 +69,6 @@ function HandlePost() } } -/** - * Crop screenshot because OCR works a lot better. - * - * Note: Using UZN files didn't worked as good (even with exact same coordinates). - */ -function CropImage($szFilename) -{ - $type = exif_imagetype($szFilename); - if ($type == IMAGETYPE_PNG) { - $im = imagecreatefrompng($szFilename); - } elseif ($type == IMAGETYPE_JPEG) { - $im = imagecreatefromjpeg($szFilename); - } else { - echo "Unsupported format $type"; - exit; - } - - // Screen name - $im2 = imagecrop($im, ['x' => 14, 'y' => 27, 'width' => 89, 'height' => 19]); - // Convert to monochrome image. - imagefilter($im2, IMG_FILTER_GRAYSCALE); - imagefilter($im2, IMG_FILTER_BRIGHTNESS, 10); - imagefilter($im2, IMG_FILTER_CONTRAST, -255); - if ($im2 !== FALSE) { - if (file_exists("$szFilename-screen.png")) { - unlink("$szFilename-screen.png"); - } - imagepng($im2, "$szFilename-screen.png"); - imagedestroy($im2); - } - - // Better rendering if we increase contrast, which increase text and reduce graphics. - imagefilter($im, IMG_FILTER_GRAYSCALE); - imagefilter($im, IMG_FILTER_BRIGHTNESS, 20); - imagefilter($im, IMG_FILTER_CONTRAST, -150); - - if (file_exists("$szFilename-filtered.png")) { - unlink("$szFilename-filtered.png"); - } - imagepng($im, "$szFilename-filtered.png"); - - // TODO: Coordinates are hardcoded for an iPhone 7 - - // Positions - $im2 = imagecrop($im, ['x' => 294, 'y' => 214, 'width' => 60, 'height' => 520]); - if ($im2 !== FALSE) { - if (file_exists("$szFilename-pos.png")) { - unlink("$szFilename-pos.png"); - } - imagepng($im2, "$szFilename-pos.png"); - imagedestroy($im2); - } - - // Names - $im2 = imagecrop($im, ['x' => 442, 'y' => 214, 'width' => 200, 'height' => 520]); - if ($im2 !== FALSE) { - if (file_exists("$szFilename-names.png")) { - unlink("$szFilename-names.png"); - } - imagepng($im2, "$szFilename-names.png"); - imagedestroy($im2); - } - - // Points - $im2 = imagecrop($im, ['x' => 778, 'y' => 214, 'width' => 80, 'height' => 520]); - if ($im2 !== FALSE) { - if (file_exists("$szFilename-pts.png")) { - unlink("$szFilename-pts.png"); - } - imagepng($im2, "$szFilename-pts.png"); - imagedestroy($im2); - } - - imagedestroy($im); -} - -function ExtractFromImage($szFilename) -{ - $bufferToForceBrowserToDisplay = str_repeat(" ", 4096); // some browser wait to display despite having receiving it. - - echo "Processing $szFilename...\n
".$bufferToForceBrowserToDisplay; - flush(); - - // Extract date from PNG file exif. - $szCreatedDate = GetImageCreationDate($szFilename); - $createdDateGroup = GetReferenceDate($szCreatedDate); - $szScreenType = false; - - // First look if it's a supported screenshot type. - $aScreenName = OCR("$szFilename-screen.png", 6, false); - if (array_search("ALLIANCE", $aScreenName) !== false) { - $szScreenType = "Alliance Ranking"; - } elseif ( - array_search("HUNT", $aScreenName) !== false || - array_search("CLASSEMENT", $aScreenName) !== false - ) { - $szScreenType = "Treasure Hunt Ranking"; - } else { - echo "-> Error, cannot find Alliance or Treasure Hunt ranking.\n
"; - echo "--> RAW output is " . print_r($aScreenName, true) . "\n
"; - return -1; - } - - //TODO: Support TH. - if ($szScreenType == "Treasure Hunt Ranking") { - echo "-> Treasure Hunt ranking are not yet supported. Skipping.\n
"; - return -1; - } - - $aPositions = OCR("$szFilename-pos.png", 6, true); // positions work a bit better with psm(6) - $aNames = OCR("$szFilename-names.png", 4, false); - $aPoints = OCR("$szFilename-pts.png", 4, true); - - // echo "-> NAME RAW data: " . print_r($aNames, true) . "
\n"; - //TODO: Hardcoded for iPhone7 screenshots with 6 players visible. - if (count($aPoints) != 6 || count($aPositions) != 6 || count($aNames) < 11) { - echo "Error, did not found 6 players for " . $szFilename . "\n
"; - echo "-> RAW data: " . print_r($aNames, true) . "
\n"; - return -1; - } - - global $g_CLEANUP_UPLOADS; - if ($g_CLEANUP_UPLOADS) { - unlink($szFilename); - unlink("$szFilename-pos.png"); - unlink("$szFilename-names.png"); - unlink("$szFilename-pts.png"); - unlink("$szFilename-filtered.png"); - } - - // Build array indexed by position. - $aComplete = array(); - $uStartingIndexForNames = (count($aNames) == 12 ? 1 : 0); - for ($i = 0; $i < count($aPositions); $i++) { - $szName = $aNames[$i + (1 * $i) + $uStartingIndexForNames]; // complex algo because names have junks in-between. - $aComplete[$aPositions[$i]] = array( - 'fullname' => $szName, - 'name_id' => preg_replace('/[^A-Za-z0-9#]/', '', $szName), // keep only alpha-numeric chars, except the #. - 'pts' => $aPoints[$i], - 'date' => $szCreatedDate, - 'time_group' => $createdDateGroup - ); - } - return $aComplete; -} - -function OCR($szFilename, $uPsm, $bNumbersOnly) -{ - // $aAcceptedChars = array_merge(range('A','Z'), range('a','z'), range(0,9)); - // array_push($aAcceptedChars, '#', '*', '\'', ' ', '_', '-'); - $aNumbersOnly = array(); - if ($bNumbersOnly) { - $aNumbersOnly = range(0, 9); - } - - $blob = (new TesseractOCR($szFilename)) - ->psm($uPsm) - ->allowlist($aNumbersOnly) - ->configFile('tesseract.ini') - ->run(); - - // $blob = (new TesseractOCR($szFilename)) - // ->psm($uPsm) - // ->tessdataDir('./tessdata/') - // ->lang('fra') - // ->allowlist($aNumbersOnly) - // ->userWords('words.txt') - // ->configFile('config2.ini') - // ->run(); - - $aLines = array_values(array_filter(explode("\n", $blob))); - // print_r($aLines); - return $aLines; -} - -/** - * Return in the format: - * 2021:12:02 13:55:01 - */ -function GetImageCreationDate($szFilename) -{ - $dateTimeOriginal = ''; - - if (PNGMetadata::isPNG($szFilename)) { - $png_metadata = new PNGMetadata($szFilename); // return a 'ArrayObject' or 'Exception' - $dateTimeOriginal = $png_metadata->get('exif:DateTimeOriginal'); - } else { - $exif_metadata = exif_read_data($szFilename); - $dateTimeOriginal = $exif_metadata['DateTimeOriginal']; - } - - return $dateTimeOriginal; -} - -/** - * Return only the date (no time) in unix timestamp. - */ -function GetReferenceDate($szDateTime) -{ - $dateTime = new DateTime($szDateTime); - $dateOnly = new DateTime($dateTime->format('Y-m-d')); - return $dateOnly->format('U'); -} - /** * Copy the uploaded files, and return the filenames. Mostly useful for debugging. */ diff --git a/src/utils/CImageOcrHelper.php b/src/utils/CImageOcrHelper.php new file mode 100644 index 0000000..76d56ba --- /dev/null +++ b/src/utils/CImageOcrHelper.php @@ -0,0 +1,213 @@ + 14, 'y' => 27, 'width' => 89, 'height' => 19]); + // Convert to monochrome image. + imagefilter($im2, IMG_FILTER_GRAYSCALE); + imagefilter($im2, IMG_FILTER_BRIGHTNESS, 10); + imagefilter($im2, IMG_FILTER_CONTRAST, -255); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-screen.png")) { + unlink("$szFilename-screen.png"); + } + imagepng($im2, "$szFilename-screen.png"); + imagedestroy($im2); + } + + // Better rendering if we increase contrast, which increase text and reduce graphics. + imagefilter($im, IMG_FILTER_GRAYSCALE); + imagefilter($im, IMG_FILTER_BRIGHTNESS, 20); + imagefilter($im, IMG_FILTER_CONTRAST, -150); + + if (file_exists("$szFilename-filtered.png")) { + unlink("$szFilename-filtered.png"); + } + imagepng($im, "$szFilename-filtered.png"); + + // TODO: Coordinates are hardcoded for an iPhone 7 + + // Positions + $im2 = imagecrop($im, ['x' => 294, 'y' => 214, 'width' => 60, 'height' => 520]); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-pos.png")) { + unlink("$szFilename-pos.png"); + } + imagepng($im2, "$szFilename-pos.png"); + imagedestroy($im2); + } + + // Names + $im2 = imagecrop($im, ['x' => 442, 'y' => 214, 'width' => 200, 'height' => 520]); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-names.png")) { + unlink("$szFilename-names.png"); + } + imagepng($im2, "$szFilename-names.png"); + imagedestroy($im2); + } + + // Points + $im2 = imagecrop($im, ['x' => 778, 'y' => 214, 'width' => 80, 'height' => 520]); + if ($im2 !== FALSE) { + if (file_exists("$szFilename-pts.png")) { + unlink("$szFilename-pts.png"); + } + imagepng($im2, "$szFilename-pts.png"); + imagedestroy($im2); + } + + imagedestroy($im); + } + + public function ExtractFromImage($szFilename, $bDebugKeepWorkImages = false) + { + // Extract date from PNG file exif. + $szCreatedDate = $this->GetImageCreationDate($szFilename); + $createdDateGroup = $this->GetReferenceDate($szCreatedDate); + $szScreenType = false; + + // First look if it's a supported screenshot type. + $aScreenName = $this->OCR("$szFilename-screen.png", 6, false); + if (array_search("ALLIANCE", $aScreenName) !== false) { + $szScreenType = "Alliance Ranking"; + } elseif ( + array_search("HUNT", $aScreenName) !== false || + array_search("CLASSEMENT", $aScreenName) !== false + ) { + $szScreenType = "Treasure Hunt Ranking"; + } else { + echo "-> Error, cannot find Alliance or Treasure Hunt ranking.\n
"; + echo "--> RAW output is " . print_r($aScreenName, true) . "\n
"; + return -1; + } + + //TODO: Support TH. + if ($szScreenType == "Treasure Hunt Ranking") { + echo "-> Treasure Hunt ranking are not yet supported. Skipping.\n
"; + return -1; + } + + $aPositions = $this->OCR("$szFilename-pos.png", 6, true); // positions work a bit better with psm(6) + $aNames = $this->OCR("$szFilename-names.png", 4, false); + $aPoints = $this->OCR("$szFilename-pts.png", 4, true); + + // echo "-> NAME RAW data: " . print_r($aNames, true) . "
\n"; + //TODO: Hardcoded for iPhone7 screenshots with 6 players visible. + if (count($aPoints) != 6 || count($aPositions) != 6 || count($aNames) < 11) { + echo "Error, did not found 6 players for " . $szFilename . "\n
"; + echo "-> RAW data: " . print_r($aNames, true) . "
\n"; + return -1; + } + + if (!$bDebugKeepWorkImages) { + unlink($szFilename); + unlink("$szFilename-pos.png"); + unlink("$szFilename-names.png"); + unlink("$szFilename-pts.png"); + unlink("$szFilename-filtered.png"); + } + + // Build array indexed by position. + $aComplete = array(); + $uStartingIndexForNames = (count($aNames) == 12 ? 1 : 0); + for ($i = 0; $i < count($aPositions); $i++) { + $szName = $aNames[$i + (1 * $i) + $uStartingIndexForNames]; // complex algo because names have junks in-between. + $aComplete[$aPositions[$i]] = array( + 'fullname' => $szName, + 'name_id' => preg_replace('/[^A-Za-z0-9#]/', '', $szName), // keep only alpha-numeric chars, except the #. + 'pts' => $aPoints[$i], + 'date' => $szCreatedDate, + 'time_group' => $createdDateGroup + ); + } + return $aComplete; + } + + private function OCR($szFilename, $uPsm, $bNumbersOnly) + { + // $aAcceptedChars = array_merge(range('A','Z'), range('a','z'), range(0,9)); + // array_push($aAcceptedChars, '#', '*', '\'', ' ', '_', '-'); + $aNumbersOnly = array(); + if ($bNumbersOnly) { + $aNumbersOnly = range(0, 9); + } + + $blob = (new TesseractOCR($szFilename)) + ->psm($uPsm) + ->allowlist($aNumbersOnly) + ->configFile('tesseract.ini') + ->run(); + + // $blob = (new TesseractOCR($szFilename)) + // ->psm($uPsm) + // ->tessdataDir('./tessdata/') + // ->lang('fra') + // ->allowlist($aNumbersOnly) + // ->userWords('words.txt') + // ->configFile('config2.ini') + // ->run(); + + $aLines = array_values(array_filter(explode("\n", $blob))); + // print_r($aLines); + return $aLines; + } + + /** + * Return in the format: + * 2021:12:02 13:55:01 + */ + private function GetImageCreationDate($szFilename) + { + $dateTimeOriginal = ''; + + if (PNGMetadata::isPNG($szFilename)) { + $png_metadata = new PNGMetadata($szFilename); // return a 'ArrayObject' or 'Exception' + $dateTimeOriginal = $png_metadata->get('exif:DateTimeOriginal'); + } else { + $exif_metadata = exif_read_data($szFilename); + $dateTimeOriginal = $exif_metadata['DateTimeOriginal']; + } + + return $dateTimeOriginal; + } + + /** + * Return only the date (no time) in unix timestamp. + */ + private function GetReferenceDate($szDateTime) + { + $dateTime = new DateTime($szDateTime); + $dateOnly = new DateTime($dateTime->format('Y-m-d')); + return $dateOnly->format('U'); + } +} From b23c3f890d674562e132da4acf9be715e0e103d0 Mon Sep 17 00:00:00 2001 From: dbernard Date: Mon, 24 Jan 2022 22:56:51 -0500 Subject: [PATCH 6/6] Remove dependency to numeric position, one less image to OCR. Support to process multiple images from different dates by inserting in the DB after each image. --- src/controllers/post-screenshots.php | 21 ++++++--------------- src/utils/CImageOcrHelper.php | 22 +++++----------------- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/src/controllers/post-screenshots.php b/src/controllers/post-screenshots.php index 2367817..7e7973b 100644 --- a/src/controllers/post-screenshots.php +++ b/src/controllers/post-screenshots.php @@ -38,7 +38,7 @@ function HandlePost() } if (count($aFiles) > 0) { - $aAll = array(); + $aMembers = array(); $bufferToForceBrowserToDisplay = str_repeat(" ", 4096); // some browser wait to display despite having receiving data. foreach ($aFiles as $szFilename) { @@ -47,24 +47,15 @@ function HandlePost() $res = $ocrHelper->CropImage($szFilename); - if ($res != -1) { - $aPart = $ocrHelper->ExtractFromImage($szFilename, isset($g_UPLOAD_DIR)); - } + if ($res !== -1) { + $aMembers = $ocrHelper->ExtractFromImage($szFilename, isset($g_UPLOAD_DIR)); - if ($aPart !== -1) { - // Append and magically ignore duplicates - // TODO: This does not support uploading screenshots from differente date (since array indexed by position). Need an index by date. - $aAll += $aPart; + if ($aMembers !== -1) { + $g_sqlModel->InsertMembers('Québec Kingdóm', $aMembers); + } } } - // Sort by array key (i.e. position) - ksort(/*INOUT*/$aAll, SORT_NUMERIC); - - // Insert in database; - $g_sqlModel->InsertMembers('Québec Kingdóm', $aAll); - - echo "
" . serialize($aAll) . "
"; echo "
\nDone!"; } } diff --git a/src/utils/CImageOcrHelper.php b/src/utils/CImageOcrHelper.php index 76d56ba..8c60d8b 100644 --- a/src/utils/CImageOcrHelper.php +++ b/src/utils/CImageOcrHelper.php @@ -55,16 +55,6 @@ public function CropImage($szFilename) imagepng($im, "$szFilename-filtered.png"); // TODO: Coordinates are hardcoded for an iPhone 7 - - // Positions - $im2 = imagecrop($im, ['x' => 294, 'y' => 214, 'width' => 60, 'height' => 520]); - if ($im2 !== FALSE) { - if (file_exists("$szFilename-pos.png")) { - unlink("$szFilename-pos.png"); - } - imagepng($im2, "$szFilename-pos.png"); - imagedestroy($im2); - } // Names $im2 = imagecrop($im, ['x' => 442, 'y' => 214, 'width' => 200, 'height' => 520]); @@ -117,13 +107,11 @@ public function ExtractFromImage($szFilename, $bDebugKeepWorkImages = false) return -1; } - $aPositions = $this->OCR("$szFilename-pos.png", 6, true); // positions work a bit better with psm(6) $aNames = $this->OCR("$szFilename-names.png", 4, false); $aPoints = $this->OCR("$szFilename-pts.png", 4, true); - // echo "-> NAME RAW data: " . print_r($aNames, true) . "
\n"; //TODO: Hardcoded for iPhone7 screenshots with 6 players visible. - if (count($aPoints) != 6 || count($aPositions) != 6 || count($aNames) < 11) { + if (count($aPoints) != 6 || count($aNames) < 11) { echo "Error, did not found 6 players for " . $szFilename . "\n
"; echo "-> RAW data: " . print_r($aNames, true) . "
\n"; return -1; @@ -131,7 +119,6 @@ public function ExtractFromImage($szFilename, $bDebugKeepWorkImages = false) if (!$bDebugKeepWorkImages) { unlink($szFilename); - unlink("$szFilename-pos.png"); unlink("$szFilename-names.png"); unlink("$szFilename-pts.png"); unlink("$szFilename-filtered.png"); @@ -140,11 +127,12 @@ public function ExtractFromImage($szFilename, $bDebugKeepWorkImages = false) // Build array indexed by position. $aComplete = array(); $uStartingIndexForNames = (count($aNames) == 12 ? 1 : 0); - for ($i = 0; $i < count($aPositions); $i++) { + for ($i = 0; $i < count($aPoints); $i++) { $szName = $aNames[$i + (1 * $i) + $uStartingIndexForNames]; // complex algo because names have junks in-between. - $aComplete[$aPositions[$i]] = array( + $szNameId = preg_replace('/[^A-Za-z0-9#]/', '', $szName); // keep only alpha-numeric chars, except the #. + $aComplete[] = array( 'fullname' => $szName, - 'name_id' => preg_replace('/[^A-Za-z0-9#]/', '', $szName), // keep only alpha-numeric chars, except the #. + 'name_id' => $szNameId, 'pts' => $aPoints[$i], 'date' => $szCreatedDate, 'time_group' => $createdDateGroup