Loading app/Libraries/MediaClipper/Config/MediaClipper.php +33 −32 Original line number Diff line number Diff line Loading @@ -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' => [ Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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, Loading app/Libraries/MediaClipper/VideoClip.php +149 −67 Original line number Diff line number Diff line Loading @@ -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 = []; Loading Loading @@ -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); Loading Loading @@ -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); Loading @@ -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); $leading = (int) ($fontsize * $lineHeight); foreach ($lines as $i => $line) { // Print line On Image imagettftext( $image, $fontsize, 0, $x, $y + $fontsize + (($fontsize + $leading) * $i), $x + ($paragraphIndent * ($i === 0 ? 1 : 0)), $y + $fontsize + ($leading * $i), $white, $fontPath, $line ); } } else { // Print Text On Image imagettftext($image, $fontsize, 0, $x, $y + $fontsize, $white, $fontPath, $text); } 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, Loading @@ -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; Loading @@ -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; } } Loading
app/Libraries/MediaClipper/Config/MediaClipper.php +33 −32 Original line number Diff line number Diff line Loading @@ -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' => [ Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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, Loading
app/Libraries/MediaClipper/VideoClip.php +149 −67 Original line number Diff line number Diff line Loading @@ -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 = []; Loading Loading @@ -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); Loading Loading @@ -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); Loading @@ -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); $leading = (int) ($fontsize * $lineHeight); foreach ($lines as $i => $line) { // Print line On Image imagettftext( $image, $fontsize, 0, $x, $y + $fontsize + (($fontsize + $leading) * $i), $x + ($paragraphIndent * ($i === 0 ? 1 : 0)), $y + $fontsize + ($leading * $i), $white, $fontPath, $line ); } } else { // Print Text On Image imagettftext($image, $fontsize, 0, $x, $y + $fontsize, $white, $fontPath, $text); } 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, Loading @@ -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; Loading @@ -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; } }