diff --git a/src/Border.php b/src/Border.php index 7984bf9..7fc802c 100644 --- a/src/Border.php +++ b/src/Border.php @@ -2,10 +2,24 @@ namespace SimonHamp\TheOg; +use Intervention\Image\Colors\Rgb\Color; + class Border { protected BorderPosition $position; - protected int $width = 10; + protected int $width; + protected string $color; + + public function color(Color $color): self + { + $this->color = $color; + return $this; + } + + public function getColor(): Color + { + return Color::create($this->color); + } public function position(BorderPosition $position): self { diff --git a/src/BorderPosition.php b/src/BorderPosition.php index 7437488..c8916bb 100644 --- a/src/BorderPosition.php +++ b/src/BorderPosition.php @@ -4,6 +4,7 @@ enum BorderPosition: string { + case None = 'none'; case All = 'all'; case Bottom = 'bottom'; case Left = 'left'; diff --git a/src/Font.php b/src/Font.php index 1f6ef2c..b3da64d 100644 --- a/src/Font.php +++ b/src/Font.php @@ -5,6 +5,7 @@ enum Font: string { case Inter = 'Inter/static/Inter-Regular.ttf'; + case InterBlack = 'Inter/static/Inter-Black.ttf'; case InterBold = 'Inter/static/Inter-Bold.ttf'; case InterLight = 'Inter/static/Inter-Light.ttf'; diff --git a/src/Image.php b/src/Image.php index f2f52c8..b8176f7 100644 --- a/src/Image.php +++ b/src/Image.php @@ -2,111 +2,162 @@ namespace SimonHamp\TheOg; +use Intervention\Image\Image as RenderedImage; +use Intervention\Image\Colors\Rgb\Color; use Intervention\Image\Encoders\PngEncoder; -use SimonHamp\TheOg\Traits\RendersImages; +use SimonHamp\TheOg\Interfaces\Layout; +use SimonHamp\TheOg\Layout\Layouts; +use SimonHamp\TheOg\Interfaces\Theme; +use SimonHamp\TheOg\Themes\Themes; class Image { - use RendersImages; - - protected string $accentColor = '#000000'; - protected ?Background $background = null; - protected string $backgroundColor = '#ffffff'; - protected float $backgroundOpacity = 1.0; - protected ?Border $border = null; - protected string $callToAction; - protected string $description; - protected int $height = 630; - protected Layout $layout = Layout::Standard; - protected Theme $theme = Theme::Light; - protected string $title; - protected ?string $url = null; - protected int $width = 1200; + public Layout $layout; + public Theme $theme; - public function url(string $url): self + public readonly string $callToAction; + public readonly string $description; + public readonly string $picture; + public readonly string $title; + public readonly string $url; + public readonly string $watermark; + + public function __construct() { - $this->url = $url; - return $this; + $this->layout(Layouts::Standard); + $this->theme(Themes::Light); } - - public function title(string $title): self + + /** + * The call to action text + */ + public function callToAction(string $content): self { - $this->title = $title; + $this->callToAction = $content; return $this; } - public function image(string $image): self + /** + * The description text + */ + public function description(string $description): self { - $this->image = $image; + $this->description = $description; return $this; } - - public function description(string $description): self + + /** + * The picture to display + */ + public function picture(string $picture): self { - $this->description = $description; + $this->picture = $picture; return $this; } - - public function layout(Layout $layout): self + + /** + * The title text + */ + public function title(string $title): self { - $this->layout = $layout; + $this->title = $title; return $this; } - - public function theme(Theme $theme): self + + /** + * The URL + */ + public function url(string $url): self { - $this->theme = $theme; + $this->url = $url; return $this; } - - public function accentColor(string $hexCode): self + + /** + * The watermark image + */ + public function watermark(string $watermark, ?float $opacity = 1.0): self { - // TODO: Make sure it's a valid hex code - $this->accentColor = $hexCode; + $this->watermark = $watermark; return $this; } - public function background(Background $background, float $opacity = 1.0): self + /** + * The layout to use + */ + public function layout(Layouts|Layout $layout): self { - $this->backgroundOpacity = $opacity < 0 ? 0 : ($opacity > 1 ? 1 : $opacity); - $this->background = $background; + if ($layout instanceof Layouts) { + $this->layout = $layout->getLayout(); + } else { + $this->layout = $layout; + } + return $this; } - public function callToAction(string $content): self + /** + * The theme to use + */ + public function theme(Themes|Theme $theme): self { - $this->callToAction = $content; + if ($theme instanceof Themes) { + $this->theme = $theme->getTheme(); + } else { + $this->theme = $theme; + } + return $this; } - public function width(int $width): self + /** + * Override the theme's default accent color + */ + public function accentColor(string $color): self { - $this->width = $width < 100 ? 100 : $width; + $this->theme->accentColor($color); return $this; } - public function height(int $height): self + /** + * Override the theme's default background + */ + public function background(Background $background, ?float $opacity = 1.0): self { - $this->height = $height < 100 ? 100 : $height; + $this->theme->background($background); + $this->theme->backgroundOpacity($opacity); return $this; } + /** + * Override the theme's default background color + */ public function backgroundColor(string $backgroundColor): self { - // TODO: Make sure it's a valid hex code - $this->backgroundColor = $backgroundColor; + $this->theme->backgroundColor($backgroundColor); return $this; } - public function border(int $width = 20, BorderPosition $position = BorderPosition::All): self + /** + * Override the layout's default border + */ + public function border(?BorderPosition $position = null, ?Color $color = null, ?int $width = null): self { - $this->border = (new Border()) - ->width($width) - ->position($position); + $this->layout->border( + (new Border()) + ->position($position ?? $this->layout->getBorderPosition()) + ->color($color ?? $this->theme->getBorderColor()) + ->width($width ?? $this->layout->getBorderWidth()) + ); return $this; } + public function render(): RenderedImage + { + return $this->layout->render($this); + } + public function save(string $path, string $format = PngEncoder::class): self { $this->render() diff --git a/src/Interfaces/Layout.php b/src/Interfaces/Layout.php new file mode 100644 index 0000000..bcf7557 --- /dev/null +++ b/src/Interfaces/Layout.php @@ -0,0 +1,23 @@ + 80, - self::Bold => 120, - self::Split => 120, - }; - } - - public function getUrlY(): int - { - return match ($this) { - self::Standard => 80, - self::Bold => 120, - self::Split => 120, - }; - } - - public function getUrlSize(): int - { - return match ($this) { - self::Standard => 28, - self::Bold => 28, - self::Split => 28, - }; - } - - public function getUrlFont(): Font - { - return match ($this) { - self::Standard => Font::InterBold, - self::Bold => Font::Inter, - self::Split => Font::Inter, - }; - } - - public function getTitleX(): int - { - return match ($this) { - self::Standard => 80, - self::Bold => 120, - self::Split => 120, - }; - } - - public function getTitleY(): int - { - return match ($this) { - self::Standard => 120, - self::Bold => 120, - self::Split => 120, - }; - } - - public function getTitleSize(): int - { - return match ($this) { - self::Standard => 72, - self::Bold => 28, - self::Split => 28, - }; - } - - public function getTitleFont(): Font - { - return match ($this) { - self::Standard => Font::InterBold, - self::Bold => Font::InterBold, - self::Split => Font::InterBold, - }; - } - - public function getDescriptionX(): int - { - return match ($this) { - self::Standard => 80, - self::Bold => 120, - self::Split => 120, - }; - } - - public function getDescriptionY(): int - { - return match ($this) { - self::Standard => 300, - self::Bold => 120, - self::Split => 120, - }; - } - - public function getDescriptionSize(): int - { - return match ($this) { - self::Standard => 40, - self::Bold => 40, - self::Split => 40, - }; - } - - public function getDescriptionFont(): Font - { - return match ($this) { - self::Standard => Font::InterLight, - self::Bold => Font::InterLight, - self::Split => Font::InterLight, - }; - } -} diff --git a/src/Layout/AbstractLayout.php b/src/Layout/AbstractLayout.php new file mode 100644 index 0000000..ba266c7 --- /dev/null +++ b/src/Layout/AbstractLayout.php @@ -0,0 +1,134 @@ +border = $border; + return $this; + } + + public function callToAction(): string + { + return $this->config->callToAction; + } + + public function getCallToAction(): TextBox + { + return $this->callToAction ??= (new TextBox()) + ->text($this->callToAction()) + ->color($this->config->theme->getCallToActionColor()) + ->font($this->config->theme->getCallToActionFont()) + ->size($this->features('call_to_action', 'font_size')) + ->box(...$this->features('call_to_action', 'dimensions')) + ->position(...$this->features('call_to_action', 'layout')); + } + + public function description(): string + { + return $this->config->description; + } + + public function getDescription(): TextBox + { + return $this->description ??= (new TextBox()) + ->text($this->description()) + ->color($this->config->theme->getDescriptionColor()) + ->font($this->config->theme->getDescriptionFont()) + ->size($this->features('description', 'font_size')) + ->box(...$this->features('description', 'dimensions')) + ->position(...$this->features('description', 'layout')); + } + + public function title(): string + { + return $this->config->title; + } + + public function getTitle(): TextBox + { + return $this->title ??= (new TextBox()) + ->text($this->title()) + ->color($this->config->theme->getTitleColor()) + ->font($this->config->theme->getTitleFont()) + ->size($this->features('title', 'font_size')) + ->box(...$this->features('title', 'dimensions')) + ->position(...$this->features('title', 'layout')); + } + + public function url(): string + { + return parse_url($this->config->url, PHP_URL_HOST) ?? $this->config->url; + } + + public function getUrl(): TextBox + { + return $this->url ??= (new TextBox()) + ->text($this->url()) + ->color($this->config->theme->getUrlColor()) + ->font($this->config->theme->getUrlFont()) + ->size($this->features('url', 'font_size')) + ->box(...$this->features('url', 'dimensions')) + ->position(...$this->features('url', 'layout')); + } + + /** + * The area within the canvas that we should be rendering content. This is just a convenience object + */ + public function mountArea(): Box + { + return (new Box) + ->box( + $this->width - (($this->padding + $this->getBorderWidth()) * 2), + $this->height - (($this->padding + $this->getBorderWidth()) * 2) + ) + ->position( + $this->padding + $this->getBorderWidth(), + $this->padding + $this->getBorderWidth() + ); + } + + public function getBorderWidth(): int + { + if (isset($this->border)) { + return $this->border->getWidth(); + } + + return $this->borderWidth; + } + + public function getBorderPosition(): BorderPosition + { + if (isset($this->border)) { + return $this->border->getPosition(); + } + + return $this->borderPosition; + } +} diff --git a/src/Layout/Box.php b/src/Layout/Box.php new file mode 100644 index 0000000..e6a7914 --- /dev/null +++ b/src/Layout/Box.php @@ -0,0 +1,116 @@ +box = new Rectangle($width, $height); + return $this; + } + + /** + * Where this box should be rendered on the canvas + */ + public function position( + int $x, + int $y, + ?callable $relativeTo = null, + Position $position = Position::TopLeft, + Position $pivot = Position::TopLeft + ): self + { + $this->position = new Point($x, $y); + + if ($relativeTo) { + $this->relativeTo = $relativeTo(); + $this->relativeToPosition = $position; + $this->pivot = $pivot; + } + + return $this; + } + + public function calculatePosition(): Point + { + if (isset($this->relativeTo)) { + $position = $this->relativeTo->getPointForPosition($this->relativeToPosition); + + return new Point( + $position->x() + $this->position->x(), + $position->y() + $this->position->y() + ); + } + + return $this->position; + } + + public function getPointForPosition(Position $position): Point + { + $box = $this->getRenderedBox(); + $origin = $this->calculatePosition(); + + $coordinates = match ($position) { + Position::BottomLeft => [ + $origin->x(), + $origin->y() + $box->height() + ], + Position::BottomRight => [ + $origin->x() + $box->width(), + $origin->y() + $box->height() + ], + Position::Center => [ + $origin->x() + intval(floor($box->width() / 2)), + $origin->y() + intval(floor($box->height() / 2)), + ], + Position::MiddleBottom => [ + $origin->x() + intval(floor($box->width() / 2)), + $origin->y() + $box->height(), + ], + Position::MiddleLeft => [ + $origin->x(), + $origin->y() + intval(floor($box->height() / 2)), + ], + Position::MiddleRight => [ + $origin->x() + $box->width(), + $origin->y() + intval(floor($box->height() / 2)), + ], + Position::MiddleTop => [ + $origin->x() + intval(floor($box->width() / 2)), + $origin->y(), + ], + Position::TopLeft => [ + $origin->x(), + $origin->y() + ], + Position::TopRight => [ + $origin->x() + $box->width(), + $origin->y() + ] + }; + + return new Point(...$coordinates); + } + + protected function getRenderedBox(): Rectangle + { + return $this->renderedBox ?? $this->box; + } + + protected function setRenderedBox(Rectangle $box): self + { + $this->renderedBox = $box; + return $this; + } +} diff --git a/src/Layout/Layouts.php b/src/Layout/Layouts.php new file mode 100644 index 0000000..7f44bd5 --- /dev/null +++ b/src/Layout/Layouts.php @@ -0,0 +1,20 @@ + new Layouts\Standard(), + self::GitHubBasic => new Layouts\GitHubBasic(), + }; + } +} diff --git a/src/Layout/Layouts/GitHubBasic.php b/src/Layout/Layouts/GitHubBasic.php new file mode 100644 index 0000000..959ecaa --- /dev/null +++ b/src/Layout/Layouts/GitHubBasic.php @@ -0,0 +1,62 @@ + [ + 'font_size' => 20, + 'dimensions' => [$this->mountArea()->box->width(), 240], + 'layout' => [ + 'x' => 0, + 'y' => 20, + 'relativeTo' => fn () => $this->getDescription(), + ], + ], + 'description' => [ + 'font_size' => 40, + 'dimensions' => [$this->mountArea()->box->width(), 240], + 'layout' => [ + 'x' => 0, + 'y' => 50, + 'relativeTo' => fn () => $this->getTitle(), + 'position' => Position::BottomLeft + ], + ], + 'title' => [ + 'font_size' => 60, + 'dimensions' => [$this->mountArea()->box->width(), 400], + 'layout' => [ + 'x' => 0, + 'y' => 20, + 'relativeTo' => fn () => $this->getUrl(), + 'position' => Position::BottomLeft + ], + ], + 'url' => [ + 'font_size' => 28, + 'dimensions' => [$this->mountArea()->box->width(), 45], + 'layout' => [ + 'x' => 0, + 'y' => 20, + 'relativeTo' => fn () => $this->mountArea(), + ], + ], + ]; + + return $settings[$feature][$setting]; + } +} diff --git a/src/Layout/Layouts/Standard.php b/src/Layout/Layouts/Standard.php new file mode 100644 index 0000000..caf847d --- /dev/null +++ b/src/Layout/Layouts/Standard.php @@ -0,0 +1,67 @@ + [ + 'font_size' => 20, + 'dimensions' => [$this->mountArea()->box->width(), 240], + 'layout' => [ + 'x' => 0, + 'y' => 20, + 'relativeTo' => fn () => $this->getDescription(), + ], + ], + 'description' => [ + 'font_size' => 40, + 'dimensions' => [$this->mountArea()->box->width(), 240], + 'layout' => [ + 'x' => 0, + 'y' => 50, + 'relativeTo' => fn () => $this->getTitle(), + 'position' => Position::BottomLeft + ], + ], + 'title' => [ + 'font_size' => 60, + 'dimensions' => [$this->mountArea()->box->width(), 400], + 'layout' => [ + 'x' => 0, + 'y' => 20, + 'relativeTo' => fn () => $this->getUrl(), + 'position' => Position::BottomLeft + ], + ], + 'url' => [ + 'font_size' => 28, + 'dimensions' => [$this->mountArea()->box->width(), 45], + 'layout' => [ + 'x' => 0, + 'y' => 20, + 'relativeTo' => fn () => $this->mountArea(), + ], + ], + ]; + + return $settings[$feature][$setting]; + } +} diff --git a/src/Layout/Position.php b/src/Layout/Position.php new file mode 100644 index 0000000..d4f34aa --- /dev/null +++ b/src/Layout/Position.php @@ -0,0 +1,16 @@ +color = $color; + return $this; + } + + public function font(Font $font): self + { + $this->font = $font; + return $this; + } + + public function hAlign(string $hAlign): self + { + $this->hAlign = $hAlign; + return $this; + } + + public function lineHeight(int $lineHeight): self + { + $this->lineHeight = $lineHeight; + return $this; + } + + public function size(int $size): self + { + $this->size = $size; + return $this; + } + + public function text(string $text): self + { + $this->text = $text; + return $this; + } + + public function vAlign(string $vAlign): self + { + $this->vAlign = $vAlign; + return $this; + } + + public function render(): ImagickTextModifier + { + return $this->ensureTextFitsBox($this->generateModifier($this->text)); + } + + protected function generateModifier(string $text): ImagickTextModifier + { + return new ImagickTextModifier( + new TextModifier( + $text, + $this->calculatePosition(), + (new FontFactory( + function(FontFactory $factory) { + $factory->filename($this->font->path()); + $factory->size($this->size); + $factory->color($this->color); + + if (isset($this->hAlign)) { + $factory->align($this->hAlign); + } + + $factory->valign($this->vAlign ?? 'top'); + + $factory->lineHeight($this->lineHeight ?? 1.6); + } + ))() + ), + new ImagickDriver + ); + } + + protected function doesTextFitInBox(Rectangle $renderedBox): bool + { + return $renderedBox->fitsInto($this->box); + } + + protected function getRenderedBoxForText(string $text, ImagickTextModifier $modifier): Rectangle + { + return $modifier->boundingBox($this->getTextBlock($text)); + } + + protected function getTextBlock(string $text): TextBlock + { + return new TextBlock($text); + } + + protected function ensureTextFitsBox(ImagickTextModifier $modifier): ImagickTextModifier + { + $text = $this->text; + $renderedBox = $this->getRenderedBoxForText($text, $modifier); + + while (! $this->doesTextFitInBox($renderedBox)) { + if ($renderedBox->width() > $this->box->width()) { + $text = wordwrap($this->text, intval(floor($this->box->width() / ($modifier->boxSize('M')->width() / 1.8)))); + $renderedBox = $this->getRenderedBoxForText($text, $modifier); + } + + if ($renderedBox->height() > $this->box->height()) { + $lines = $this->getTextBlock($text); + + if ($lines->count() > 1) { + $take = intval(floor($this->box->height() / $modifier->leadingInPixels())); + $lines = array_slice($lines->map(fn($line) => (string) $line)->toArray(), 0, $take); + $text = implode("\n", $lines).'...'; + } + + $renderedBox = $this->getRenderedBoxForText($text, $modifier); + } + + $modifier = $this->generateModifier($text); + } + + $this->setRenderedBox($renderedBox); + + return $modifier; + } +} diff --git a/src/Theme.php b/src/Theme.php deleted file mode 100644 index b2cae5f..0000000 --- a/src/Theme.php +++ /dev/null @@ -1,9 +0,0 @@ -backgroundOpacity($backgroundOpacity); + } + + public function accentColor(string $color): self + { + $this->accentColor = $color; + return $this; + } + + public function getAccentColor(): Color + { + return Color::create($this->accentColor); + } + + public function background(Background $background): self + { + $this->background = $background; + return $this; + } + + public function getBackground(): ?Background + { + return $this->background; + } + + public function backgroundColor(string $color): self + { + $this->backgroundColor = $color; + return $this; + } + + public function getBackgroundColor(): Color + { + return Color::create($this->backgroundColor); + } + + public function backgroundOpacity(float $opacity): self + { + $this->backgroundOpacity = $opacity < 0 ? 0 : ($opacity > 1 ? 1 : $opacity); + return $this; + } + + public function getBackgroundOpacity(): float + { + return $this->backgroundOpacity; + } + + public function baseColor(string $color): self + { + $this->baseColor = $color; + return $this; + } + + public function getBaseColor(): Color + { + return Color::create($this->baseColor); + } + + public function baseFont(Font $font): self + { + $this->baseFont = $font; + return $this; + } + + public function getBaseFont(): Font + { + return $this->baseFont; + } + + public function borderColor(string $color): self + { + $this->borderColor = $color; + return $this; + } + + public function getBorderColor(): Color + { + return Color::create($this->borderColor ?? $this->accentColor); + } + + public function callToActionBackgroundColor(string $color): self + { + $this->callToActionBackgroundColor = $color; + return $this; + } + + public function getCallToActionBackgroundColor(): Color + { + return Color::create($this->callToActionBackgroundColor ?? $this->accentColor); + } + + public function callToActionColor(string $color): self + { + $this->callToActionColor = $color; + return $this; + } + + public function getCallToActionColor(): Color + { + return Color::create($this->callToActionColor ?? $this->baseColor); + } + + public function callToActionFont(Font $font): self + { + $this->callToActionFont = $font; + return $this; + } + + public function getCallToActionFont(): Font + { + return $this->callToActionFont ?? $this->baseFont; + } + + public function descriptionColor(string $color): self + { + $this->descriptionColor = $color; + return $this; + } + + public function getDescriptionColor(): Color + { + return Color::create($this->descriptionColor ?? $this->baseColor); + } + + public function descriptionFont(Font $font): self + { + $this->descriptionFont = $font; + return $this; + } + + public function getDescriptionFont(): Font + { + return $this->descriptionFont ?? $this->baseFont; + } + + public function titleColor(string $color): self + { + $this->titleColor = $color; + return $this; + } + + public function getTitleColor(): Color + { + return Color::create($this->titleColor ?? $this->baseColor); + } + + public function titleFont(Font $font): self + { + $this->titleFont = $font; + return $this; + } + + public function getTitleFont(): Font + { + return $this->titleFont ?? $this->baseFont; + } + + public function urlColor(string $color): self + { + $this->urlColor = $color; + return $this; + } + + public function getUrlColor(): Color + { + return Color::create($this->urlColor ?? $this->accentColor); + } + + public function urlFont(Font $font): self + { + $this->baseFont = $font; + return $this; + } + + public function getUrlFont(): Font + { + return $this->urlFont ?? $this->baseFont; + } +} diff --git a/src/Themes/Themes.php b/src/Themes/Themes.php new file mode 100644 index 0000000..e0fb2f9 --- /dev/null +++ b/src/Themes/Themes.php @@ -0,0 +1,55 @@ + $this->lightTheme(), + self::Dark => $this->darkTheme(), + }; + } + + /** + * https://coolors.co/ecebe4-cc998d-16f4d0-429ea6-153b50 + */ + protected function lightTheme(): Theme + { + return new class( + accentColor: '#247BA0', + backgroundColor: '#ECEBE4', + baseColor: '#153B50', + baseFont: Font::InterBold, + callToActionBackgroundColor: '#153B50', + callToActionColor: '#ECEBE4', + descriptionColor: '#429EA6', + descriptionFont: Font::InterLight, + titleFont: Font::InterBlack, + ) extends AbstractTheme {}; + } + + /** + * https://coolors.co/02111b-3f4045-30292f-5d737e-fcfcfc + */ + protected function darkTheme(): Theme + { + return new class( + accentColor: '#5D737E', + backgroundColor: '#02111B', + baseColor: '#FCFCFC', + baseFont: Font::InterBold, + descriptionColor: '#3F4045', + descriptionFont: Font::InterLight, + titleFont: Font::InterBlack, + urlColor: '#30292F', + ) extends AbstractTheme {}; + } +} diff --git a/src/Traits/HasFeatures.php b/src/Traits/HasFeatures.php new file mode 100644 index 0000000..9f42b06 --- /dev/null +++ b/src/Traits/HasFeatures.php @@ -0,0 +1,15 @@ +config = $config; + + $this->manager = ImageManager::imagick(); + + $this->canvas = $this->manager->create($this->width, $this->height) + ->fill($this->config->theme->getBackgroundColor()); + + // TODO: This would be better as a homogenous stack where we can simply add items of a given type to the stack + // and then loop over the items on the stack and call `render` on each one + + // Worth noting here that the order of the items in the stack should determine the order of execution, which + // may have implications on the rendering of later elements due to dependencies on rendered dimensions + + // Basically, it's up to the layout developer to know the order of the dependencies and layering needs of the + // design and reconcile that themselves by ordering the stack appropriately + + if ($this->config->theme->getBackground() instanceof Background) { + $this->renderBackground(); + } + + if (isset($this->config->url) && $url = $this->getUrl($this->config->url)) { + $this->renderTextBox($url); + } + + if (isset($this->config->title) && $title = $this->getTitle($this->config->title)) { + $this->renderTextBox($title); + } + + if (isset($this->config->description) && $description = $this->getDescription($this->config->description)) { + $this->renderTextBox($description); + } + + // TODO: Render callToActionBackground + + if (isset($this->config->callToAction) && $callToAction = $this->getCallToAction($this->config->callToAction)) { + $this->renderTextBox($callToAction); + } + + if (! isset($this->border)) { + $this->border( + (new Border()) + ->position($this->borderPosition) + ->color($this->config->theme->getBorderColor()) + ->width($this->borderWidth) + ); + } + + $this->renderBorder(); + + return $this->canvas; + } + + protected function renderTextBox(TextBox $textBox): void + { + $textBox->render()->apply($this->canvas); + } + + protected function renderBorder(): void + { + match ($this->border->getPosition()) { + BorderPosition::All => $this->renderBorderLeft() || $this->renderBorderRight() || $this->renderBorderTop() || $this->renderBorderBottom(), + BorderPosition::Bottom => $this->renderBorderBottom(), + BorderPosition::Left => $this->renderBorderLeft(), + BorderPosition::Right => $this->renderBorderRight(), + BorderPosition::Top => $this->renderBorderTop(), + BorderPosition::X => $this->renderBorderTop() || $this->renderBorderBottom(), + BorderPosition::Y => $this->renderBorderLeft() || $this->renderBorderRight(), + default => null, + }; + } + + protected function renderBorderLeft(): void + { + $this->canvas->drawRectangle(0, 0, $this->renderVerticalAccentedRectangle()); + } + + protected function renderBorderRight(): void + { + $this->canvas->drawRectangle( + $this->width - $this->border->getWidth(), + 0, + $this->renderVerticalAccentedRectangle() + ); + } + + protected function renderBorderTop(): void + { + $this->canvas->drawRectangle(0, 0, $this->renderHorizontalAccentedRectangle()); + } + + protected function renderBorderBottom(): void + { + $this->canvas->drawRectangle( + 0, + $this->height - $this->border->getWidth(), + $this->renderHorizontalAccentedRectangle() + ); + } + + protected function renderVerticalAccentedRectangle(): callable + { + return function ($rectangle) { + $rectangle->size($this->border->getWidth(), $this->height); + $rectangle->background($this->border->getColor()); + }; + } + + protected function renderHorizontalAccentedRectangle(): callable + { + return function ($rectangle) { + $rectangle->size($this->width, $this->border->getWidth()); + $rectangle->background($this->border->getColor()); + }; + } + + /** + * Repeats the supplied background image across the canvas + */ + protected function renderBackground(): void + { + $panel = $this->manager->read($this->config->theme->getBackground()->path()); + + $imagick = $panel->core()->native(); + + $imagick->setImageVirtualPixelMethod(1); + $imagick->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE); + + $imagick->evaluateImage( + Imagick::EVALUATE_MULTIPLY, + $this->config->theme->getBackgroundOpacity(), + Imagick::CHANNEL_ALPHA + ); + + $width = $panel->width(); + $height = $panel->height(); + + $columns = ceil($this->width / $width); + $rows = ceil($this->height / $height); + + $filledRows = 0; + + while ($filledRows <= $rows) { + $filledColumns = 0; + + while ($filledColumns <= $columns) { + $this->canvas->place( + element: $panel, + offset_x: $filledColumns * $width, + offset_y: $filledRows * $height, + ); + + $filledColumns++; + } + + $filledRows++; + } + } +} diff --git a/src/Traits/RendersImages.php b/src/Traits/RendersImages.php deleted file mode 100644 index e154aac..0000000 --- a/src/Traits/RendersImages.php +++ /dev/null @@ -1,181 +0,0 @@ -manager = ImageManager::imagick(); - - $this->image = $this->manager->create($this->width, $this->height) - ->fill($this->backgroundColor); - - if ($this->background instanceof Background) { - $this->renderBackground(); - } - - if ($this->url) { - $this->renderUrl(); - } - - if ($this->title) { - $this->renderTitle(); - } - - if ($this->description) { - $this->renderDescription(); - } - - if ($this->border instanceof Border) { - $this->renderBorder(); - } - - return $this->image; - } - - protected function renderTitle(): void - { - $this->renderText( - wordwrap($this->title, 30), - '#000000', - $this->layout->getTitleSize(), - $this->layout->getTitleX(), - $this->layout->getTitleY(), - $this->layout->getTitleFont(), - ); - } - - protected function renderDescription(): void - { - $this->renderText( - wordwrap($this->description, 50), - '#999999', - $this->layout->getDescriptionSize(), - $this->layout->getDescriptionX(), - $this->layout->getDescriptionY(), - $this->layout->getDescriptionFont(), - ); - } - - protected function renderUrl(): void - { - $this->renderText( - strtoupper(parse_url($this->url, PHP_URL_HOST) ?? $this->url), - $this->accentColor, - $this->layout->getUrlSize(), - $this->layout->getUrlX(), - $this->layout->getUrlY(), - $this->layout->getUrlFont(), - ); - } - - protected function renderText(string $text, string $color, int $size, int $x, int $y, Font $font): void - { - $this->image->text($text, $x, $y, function(FontFactory $fontFactory) use ($color, $size, $font) { - $fontFactory->filename($font->path()); - $fontFactory->size($size); - $fontFactory->color($color); - $fontFactory->valign('top'); - $fontFactory->lineHeight(1.6); - }); - } - - protected function renderBorder(): void - { - match ($this->border->getPosition()) { - BorderPosition::All => $this->renderBorderLeft() || $this->renderBorderRight() || $this->renderBorderTop() || $this->renderBorderBottom(), - BorderPosition::Bottom => $this->renderBorderBottom(), - BorderPosition::Left => $this->renderBorderLeft(), - BorderPosition::Right => $this->renderBorderRight(), - BorderPosition::Top => $this->renderBorderTop(), - BorderPosition::X => $this->renderBorderTop() || $this->renderBorderBottom(), - BorderPosition::Y => $this->renderBorderLeft() || $this->renderBorderRight(), - default => null, - }; - } - - protected function renderBorderLeft(): void - { - $this->image->drawRectangle(0, 0, $this->renderVerticalAccentedRectangle()); - } - - protected function renderBorderRight(): void - { - $this->image->drawRectangle($this->width - $this->border->getWidth(), 0, $this->renderVerticalAccentedRectangle()); - } - - protected function renderBorderTop(): void - { - $this->image->drawRectangle(0, 0, $this->renderHorizontalAccentedRectangle()); - } - - protected function renderBorderBottom(): void - { - $this->image->drawRectangle(0, $this->height - $this->border->getWidth(), $this->renderHorizontalAccentedRectangle()); - } - - protected function renderVerticalAccentedRectangle(): callable - { - return function ($rectangle) { - $rectangle->size($this->border->getWidth(), $this->height); - $rectangle->background($this->accentColor); - }; - } - - protected function renderHorizontalAccentedRectangle(): callable - { - return function ($rectangle) { - $rectangle->size($this->width, $this->border->getWidth()); - $rectangle->background($this->accentColor); - }; - } - - protected function renderBackground(): void - { - $panel = $this->manager->read($this->background->path()); - - $imagick = $panel->core()->native(); - - $imagick->setImageVirtualPixelMethod(1); - $imagick->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE); - - $imagick->evaluateImage(Imagick::EVALUATE_MULTIPLY, $this->backgroundOpacity, Imagick::CHANNEL_ALPHA); - - $width = $panel->width(); - $height = $panel->height(); - - $columns = ceil($this->width / $width); - $rows = ceil($this->height / $height); - - $filledRows = 0; - - while ($filledRows <= $rows) { - $filledColumns = 0; - - while ($filledColumns <= $columns) { - $this->image->place( - element: $panel, - offset_x: $filledColumns * $width, - offset_y: $filledRows * $height, - ); - - $filledColumns++; - } - - $filledRows++; - } - } -} diff --git a/tests/test.php b/tests/test.php index c465049..33c5b28 100644 --- a/tests/test.php +++ b/tests/test.php @@ -2,14 +2,30 @@ use SimonHamp\TheOg\Image; use SimonHamp\TheOg\Background; +use SimonHamp\TheOg\Themes\Themes; include_once __DIR__.'/../vendor/autoload.php'; +// Basic +(new Image()) + ->url('https://example.com/blog/some-blog-post-url') + ->title('Some blog post title that is quite big and quite long') + ->description('Some slightly smaller but potentially much longer subtext. It could be really long so we might need to trim it completely after many words') + ->save(__DIR__.'/test1.png'); + +// Different theme +(new Image()) + ->theme(Themes::Dark) + ->url('https://example.com/blog/some-blog-post-url') + ->title('Some blog post title that is quite big and quite long') + ->description('Some slightly smaller but potentially much longer subtext. It could be really long so we might need to trim it completely after many words') + ->save(__DIR__.'/test2.png'); + +// Override some elements (new Image()) ->accentColor('#cc0000') - ->border() ->url('https://example.com/blog/some-blog-post-url') ->title('Some blog post title that is quite big and quite long') ->description('Some slightly smaller but potentially much longer subtext. It could be really long so we might need to trim it completely after many words') ->background(Background::JustWaves, 0.2) - ->save(__DIR__.'/test.png'); + ->save(__DIR__.'/test3.png'); diff --git a/tests/test.png b/tests/test.png index 8539dfa..84a70f1 100644 Binary files a/tests/test.png and b/tests/test.png differ diff --git a/tests/test1.png b/tests/test1.png new file mode 100644 index 0000000..d3fe65d Binary files /dev/null and b/tests/test1.png differ diff --git a/tests/test2.png b/tests/test2.png new file mode 100644 index 0000000..433ff9b Binary files /dev/null and b/tests/test2.png differ diff --git a/tests/test3.png b/tests/test3.png new file mode 100644 index 0000000..84a70f1 Binary files /dev/null and b/tests/test3.png differ diff --git a/thumbnail.php b/thumbnail.php new file mode 100644 index 0000000..c1eddfa --- /dev/null +++ b/thumbnail.php @@ -0,0 +1,15 @@ +url('simonhamp/the-og') + ->layout(Layouts::GitHubBasic) + ->title('An opinionated OpenGraph image generator written in pure PHP') + ->description("No need for a third-party service, a separate serverless micro-service or installing Puppeteer everywhere just so you can generate dynamic images.\n\nIt's bananas!") + ->background(Background::Bananas, 0.4) + ->save(__DIR__.'/thumbnail.png'); diff --git a/thumbnail.png b/thumbnail.png new file mode 100644 index 0000000..088f3d4 Binary files /dev/null and b/thumbnail.png differ