mirror of
https://code.castopod.org/adaures/castopod
synced 2025-06-06 18:31:05 +00:00
feat(video-clips): allow episodeNumbering text to stand in the indent of episodeTitle paragraph
This commit is contained in:
parent
3af404da3d
commit
71a063dac3
@ -15,7 +15,7 @@ class MediaClipper extends BaseConfig
|
|||||||
public string $wavesMask = APPPATH . 'Libraries/MediaClipper/waves-mask.png';
|
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 = [
|
public array $formats = [
|
||||||
'landscape' => [
|
'landscape' => [
|
||||||
@ -34,25 +34,25 @@ class MediaClipper extends BaseConfig
|
|||||||
'x' => 810,
|
'x' => 810,
|
||||||
'y' => 210,
|
'y' => 210,
|
||||||
],
|
],
|
||||||
|
'podcastTitle' => [
|
||||||
|
'fontsize' => 20,
|
||||||
|
'x' => 150,
|
||||||
|
'y' => 620,
|
||||||
|
'lineWidth' => 510,
|
||||||
|
],
|
||||||
'episodeTitle' => [
|
'episodeTitle' => [
|
||||||
'fontsize' => 32,
|
'fontsize' => 32,
|
||||||
'x' => 150,
|
'x' => 150,
|
||||||
'y' => 660,
|
'y' => 660,
|
||||||
'lines' => 3,
|
'lines' => 3,
|
||||||
'lineWidth' => 28,
|
'lineWidth' => 510,
|
||||||
'leading' => 20,
|
'lineHeight' => 1.5,
|
||||||
],
|
|
||||||
'podcastTitle' => [
|
|
||||||
'fontsize' => 20,
|
|
||||||
'x' => 150,
|
|
||||||
'y' => 620,
|
|
||||||
],
|
],
|
||||||
'episodeNumbering' => [
|
'episodeNumbering' => [
|
||||||
'fontsize' => 18,
|
'fontsize' => 18,
|
||||||
'paddingX' => 10,
|
'paddingX' => 10,
|
||||||
'paddingY' => 5,
|
'paddingY' => 5,
|
||||||
'x' => 180,
|
'marginRight' => 10,
|
||||||
'y' => 540,
|
|
||||||
],
|
],
|
||||||
'timestamp' => [
|
'timestamp' => [
|
||||||
'fontsize' => 32,
|
'fontsize' => 32,
|
||||||
@ -95,25 +95,25 @@ class MediaClipper extends BaseConfig
|
|||||||
'x' => 75,
|
'x' => 75,
|
||||||
'y' => 520,
|
'y' => 520,
|
||||||
],
|
],
|
||||||
|
'podcastTitle' => [
|
||||||
|
'fontsize' => 32,
|
||||||
|
'x' => 360,
|
||||||
|
'y' => 55,
|
||||||
|
'lineWidth' => 670,
|
||||||
|
],
|
||||||
'episodeTitle' => [
|
'episodeTitle' => [
|
||||||
'fontsize' => 42,
|
'fontsize' => 42,
|
||||||
'x' => 360,
|
'x' => 360,
|
||||||
'y' => 110,
|
'y' => 110,
|
||||||
'lines' => 3,
|
'lines' => 3,
|
||||||
'lineWidth' => 32,
|
'lineWidth' => 670,
|
||||||
'leading' => 20,
|
'lineHeight' => 1.5,
|
||||||
],
|
|
||||||
'podcastTitle' => [
|
|
||||||
'fontsize' => 32,
|
|
||||||
'x' => 360,
|
|
||||||
'y' => 55,
|
|
||||||
],
|
],
|
||||||
'episodeNumbering' => [
|
'episodeNumbering' => [
|
||||||
'fontsize' => 28,
|
'fontsize' => 28,
|
||||||
'paddingX' => 0,
|
'paddingX' => 10,
|
||||||
'paddingY' => 10,
|
'paddingY' => 10,
|
||||||
'x' => 50,
|
'marginRight' => 10,
|
||||||
'y' => 330,
|
|
||||||
],
|
],
|
||||||
'timestamp' => [
|
'timestamp' => [
|
||||||
'fontsize' => 48,
|
'fontsize' => 48,
|
||||||
@ -156,25 +156,26 @@ class MediaClipper extends BaseConfig
|
|||||||
'x' => 85,
|
'x' => 85,
|
||||||
'y' => 320,
|
'y' => 320,
|
||||||
],
|
],
|
||||||
|
'podcastTitle' => [
|
||||||
|
'fontsize' => 28,
|
||||||
|
'x' => 260,
|
||||||
|
'y' => 50,
|
||||||
|
'lines' => 1,
|
||||||
|
'lineWidth' => 700,
|
||||||
|
],
|
||||||
'episodeTitle' => [
|
'episodeTitle' => [
|
||||||
'fontsize' => 36,
|
'fontsize' => 36,
|
||||||
'x' => 260,
|
'x' => 260,
|
||||||
'y' => 90,
|
'y' => 90,
|
||||||
'lines' => 2,
|
'lines' => 2,
|
||||||
'lineWidth' => 38,
|
'lineWidth' => 700,
|
||||||
'leading' => 20,
|
'lineHeight' => 1.5,
|
||||||
],
|
|
||||||
'podcastTitle' => [
|
|
||||||
'fontsize' => 28,
|
|
||||||
'x' => 260,
|
|
||||||
'y' => 50,
|
|
||||||
],
|
],
|
||||||
'episodeNumbering' => [
|
'episodeNumbering' => [
|
||||||
'fontsize' => 20,
|
'fontsize' => 24,
|
||||||
'paddingX' => 0,
|
'paddingX' => 10,
|
||||||
'paddingY' => 10,
|
'paddingY' => 5,
|
||||||
'x' => 40,
|
'marginRight' => 10,
|
||||||
'y' => 240,
|
|
||||||
],
|
],
|
||||||
'timestamp' => [
|
'timestamp' => [
|
||||||
'fontsize' => 48,
|
'fontsize' => 48,
|
||||||
|
@ -45,7 +45,7 @@ class VideoClip
|
|||||||
protected ?string $episodeNumbering = null;
|
protected ?string $episodeNumbering = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<string, int|array<string, int|string>>
|
* @var array<string, mixed>
|
||||||
*/
|
*/
|
||||||
protected array $dimensions = [];
|
protected array $dimensions = [];
|
||||||
|
|
||||||
@ -224,38 +224,63 @@ class VideoClip
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addTextToImage(
|
$this->addParagraphToImage(
|
||||||
$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(
|
|
||||||
$background,
|
$background,
|
||||||
$this->dimensions['podcastTitle']['x'],
|
$this->dimensions['podcastTitle']['x'],
|
||||||
$this->dimensions['podcastTitle']['y'],
|
$this->dimensions['podcastTitle']['y'],
|
||||||
$this->episode->podcast->title,
|
$this->episode->podcast->title,
|
||||||
$this->getFont('podcastTitle'),
|
$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) {
|
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(
|
$this->addTextWithBox(
|
||||||
$background,
|
$background,
|
||||||
$this->dimensions['episodeNumbering']['x'],
|
$this->dimensions['episodeTitle']['x'],
|
||||||
$this->dimensions['episodeNumbering']['y'],
|
$this->dimensions['episodeTitle']['y'] + $episodeTitleCenter - $episodeNumberingCenter,
|
||||||
$this->episodeNumbering,
|
$this->episodeNumbering,
|
||||||
$this->getFont('episodeNumbering'),
|
$this->getFont('episodeNumbering'),
|
||||||
$this->dimensions['episodeNumbering']['fontsize'],
|
$this->dimensions['episodeNumbering']['fontsize'],
|
||||||
$this->dimensions['episodeNumbering']['paddingX'],
|
$this->dimensions['episodeNumbering']['paddingX'],
|
||||||
$this->dimensions['episodeNumbering']['paddingY'],
|
$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
|
// Add quotes for subtitles
|
||||||
$quotes = imagecreatefrompng(config('MediaClipper')->quotesImage);
|
$quotes = imagecreatefrompng(config('MediaClipper')->quotesImage);
|
||||||
@ -412,16 +437,17 @@ class VideoClip
|
|||||||
return imagecopy($background, $foreground, $x, $y, 0, 0, $width, $height);
|
return imagecopy($background, $foreground, $x, $y, 0, 0, $width, $height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addTextToImage(
|
private function addParagraphToImage(
|
||||||
GdImage $image,
|
GdImage $image,
|
||||||
int $x,
|
int $x,
|
||||||
int $y,
|
int $y,
|
||||||
string $text,
|
string $text,
|
||||||
string $fontPath,
|
string $fontPath,
|
||||||
int $fontsize,
|
int $fontsize,
|
||||||
|
int $lineWidth,
|
||||||
int $numberOfLines = 1,
|
int $numberOfLines = 1,
|
||||||
int $lineWidth = 32,
|
float $lineHeight = 1,
|
||||||
int $leading = 5,
|
int $paragraphIndent = 0,
|
||||||
): bool {
|
): bool {
|
||||||
// Allocate A Color For The Text
|
// Allocate A Color For The Text
|
||||||
$white = imagecolorallocate($image, 255, 255, 255);
|
$white = imagecolorallocate($image, 255, 255, 255);
|
||||||
@ -430,66 +456,25 @@ class VideoClip
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($numberOfLines > 1) {
|
$lines = $this->textToParagraph($text, $fontPath, $fontsize, $lineWidth, $numberOfLines, $paragraphIndent);
|
||||||
$text = wordwrap($text, $lineWidth, PHP_EOL);
|
if (! $lines) {
|
||||||
preg_match_all('~' . PHP_EOL . '~', $text, $matches, PREG_OFFSET_CAPTURE);
|
return false;
|
||||||
if (array_key_exists($numberOfLines - 1, $matches[0])) {
|
|
||||||
$text = substr($text, 0, (int) $matches[0][$numberOfLines - 1][1]) . '…';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines = explode(PHP_EOL, $text);
|
$leading = (int) ($fontsize * $lineHeight);
|
||||||
foreach ($lines as $i => $line) {
|
foreach ($lines as $i => $line) {
|
||||||
// Print line On Image
|
// Print line On Image
|
||||||
imagettftext(
|
imagettftext(
|
||||||
$image,
|
$image,
|
||||||
$fontsize,
|
$fontsize,
|
||||||
0,
|
0,
|
||||||
$x,
|
$x + ($paragraphIndent * ($i === 0 ? 1 : 0)),
|
||||||
$y + $fontsize + (($fontsize + $leading) * $i),
|
$y + $fontsize + ($leading * $i),
|
||||||
$white,
|
$white,
|
||||||
$fontPath,
|
$fontPath,
|
||||||
$line
|
$line
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Print Text On Image
|
|
||||||
imagettftext($image, $fontsize, 0, $x, $y + $fontsize, $white, $fontPath, $text);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addTextWithBox(
|
|
||||||
GdImage $image,
|
|
||||||
int $x,
|
|
||||||
int $y,
|
|
||||||
string $text,
|
|
||||||
string $fontPath,
|
|
||||||
int $fontsize,
|
|
||||||
int $paddingX = 0,
|
|
||||||
int $paddingY = 0,
|
|
||||||
): bool {
|
|
||||||
// Create some colors
|
|
||||||
$white = imagecolorallocate($image, 255, 255, 255);
|
|
||||||
$bgColor = imagecolorallocate($image, 0, 86, 74);
|
|
||||||
|
|
||||||
if ($white === false || $bgColor === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$bbox = $this->calculateTextBox($fontsize, 0, $fontPath, $text);
|
|
||||||
|
|
||||||
if ($bbox === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$x1 = $x + $bbox['left'] + $paddingX;
|
|
||||||
$y1 = $y + $bbox['top'] + $paddingY;
|
|
||||||
$x2 = $x + $bbox['width'] + ($paddingX * 2);
|
|
||||||
$y2 = $y + $bbox['height'] + ($paddingY * 2);
|
|
||||||
|
|
||||||
imagefilledrectangle($image, $x, $y, $x2, $y2, $bgColor);
|
|
||||||
imagettftext($image, $fontsize, 0, $x1, $y1, $white, $fontPath, $text);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -524,4 +509,101 @@ class VideoClip
|
|||||||
'box' => $bbox,
|
'box' => $bbox,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addTextWithBox(
|
||||||
|
GdImage $image,
|
||||||
|
int $x,
|
||||||
|
int $y,
|
||||||
|
string $text,
|
||||||
|
string $fontPath,
|
||||||
|
int $fontsize,
|
||||||
|
int $paddingX = 0,
|
||||||
|
int $paddingY = 0,
|
||||||
|
): bool {
|
||||||
|
// Create some colors
|
||||||
|
$white = imagecolorallocate($image, 255, 255, 255);
|
||||||
|
$bgColor = imagecolorallocate($image, 0, 61, 11);
|
||||||
|
|
||||||
|
if ($white === false || $bgColor === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bbox = $this->calculateTextBox($fontsize, 0, $fontPath, $text);
|
||||||
|
|
||||||
|
if ($bbox === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x1 = $x + $bbox['left'] + $paddingX;
|
||||||
|
$y1 = $y + $bbox['top'] + $paddingY;
|
||||||
|
$x2 = $x + $bbox['width'] + ($paddingX * 2);
|
||||||
|
$y2 = $y + $bbox['height'] + ($paddingY * 2);
|
||||||
|
|
||||||
|
imagefilledrectangle($image, $x, $y, $x2, $y2, $bgColor);
|
||||||
|
imagettftext($image, $fontsize, 0, $x1, $y1, $white, $fontPath, $text);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>|false
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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…
x
Reference in New Issue
Block a user