Skip to content

Commit

Permalink
Generate links images previews
Browse files Browse the repository at this point in the history
  • Loading branch information
marienfressinaud committed Oct 26, 2020
1 parent 4ee0e16 commit 11a2e67
Show file tree
Hide file tree
Showing 17 changed files with 499 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@
!/policies/.keep

/public/dev_assets

/public/media/*
!/public/media/.keep
1 change: 1 addition & 0 deletions configuration/environment_development.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'brand' => $dotenv->pop('APP_BRAND', 'flusio'),
'version' => trim(@file_get_contents($app_path . '/VERSION.txt')),
'cache_path' => $dotenv->pop('APP_CACHE_PATH', $app_path . '/cache'),
'media_path' => $dotenv->pop('APP_MEDIA_PATH', $app_path . '/public/media'),
'demo' => filter_var($dotenv->pop('APP_DEMO', false), FILTER_VALIDATE_BOOLEAN),
'registrations_opened' => filter_var($dotenv->pop('APP_OPEN_REGISTRATIONS', true), FILTER_VALIDATE_BOOLEAN),
'subscriptions_enabled' => $subscriptions_host !== null,
Expand Down
1 change: 1 addition & 0 deletions configuration/environment_production.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
'brand' => $dotenv->pop('APP_BRAND', 'flusio'),
'version' => trim(@file_get_contents($app_path . '/VERSION.txt')),
'cache_path' => $dotenv->pop('APP_CACHE_PATH', $app_path . '/cache'),
'media_path' => $dotenv->pop('APP_MEDIA_PATH', $app_path . '/public/media'),
'demo' => filter_var($dotenv->pop('APP_DEMO', false), FILTER_VALIDATE_BOOLEAN),
'registrations_opened' => filter_var($dotenv->pop('APP_OPEN_REGISTRATIONS', true), FILTER_VALIDATE_BOOLEAN),
'subscriptions_enabled' => $subscriptions_host !== null,
Expand Down
3 changes: 3 additions & 0 deletions configuration/environment_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
$temporary_directory = sys_get_temp_dir() . '/flusio/' . \flusio\utils\Random::hex(10);
$data_directory = $temporary_directory . '/data';
$cache_directory = $temporary_directory . '/cache';
$media_directory = $temporary_directory . '/media';
@mkdir($temporary_directory, 0777, true);
@mkdir($data_directory, 0777, true);
@mkdir($cache_directory, 0777, true);
@mkdir($media_directory, 0777, true);

$subscriptions_host = $dotenv->pop('APP_SUBSCRIPTIONS_HOST');

Expand All @@ -29,6 +31,7 @@
'brand' => 'flusio',
'version' => trim(@file_get_contents($app_path . '/VERSION.txt')),
'cache_path' => $cache_directory,
'media_path' => $media_directory,
'demo' => false,
'registrations_opened' => true,
'subscriptions_enabled' => false, // should be enable on a case-by-case basis
Expand Down
5 changes: 5 additions & 0 deletions env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ APP_PORT=8000
# directory presents in the current directory.
# APP_CACHE_PATH=/tmp

# You can uncomment and change the path for the media files. Default is the
# media directory presents in the public/ directory. If you change this value,
# make sure the /media URL path is correctly served by your Web server.
# APP_MEDIA_PATH=/some/path/to/media

##################################
# Database environment variables #
##################################
Expand Down
31 changes: 31 additions & 0 deletions lib/SpiderBits/src/DomExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,37 @@ public static function title($dom)
return '';
}

/**
* Return the illustration URL of the DOM document.
*
* @param \SpiderBits\Dom $dom
*
* @return string
*/
public static function illustration($dom)
{
$xpath_queries = [
// Look for OpenGraph first
'/html/head/meta[@property = "og:image"][1]/attribute::content',
// Then Twitter meta tag
'/html/head/meta[@name = "twitter:image"][1]/attribute::content',
// Err, still nothing! Let's try to be more tolerant (e.g. Youtube
// puts the meta tags in the body :/)
'//meta[@property = "og:image"][1]/attribute::content',
'//meta[@name = "twitter:image"][1]/attribute::content',
];

foreach ($xpath_queries as $query) {
$illustration = $dom->select($query);
if ($illustration) {
return $illustration->text();
}
}

// It's hopeless...
return '';
}

/**
* Return the main content of the DOM document.
*
Expand Down
Empty file added public/media/.keep
Empty file.
5 changes: 5 additions & 0 deletions src/Links.php
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,11 @@ public function fetch($request)
if (isset($info['reading_time'])) {
$link->reading_time = $info['reading_time'];
}
if (isset($info['url_illustration'])) {
$image_service = new services\Image();
$image_filename = $image_service->generatePreviews($info['url_illustration']);
$link->image_filename = $image_filename;
}

$link_dao = new models\dao\Link();
$link_dao->save($link);
Expand Down
30 changes: 30 additions & 0 deletions src/migrations/Migration202010220001AddImageFilenameToLinks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace flusio\migrations;

class Migration202010220001AddImageFilenameToLinks
{
public function migrate()
{
$database = \Minz\Database::get();

$database->exec(<<<'SQL'
ALTER TABLE links
ADD COLUMN image_filename TEXT;
SQL);

return true;
}

public function rollback()
{
$database = \Minz\Database::get();

$database->exec(<<<'SQL'
ALTER TABLE links
DROP COLUMN image_filename;
SQL);

return true;
}
}
203 changes: 203 additions & 0 deletions src/models/Image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<?php

namespace flusio\models;

/**
* @author Marien Fressinaud <[email protected]>
* @license http://www.gnu.org/licenses/agpl-3.0.en.html AGPL
*/
class Image
{
/** @var resource */
private $resource;

/** @var integer */
private $width;

/** @var integer */
private $height;

/** @var string */
private $type;

/**
* @param resource $resource The image as GD resource
* @param string $type jpeg, png or webp
*/
public function __construct($resource, $type)
{
$this->resource = $resource;
$this->type = $type;
$this->width = imagesx($resource);
$this->height = imagesy($resource);
}

/**
* Initialize an image from a string
*
* @param string $string_image
*
* @return \flusio\models\Image
*/
public static function fromString($string_image)
{
$mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $string_image);
switch (strtolower($mime)) {
case 'image/jpg':
case 'image/jpeg':
case 'image/pjpeg':
$type = 'jpeg';
break;
case 'image/png':
case 'image/x-png':
$type = 'png';
break;
case 'image/webp':
case 'image/x-webp':
$type = 'webp';
break;
default:
throw new \DomainException("The given string doesn’t look like a supported image ({$mime})");
}

$resource = @imagecreatefromstring($string_image);
if (!$resource) {
throw new \DomainException('Cannot create an image from the given string');
}

return new self($resource, $type);
}

/**
* @return string
*/
public function type()
{
return $this->type;
}

/**
* Resize the current image to the given size.
*
* The image is cropped in the middle to keep the proportion of the image.
*
* @param integer $width
* @param integer $height
*/
public function resize($width, $height)
{
$new_resource = imagecreatetruecolor($width, $height);

// Preserve transparency
// Code from the Intervention Image library
// @see https://github.com/Intervention/image/blob/8ee5f346ce8c6dcbdc7dec443486bd5f6ad924ff/src/Intervention/Image/Gd/Commands/ResizeCommand.php
$transparent_index = imagecolortransparent($this->resource);
if ($transparent_index !== -1) {
$rgba = imagecolorsforindex($new_resource, $transparent_index);
$transparent_color = imagecolorallocatealpha(
$new_resource,
$rgba['red'],
$rgba['green'],
$rgba['blue'],
127
);
imagefill($new_resource, 0, 0, $transparent_color);
imagecolortransparent($new_resource, $transparent_color);
} else {
imagealphablending($new_resource, false);
imagesavealpha($new_resource, true);
}

$src_rect = self::resizeRectangle(
[$this->width, $this->height],
[$width, $height]
);

imagecopyresampled(
$new_resource,
$this->resource,
0,
0,
$src_rect['x'],
$src_rect['y'],
$width,
$height,
$src_rect['width'],
$src_rect['height']
);

imagedestroy($this->resource);
$this->resource = $new_resource;
$this->width = $width;
$this->height = $height;
}

/**
* Save the image on disk.
*
* @param string $filepath
*/
public function save($filepath)
{
switch ($this->type) {
case 'jpeg':
imagejpeg($this->resource, $filepath);
break;
case 'png':
imagepng($this->resource, $filepath);
break;
case 'webp':
imagewebp($this->resource, $filepath);
break;
}
}

/**
* Get the src rectangle to be used to resize the initial image
*
* This function return the bigger rectangle with destination ratio that
* fits in the initial rectangle.
*
* @see https://www.php.net/manual/function.imagecopyresampled
*
* @param array $initial_size
* The size of the initial image (first value is width, second is height)
* @param array $destination_size
* The size of the desired image (first value is width, second is height)
*
* @return array
* The src rectange to use in imagecopyresampled, array indexes are:
* x, y, width and height.
*/
public static function resizeRectangle($initial_size, $destination_size)
{
list($initial_width, $initial_height) = $initial_size;
list($destination_width, $destination_height) = $destination_size;

$initial_ratio = $initial_width / $initial_height;
$destination_ratio = $destination_width / $destination_height;
if ($initial_ratio <= $destination_ratio) {
// The current width will entirely fit in the future image, so we
// can take the full width. We have to crop the height though
// (taking the middle of the image).
$src_width = $initial_width;
$src_height = $initial_width / $destination_ratio;
$src_x = 0;
$src_y = ($initial_height - $src_height) / 2;
} else {
// Same thing, but with height which entirely fit in the future
// image.
$src_width = $initial_height * $destination_ratio;
$src_height = $initial_height;
$src_x = ($initial_width - $src_width) / 2;
$src_y = 0;
}

return [
'x' => $src_x,
'y' => $src_y,
'width' => $src_width,
'height' => $src_height,
];
}
}
4 changes: 4 additions & 0 deletions src/models/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class Link extends \Minz\Model
'required' => true,
],

'image_filename' => [
'type' => 'string',
],

'fetched_at' => [
'type' => 'datetime',
],
Expand Down
1 change: 1 addition & 0 deletions src/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ CREATE TABLE links (
url TEXT NOT NULL,
is_public BOOLEAN NOT NULL DEFAULT false,
reading_time INTEGER NOT NULL DEFAULT 0,
image_filename TEXT,
fetched_at TIMESTAMPTZ,
fetched_code INTEGER NOT NULL DEFAULT 0,
fetched_error TEXT,
Expand Down
8 changes: 8 additions & 0 deletions src/services/Fetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function __construct()
* - error
* - title
* - reading_time
* - url_illustration
*/
public function fetch($url)
{
Expand Down Expand Up @@ -87,6 +88,13 @@ public function fetch($url)
$words = array_filter(explode(' ', $content));
$info['reading_time'] = intval(count($words) / 200);

// Get the illustration URL if any
$url_illustration = \SpiderBits\DomExtractor::illustration($dom);
$url_illustration = \SpiderBits\Url::sanitize($url_illustration);
if ($url_illustration) {
$info['url_illustration'] = $url_illustration;
}

return $info;
}
}
Loading

0 comments on commit 11a2e67

Please sign in to comment.