Commit 71a063da authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(video-clips): allow episodeNumbering text to stand in the indent of episodeTitle paragraph

parent 3af404da
......@@ -15,7 +15,7 @@ class MediaClipper extends BaseConfig
public string $wavesMask = APPPATH . 'Libraries/MediaClipper/waves-mask.png';
/**
* @var array<string, array<string, int|array<string, int|string>>>
* @var array<string, array<string, int|array<string, float|int|string>>>
*/
public array $formats = [
'landscape' => [
......@@ -34,25 +34,25 @@ class MediaClipper extends BaseConfig
'x' => 810,
'y' => 210,
],
'podcastTitle' => [
'fontsize' => 20,
'x' => 150,
'y' => 620,
'lineWidth' => 510,
],
'episodeTitle' => [
'fontsize' => 32,
'x' => 150,
'y' => 660,
'lines' => 3,
'lineWidth' => 28,
'leading' => 20,
],
'podcastTitle' => [
'fontsize' => 20,
'x' => 150,
'y' => 620,
'lineWidth' => 510,
'lineHeight' => 1.5,
],
'episodeNumbering' => [
'fontsize' => 18,
'paddingX' => 10,
'paddingY' => 5,
'x' => 180,
'y' => 540,
'marginRight' => 10,
],
'timestamp' => [
'fontsize' => 32,
......@@ -95,25 +95,25 @@ class MediaClipper extends BaseConfig
'x' => 75,
'y' => 520,
],
'podcastTitle' => [
'fontsize' => 32,
'x' => 360,
'y' => 55,
'lineWidth' => 670,
],
'episodeTitle' => [
'fontsize' => 42,
'x' => 360,
'y' => 110,
'lines' => 3,
'lineWidth' => 32,
'leading' => 20,
],
'podcastTitle' => [
'fontsize' => 32,
'x' => 360,
'y' => 55,
'lineWidth' => 670,
'lineHeight' => 1.5,
],
'episodeNumbering' => [
'fontsize' => 28,
'paddingX' => 0,
'paddingX' => 10,
'paddingY' => 10,
'x' => 50,
'y' => 330,
'marginRight' => 10,
],
'timestamp' => [
'fontsize' => 48,
......@@ -156,25 +156,26 @@ class MediaClipper extends BaseConfig
'x' => 85,
'y' => 320,
],
'podcastTitle' => [
'fontsize' => 28,
'x' => 260,
'y' => 50,
'lines' => 1,
'lineWidth' => 700,
],
'episodeTitle' => [
'fontsize' => 36,
'x' => 260,
'y' => 90,
'lines' => 2,
'lineWidth' => 38,
'leading' => 20,
],
'podcastTitle' => [
'fontsize' => 28,
'x' => 260,
'y' => 50,
'lineWidth' => 700,
'lineHeight' => 1.5,
],
'episodeNumbering' => [
'fontsize' => 20,
'paddingX' => 0,
'paddingY' => 10,
'x' => 40,
'y' => 240,
'fontsize' => 24,
'paddingX' => 10,
'paddingY' => 5,
'marginRight' => 10,
],
'timestamp' => [
'fontsize' => 48,
......
......@@ -45,7 +45,7 @@ class VideoClip
protected ?string $episodeNumbering = null;
/**
* @var array<string, int|array<string, int|string>>
* @var array<string, mixed>
*/
protected array $dimensions = [];
......@@ -224,38 +224,63 @@ class VideoClip
return false;
}
$this->addTextToImage(
$background,
$this->dimensions['episodeTitle']['x'],
$this->dimensions['episodeTitle']['y'],
$this->episode->title,
$this->getFont('episodeTitle'),
$this->dimensions['episodeTitle']['fontsize'],
$this->dimensions['episodeTitle']['lines'],
$this->dimensions['episodeTitle']['lineWidth'],
$this->dimensions['episodeTitle']['leading'],
);
$this->addTextToImage(
$this->addParagraphToImage(
$background,
$this->dimensions['podcastTitle']['x'],
$this->dimensions['podcastTitle']['y'],
$this->episode->podcast->title,
$this->getFont('podcastTitle'),
$this->dimensions['podcastTitle']['fontsize']
$this->dimensions['podcastTitle']['fontsize'],
$this->dimensions['podcastTitle']['lineWidth'],
$this->dimensions['podcastTitle']['lines'] ?? 1,
$this->dimensions['podcastTitle']['lineHeight'] ?? 1,
);
$episodeNumberingWidth = 0;
if ($this->episodeNumbering) {
$episodeTitleBox = $this->calculateTextBox(
$this->dimensions['episodeTitle']['fontsize'],
0,
$this->getFont('episodeTitle'),
$this->episode->title
);
$episodeNumberingBox = $this->calculateTextBox(
$this->dimensions['episodeNumbering']['fontsize'],
0,
$this->getFont('episodeNumbering'),
$this->episodeNumbering
);
if (! $episodeTitleBox || ! $episodeNumberingBox) {
return false;
}
$episodeTitleCenter = (int) ($episodeTitleBox['height'] / 2);
$episodeNumberingCenter = (int) (($episodeNumberingBox['height'] + ($this->dimensions['episodeNumbering']['paddingY'] * 2)) / 2);
$episodeNumberingWidth = $episodeNumberingBox['width'] + ($this->dimensions['episodeNumbering']['paddingX'] * 2);
$this->addTextWithBox(
$background,
$this->dimensions['episodeNumbering']['x'],
$this->dimensions['episodeNumbering']['y'],
$this->dimensions['episodeTitle']['x'],
$this->dimensions['episodeTitle']['y'] + $episodeTitleCenter - $episodeNumberingCenter,
$this->episodeNumbering,
$this->getFont('episodeNumbering'),
$this->dimensions['episodeNumbering']['fontsize'],
$this->dimensions['episodeNumbering']['paddingX'],
$this->dimensions['episodeNumbering']['paddingY'],
);
// dd($this->episodeNumbering);
}
$this->addParagraphToImage(
$background,
$this->dimensions['episodeTitle']['x'],
$this->dimensions['episodeTitle']['y'],
$this->episode->title,
$this->getFont('episodeTitle'),
$this->dimensions['episodeTitle']['fontsize'],
$this->dimensions['episodeTitle']['lineWidth'],
$this->dimensions['episodeTitle']['lines'],
$this->dimensions['episodeTitle']['lineHeight'] ?? 1,
$episodeNumberingWidth + ($episodeNumberingWidth === 0 ? 0 : $this->dimensions['episodeNumbering']['marginRight']),
);
// Add quotes for subtitles
$quotes = imagecreatefrompng(config('MediaClipper')->quotesImage);
......@@ -412,16 +437,17 @@ class VideoClip
return imagecopy($background, $foreground, $x, $y, 0, 0, $width, $height);
}
private function addTextToImage(
private function addParagraphToImage(
GdImage $image,
int $x,
int $y,
string $text,
string $fontPath,
int $fontsize,
int $lineWidth,
int $numberOfLines = 1,
int $lineWidth = 32,
int $leading = 5,
float $lineHeight = 1,
int $paragraphIndent = 0,
): bool {
// Allocate A Color For The Text
$white = imagecolorallocate($image, 255, 255, 255);
......@@ -430,35 +456,60 @@ class VideoClip
return false;
}
if ($numberOfLines > 1) {
$text = wordwrap($text, $lineWidth, PHP_EOL);
preg_match_all('~' . PHP_EOL . '~', $text, $matches, PREG_OFFSET_CAPTURE);
if (array_key_exists($numberOfLines - 1, $matches[0])) {
$text = substr($text, 0, (int) $matches[0][$numberOfLines - 1][1]) . '…';
}
$lines = $this->textToParagraph($text, $fontPath, $fontsize, $lineWidth, $numberOfLines, $paragraphIndent);
if (! $lines) {
return false;
}
$lines = explode(PHP_EOL, $text);
foreach ($lines as $i => $line) {
// Print line On Image
imagettftext(
$image,
$fontsize,
0,
$x,
$y + $fontsize + (($fontsize + $leading) * $i),
$white,
$fontPath,
$line
);
}
} else {
// Print Text On Image
imagettftext($image, $fontsize, 0, $x, $y + $fontsize, $white, $fontPath, $text);
$leading = (int) ($fontsize * $lineHeight);
foreach ($lines as $i => $line) {
// Print line On Image
imagettftext(
$image,
$fontsize,
0,
$x + ($paragraphIndent * ($i === 0 ? 1 : 0)),
$y + $fontsize + ($leading * $i),
$white,
$fontPath,
$line
);
}
return true;
}
/**
* Adapted from: https://www.php.net/manual/fr/function.imagettfbbox.php#105593
*
* @return array<string, mixed>|false
*/
private function calculateTextBox(int $fontSize, int $fontAngle, string $fontFile, string $text): array | false
{
/************
simple function that calculates the *exact* bounding box (single pixel precision).
The function returns an associative array with these keys:
left, top: coordinates you will pass to imagettftext
width, height: dimension of the image you have to create
*************/
$bbox = imagettfbbox($fontSize, $fontAngle, $fontFile, $text);
if (! $bbox) {
return false;
}
$minX = min([$bbox[0], $bbox[2], $bbox[4], $bbox[6]]);
$maxX = max([$bbox[0], $bbox[2], $bbox[4], $bbox[6]]);
$minY = min([$bbox[1], $bbox[3], $bbox[5], $bbox[7]]);
$maxY = max([$bbox[1], $bbox[3], $bbox[5], $bbox[7]]);
return [
'left' => abs($minX) - 1,
'top' => abs($minY),
'width' => $maxX - $minX,
'height' => $maxY - $minY,
'box' => $bbox,
];
}
private function addTextWithBox(
GdImage $image,
int $x,
......@@ -471,7 +522,7 @@ class VideoClip
): bool {
// Create some colors
$white = imagecolorallocate($image, 255, 255, 255);
$bgColor = imagecolorallocate($image, 0, 86, 74);
$bgColor = imagecolorallocate($image, 0, 61, 11);
if ($white === false || $bgColor === false) {
return false;
......@@ -495,33 +546,64 @@ class VideoClip
}
/**
* Adapted from: https://www.php.net/manual/fr/function.imagettfbbox.php#105593
*
* @return array<string, mixed>|false
* @return array<int, string>|false
*/
private function calculateTextBox(int $fontSize, int $fontAngle, string $fontFile, string $text): array | false
{
/************
simple function that calculates the *exact* bounding box (single pixel precision).
The function returns an associative array with these keys:
left, top: coordinates you will pass to imagettftext
width, height: dimension of the image you have to create
*************/
$bbox = imagettfbbox($fontSize, $fontAngle, $fontFile, $text);
private function textToParagraph(
string $text,
string $fontPath,
int $fontsize,
int $lineWidth,
int $numberOfLines,
int $paragraphIndent = 0,
): array | false {
// check length of text
$bbox = $this->calculateTextBox($fontsize, 0, $fontPath, $text);
if (! $bbox) {
return false;
}
$minX = min([$bbox[0], $bbox[2], $bbox[4], $bbox[6]]);
$maxX = max([$bbox[0], $bbox[2], $bbox[4], $bbox[6]]);
$minY = min([$bbox[1], $bbox[3], $bbox[5], $bbox[7]]);
$maxY = max([$bbox[1], $bbox[3], $bbox[5], $bbox[7]]);
return [
'left' => abs($minX) - 1,
'top' => abs($minY),
'width' => $maxX - $minX,
'height' => $maxY - $minY,
'box' => $bbox,
];
// return early if text width is less than line width
if ($bbox['width'] <= $lineWidth) {
return [$text];
}
// cut text in multiple lines based on the lineWidth property
$lines = [''];
$length = $paragraphIndent;
$words = preg_split('~\b(?=\S)|(?=\s)~', $text);
if (! $words) {
return false;
}
$wordCount = count($words);
$lineNumber = 0;
for ($i = 0; $i < $wordCount; ++$i) {
$word = $words[$i];
$wordBox = $this->calculateTextBox($fontsize, 0, $fontPath, $word);
if (! $wordBox) {
return false;
}
$wordWidth = $wordBox['width'];
if (($wordWidth + $length) > $lineWidth) {
++$lineNumber;
if ($lineNumber > $numberOfLines - 1) {
$lines[$numberOfLines - 1] .= '…';
break;
}
$lines[$lineNumber] = '';
$length = 0;
// If the current word is just a space, don't bother. Skip (saves a weird-looking gap in the text).
if ($word === ' ') {
continue;
}
}
$lines[$lineNumber] .= $word;
$length += $wordWidth;
}
return $lines;
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment