🎉 Hello world

This commit is contained in:
Clément 2021-02-22 16:38:47 +01:00
commit c8bbc35886
15 changed files with 2168 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/.idea
/main.php
/test.jpg
/pool/*
!/pool/.gitkeep
/vendor
/config.json

21
composer.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "shikiryu/webgobbler",
"description": "WebGobbler in PHP",
"type": "project",
"authors": [
{
"name": "Clément",
"email": "clement@desmidt.fr"
}
],
"require": {
"fabpot/goutte": "^4.0",
"ext-imagick": "*"
},
"autoload": {
"psr-4": {
"Shikiryu\\WebGobbler\\": "src/",
"Shikiryu\\WebGobbler\\Collector\\": "src/Collector"
}
}
}

1348
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
config.json.template Normal file
View File

@ -0,0 +1,16 @@
{
"collectors": [],
"pool": {
"directory": "/home/user/webcollage/pool",
"nb_images": 10
},
"assembler": {
"sizex": 2000,
"sizey": 2000,
"mirror": false,
"emboss": true,
"invert": false,
"nbx": 5,
"nby": 5
}
}

0
pool/.gitkeep Normal file
View File

37
src/Assembler.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace Shikiryu\WebGobbler;
abstract class Assembler
{
/**
* @var \Shikiryu\WebGobbler\Pool
*/
protected $pool;
/**
* @var \Shikiryu\WebGobbler\Config
*/
protected $config;
/**
* Assembler constructor.
*
* @param \Shikiryu\WebGobbler\Pool $pool
* @param \Shikiryu\WebGobbler\Config $config
*/
public function __construct(Pool $pool, Config $config)
{
$this->pool = $pool;
$this->config = $config;
}
/**
* @param string $file
*
* @return void
*/
abstract public function saveTo($file);
abstract public function display();
}

48
src/Assembler/Mosaic.php Normal file
View File

@ -0,0 +1,48 @@
<?php
namespace Shikiryu\WebGobbler\Assembler;
use Shikiryu\WebGobbler\Assembler;
class Mosaic extends Assembler
{
/**
* @param string $file
*
* @return void
* @throws \ImagickException
*/
public function saveTo($file)
{
$final_image = new \Imagick();
$final_image->setColorspace(\Imagick::COLORSPACE_RGB);
$final_image->newImage($this->config->get('assembler.sizex'), $this->config->get('assembler.sizey'), 'none');
$image_size_x = $this->config->get('assembler.sizex') / $this->config->get('assembler.nbx');
$image_size_y = $this->config->get('assembler.sizey') / $this->config->get('assembler.nby');
for ($y = 0; $y < $this->config->get('assembler.nby'); $y++) {
for ($x = 0; $x < $this->config->get('assembler.nbx'); $x++) {
$image = new \Imagick($this->pool->getImage());
if ($image->getColorspace() !== \Imagick::COLORSPACE_RGB) {
$image->setColorspace(\Imagick::COLORSPACE_RGB);
}
$image->scaleImage($image_size_x, $image_size_y, true);
$final_image->compositeImage($image, \Imagick::COMPOSITE_DEFAULT, $x * $image_size_x, $y * $image_size_y);
}
}
if (true === $this->config->get('assembler.mirror')) {
$final_image->flopImage();
}
if (true === $this->config->get('assembler.emboss')) {
$final_image->embossImage(0, 1);
}
if (true === $this->config->get('assembler.invert')) {
$final_image->negateImage(false);
}
$final_image->writeImage($file);
}
public function display()
{
// TODO: Implement display() method.
}
}

44
src/Assembler/Simple.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace Shikiryu\WebGobbler\Assembler;
use Shikiryu\WebGobbler\Assembler;
class Simple extends Assembler
{
/**
* @param string $file
*/
public function saveTo($file)
{
try {
$image = new \Imagick($this->pool->getImage());
if ($image->getColorspace() !== \Imagick::COLORSPACE_RGB) {
$image->setColorspace(\Imagick::COLORSPACE_RGB);
}
$size = $image->getSize();
$imagex = $size['columns'];
$imagey = $size['rows'];
if ($imagex !== $this->config->get('assembler.sizex') || $imagey !== $this->config->get('assembler.sizey')) {
$image->thumbnailImage($this->config->get('assembler.sizex'), $this->config->get('assembler.sizey'));
}
if (true === $this->config->get('assembler.mirror')) {
$image->flopImage();
}
if (true === $this->config->get('assembler.emboss')) {
$image->embossImage(0, 1);
}
if (true === $this->config->get('assembler.invert')) {
$image->flipImage();
}
$image->writeImage($file);
} catch (\ImagickException $e) {
echo $e->getMessage();
}
}
public function display()
{
// TODO: Implement display() method.
}
}

