Merge pull request #13476 from MrPetovan/bug/13467-image-reliable-dimensions
Redux horizontal masonry, height allocation feature with ensured dimensions
This commit is contained in:
commit
2911895cdb
26 changed files with 1577 additions and 292 deletions
|
@ -129,6 +129,24 @@ class BaseCollection extends \ArrayIterator
|
|||
return new static(array_reverse($this->getArrayCopy()), $this->getTotalCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the collection in smaller collections no bigger than the provided length
|
||||
*
|
||||
* @param int $length
|
||||
* @return static[]
|
||||
*/
|
||||
public function chunk(int $length): array
|
||||
{
|
||||
if ($length < 1) {
|
||||
throw new \RangeException('BaseCollection->chunk(): Size parameter expected to be greater than 0');
|
||||
}
|
||||
|
||||
return array_map(function ($array) {
|
||||
return new static($array);
|
||||
}, array_chunk($this->getArrayCopy(), $length));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
|
|
154
src/Content/Image.php
Normal file
154
src/Content/Image.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
57
src/Content/Image/Collection/MasonryImageRow.php
Normal file
57
src/Content/Image/Collection/MasonryImageRow.php
Normal 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;
|
||||
}
|
||||
}
|
60
src/Content/Image/Entity/MasonryImage.php
Normal file
60
src/Content/Image/Entity/MasonryImage.php
Normal 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;
|
||||
}
|
||||
}
|
57
src/Content/Post/Collection/PostMedias.php
Normal file
57
src/Content/Post/Collection/PostMedias.php
Normal 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\Post\Collection;
|
||||
|
||||
use Friendica\BaseCollection;
|
||||
use Friendica\Content\Post\Entity;
|
||||
|
||||
class PostMedias extends BaseCollection
|
||||
{
|
||||
/**
|
||||
* @param Entity\PostMedia[] $entities
|
||||
* @param int|null $totalCount
|
||||
*/
|
||||
public function __construct(array $entities = [], int $totalCount = null)
|
||||
{
|
||||
parent::__construct($entities, $totalCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Entity\PostMedia
|
||||
*/
|
||||
public function current(): Entity\PostMedia
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
300
src/Content/Post/Entity/PostMedia.php
Normal file
300
src/Content/Post/Entity/PostMedia.php
Normal file
|
@ -0,0 +1,300 @@
|
|||
<?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\Post\Entity;
|
||||
|
||||
use Friendica\BaseEntity;
|
||||
use Friendica\Network\Entity\MimeType;
|
||||
use Friendica\Util\Images;
|
||||
use Friendica\Util\Proxy;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
|
||||
/**
|
||||
* @property-read int $id
|
||||
* @property-read int $uriId
|
||||
* @property-read ?int $activityUriId
|
||||
* @property-read UriInterface $url
|
||||
* @property-read int $type
|
||||
* @property-read MimeType $mimetype
|
||||
* @property-read ?int $width
|
||||
* @property-read ?int $height
|
||||
* @property-read ?int $size
|
||||
* @property-read ?UriInterface $preview
|
||||
* @property-read ?int $previewWidth
|
||||
* @property-read ?int $previewHeight
|
||||
* @property-read ?string $description
|
||||
* @property-read ?string $name
|
||||
* @property-read ?UriInterface $authorUrl
|
||||
* @property-read ?string $authorName
|
||||
* @property-read ?UriInterface $authorImage
|
||||
* @property-read ?UriInterface $publisherUrl
|
||||
* @property-read ?string $publisherName
|
||||
* @property-read ?UriInterface $publisherImage
|
||||
* @property-read ?string $blurhash
|
||||
*/
|
||||
class PostMedia extends BaseEntity
|
||||
{
|
||||
const TYPE_UNKNOWN = 0;
|
||||
const TYPE_IMAGE = 1;
|
||||
const TYPE_VIDEO = 2;
|
||||
const TYPE_AUDIO = 3;
|
||||
const TYPE_TEXT = 4;
|
||||
const TYPE_APPLICATION = 5;
|
||||
const TYPE_TORRENT = 16;
|
||||
const TYPE_HTML = 17;
|
||||
const TYPE_XML = 18;
|
||||
const TYPE_PLAIN = 19;
|
||||
const TYPE_ACTIVITY = 20;
|
||||
const TYPE_ACCOUNT = 21;
|
||||
const TYPE_DOCUMENT = 128;
|
||||
|
||||
/** @var int */
|
||||
protected $id;
|
||||
/** @var int */
|
||||
protected $uriId;
|
||||
/** @var UriInterface */
|
||||
protected $url;
|
||||
/** @var int One of TYPE_* */
|
||||
protected $type;
|
||||
/** @var MimeType */
|
||||
protected $mimetype;
|
||||
/** @var ?int */
|
||||
protected $activityUriId;
|
||||
/** @var ?int In pixels */
|
||||
protected $width;
|
||||
/** @var ?int In pixels */
|
||||
protected $height;
|
||||
/** @var ?int In bytes */
|
||||
protected $size;
|
||||
/** @var ?UriInterface Preview URL */
|
||||
protected $preview;
|
||||
/** @var ?int In pixels */
|
||||
protected $previewWidth;
|
||||
/** @var ?int In pixels */
|
||||
protected $previewHeight;
|
||||
/** @var ?string Alternative text like for images */
|
||||
protected $description;
|
||||
/** @var ?string */
|
||||
protected $name;
|
||||
/** @var ?UriInterface */
|
||||
protected $authorUrl;
|
||||
/** @var ?string */
|
||||
protected $authorName;
|
||||
/** @var ?UriInterface Image URL */
|
||||
protected $authorImage;
|
||||
/** @var ?UriInterface */
|
||||
protected $publisherUrl;
|
||||
/** @var ?string */
|
||||
protected $publisherName;
|
||||
/** @var ?UriInterface Image URL */
|
||||
protected $publisherImage;
|
||||
/** @var ?string Blurhash string representation for images
|
||||
* @see https://github.com/woltapp/blurhash
|
||||
* @see https://blurha.sh/
|
||||
*/
|
||||
protected $blurhash;
|
||||
|
||||
public function __construct(
|
||||
int $uriId,
|
||||
UriInterface $url,
|
||||
int $type,
|
||||
MimeType $mimetype,
|
||||
?int $activityUriId,
|
||||
?int $width = null,
|
||||
?int $height = null,
|
||||
?int $size = null,
|
||||
?UriInterface $preview = null,
|
||||
?int $previewWidth = null,
|
||||
?int $previewHeight = null,
|
||||
?string $description = null,
|
||||
?string $name = null,
|
||||
?UriInterface $authorUrl = null,
|
||||
?string $authorName = null,
|
||||
?UriInterface $authorImage = null,
|
||||
?UriInterface $publisherUrl = null,
|
||||
?string $publisherName = null,
|
||||
?UriInterface $publisherImage = null,
|
||||
?string $blurhash = null,
|
||||
int $id = null
|
||||
)
|
||||
{
|
||||
$this->uriId = $uriId;
|
||||
$this->url = $url;
|
||||
$this->type = $type;
|
||||
$this->mimetype = $mimetype;
|
||||
$this->activityUriId = $activityUriId;
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->size = $size;
|
||||
$this->preview = $preview;
|
||||
$this->previewWidth = $previewWidth;
|
||||
$this->previewHeight = $previewHeight;
|
||||
$this->description = $description;
|
||||
$this->name = $name;
|
||||
$this->authorUrl = $authorUrl;
|
||||
$this->authorName = $authorName;
|
||||
$this->authorImage = $authorImage;
|
||||
$this->publisherUrl = $publisherUrl;
|
||||
$this->publisherName = $publisherName;
|
||||
$this->publisherImage = $publisherImage;
|
||||
$this->blurhash = $blurhash;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get media link for given media id
|
||||
*
|
||||
* @param string $size One of the Proxy::SIZE_* constants
|
||||
* @return string media link
|
||||
*/
|
||||
public function getPhotoPath(string $size = ''): string
|
||||
{
|
||||
return '/photo/media/' .
|
||||
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
|
||||
$this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview path for given media id relative to the base URL
|
||||
*
|
||||
* @param string $size One of the Proxy::SIZE_* constants
|
||||
* @return string preview link
|
||||
*/
|
||||
public function getPreviewPath(string $size = ''): string
|
||||
{
|
||||
return '/photo/preview/' .
|
||||
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
|
||||
$this->id;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param \GuzzleHttp\Psr7\Uri $preview
|
||||
* @param string $size
|
||||
* @return $this
|
||||
*/
|
||||
public function withPreview(\GuzzleHttp\Psr7\Uri $preview, string $size = ''): self
|
||||
{
|
||||
if ($this->width || $this->height) {
|
||||
$newWidth = $this->width;
|
||||
$newHeight = $this->height;
|
||||
} else {
|
||||
$newWidth = $this->previewWidth;
|
||||
$newHeight = $this->previewHeight;
|
||||
}
|
||||
|
||||
if ($newWidth && $newHeight && $size) {
|
||||
$dimensionts = Images::getScalingDimensions($newWidth, $newHeight, Proxy::getPixelsFromSize($size));
|
||||
$newWidth = $dimensionts['width'];
|
||||
$newHeight = $dimensionts['height'];
|
||||
}
|
||||
|
||||
return new static(
|
||||
$this->uriId,
|
||||
$this->url,
|
||||
$this->type,
|
||||
$this->mimetype,
|
||||
$this->activityUriId,
|
||||
$this->width,
|
||||
$this->height,
|
||||
$this->size,
|
||||
$preview,
|
||||
$newWidth,
|
||||
$newHeight,
|
||||
$this->description,
|
||||
$this->name,
|
||||
$this->authorUrl,
|
||||
$this->authorName,
|
||||
$this->authorImage,
|
||||
$this->publisherUrl,
|
||||
$this->publisherName,
|
||||
$this->publisherImage,
|
||||
$this->blurhash,
|
||||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
public function withUrl(\GuzzleHttp\Psr7\Uri $url): self
|
||||
{
|
||||
return new static(
|
||||
$this->uriId,
|
||||
$url,
|
||||
$this->type,
|
||||
$this->mimetype,
|
||||
$this->activityUriId,
|
||||
$this->width,
|
||||
$this->height,
|
||||
$this->size,
|
||||
$this->preview,
|
||||
$this->previewWidth,
|
||||
$this->previewHeight,
|
||||
$this->description,
|
||||
$this->name,
|
||||
$this->authorUrl,
|
||||
$this->authorName,
|
||||
$this->authorImage,
|
||||
$this->publisherUrl,
|
||||
$this->publisherName,
|
||||
$this->publisherImage,
|
||||
$this->blurhash,
|
||||
$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;
|
||||
}
|
||||
}
|
117
src/Content/Post/Factory/PostMedia.php
Normal file
117
src/Content/Post/Factory/PostMedia.php
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?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\Post\Factory;
|
||||
|
||||
use Friendica\BaseFactory;
|
||||
use Friendica\Capabilities\ICanCreateFromTableRow;
|
||||
use Friendica\Content\Post\Entity;
|
||||
use Friendica\Network;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use stdClass;
|
||||
|
||||
class PostMedia extends BaseFactory implements ICanCreateFromTableRow
|
||||
{
|
||||
/** @var Network\Factory\MimeType */
|
||||
private $mimeTypeFactory;
|
||||
|
||||
public function __construct(Network\Factory\MimeType $mimeTypeFactory, LoggerInterface $logger)
|
||||
{
|
||||
parent::__construct($logger);
|
||||
|
||||
$this->mimeTypeFactory = $mimeTypeFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createFromTableRow(array $row)
|
||||
{
|
||||
return new Entity\PostMedia(
|
||||
$row['uri-id'],
|
||||
$row['url'] ? new Uri($row['url']) : null,
|
||||
$row['type'],
|
||||
$this->mimeTypeFactory->createFromContentType($row['mimetype']),
|
||||
$row['media-uri-id'],
|
||||
$row['width'],
|
||||
$row['height'],
|
||||
$row['size'],
|
||||
$row['preview'] ? new Uri($row['preview']) : null,
|
||||
$row['preview-width'],
|
||||
$row['preview-height'],
|
||||
$row['description'],
|
||||
$row['name'],
|
||||
$row['author-url'] ? new Uri($row['author-url']) : null,
|
||||
$row['author-name'],
|
||||
$row['author-image'] ? new Uri($row['author-image']) : null,
|
||||
$row['publisher-url'] ? new Uri($row['publisher-url']) : null,
|
||||
$row['publisher-name'],
|
||||
$row['publisher-image'] ? new Uri($row['publisher-image']) : null,
|
||||
$row['blurhash'],
|
||||
$row['id']
|
||||
);
|
||||
}
|
||||
|
||||
public function createFromBlueskyImageEmbed(int $uriId, stdClass $image): Entity\PostMedia
|
||||
{
|
||||
return new Entity\PostMedia(
|
||||
$uriId,
|
||||
new Uri($image->fullsize),
|
||||
Entity\PostMedia::TYPE_IMAGE,
|
||||
new Network\Entity\MimeType('unkn', 'unkn'),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new Uri($image->thumb),
|
||||
null,
|
||||
null,
|
||||
$image->alt,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function createFromBlueskyExternalEmbed(int $uriId, stdClass $external): Entity\PostMedia
|
||||
{
|
||||
return new Entity\PostMedia(
|
||||
$uriId,
|
||||
new Uri($external->uri),
|
||||
Entity\PostMedia::TYPE_HTML,
|
||||
new Network\Entity\MimeType('text', 'html'),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$external->description,
|
||||
$external->title
|
||||
);
|
||||
}
|
||||
|
||||
public function createFromAttachment(int $uriId, array $attachment)
|
||||
{
|
||||
$attachment['uri-id'] = $uriId;
|
||||
return $this->createFromTableRow($attachment);
|
||||
}
|
||||
}
|
204
src/Content/Post/Repository/PostMedia.php
Normal file
204
src/Content/Post/Repository/PostMedia.php
Normal file
|
@ -0,0 +1,204 @@
|
|||
<?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\Post\Repository;
|
||||
|
||||
use Friendica\BaseCollection;
|
||||
use Friendica\BaseRepository;
|
||||
use Friendica\Content\Post\Collection;
|
||||
use Friendica\Content\Post\Entity;
|
||||
use Friendica\Content\Post\Factory;
|
||||
use Friendica\Database\Database;
|
||||
use Friendica\Util\Strings;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class PostMedia extends BaseRepository
|
||||
{
|
||||
protected static $table_name = 'post-media';
|
||||
|
||||
public function __construct(Database $database, LoggerInterface $logger, Factory\PostMedia $factory)
|
||||
{
|
||||
parent::__construct($database, $logger, $factory);
|
||||
}
|
||||
|
||||
protected function _select(array $condition, array $params = []): BaseCollection
|
||||
{
|
||||
$rows = $this->db->selectToArray(static::$table_name, [], $condition, $params);
|
||||
|
||||
$Entities = new Collection\PostMedias();
|
||||
foreach ($rows as $fields) {
|
||||
$Entities[] = $this->factory->createFromTableRow($fields);
|
||||
}
|
||||
|
||||
return $Entities;
|
||||
}
|
||||
|
||||
public function selectOneById(int $postMediaId): Entity\PostMedia
|
||||
{
|
||||
return $this->_selectOne(['id' => $postMediaId]);
|
||||
}
|
||||
|
||||
public function selectByUriId(int $uriId): Collection\PostMedias
|
||||
{
|
||||
return $this->_select(['uri-id' => $uriId]);
|
||||
}
|
||||
|
||||
public function save(Entity\PostMedia $PostMedia): Entity\PostMedia
|
||||
{
|
||||
$fields = [
|
||||
'uri-id' => $PostMedia->uriId,
|
||||
'url' => $PostMedia->url->__toString(),
|
||||
'type' => $PostMedia->type,
|
||||
'mimetype' => $PostMedia->mimetype->__toString(),
|
||||
'height' => $PostMedia->height,
|
||||
'width' => $PostMedia->width,
|
||||
'size' => $PostMedia->size,
|
||||
'preview' => $PostMedia->preview ? $PostMedia->preview->__toString() : null,
|
||||
'preview-height' => $PostMedia->previewHeight,
|
||||
'preview-width' => $PostMedia->previewWidth,
|
||||
'description' => $PostMedia->description,
|
||||
'name' => $PostMedia->name,
|
||||
'author-url' => $PostMedia->authorUrl ? $PostMedia->authorUrl->__toString() : null,
|
||||
'author-name' => $PostMedia->authorName,
|
||||
'author-image' => $PostMedia->authorImage ? $PostMedia->authorImage->__toString() : null,
|
||||
'publisher-url' => $PostMedia->publisherUrl ? $PostMedia->publisherUrl->__toString() : null,
|
||||
'publisher-name' => $PostMedia->publisherName,
|
||||
'publisher-image' => $PostMedia->publisherImage ? $PostMedia->publisherImage->__toString() : null,
|
||||
'media-uri-id' => $PostMedia->activityUriId,
|
||||
'blurhash' => $PostMedia->blurhash,
|
||||
];
|
||||
|
||||
if ($PostMedia->id) {
|
||||
$this->db->update(self::$table_name, $fields, ['id' => $PostMedia->id]);
|
||||
} else {
|
||||
$this->db->insert(self::$table_name, $fields, Database::INSERT_IGNORE);
|
||||
|
||||
$newPostMediaId = $this->db->lastInsertId();
|
||||
|
||||
$PostMedia = $this->selectOneById($newPostMediaId);
|
||||
}
|
||||
|
||||
return $PostMedia;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Split the attachment media in the three segments "visual", "link" and "additional"
|
||||
*
|
||||
* @param int $uri_id URI id
|
||||
* @param array $links list of links that shouldn't be added
|
||||
* @param bool $has_media
|
||||
* @return Collection\PostMedias[] Three collections in "visual", "link" and "additional" keys
|
||||
*/
|
||||
public function splitAttachments(int $uri_id, array $links = [], bool $has_media = true): array
|
||||
{
|
||||
$attachments = [
|
||||
'visual' => new Collection\PostMedias(),
|
||||
'link' => new Collection\PostMedias(),
|
||||
'additional' => new Collection\PostMedias(),
|
||||
];
|
||||
|
||||
if (!$has_media) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
$PostMedias = $this->selectByUriId($uri_id);
|
||||
if (!count($PostMedias)) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
$heights = [];
|
||||
$selected = '';
|
||||
$previews = [];
|
||||
|
||||
foreach ($PostMedias as $PostMedia) {
|
||||
foreach ($links as $link) {
|
||||
if (Strings::compareLink($link, $PostMedia->url)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid adding separate media entries for previews
|
||||
foreach ($previews as $preview) {
|
||||
if (Strings::compareLink($preview, $PostMedia->url)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently these two types are ignored here.
|
||||
// Posts are added differently and contacts are not displayed as attachments.
|
||||
if (in_array($PostMedia->type, [Entity\PostMedia::TYPE_ACCOUNT, Entity\PostMedia::TYPE_ACTIVITY])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($PostMedia->preview)) {
|
||||
$previews[] = $PostMedia->preview;
|
||||
}
|
||||
|
||||
//$PostMedia->filetype = $filetype;
|
||||
//$PostMedia->subtype = $subtype;
|
||||
|
||||
if ($PostMedia->type == Entity\PostMedia::TYPE_HTML || ($PostMedia->mimetype->type == 'text' && $PostMedia->mimetype->subtype == 'html')) {
|
||||
$attachments['link'][] = $PostMedia;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($PostMedia->type, [Entity\PostMedia::TYPE_AUDIO, Entity\PostMedia::TYPE_IMAGE]) ||
|
||||
in_array($PostMedia->mimetype->type, ['audio', 'image'])
|
||||
) {
|
||||
$attachments['visual'][] = $PostMedia;
|
||||
} elseif (($PostMedia->type == Entity\PostMedia::TYPE_VIDEO) || ($PostMedia->mimetype->type == 'video')) {
|
||||
if (!empty($PostMedia->height)) {
|
||||
// Peertube videos are delivered in many different resolutions. We pick a moderate one.
|
||||
// Since only Peertube provides a "height" parameter, this wouldn't be executed
|
||||
// when someone for example on Mastodon was sharing multiple videos in a single post.
|
||||
$heights[$PostMedia->height] = (string)$PostMedia->url;
|
||||
$video[(string) $PostMedia->url] = $PostMedia;
|
||||
} else {
|
||||
$attachments['visual'][] = $PostMedia;
|
||||
}
|
||||
} else {
|
||||
$attachments['additional'][] = $PostMedia;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($heights)) {
|
||||
ksort($heights);
|
||||
foreach ($heights as $height => $url) {
|
||||
if (empty($selected) || $height <= 480) {
|
||||
$selected = $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($selected)) {
|
||||
$attachments['visual'][] = $video[$selected];
|
||||
unset($video[$selected]);
|
||||
foreach ($video as $element) {
|
||||
$attachments['additional'][] = $element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
}
|
|
@ -731,4 +731,9 @@ abstract class DI
|
|||
{
|
||||
return self::$dice->create(Util\Emailer::class);
|
||||
}
|
||||
|
||||
public static function postMediaRepository(): Content\Post\Repository\PostMedia
|
||||
{
|
||||
return self::$dice->create(Content\Post\Repository\PostMedia::class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
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;
|
||||
use Friendica\Content\Text\HTML;
|
||||
use Friendica\Core\Hook;
|
||||
|
@ -34,6 +37,7 @@ use Friendica\Database\DBA;
|
|||
use Friendica\DI;
|
||||
use Friendica\Model\Post\Category;
|
||||
use Friendica\Network\HTTPException\InternalServerErrorException;
|
||||
use Friendica\Network\HTTPException\ServiceUnavailableException;
|
||||
use Friendica\Protocol\Activity;
|
||||
use Friendica\Protocol\ActivityPub;
|
||||
use Friendica\Protocol\Delivery;
|
||||
|
@ -3175,15 +3179,15 @@ class Item
|
|||
if (!empty($shared_item['uri-id'])) {
|
||||
$shared_uri_id = $shared_item['uri-id'];
|
||||
$shared_links[] = strtolower($shared_item['plink']);
|
||||
$shared_attachments = Post\Media::splitAttachments($shared_uri_id, [], $shared_item['has-media']);
|
||||
$shared_links = array_merge($shared_links, array_column($shared_attachments['visual'], 'url'));
|
||||
$shared_links = array_merge($shared_links, array_column($shared_attachments['link'], 'url'));
|
||||
$shared_links = array_merge($shared_links, array_column($shared_attachments['additional'], 'url'));
|
||||
$item['body'] = self::replaceVisualAttachments($shared_attachments, $item['body']);
|
||||
$sharedSplitAttachments = DI::postMediaRepository()->splitAttachments($shared_uri_id, [], $shared_item['has-media']);
|
||||
$shared_links = array_merge($shared_links, $sharedSplitAttachments['visual']->column('url'));
|
||||
$shared_links = array_merge($shared_links, $sharedSplitAttachments['link']->column('url'));
|
||||
$shared_links = array_merge($shared_links, $sharedSplitAttachments['additional']->column('url'));
|
||||
$item['body'] = self::replaceVisualAttachments($sharedSplitAttachments['visual'], $item['body']);
|
||||
}
|
||||
|
||||
$attachments = Post\Media::splitAttachments($item['uri-id'], $shared_links, $item['has-media'] ?? false);
|
||||
$item['body'] = self::replaceVisualAttachments($attachments, $item['body'] ?? '');
|
||||
$itemSplitAttachments = DI::postMediaRepository()->splitAttachments($item['uri-id'], $shared_links, $item['has-media'] ?? false);
|
||||
$item['body'] = self::replaceVisualAttachments($itemSplitAttachments['visual'], $item['body'] ?? '');
|
||||
|
||||
self::putInCache($item);
|
||||
$item['body'] = $body;
|
||||
|
@ -3208,7 +3212,7 @@ class Item
|
|||
$filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']);
|
||||
}
|
||||
|
||||
$item['attachments'] = $attachments;
|
||||
$item['attachments'] = $itemSplitAttachments;
|
||||
|
||||
$hook_data = [
|
||||
'item' => $item,
|
||||
|
@ -3237,11 +3241,11 @@ class Item
|
|||
return $s;
|
||||
}
|
||||
|
||||
if (!empty($shared_attachments)) {
|
||||
$s = self::addGallery($s, $shared_attachments, $item['uri-id']);
|
||||
$s = self::addVisualAttachments($shared_attachments, $shared_item, $s, true);
|
||||
$s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $shared_attachments, $body, $s, true, $quote_shared_links);
|
||||
$s = self::addNonVisualAttachments($shared_attachments, $item, $s, true);
|
||||
if (!empty($sharedSplitAttachments)) {
|
||||
$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);
|
||||
$body = BBCode::removeSharedData($body);
|
||||
}
|
||||
|
||||
|
@ -3251,10 +3255,10 @@ class Item
|
|||
$s = substr($s, 0, $pos);
|
||||
}
|
||||
|
||||
$s = self::addGallery($s, $attachments, $item['uri-id']);
|
||||
$s = self::addVisualAttachments($attachments, $item, $s, false);
|
||||
$s = self::addLinkAttachment($item['uri-id'], $attachments, $body, $s, false, $shared_links);
|
||||
$s = self::addNonVisualAttachments($attachments, $item, $s, false);
|
||||
$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);
|
||||
$s = self::addQuestions($item, $s);
|
||||
|
||||
// Map.
|
||||
|
@ -3282,45 +3286,34 @@ class Item
|
|||
return $hook_data['html'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $images
|
||||
* @return string
|
||||
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
|
||||
*/
|
||||
private static function makeImageGrid(array $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 array $attachments
|
||||
* @param integer $uri_id
|
||||
* @param string $s
|
||||
* @param PostMedias $PostMedias
|
||||
* @return string
|
||||
*/
|
||||
private static function addGallery(string $s, array $attachments, int $uri_id): string
|
||||
private static function addGallery(string $s, PostMedias $PostMedias): string
|
||||
{
|
||||
foreach ($attachments['visual'] as $attachment) {
|
||||
if (empty($attachment['preview']) || ($attachment['type'] != Post\Media::IMAGE)) {
|
||||
foreach ($PostMedias as $PostMedia) {
|
||||
if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) {
|
||||
continue;
|
||||
}
|
||||
$s = str_replace('<a href="' . $attachment['url'] . '"', '<a data-fancybox="' . $uri_id . '" href="' . $attachment['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;
|
||||
}
|
||||
|
||||
|
@ -3378,30 +3371,30 @@ class Item
|
|||
/**
|
||||
* Replace visual attachments in the body
|
||||
*
|
||||
* @param array $attachments
|
||||
* @param string $body
|
||||
* @param PostMedias $PostMedias
|
||||
* @param string $body
|
||||
* @return string modified body
|
||||
*/
|
||||
private static function replaceVisualAttachments(array $attachments, string $body): string
|
||||
private static function replaceVisualAttachments(PostMedias $PostMedias, string $body): string
|
||||
{
|
||||
DI::profiler()->startRecording('rendering');
|
||||
|
||||
foreach ($attachments['visual'] as $attachment) {
|
||||
if (!empty($attachment['preview'])) {
|
||||
if (Network::isLocalLink($attachment['preview'])) {
|
||||
foreach ($PostMedias as $PostMedia) {
|
||||
if ($PostMedia->preview) {
|
||||
if (DI::baseUrl()->isLocalUri($PostMedia->preview)) {
|
||||
continue;
|
||||
}
|
||||
$proxy = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_LARGE);
|
||||
$search = ['[img=' . $attachment['preview'] . ']', ']' . $attachment['preview'] . '[/img]'];
|
||||
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
|
||||
$search = ['[img=' . $PostMedia->preview . ']', ']' . $PostMedia->preview . '[/img]'];
|
||||
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
|
||||
|
||||
$body = str_replace($search, $replace, $body);
|
||||
} elseif ($attachment['filetype'] == 'image') {
|
||||
if (Network::isLocalLink($attachment['url'])) {
|
||||
} elseif ($PostMedia->mimetype->type == 'image') {
|
||||
if (DI::baseUrl()->isLocalUri($PostMedia->url)) {
|
||||
continue;
|
||||
}
|
||||
$proxy = Post\Media::getUrlForId($attachment['id']);
|
||||
$search = ['[img=' . $attachment['url'] . ']', ']' . $attachment['url'] . '[/img]'];
|
||||
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
|
||||
$search = ['[img=' . $PostMedia->url . ']', ']' . $PostMedia->url . '[/img]'];
|
||||
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
|
||||
|
||||
$body = str_replace($search, $replace, $body);
|
||||
|
@ -3414,29 +3407,34 @@ class Item
|
|||
/**
|
||||
* Add visual attachments to the content
|
||||
*
|
||||
* @param array $attachments
|
||||
* @param array $item
|
||||
* @param string $content
|
||||
* @param PostMedias $PostMedias
|
||||
* @param array $item
|
||||
* @param string $content
|
||||
* @param bool $shared
|
||||
* @return string modified content
|
||||
* @throws ServiceUnavailableException
|
||||
*/
|
||||
private static function addVisualAttachments(array $attachments, array $item, string $content, bool $shared): string
|
||||
private static function addVisualAttachments(PostMedias $PostMedias, array $item, string $content, bool $shared): string
|
||||
{
|
||||
DI::profiler()->startRecording('rendering');
|
||||
$leading = '';
|
||||
$trailing = '';
|
||||
$images = [];
|
||||
$images = new PostMedias();
|
||||
|
||||
// @todo In the future we should make a single for the template engine with all media in it. This allows more flexibilty.
|
||||
foreach ($attachments['visual'] as $attachment) {
|
||||
if (self::containsLink($item['body'], $attachment['preview'] ?? $attachment['url'], $attachment['type'])) {
|
||||
foreach ($PostMedias as $PostMedia) {
|
||||
if (self::containsLink($item['body'], $PostMedia->preview ?? $PostMedia->url, $PostMedia->type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($attachment['filetype'] == 'image') {
|
||||
$preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE);
|
||||
} elseif (!empty($attachment['preview'])) {
|
||||
$preview_url = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_LARGE);
|
||||
if ($PostMedia->mimetype->type == 'image') {
|
||||
$preview_size = $PostMedia->width > $PostMedia->height ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE;
|
||||
$preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size);
|
||||
} elseif ($PostMedia->preview) {
|
||||
$preview_size = Proxy::SIZE_LARGE;
|
||||
$preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size);
|
||||
} else {
|
||||
$preview_size = 0;
|
||||
$preview_url = '';
|
||||
}
|
||||
|
||||
|
@ -3444,15 +3442,15 @@ class Item
|
|||
continue;
|
||||
}
|
||||
|
||||
if (($attachment['filetype'] == 'video')) {
|
||||
if ($PostMedia->mimetype->type == 'video') {
|
||||
/// @todo Move the template to /content as well
|
||||
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [
|
||||
'$video' => [
|
||||
'id' => $attachment['id'],
|
||||
'src' => $attachment['url'],
|
||||
'name' => $attachment['name'] ?: $attachment['url'],
|
||||
'id' => $PostMedia->id,
|
||||
'src' => (string)$PostMedia->url,
|
||||
'name' => $PostMedia->name ?: $PostMedia->url,
|
||||
'preview' => $preview_url,
|
||||
'mime' => $attachment['mimetype'],
|
||||
'mime' => (string)$PostMedia->mimetype,
|
||||
],
|
||||
]);
|
||||
if (($item['post-type'] ?? null) == Item::PT_VIDEO) {
|
||||
|
@ -3460,13 +3458,13 @@ class Item
|
|||
} else {
|
||||
$trailing .= $media;
|
||||
}
|
||||
} elseif ($attachment['filetype'] == 'audio') {
|
||||
} elseif ($PostMedia->mimetype->type == 'audio') {
|
||||
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/audio.tpl'), [
|
||||
'$audio' => [
|
||||
'id' => $attachment['id'],
|
||||
'src' => $attachment['url'],
|
||||
'name' => $attachment['name'] ?: $attachment['url'],
|
||||
'mime' => $attachment['mimetype'],
|
||||
'id' => $PostMedia->id,
|
||||
'src' => (string)$PostMedia->url,
|
||||
'name' => $PostMedia->name ?: $PostMedia->url,
|
||||
'mime' => (string)$PostMedia->mimetype,
|
||||
],
|
||||
]);
|
||||
if (($item['post-type'] ?? null) == Item::PT_AUDIO) {
|
||||
|
@ -3474,23 +3472,17 @@ class Item
|
|||
} else {
|
||||
$trailing .= $media;
|
||||
}
|
||||
} elseif ($attachment['filetype'] == 'image') {
|
||||
$src_url = Post\Media::getUrlForId($attachment['id']);
|
||||
} elseif ($PostMedia->mimetype->type == 'image') {
|
||||
$src_url = DI::baseUrl() . $PostMedia->getPhotoPath();
|
||||
if (self::containsLink($item['body'], $src_url)) {
|
||||
continue;
|
||||
}
|
||||
$images[] = ['src' => $src_url, 'preview' => $preview_url, 'attachment' => $attachment, 'uri_id' => $item['uri-id']];
|
||||
|
||||
$images[] = $PostMedia->withUrl(new Uri($src_url))->withPreview(new Uri($preview_url), $preview_size);
|
||||
}
|
||||
}
|
||||
|
||||
$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) {
|
||||
|
@ -3519,59 +3511,62 @@ class Item
|
|||
/**
|
||||
* Add link attachment to the content
|
||||
*
|
||||
* @param array $attachments
|
||||
* @param string $body
|
||||
* @param string $content
|
||||
* @param bool $shared
|
||||
* @param array $ignore_links A list of URLs to ignore
|
||||
* @param int $uriid
|
||||
* @param PostMedias[] $attachments
|
||||
* @param string $body
|
||||
* @param string $content
|
||||
* @param bool $shared
|
||||
* @param array $ignore_links A list of URLs to ignore
|
||||
* @return string modified content
|
||||
* @throws InternalServerErrorException
|
||||
* @throws ServiceUnavailableException
|
||||
*/
|
||||
private static function addLinkAttachment(int $uriid, array $attachments, string $body, string $content, bool $shared, array $ignore_links): string
|
||||
{
|
||||
DI::profiler()->startRecording('rendering');
|
||||
// Don't show a preview when there is a visual attachment (audio or video)
|
||||
$types = array_column($attachments['visual'], 'type');
|
||||
$preview = !in_array(Post\Media::IMAGE, $types) && !in_array(Post\Media::VIDEO, $types);
|
||||
$types = $attachments['visual']->column('type');
|
||||
$preview = !in_array(PostMedia::TYPE_IMAGE, $types) && !in_array(PostMedia::TYPE_VIDEO, $types);
|
||||
|
||||
if (!empty($attachments['link'])) {
|
||||
foreach ($attachments['link'] as $link) {
|
||||
$found = false;
|
||||
foreach ($ignore_links as $ignore_link) {
|
||||
if (Strings::compareLink($link['url'], $ignore_link)) {
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
// @todo Judge between the links to use the one with most information
|
||||
if (!$found && (empty($attachment) || !empty($link['author-name']) ||
|
||||
(empty($attachment['name']) && !empty($link['name'])) ||
|
||||
(empty($attachment['description']) && !empty($link['description'])) ||
|
||||
(empty($attachment['preview']) && !empty($link['preview'])))) {
|
||||
$attachment = $link;
|
||||
/** @var ?PostMedia $attachment */
|
||||
$attachment = null;
|
||||
foreach ($attachments['link'] as $PostMedia) {
|
||||
$found = false;
|
||||
foreach ($ignore_links as $ignore_link) {
|
||||
if (Strings::compareLink($PostMedia->url, $ignore_link)) {
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
// @todo Judge between the links to use the one with most information
|
||||
if (!$found && (empty($attachment) || $PostMedia->authorName ||
|
||||
(!$attachment->name && $PostMedia->name) ||
|
||||
(!$attachment->description && $PostMedia->description) ||
|
||||
(!$attachment->preview && $PostMedia->preview))) {
|
||||
$attachment = $PostMedia;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($attachment)) {
|
||||
$data = [
|
||||
'after' => '',
|
||||
'author_name' => $attachment['author-name'] ?? '',
|
||||
'author_url' => $attachment['author-url'] ?? '',
|
||||
'description' => $attachment['description'] ?? '',
|
||||
'author_name' => $attachment->authorName ?? '',
|
||||
'author_url' => (string)($attachment->authorUrl ?? ''),
|
||||
'description' => $attachment->description ?? '',
|
||||
'image' => '',
|
||||
'preview' => '',
|
||||
'provider_name' => $attachment['publisher-name'] ?? '',
|
||||
'provider_url' => $attachment['publisher-url'] ?? '',
|
||||
'provider_name' => $attachment->publisherName ?? '',
|
||||
'provider_url' => (string)($attachment->publisherUrl ?? ''),
|
||||
'text' => '',
|
||||
'title' => $attachment['name'] ?? '',
|
||||
'title' => $attachment->name ?? '',
|
||||
'type' => 'link',
|
||||
'url' => $attachment['url']
|
||||
'url' => (string)$attachment->url,
|
||||
];
|
||||
|
||||
if ($preview && !empty($attachment['preview'])) {
|
||||
if ($attachment['preview-width'] >= 500) {
|
||||
$data['image'] = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM);
|
||||
if ($preview && $attachment->preview) {
|
||||
if ($attachment->previewWidth >= 500) {
|
||||
$data['image'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
|
||||
} else {
|
||||
$data['preview'] = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM);
|
||||
$data['preview'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3639,19 +3634,21 @@ class Item
|
|||
}
|
||||
|
||||
/**
|
||||
* Add non visual attachments to the content
|
||||
* Add non-visual attachments to the content
|
||||
*
|
||||
* @param array $attachments
|
||||
* @param array $item
|
||||
* @param string $content
|
||||
* @param PostMedias $PostMedias
|
||||
* @param array $item
|
||||
* @param string $content
|
||||
* @return string modified content
|
||||
* @throws InternalServerErrorException
|
||||
* @throws \ImagickException
|
||||
*/
|
||||
private static function addNonVisualAttachments(array $attachments, array $item, string $content): string
|
||||
private static function addNonVisualAttachments(PostMedias $PostMedias, array $item, string $content): string
|
||||
{
|
||||
DI::profiler()->startRecording('rendering');
|
||||
$trailing = '';
|
||||
foreach ($attachments['additional'] as $attachment) {
|
||||
if (strpos($item['body'], $attachment['url'])) {
|
||||
foreach ($PostMedias as $PostMedia) {
|
||||
if (strpos($item['body'], $PostMedia->url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -3662,16 +3659,16 @@ class Item
|
|||
'url' => $item['author-link'],
|
||||
'alias' => $item['author-alias']
|
||||
];
|
||||
$the_url = Contact::magicLinkByContact($author, $attachment['url']);
|
||||
$the_url = Contact::magicLinkByContact($author, $PostMedia->url);
|
||||
|
||||
$title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url']));
|
||||
$title = Strings::escapeHtml(trim($PostMedia->description ?? '' ?: $PostMedia->url));
|
||||
|
||||
if (!empty($attachment['size'])) {
|
||||
$title .= ' ' . $attachment['size'] . ' ' . DI::l10n()->t('bytes');
|
||||
if ($PostMedia->size) {
|
||||
$title .= ' ' . $PostMedia->size . ' ' . DI::l10n()->t('bytes');
|
||||
}
|
||||
|
||||
/// @todo Use a template
|
||||
$icon = '<div class="attachtype icon s22 type-' . $attachment['filetype'] . ' subtype-' . $attachment['subtype'] . '"></div>';
|
||||
$icon = '<div class="attachtype icon s22 type-' . $PostMedia->mimetype->type . ' subtype-' . $PostMedia->mimetype->subtype . '"></div>';
|
||||
$trailing .= '<a href="' . strip_tags($the_url) . '" title="' . $title . '" class="attachlink" target="_blank" rel="noopener noreferrer" >' . $icon . '</a>';
|
||||
}
|
||||
|
||||
|
|
|
@ -874,113 +874,6 @@ class Media
|
|||
return DBA::delete('post-media', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the attachment media in the three segments "visual", "link" and "additional"
|
||||
*
|
||||
* @param int $uri_id URI id
|
||||
* @param array $links list of links that shouldn't be added
|
||||
* @param bool $has_media
|
||||
* @return array attachments
|
||||
*/
|
||||
public static function splitAttachments(int $uri_id, array $links = [], bool $has_media = true): array
|
||||
{
|
||||
$attachments = ['visual' => [], 'link' => [], 'additional' => []];
|
||||
|
||||
if (!$has_media) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
$media = self::getByURIId($uri_id);
|
||||
if (empty($media)) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
$heights = [];
|
||||
$selected = '';
|
||||
$previews = [];
|
||||
|
||||
foreach ($media as $medium) {
|
||||
foreach ($links as $link) {
|
||||
if (Strings::compareLink($link, $medium['url'])) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid adding separate media entries for previews
|
||||
foreach ($previews as $preview) {
|
||||
if (Strings::compareLink($preview, $medium['url'])) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently these two types are ignored here.
|
||||
// Posts are added differently and contacts are not displayed as attachments.
|
||||
if (in_array($medium['type'], [self::ACCOUNT, self::ACTIVITY])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($medium['preview'])) {
|
||||
$previews[] = $medium['preview'];
|
||||
}
|
||||
|
||||
$type = explode('/', explode(';', $medium['mimetype'] ?? '')[0]);
|
||||
if (count($type) < 2) {
|
||||
Logger::info('Unknown MimeType', ['type' => $type, 'media' => $medium]);
|
||||
$filetype = 'unkn';
|
||||
$subtype = 'unkn';
|
||||
} else {
|
||||
$filetype = strtolower($type[0]);
|
||||
$subtype = strtolower($type[1]);
|
||||
}
|
||||
|
||||
$medium['filetype'] = $filetype;
|
||||
$medium['subtype'] = $subtype;
|
||||
|
||||
if ($medium['type'] == self::HTML || (($filetype == 'text') && ($subtype == 'html'))) {
|
||||
$attachments['link'][] = $medium;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($medium['type'], [self::AUDIO, self::IMAGE]) ||
|
||||
in_array($filetype, ['audio', 'image'])
|
||||
) {
|
||||
$attachments['visual'][] = $medium;
|
||||
} elseif (($medium['type'] == self::VIDEO) || ($filetype == 'video')) {
|
||||
if (!empty($medium['height'])) {
|
||||
// Peertube videos are delivered in many different resolutions. We pick a moderate one.
|
||||
// Since only Peertube provides a "height" parameter, this wouldn't be executed
|
||||
// when someone for example on Mastodon was sharing multiple videos in a single post.
|
||||
$heights[$medium['height']] = $medium['url'];
|
||||
$video[$medium['url']] = $medium;
|
||||
} else {
|
||||
$attachments['visual'][] = $medium;
|
||||
}
|
||||
} else {
|
||||
$attachments['additional'][] = $medium;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($heights)) {
|
||||
ksort($heights);
|
||||
foreach ($heights as $height => $url) {
|
||||
if (empty($selected) || $height <= 480) {
|
||||
$selected = $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($selected)) {
|
||||
$attachments['visual'][] = $video[$selected];
|
||||
unset($video[$selected]);
|
||||
foreach ($video as $element) {
|
||||
$attachments['additional'][] = $element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media attachments to the body
|
||||
*
|
||||
|
@ -1119,25 +1012,9 @@ class Media
|
|||
*/
|
||||
public static function getPreviewUrlForId(int $id, string $size = ''): string
|
||||
{
|
||||
$url = DI::baseUrl() . '/photo/preview/';
|
||||
switch ($size) {
|
||||
case Proxy::SIZE_MICRO:
|
||||
$url .= Proxy::PIXEL_MICRO . '/';
|
||||
break;
|
||||
case Proxy::SIZE_THUMB:
|
||||
$url .= Proxy::PIXEL_THUMB . '/';
|
||||
break;
|
||||
case Proxy::SIZE_SMALL:
|
||||
$url .= Proxy::PIXEL_SMALL . '/';
|
||||
break;
|
||||
case Proxy::SIZE_MEDIUM:
|
||||
$url .= Proxy::PIXEL_MEDIUM . '/';
|
||||
break;
|
||||
case Proxy::SIZE_LARGE:
|
||||
$url .= Proxy::PIXEL_LARGE . '/';
|
||||
break;
|
||||
}
|
||||
return $url . $id;
|
||||
return '/photo/preview/' .
|
||||
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
|
||||
$id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1149,24 +1026,8 @@ class Media
|
|||
*/
|
||||
public static function getUrlForId(int $id, string $size = ''): string
|
||||
{
|
||||
$url = DI::baseUrl() . '/photo/media/';
|
||||
switch ($size) {
|
||||
case Proxy::SIZE_MICRO:
|
||||
$url .= Proxy::PIXEL_MICRO . '/';
|
||||
break;
|
||||
case Proxy::SIZE_THUMB:
|
||||
$url .= Proxy::PIXEL_THUMB . '/';
|
||||
break;
|
||||
case Proxy::SIZE_SMALL:
|
||||
$url .= Proxy::PIXEL_SMALL . '/';
|
||||
break;
|
||||
case Proxy::SIZE_MEDIUM:
|
||||
$url .= Proxy::PIXEL_MEDIUM . '/';
|
||||
break;
|
||||
case Proxy::SIZE_LARGE:
|
||||
$url .= Proxy::PIXEL_LARGE . '/';
|
||||
break;
|
||||
}
|
||||
return $url . $id;
|
||||
return '/photo/media/' .
|
||||
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
|
||||
$id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -813,12 +813,14 @@ class Profile
|
|||
|
||||
/**
|
||||
* Set the visitor cookies (see remote_user()) for signed HTTP requests
|
||||
(
|
||||
*
|
||||
* @param array $server The content of the $_SERVER superglobal
|
||||
* @return array Visitor contact array
|
||||
* @throws InternalServerErrorException
|
||||
*/
|
||||
public static function addVisitorCookieForHTTPSigner(): array
|
||||
public static function addVisitorCookieForHTTPSigner(array $server): array
|
||||
{
|
||||
$requester = HTTPSignature::getSigner('', $_SERVER);
|
||||
$requester = HTTPSignature::getSigner('', $server);
|
||||
if (empty($requester)) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ class Photo extends BaseApi
|
|||
throw new NotModifiedException();
|
||||
}
|
||||
|
||||
Profile::addVisitorCookieForHTTPSigner();
|
||||
Profile::addVisitorCookieForHTTPSigner($this->server);
|
||||
|
||||
$customsize = 0;
|
||||
$square_resize = true;
|
||||
|
|
69
src/Network/Entity/MimeType.php
Normal file
69
src/Network/Entity/MimeType.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?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\Network\Entity;
|
||||
|
||||
use Friendica\BaseEntity;
|
||||
|
||||
/**
|
||||
* Implementation of the Content-Type header value from the MIME type RFC
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc2045#section-5
|
||||
*
|
||||
* @property-read string $type
|
||||
* @property-read string $subtype
|
||||
* @property-read array $parameters
|
||||
*/
|
||||
class MimeType extends BaseEntity
|
||||
{
|
||||
/** @var string */
|
||||
protected $type;
|
||||
/** @var string */
|
||||
protected $subtype;
|
||||
/** @var array */
|
||||
protected $parameters;
|
||||
|
||||
public function __construct(string $type, string $subtype, array $parameters = [])
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->subtype = $subtype;
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$parameters = array_map(function (string $attribute, string $value) {
|
||||
if (
|
||||
strpos($value, '"') !== false ||
|
||||
strpos($value, '\\') !== false ||
|
||||
strpos($value, "\r") !== false
|
||||
) {
|
||||
$value = '"' . str_replace(['\\', '"', "\r"], ['\\\\', '\\"', "\\\r"], $value) . '"';
|
||||
}
|
||||
|
||||
return '; ' . $attribute . '=' . $value;
|
||||
}, array_keys($this->parameters), array_values($this->parameters));
|
||||
|
||||
return $this->type . '/' .
|
||||
$this->subtype .
|
||||
implode('', $parameters);
|
||||
}
|
||||
}
|
76
src/Network/Factory/MimeType.php
Normal file
76
src/Network/Factory/MimeType.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?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\Network\Factory;
|
||||
|
||||
use Friendica\BaseFactory;
|
||||
use Friendica\Core\System;
|
||||
use Friendica\Network\Entity;
|
||||
|
||||
/**
|
||||
* Implementation of the Content-Type header value from the MIME type RFC
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc2045#section-5
|
||||
*/
|
||||
class MimeType extends BaseFactory
|
||||
{
|
||||
public function createFromContentType(?string $contentType): Entity\MimeType
|
||||
{
|
||||
if ($contentType) {
|
||||
$parameterStrings = explode(';', $contentType);
|
||||
$mimetype = array_shift($parameterStrings);
|
||||
|
||||
$types = explode('/', $mimetype);
|
||||
if (count($types) >= 2) {
|
||||
$filetype = strtolower($types[0]);
|
||||
$subtype = strtolower($types[1]);
|
||||
} else {
|
||||
$this->logger->notice('Unknown MimeType', ['type' => $contentType, 'callstack' => System::callstack(10)]);
|
||||
}
|
||||
|
||||
$parameters = [];
|
||||
foreach ($parameterStrings as $parameterString) {
|
||||
$parameterString = trim($parameterString);
|
||||
$parameterParts = explode('=', $parameterString, 2);
|
||||
if (count($parameterParts) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attribute = trim($parameterParts[0]);
|
||||
$valueString = trim($parameterParts[1]);
|
||||
|
||||
if ($valueString[0] == '"' && $valueString[strlen($valueString) - 1] == '"') {
|
||||
$valueString = substr(str_replace(['\\"', '\\\\', "\\\r"], ['"', '\\', "\r"], $valueString), 1, -1);
|
||||
}
|
||||
|
||||
$value = preg_replace('#\s*\([^()]*?\)#', '', $valueString);
|
||||
|
||||
$parameters[$attribute] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return new Entity\MimeType(
|
||||
$filetype ?? 'unkn',
|
||||
$subtype ?? 'unkn',
|
||||
$parameters ?? [],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -211,4 +211,21 @@ class Proxy
|
|||
return $matches[1] . self::proxifyUrl(htmlspecialchars_decode($matches[2])) . $matches[3];
|
||||
}
|
||||
|
||||
public static function getPixelsFromSize(string $size): int
|
||||
{
|
||||
switch ($size) {
|
||||
case Proxy::SIZE_MICRO:
|
||||
return Proxy::PIXEL_MICRO;
|
||||
case Proxy::SIZE_THUMB:
|
||||
return Proxy::PIXEL_THUMB;
|
||||
case Proxy::SIZE_SMALL:
|
||||
return Proxy::PIXEL_SMALL;
|
||||
case Proxy::SIZE_MEDIUM:
|
||||
return Proxy::PIXEL_MEDIUM;
|
||||
case Proxy::SIZE_LARGE:
|
||||
return Proxy::PIXEL_LARGE;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
66
tests/src/BaseCollectionTest.php
Normal file
66
tests/src/BaseCollectionTest.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?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\Test\src;
|
||||
|
||||
use Friendica\BaseCollection;
|
||||
use Friendica\BaseEntity;
|
||||
use Mockery\Mock;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BaseCollectionTest extends TestCase
|
||||
{
|
||||
public function testChunk()
|
||||
{
|
||||
$entity1 = \Mockery::mock(BaseEntity::class);
|
||||
$entity2 = \Mockery::mock(BaseEntity::class);
|
||||
$entity3 = \Mockery::mock(BaseEntity::class);
|
||||
$entity4 = \Mockery::mock(BaseEntity::class);
|
||||
|
||||
$collection = new BaseCollection([$entity1, $entity2]);
|
||||
|
||||
$this->assertEquals([new BaseCollection([$entity1]), new BaseCollection([$entity2])], $collection->chunk(1));
|
||||
$this->assertEquals([new BaseCollection([$entity1, $entity2])], $collection->chunk(2));
|
||||
|
||||
$collection = new BaseCollection([$entity1, $entity2, $entity3]);
|
||||
|
||||
$this->assertEquals([new BaseCollection([$entity1]), new BaseCollection([$entity2]), new BaseCollection([$entity3])], $collection->chunk(1));
|
||||
$this->assertEquals([new BaseCollection([$entity1, $entity2]), new BaseCollection([$entity3])], $collection->chunk(2));
|
||||
$this->assertEquals([new BaseCollection([$entity1, $entity2, $entity3])], $collection->chunk(3));
|
||||
|
||||
$collection = new BaseCollection([$entity1, $entity2, $entity3, $entity4]);
|
||||
|
||||
$this->assertEquals([new BaseCollection([$entity1, $entity2]), new BaseCollection([$entity3, $entity4])], $collection->chunk(2));
|
||||
$this->assertEquals([new BaseCollection([$entity1, $entity2, $entity3]), new BaseCollection([$entity4])], $collection->chunk(3));
|
||||
$this->assertEquals([new BaseCollection([$entity1, $entity2, $entity3, $entity4])], $collection->chunk(4));
|
||||
}
|
||||
|
||||
public function testChunkLengthException()
|
||||
{
|
||||
$this->expectException(\RangeException::class);
|
||||
|
||||
$entity1 = \Mockery::mock(BaseEntity::class);
|
||||
|
||||
$collection = new BaseCollection([$entity1]);
|
||||
|
||||
$collection->chunk(0);
|
||||
}
|
||||
}
|
152
tests/src/Network/MimeTypeTest.php
Normal file
152
tests/src/Network/MimeTypeTest.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?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\Test\src\Network;
|
||||
|
||||
use Friendica\Network\Entity;
|
||||
use Friendica\Network\Factory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class MimeTypeTest extends TestCase
|
||||
{
|
||||
public function dataCreateFromContentType(): array
|
||||
{
|
||||
return [
|
||||
'image/jpg' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg'),
|
||||
'contentType' => 'image/jpg',
|
||||
],
|
||||
'image/jpg;charset=utf8' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']),
|
||||
'contentType' => 'image/jpg; charset=utf8',
|
||||
],
|
||||
'image/jpg; charset=utf8' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']),
|
||||
'contentType' => 'image/jpg; charset=utf8',
|
||||
],
|
||||
'image/jpg; charset = utf8' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']),
|
||||
'contentType' => 'image/jpg; charset=utf8',
|
||||
],
|
||||
'image/jpg; charset="utf8"' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']),
|
||||
'contentType' => 'image/jpg; charset="utf8"',
|
||||
],
|
||||
'image/jpg; charset="\"utf8\""' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => '"utf8"']),
|
||||
'contentType' => 'image/jpg; charset="\"utf8\""',
|
||||
],
|
||||
'image/jpg; charset="\"utf8\" (comment)"' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => '"utf8"']),
|
||||
'contentType' => 'image/jpg; charset="\"utf8\" (comment)"',
|
||||
],
|
||||
'image/jpg; charset=utf8 (comment)' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']),
|
||||
'contentType' => 'image/jpg; charset="utf8 (comment)"',
|
||||
],
|
||||
'image/jpg; charset=utf8; attribute=value' => [
|
||||
'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8', 'attribute' => 'value']),
|
||||
'contentType' => 'image/jpg; charset=utf8; attribute=value',
|
||||
],
|
||||
'empty' => [
|
||||
'expected' => new Entity\MimeType('unkn', 'unkn'),
|
||||
'contentType' => '',
|
||||
],
|
||||
'unknown' => [
|
||||
'expected' => new Entity\MimeType('unkn', 'unkn'),
|
||||
'contentType' => 'unknown',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataCreateFromContentType
|
||||
* @param Entity\MimeType $expected
|
||||
* @param string $contentType
|
||||
* @return void
|
||||
*/
|
||||
public function testCreateFromContentType(Entity\MimeType $expected, string $contentType)
|
||||
{
|
||||
$factory = new Factory\MimeType(new NullLogger());
|
||||
|
||||
$this->assertEquals($expected, $factory->createFromContentType($contentType));
|
||||
}
|
||||
|
||||
public function dataToString(): array
|
||||
{
|
||||
return [
|
||||
'image/jpg' => [
|
||||
'expected' => 'image/jpg',
|
||||
'mimeType' => new Entity\MimeType('image', 'jpg'),
|
||||
],
|
||||
'image/jpg;charset=utf8' => [
|
||||
'expected' => 'image/jpg; charset=utf8',
|
||||
'mimeType' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']),
|
||||
],
|
||||
'image/jpg; charset="\"utf8\""' => [
|
||||
'expected' => 'image/jpg; charset="\"utf8\""',
|
||||
'mimeType' => new Entity\MimeType('image', 'jpg', ['charset' => '"utf8"']),
|
||||
],
|
||||
'image/jpg; charset=utf8; attribute=value' => [
|
||||
'expected' => 'image/jpg; charset=utf8; attribute=value',
|
||||
'mimeType' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8', 'attribute' => 'value']),
|
||||
],
|
||||
'empty' => [
|
||||
'expected' => 'unkn/unkn',
|
||||
'mimeType' => new Entity\MimeType('unkn', 'unkn'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataToString
|
||||
* @param string $expected
|
||||
* @param Entity\MimeType $mimeType
|
||||
* @return void
|
||||
*/
|
||||
public function testToString(string $expected, Entity\MimeType $mimeType)
|
||||
{
|
||||
$this->assertEquals($expected, $mimeType->__toString());
|
||||
}
|
||||
|
||||
public function dataRoundtrip(): array
|
||||
{
|
||||
return [
|
||||
['image/jpg'],
|
||||
['image/jpg; charset=utf8'],
|
||||
['image/jpg; charset="\"utf8\""'],
|
||||
['image/jpg; charset=utf8; attribute=value'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataRoundtrip
|
||||
* @param string $expected
|
||||
* @return void
|
||||
*/
|
||||
public function testRoundtrip(string $expected)
|
||||
{
|
||||
$factory = new Factory\MimeType(new NullLogger());
|
||||
|
||||
$this->assertEquals($expected, $factory->createFromContentType($expected)->__toString());
|
||||
}
|
||||
}
|
|
@ -706,6 +706,39 @@ audio {
|
|||
* Image grid settings END
|
||||
**/
|
||||
|
||||
/* This helps allocating space for image before they are loaded, preventing content shifting once they are.
|
||||
* Inspired by https://www.smashingmagazine.com/2016/08/ways-to-reduce-content-shifting-on-page-load/
|
||||
* Please note: The space is effectively allocated using padding-bottom using the image ratio as a value.
|
||||
* This ratio is never known in advance so no value is set in the stylesheet.
|
||||
*/
|
||||
figure.img-allocated-height {
|
||||
position: relative;
|
||||
background: center / auto rgba(0, 0, 0, 0.05) url(/images/icons/image.png) no-repeat;
|
||||
margin: 0;
|
||||
}
|
||||
figure.img-allocated-height img{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal masonry settings START
|
||||
**/
|
||||
.masonry-row {
|
||||
display: -ms-flexbox; /* IE10 */
|
||||
display: flex;
|
||||
/* Both the following values should be the same to ensure consistent margins between images in the grid */
|
||||
column-gap: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
/**
|
||||
* Horizontal masonry settings AND
|
||||
**/
|
||||
|
||||
#contactblock .icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{{if $image.preview}}
|
||||
<a data-fancybox="{{$image.uri_id}}" href="{{$image.attachment.url}}"><img src="{{$image.preview}}" alt="{{$image.attachment.description}}" title="{{$image.attachment.description}}" loading="lazy"></a>
|
||||
{{else}}
|
||||
<img src="{{$image.src}}" alt="{{$image.attachment.description}}" title="{{$image.attachment.description}}" loading="lazy">
|
||||
{{/if}}
|
|
@ -1,12 +1,12 @@
|
|||
<div class="imagegrid-row">
|
||||
<div class="imagegrid-column">
|
||||
{{foreach $columns.fc as $img}}
|
||||
{{include file="content/image.tpl" image=$img}}
|
||||
{{include file="content/image/single.tpl" image=$img}}
|
||||
{{/foreach}}
|
||||
</div>
|
||||
<div class="imagegrid-column">
|
||||
{{foreach $columns.sc as $img}}
|
||||
{{include file="content/image.tpl" image=$img}}
|
||||
{{include file="content/image/single.tpl" image=$img}}
|
||||
{{/foreach}}
|
||||
</div>
|
||||
</div>
|
12
view/templates/content/image/horizontal_masonry.tpl
Normal file
12
view/templates/content/image/horizontal_masonry.tpl
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{foreach $rows as $images}}
|
||||
<div class="masonry-row" style="height: {{$images->getHeightRatio()}}%">
|
||||
{{foreach $images as $image}}
|
||||
{{* The absolute pixel value in the calc() should be mirrored from the .imagegrid-row column-gap value *}}
|
||||
{{include file="content/image/single_with_height_allocation.tpl"
|
||||
image=$image
|
||||
allocated_height="calc(`$image->heightRatio * $image->widthRatio / 100`% - 5px / `$column_size`)"
|
||||
allocated_width="`$image->widthRatio`%"
|
||||
}}
|
||||
{{/foreach}}
|
||||
</div>
|
||||
{{/foreach}}
|
5
view/templates/content/image/single.tpl
Normal file
5
view/templates/content/image/single.tpl
Normal file
|
@ -0,0 +1,5 @@
|
|||
{{if $image->preview}}
|
||||
<a data-fancybox="{{$image->uriId}}" href="{{$image->url}}"><img src="{{$image->preview}}" alt="{{$image->description}}" title="{{$image->description}}" loading="lazy"></a>
|
||||
{{else}}
|
||||
<img src="{{$image->url}}" alt="{{$image->description}}" title="{{$image->description}}" loading="lazy">
|
||||
{{/if}}
|
|
@ -0,0 +1,20 @@
|
|||
{{* The padding-top height allocation trick only works if the <figure> fills its parent's width completely or with flex. 🤷♂️
|
||||
As a result, we need to add a wrapping element for non-flex (non-image grid) environments, mostly single-image cases.
|
||||
*}}
|
||||
{{if $allocated_max_width}}
|
||||
<div style="max-width: {{$allocated_max_width|default:"auto"}};">
|
||||
{{/if}}
|
||||
|
||||
<figure class="img-allocated-height" style="width: {{$allocated_width|default:"auto"}}; padding-bottom: {{$allocated_height}}">
|
||||
{{if $image->preview}}
|
||||
<a data-fancybox="uri-id-{{$image->uriId}}" href="{{$image->url}}">
|
||||
<img src="{{$image->preview}}" alt="{{$image->description}}" title="{{$image->description}}" loading="lazy">
|
||||
</a>
|
||||
{{else}}
|
||||
<img src="{{$image->url}}" alt="{{$image->description}}" title="{{$image->description}}" loading="lazy">
|
||||
{{/if}}
|
||||
</figure>
|
||||
|
||||
{{if $allocated_max_width}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -394,3 +394,7 @@ input[type="text"].tt-input {
|
|||
textarea#profile-jot-text:focus + #preview_profile-jot-text, textarea.comment-edit-text:focus + .comment-edit-form .preview {
|
||||
border-color: $link_color;
|
||||
}
|
||||
|
||||
figure.img-allocated-height {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
|
|
@ -354,3 +354,7 @@ input[type="text"].tt-input {
|
|||
textarea#profile-jot-text:focus + #preview_profile-jot-text, textarea.comment-edit-text:focus + .comment-edit-form .preview {
|
||||
border-color: $link_color;
|
||||
}
|
||||
|
||||
figure.img-allocated-height {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue