From 71a063dac311cb21639801fbae6af7c5106c2699 Mon Sep 17 00:00:00 2001 From: Yassine Doghri <yassine@doghri.fr> Date: Mon, 6 Dec 2021 16:27:00 +0000 Subject: [PATCH] feat(video-clips): allow episodeNumbering text to stand in the indent of episodeTitle paragraph --- .../MediaClipper/Config/MediaClipper.php | 65 +++--- app/Libraries/MediaClipper/VideoClip.php | 216 ++++++++++++------ 2 files changed, 182 insertions(+), 99 deletions(-) diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php index 5f5420d6ef..52702da64b 100644 --- a/app/Libraries/MediaClipper/Config/MediaClipper.php +++ b/app/Libraries/MediaClipper/Config/MediaClipper.php @@ -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, diff --git a/app/Libraries/MediaClipper/VideoClip.php b/app/Libraries/MediaClipper/VideoClip.php index e2f7ad6e21..9be8e42f37 100644 --- a/app/Libraries/MediaClipper/VideoClip.php +++ b/app/Libraries/MediaClipper/VideoClip.php @@ -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; } } -- GitLab