177
src/Assembler/Superpose.php Normal file
View File

@ -0,0 +1,177 @@
<?php
namespace Shikiryu\WebGobbler\Assembler;
use Imagick;
use Shikiryu\WebGobbler\Assembler;
class Superpose extends Assembler
{
/**
* @var Imagick
*/
private $current_image;
/**
* @param string $file
*
* @return void
* @throws \ImagickException
* @throws \Exception
*/
public function saveTo($file)
{
$image_to_superpose = new \Imagick($this->pool->getImage());
$image_x = $image_to_superpose->getImageWidth();
$image_y = $image_to_superpose->getImageHeight();
if ($image_x < 32 || $image_y < 32) {
throw new \Exception('Image too small.');
}
$this->current_image = $image_to_superpose;
$nb_images = $this->config->get('assembler.superpose.min_num_images', 5);
for ($i = 0; $i < $nb_images; $i++) {
$this->current_image = $this->superpose();
}
$this->current_image->writeImage($file);
}
public function superpose()
{
return $this->superposeOneImage($this->current_image, new \Imagick($this->pool->getImage()));
}
/**
* Superposes one image in the current image.
* This method must only be called by the assembler_superpose thread !
*
* @param \Imagick $current_image
* @param \Imagick $image_to_superpose
*
* @return \Imagick
* @throws \Exception
*/
private function superposeOneImage($current_image, $image_to_superpose)
{
if ($this->config->get('assembler.superpose.variante', 0) === 1) {
$current_image->brightnessContrastImage(0.99, 1);
}
if ($image_to_superpose->getColorspace() !== \Imagick::COLORSPACE_RGB) {
$image_to_superpose->setColorspace(\Imagick::COLORSPACE_RGB);
}
# If the image is bigger than current image, scale it down to 1/2 of final picture dimensions
# (while keeping its ratio)
$image_x = $image_to_superpose->getImageWidth();
$image_y = $image_to_superpose->getImageHeight();
if ($image_x > $this->config->get('assembler.sizex') || $image_y > $this->config->get('assembler.sizey')) {
try {
$image_to_superpose->scaleImage($this->config->get('assembler.sizex') / 2, $this->config->get('assembler.sizey') / 2, true);
} catch (\ImagickException $e) {
}
}
# Scale down/up image if required.
/*scaleValue = self.CONFIG["assembler.superpose.scale"]
if str(scaleValue) != "1.0":
try:
imageToSuperpose.thumbnail((int(float(imagex)*scaleValue),int(float(imagey)*scaleValue)),Image.ANTIALIAS)
except TypeError: #TypeError: unsubscriptable object ; Spurious exception in PIL. :-(
raise BadImage
(imagex,imagey) = imageToSuperpose.size*/
# Compensate for poorly-contrasted images on the web
/*try:
imageToSuperpose = ImageOps.autocontrast(imageToSuperpose)
except TypeError: # Aaron tells me that this exception occurs with PNG images.
raise BadImage*/
# Some image are too white.
# For example, the photo of a coin on a white background.
# These picture degrad the quality of the final image.
# We try to dectect them by summing the value of the pixels
# on the borders.
# If the image is considered "white", we invert it.
/*pixelcount = 1 # 1 to prevent divide by zero error.
valuecount = 0
try:
for x in range(0,imagex,20):
(r,g,b) = imageToSuperpose.getpixel((x,5))
valuecount += r+g+b
(r,g,b) = imageToSuperpose.getpixel((x,imagey-5))
valuecount += r+g+b
pixelcount += 2
for y in range(0,imagey,20):
(r,g,b) = imageToSuperpose.getpixel((5,y))
valuecount += r+g+b
(r,g,b) = imageToSuperpose.getpixel((imagex-5,y))
valuecount += r+g+b
pixelcount += 2
except TypeError: #unsubscriptable object Arrggghh... not again !
raise BadImage # Aggrrreeeuuuu...
# If the average r+g+b of the border pixels exceed this value,
# we consider the image is too white, and we invert it.
if (100*(valuecount/(255*3))/pixelcount)>60: # Cut at 60%. (100% is RGB=(255,255,255))
imageToSuperpose = ImageOps.invert(imageToSuperpose)*/
$paste_coords_x = random_int(-$image_x, $this->config->get('assembler.sizex'));
$paste_coords_y = random_int(-$image_y, $this->config->get('assembler.sizey'));
# Darken image borders
$image_to_superpose = $this->darkenImageBorder($image_to_superpose, 30);
if ($this->config->get('assembler.superpose.randomrotation', false)) {
$image_to_superpose->rotateImage('none', random_int(0, 359));
$image_to_superpose = $this->darkenImageBorder($image_to_superpose, 30);
}
// mask_image = ImageOps.autocontrast(imageToSuperpose.convert('L'))
// if (self.CONFIG["assembler.superpose.variante"]==1) and (random.randint(0,100)<5): # Invert the transparency of 5% of the images (Except if we are in variante 1 mode)
// mask_image = ImageOps.invert(mask_image)
$current_image->compositeImage($image_to_superpose, \Imagick::COMPOSITE_DEFAULT, $paste_coords_x, $paste_coords_y);
if ($this->config->get('assembler.superpose.variante') === 0) {
$current_image->equalizeImage();
} else {
$current_image->autoLevelImage();
}
return $current_image;
}
/**
* @param \Imagick $image
* @param int|null $border_size
*
* @return \Imagick
*/
private function darkenImageBorder(Imagick $image, $border_size = null)
{
if (null === $border_size) {
$border_size = $this->config->get('assembler.superpose.bordersmooth');
}
$image_x = $image->getImageWidth();
$image_y = $image->getImageHeight();
for ($i = 0; $i < $border_size; $i++) {
$draw = new \ImagickDraw();
$draw->setStrokeOpacity(($border_size-$i)/$border_size);
$draw->setStrokeWidth(1);
$draw->setStrokeColor('black');
$draw->setFillColor('none');
$draw->rectangle($i, $i, $image_x - $i, $image_y - $i);
$image->drawImage($draw);
}
return $image;
}
public function display()
{
// TODO: Implement display() method.
}
}

