Add new horizontal masonry and image height allocation

- Move image templates to content/image sub-folder
This commit is contained in:
Hypolite Petovan 2023-09-29 03:28:22 -04:00
parent e01040a2e8
commit 163a85c78f
13 changed files with 410 additions and 39 deletions

154
src/Content/Image.php Normal file
View file

@ -0,0 +1,154 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content;
use Friendica\Content\Image\Collection\MasonryImageRow;
use Friendica\Content\Image\Entity\MasonryImage;
use Friendica\Content\Post\Collection\PostMedias;
use Friendica\Core\Renderer;
class Image
{
public static function getBodyAttachHtml(PostMedias $PostMediaImages): string
{
$media = '';
if ($PostMediaImages->haveDimensions()) {
if (count($PostMediaImages) > 1) {
$media = self::getHorizontalMasonryHtml($PostMediaImages);
} elseif (count($PostMediaImages) == 1) {
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
'$image' => $PostMediaImages[0],
'$allocated_height' => $PostMediaImages[0]->getAllocatedHeight(),
'$allocated_max_width' => ($PostMediaImages[0]->previewWidth ?? $PostMediaImages[0]->width) . 'px',
]);
}
} else {
if (count($PostMediaImages) > 1) {
$media = self::getImageGridHtml($PostMediaImages);
} elseif (count($PostMediaImages) == 1) {
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single.tpl'), [
'$image' => $PostMediaImages[0],
]);
}
}
return $media;
}
/**
* @param PostMedias $images
* @return string
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
private static function getImageGridHtml(PostMedias $images): string
{
// Image for first column (fc) and second column (sc)
$images_fc = [];
$images_sc = [];
for ($i = 0; $i < count($images); $i++) {
($i % 2 == 0) ? ($images_fc[] = $images[$i]) : ($images_sc[] = $images[$i]);
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/grid.tpl'), [
'columns' => [
'fc' => $images_fc,
'sc' => $images_sc,
],
]);
}
/**
* Creates a horizontally masoned gallery with a fixed maximum number of pictures per row.
*
* For each row, we calculate how much of the total width each picture will take depending on their aspect ratio
* and how much relative height it needs to accomodate all pictures next to each other with their height normalized.
*
* @param array $images
* @return string
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
private static function getHorizontalMasonryHtml(PostMedias $images): string
{
static $column_size = 2;
$rows = array_map(
function (PostMedias $PostMediaImages) {
if ($singleImageInRow = count($PostMediaImages) == 1) {
$PostMediaImages[] = $PostMediaImages[0];
}
$widths = [];
$heights = [];
foreach ($PostMediaImages as $PostMediaImage) {
if ($PostMediaImage->width && $PostMediaImage->height) {
$widths[] = $PostMediaImage->width;
$heights[] = $PostMediaImage->height;
} else {
$widths[] = $PostMediaImage->previewWidth;
$heights[] = $PostMediaImage->previewHeight;
}
}
$maxHeight = max($heights);
// Corrected width preserving aspect ratio when all images on a row are the same height
$correctedWidths = [];
foreach ($widths as $i => $width) {
$correctedWidths[] = $width * $maxHeight / $heights[$i];
}
$totalWidth = array_sum($correctedWidths);
$row_images2 = [];
if ($singleImageInRow) {
unset($PostMediaImages[1]);
}
foreach ($PostMediaImages as $i => $PostMediaImage) {
$row_images2[] = new MasonryImage(
$PostMediaImage->uriId,
$PostMediaImage->url,
$PostMediaImage->preview,
$PostMediaImage->description ?? '',
100 * $correctedWidths[$i] / $totalWidth,
100 * $maxHeight / $correctedWidths[$i]
);
}
// This magic value will stay constant for each image of any given row and is ultimately
// used to determine the height of the row container relative to the available width.
$commonHeightRatio = 100 * $correctedWidths[0] / $totalWidth / ($widths[0] / $heights[0]);
return new MasonryImageRow($row_images2, count($row_images2), $commonHeightRatio);
},
$images->chunk($column_size)
);
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/horizontal_masonry.tpl'), [
'$rows' => $rows,
'$column_size' => $column_size,
]);
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Image\Collection;
use Friendica\Content\Image\Entity;
use Friendica\BaseCollection;
use Friendica\Content\Image\Entity\MasonryImage;
class MasonryImageRow extends BaseCollection
{
/** @var ?float */
protected $heightRatio;
/**
* @param MasonryImage[] $entities
* @param int|null $totalCount
* @param float|null $heightRatio
*/
public function __construct(array $entities = [], int $totalCount = null, float $heightRatio = null)
{
parent::__construct($entities, $totalCount);
$this->heightRatio = $heightRatio;
}
/**
* @return Entity\MasonryImage
*/
public function current(): Entity\MasonryImage
{
return parent::current();
}
public function getHeightRatio(): ?float
{
return $this->heightRatio;
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Image\Entity;
use Friendica\BaseEntity;
use Psr\Http\Message\UriInterface;
/**
* @property-read int $uriId
* @property-read UriInterface $url
* @property-read ?UriInterface $preview
* @property-read string $description
* @property-read float $heightRatio
* @property-read float $widthRatio
* @see \Friendica\Content\Image::getHorizontalMasonryHtml()
*/
class MasonryImage extends BaseEntity
{
/** @var int */
protected $uriId;
/** @var UriInterface */
protected $url;
/** @var ?UriInterface */
protected $preview;
/** @var string */
protected $description;
/** @var float Ratio of the width of the image relative to the total width of the images on the row */
protected $widthRatio;
/** @var float Ratio of the height of the image relative to its width for height allocation */
protected $heightRatio;
public function __construct(int $uriId, UriInterface $url, ?UriInterface $preview, string $description, float $widthRatio, float $heightRatio)
{
$this->url = $url;
$this->uriId = $uriId;
$this->preview = $preview;
$this->description = $description;
$this->widthRatio = $widthRatio;
$this->heightRatio = $heightRatio;
}
}

View file

@ -42,4 +42,16 @@ class PostMedias extends BaseCollection
{
return parent::current();
}
/**
* Determine whether all the collection's item have at least one set of dimensions provided
*
* @return bool
*/
public function haveDimensions(): bool
{
return array_reduce($this->getArrayCopy(), function (bool $carry, Entity\PostMedia $item) {
return $carry && $item->hasDimensions();
}, true);
}
}

View file

@ -188,6 +188,30 @@ class PostMedia extends BaseEntity
}
/**
* Computes the allocated height value used in the content/image/single_with_height_allocation.tpl template
*
* Either base or preview dimensions need to be set at runtime.
*
* @return string
*/
public function getAllocatedHeight(): string
{
if (!$this->hasDimensions()) {
throw new \RangeException('Either width and height or previewWidth and previewHeight must be defined to use this method.');
}
if ($this->width && $this->height) {
$width = $this->width;
$height = $this->height;
} else {
$width = $this->previewWidth;
$height = $this->previewHeight;
}
return (100 * $height / $width) . '%';
}
/**
* Return a new PostMedia entity with a different preview URI and an optional proxy size name.
* The new entity preview's width and height are rescaled according to the provided size.
@ -263,4 +287,14 @@ class PostMedia extends BaseEntity
$this->id,
);
}
/**
* Checks the media has at least one full set of dimensions, needed for the height allocation feature
*
* @return bool
*/
public function hasDimensions(): bool
{
return $this->width && $this->height || $this->previewWidth && $this->previewHeight;
}
}

View file

@ -22,6 +22,7 @@
namespace Friendica\Model;
use Friendica\Contact\LocalRelationship\Entity\LocalRelationship;
use Friendica\Content\Image;
use Friendica\Content\Post\Collection\PostMedias;
use Friendica\Content\Post\Entity\PostMedia;
use Friendica\Content\Text\BBCode;
@ -3241,7 +3242,7 @@ class Item
}
if (!empty($sharedSplitAttachments)) {
$s = self::addGallery($s, $sharedSplitAttachments['visual'], $item['uri-id']);
$s = self::addGallery($s, $sharedSplitAttachments['visual']);
$s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true);
$s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links);
$s = self::addNonVisualAttachments($sharedSplitAttachments['additional'], $item, $s, true);
@ -3254,7 +3255,7 @@ class Item
$s = substr($s, 0, $pos);
}
$s = self::addGallery($s, $itemSplitAttachments['visual'], $item['uri-id']);
$s = self::addGallery($s, $itemSplitAttachments['visual']);
$s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false);
$s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links);
$s = self::addNonVisualAttachments($itemSplitAttachments['additional'], $item, $s, false);
@ -3285,45 +3286,32 @@ class Item
return $hook_data['html'];
}
/**
* @param PostMedias $images
* @return string
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
private static function makeImageGrid(PostMedias $images): string
{
// Image for first column (fc) and second column (sc)
$images_fc = [];
$images_sc = [];
for ($i = 0; $i < count($images); $i++) {
($i % 2 == 0) ? ($images_fc[] = $images[$i]) : ($images_sc[] = $images[$i]);
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [
'columns' => [
'fc' => $images_fc,
'sc' => $images_sc,
],
]);
}
/**
* Modify links to pictures to links for the "Fancybox" gallery
*
* @param string $s
* @param PostMedias $PostMedias
* @param int $uri_id
* @return string
*/
private static function addGallery(string $s, PostMedias $PostMedias, int $uri_id): string
private static function addGallery(string $s, PostMedias $PostMedias): string
{
foreach ($PostMedias as $PostMedia) {
if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) {
continue;
}
$s = str_replace('<a href="' . $PostMedia->url . '"', '<a data-fancybox="' . $uri_id . '" href="' . $PostMedia->url . '"', $s);
if ($PostMedia->hasDimensions()) {
$pattern = '#<a href="' . preg_quote($PostMedia->url) . '">(.*?)"></a>#';
$s = preg_replace_callback($pattern, function () use ($PostMedia) {
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
'$image' => $PostMedia,
'$allocated_height' => $PostMedia->getAllocatedHeight(),
]);
}, $s);
} else {
$s = str_replace('<a href="' . $PostMedia->url . '"', '<a data-fancybox="uri-id-' . $PostMedia->uriId . '" href="' . $PostMedia->url . '"', $s);
}
}
return $s;
@ -3494,14 +3482,7 @@ class Item
}
}
$media = '';
if (count($images) > 1) {
$media = self::makeImageGrid($images);
} elseif (count($images) == 1) {
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [
'$image' => $images[0],
]);
}
$media = Image::getBodyAttachHtml($images);
// On Diaspora posts the attached pictures are leading
if ($item['network'] == Protocol::DIASPORA) {