120
src/Collector.php Normal file
View File

@ -0,0 +1,120 @@
<?php
namespace Shikiryu\WebGobbler;
abstract class Collector
{
/**
* Collector constructor.
*
* @param \Shikiryu\WebGobbler\Pool $pool
*/
public function __construct(Pool $pool)
{
$this->pool = $pool;
}
/**
* @return string
*/
abstract public function getName();
/**
* @return string
*/
public function getPoolDirectory()
{
return sprintf('%s/%s', $this->pool->getPoolDirectory(), $this->getName());
}
/**
* @return int
*/
abstract public function getRandomImage();
/**
* @param int $number
*
* @return int
*/
abstract public function getRandomImages(int $number);
/**
* Generates a random word.
* This method can be used by all derived classes.
* Usefull to get random result from search engines when you do not have
* a dictionnary at hand.
* The generated word can be a number (containing only digits),
* a word (containing only letters) or both mixed.
* Output: string (a random word)
*
* @return string
*/
protected function generateRandomWord()
{
$word = '1';
try {
if (random_int(0, 100) < 30) { // Sometimes use only digits
if (random_int(0, 100) < 30) {
$word = random_int(1, 999);
} else {
$word = random_int(1, 999999);
}
} else { // Generate a word containing letters
$word = '';
$charset = 'abcdefghijklmnopqrstuvwxyz'; // Search for random word containing letter only.
if (random_int(0, 100) < 60) { // Sometimes include digits with letters
$charset = 'abcdefghijklmnopqrstuvwxyz' . 'abcdefghijklmnopqrstuvwxyz' . '0123456789'; // *2 to have more letters than digits
}
$charset = str_split($charset);
for ($i = 0, $l = random_int(2, 5); $i <= $l; $i++) { // Only generate short words (2 to 5 characters)
$word .= $charset[array_rand($charset)];
}
}
} catch (\Exception $e) {
}
return $word;
}
/**
* @param array $parts
*
* @return string
*/
protected function reverse_url(array $parts) {
if (array_key_exists('query', $parts) && is_array($parts['query'])) {
$parts['query'] = http_build_query($parts['query']);
}
return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') .
((isset($parts['user']) || isset($parts['host'])) ? '//' : '') .
(isset($parts['user']) ? "{$parts['user']}" : '') .
(isset($parts['pass']) ? ":{$parts['pass']}" : '') .
(isset($parts['user']) ? '@' : '') .
(isset($parts['host']) ? "{$parts['host']}" : '') .
(isset($parts['port']) ? ":{$parts['port']}" : '') .
(isset($parts['path']) ? "{$parts['path']}" : '') .
(isset($parts['query']) ? "?{$parts['query']}" : '') .
(isset($parts['fragment']) ? "#{$parts['fragment']}" : '');
}
/**
* @param \Shikiryu\WebGobbler\Pool $pool
*
* @return \Shikiryu\WebGobbler\Collector[]
*/
public static function getAllCollectors(Pool $pool)
{
$collectors = [];
foreach (glob(__DIR__ . '/Collector/*.php') as $class) {
$classname = sprintf('Shikiryu\\WebGobbler\\Collector\\%s', basename($class, '.php'));
$tmp_class = new $classname($pool);
if (is_subclass_of($tmp_class, __CLASS__)) {
$collectors[] = $tmp_class;
}
}
return $collectors;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Shikiryu\WebGobbler\Collector;
use Goutte\Client;
use Shikiryu\WebGobbler\Collector;
class DeviantartCollector extends Collector
{
/**
*
*/
public const RANDOM_URL = 'https://www.deviantart.com/popular/deviations';
/**
* Regular expression to extract the maximum deviantionID from homepage
*/
public const RE_ALLDEVIATIONID = '/href="https:\/\/www.deviantart.com\/[^\/]+\/art\/([^"]+)"/m';
/**
* @return int
*/
public function getRandomImage()
{
return $this->getRandomImages(1);
}
/**
* @param int $number
*
* @return int
*/
public function getRandomImages(int $number = 1)
{
$html = file_get_contents(self::RANDOM_URL);
preg_match_all(self::RE_ALLDEVIATIONID, $html, $deviant_ids);
$deviant_ids = array_map(static function ($deviant_id) {
$array = explode('-', $deviant_id);
return (int)end($array);
}, $deviant_ids[1]);
$deviant_ids = array_unique($deviant_ids);
$index_to_download = array_rand($deviant_ids, $number);
if (!is_array($index_to_download)) {
$index_to_download = [$index_to_download];
}
foreach ($index_to_download as $deviant_id) {
$client = new Client();
$crawler = $client->request('GET', 'https://www.deviantart.com/deviation/'.$deviant_ids[$deviant_id]);
$img_url = $crawler->filter('[data-hook="art_stage"] img')->eq(0)->attr('src');
file_put_contents($this->getPoolDirectory() . '/' . basename(parse_url($img_url, PHP_URL_PATH)), file_get_contents($img_url));
}
return $number; // Fixme
}
/**
* @return string
*/
public function getName()
{
return 'collector_deviantart';
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Shikiryu\WebGobbler\Collector;
use Shikiryu\WebGobbler\Collector;
class LocalCollector extends Collector
{
/**
* @var string
*/
public $directory_to_scan = '';
/**
* @var string[]
*/
public $filepaths = [];
/**
* @return int|void
*/
public function getRandomImage()
{
}
/**
* @return string
*/
public function getName()
{
return 'collector_local';
}
/**
* @param int $number
*
* @return int
*/
public function getRandomImages(int $number)
{
// TODO: Implement getRandomImages() method.
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Shikiryu\WebGobbler\Collector;
use Goutte\Client;
use Shikiryu\WebGobbler\Collector;
class YahooImageCollector extends Collector
{
public const SEARCH_URL = 'https://images.search.yahoo.com/search/images?p=%s';
/**
* @return string
*/
public function getName()
{
return 'collector_yahooimage';
}
/**
* @return int
*/
public function getRandomImage()
{
$word_to_search = $this->generateRandomWord();
$client = new Client();
$crawler = $client->request('GET', sprintf(self::SEARCH_URL, $word_to_search));
$imgs = $crawler->filter('noscript img');
if ($imgs->count() < 20) {
return $this->getRandomImage();
}
$img_url = $imgs->eq(random_int(0, $imgs->count() - 1))->attr('src');
$parsed_url = parse_url( $img_url );
parse_str( $parsed_url['query'] , $url_vars );
$name = $url_vars['id'];
unset($url_vars['w'], $url_vars['h']);
$parsed_url['query'] = $url_vars;
$img_url = $this->reverse_url($parsed_url);
file_put_contents($this->getPoolDirectory() . '/'. $name . '.jpg', file_get_contents($img_url));
return 1;
}
/**
* @param int $number
*
* @return int
*/
public function getRandomImages(int $number)
{
$count = 0;
for ($i = 0; $i < $number; $i++) {
$count += $this->getRandomImage();
}
return $count;
}
}

72
src/Config.php Normal file
View File

@ -0,0 +1,72 @@
<?php
namespace Shikiryu\WebGobbler;
class Config
{
protected $data = null;
protected $cache = [];
/**
* Config constructor.
* @throws \JsonException
* @throws \Exception
*/
public function __construct($config_file)
{
if (!is_readable($config_file)) {
throw new \Exception('Config file doesn\'t exist or is unreadable.');
}
$this->data = json_decode(file_get_contents($config_file), true, 512, JSON_THROW_ON_ERROR);
}
/**
* @param $key
* @param null $default
*
* @return mixed|null
*/
public function get($key, $default = null)
{
if ($this->has($key)) {
return $this->cache[$key];
}
return $default;
}
/**
* @param string $key
*
* @return bool
*/
public function has($key)
{
// Check if already cached
if (isset($this->cache[$key])) {
return true;
}
$segments = explode('.', $key);
$root = $this->data;
// nested case
foreach ($segments as $segment) {
if (array_key_exists($segment, $root)) {
$root = $root[$segment];
continue;
}
return false;
}
// Set cache for the given key
$this->cache[$key] = $root;
return true;
}
}

109
src/Pool.php Normal file
View File

@ -0,0 +1,109 @@
<?php
namespace Shikiryu\WebGobbler;
class Pool
{
protected $file_list = [];
/**
* @var \Shikiryu\WebGobbler\Collector[]
*/
protected $collectors = [];
/**
* @var string
*/
protected $pool_directory;
/**
* @var int
*/
protected $nb_images = 0;
/**
* Pool constructor.
*
* @param array $config
*/
public function __construct(array $config)
{
$collectors = [];
if (array_key_exists('collectors', $config)) {
$collectors = $config['collectors'];
}
$all_collectors = Collector::getAllCollectors($this);
if (empty($collectors)) {
$this->collectors = $all_collectors;
} else {
foreach ($all_collectors as $collector) {
if (in_array($collector->getName(), $collectors, true)) {
$this->collectors[] = $collector;
}
}
}
$pool_dir = $config['directory'];
if (!is_dir($pool_dir) && !mkdir($pool_dir) && !is_dir($pool_dir)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $pool_dir));
}
$this->pool_directory = $pool_dir;
$this->nb_images = $config['nb_images']; // FIXME
$this->prepareCollectors();
}
public function getPoolDirectory()
{
return $this->pool_directory;
}
/**
* @return string
*/
public function getImage()
{
$images = $this->getFileList();
$index = array_rand($images);
$image = $images[$index];
unset($this->file_list[$index]);
return $image;
}
/**
* @return array
*/
private function getFileList()
{
if (!empty($this->file_list)) {
return $this->file_list;
}
$file_list = [];
foreach ($this->collectors as $collector) {
$directory = $collector->getPoolDirectory();
$file_list = array_merge($file_list, glob($directory.'/*.{jpg,gif,png}', GLOB_BRACE));
}
$this->file_list = $file_list;
return $file_list;
}
private function prepareCollectors()
{
foreach ($this->collectors as $collector) {
$directory = $collector->getPoolDirectory();
$images = glob($directory.'/*.{jpg,gif,png}', GLOB_BRACE);
if (count($images) < $this->nb_images) {
$collector->getRandomImages($this->nb_images - count($images));
}
}
}
}