From 319593565ff7cf16c3de2a2422796fec71529b0c Mon Sep 17 00:00:00 2001 From: aptyp4uk1337 Date: Mon, 27 Apr 2026 00:50:48 +0300 Subject: [PATCH 1/4] feat: Added labelColor, logo, logoColor, new style - social --- src/Badge.php | 49 +++++++- src/Calculator/SvgTextSizeCalculator.php | 45 +++++--- src/Poser.php | 2 +- src/Render/LocalSvgRenderer.php | 130 +++++++++++++++++----- src/Render/SvgBaseRenderer.php | 28 +++++ src/Render/SvgFlatRender.php | 7 +- src/Render/SvgFlatSquareRender.php | 7 +- src/Render/SvgForTheBadgeRenderer.php | 83 ++++++++------ src/Render/SvgPlasticRender.php | 7 +- src/Render/SvgSocialRender.php | 48 ++++++++ src/Resources/templates/flat-square.svg | 26 ++--- src/Resources/templates/flat.svg | 28 ++--- src/Resources/templates/for-the-badge.svg | 23 ++-- src/Resources/templates/plastic.svg | 34 +++--- src/Resources/templates/social.svg | 27 +++++ src/UI/Command.php | 14 +-- 16 files changed, 395 insertions(+), 163 deletions(-) create mode 100644 src/Render/SvgBaseRenderer.php create mode 100644 src/Render/SvgSocialRender.php create mode 100644 src/Resources/templates/social.svg diff --git a/src/Badge.php b/src/Badge.php index 4a9e722d..7c2b9901 100644 --- a/src/Badge.php +++ b/src/Badge.php @@ -32,18 +32,30 @@ class Badge private string $color; private string $style; private string $format; + private ?string $labelColor; + private ?string $logo; + private ?string $logoColor; - public function __construct(string $subject, string $status, string $color, string $style = self::DEFAULT_STYLE, string $format = self::DEFAULT_FORMAT) + public function __construct(string $subject, string $status, string $color, string $style = self::DEFAULT_STYLE, string $format = self::DEFAULT_FORMAT, ?string $labelColor = null, ?string $logo = null, ?string $logoColor = null) { $this->subject = $this->escapeValue($subject); $this->status = $this->escapeValue($status); $this->color = $this->getColorHex($color); $this->style = $this->escapeValue($style); $this->format = $this->escapeValue($format); + $this->labelColor = $labelColor ? $this->getColorHex($labelColor) : null; + $this->logo = $logo ? $this->escapeValue($logo) : null; + $this->logoColor = $logoColor ? $this->getColorHex($logoColor) : null; if (!$this->isValidColorHex($this->color)) { throw new \InvalidArgumentException(\sprintf('Color not valid %s', $this->color)); } + if ($this->labelColor && !$this->isValidColorHex($this->labelColor)) { + throw new \InvalidArgumentException(\sprintf('Label color not valid %s', $this->labelColor)); + } + if ($this->logoColor && !$this->isValidColorHex($this->logoColor)) { + throw new \InvalidArgumentException(\sprintf('Logo color not valid %s', $this->logoColor)); + } } /** @@ -82,8 +94,11 @@ public static function fromURI(string $URI): self $color = $match[5]; $style = isset($query['style']) && '' !== $query['style'] ? $query['style'] : self::DEFAULT_STYLE; $format = $match[8] ?? self::DEFAULT_FORMAT; + $labelColor = $query['labelColor'] ?? null; + $logo = $query['logo'] ?? null; + $logoColor = $query['logoColor'] ?? null; - return new self($subject, $status, $color, $style, $format); + return new self($subject, $status, $color, $style, $format, $labelColor, $logo, $logoColor); } /** @@ -120,6 +135,21 @@ public function getSubject(): string return $this->subject; } + public function getLabelColor(): ?string + { + return $this->labelColor ? '#' . $this->labelColor : null; + } + + public function getLogo(): ?string + { + return $this->logo; + } + + public function getLogoColor(): ?string + { + return $this->logoColor ? '#' . $this->logoColor : null; + } + public function __toString(): string { return \sprintf( @@ -156,7 +186,18 @@ private function escapeValue(string $value): string private function getColorHex(string $color): string { - return \array_key_exists($color, self::$colorScheme) ? self::$colorScheme[$color] : $color; + $color = \array_key_exists($color, self::$colorScheme) ? self::$colorScheme[$color] : $color; + $color = \ltrim($color, '#'); + + // Convert 3-digit hex to 6-digit hex + if (\strlen($color) === 3) { + $r = $color[0] . $color[0]; + $g = $color[1] . $color[1]; + $b = $color[2] . $color[2]; + $color = $r . $g . $b; + } + + return $color; } /** @@ -165,7 +206,7 @@ private function getColorHex(string $color): string private function isValidColorHex(string $color) { $color = \ltrim($color, '#'); - $regex = '/^[0-9a-fA-F]{6}$/'; + $regex = '/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/'; return \preg_match($regex, $color); } diff --git a/src/Calculator/SvgTextSizeCalculator.php b/src/Calculator/SvgTextSizeCalculator.php index 887fa3c9..0b0f1334 100644 --- a/src/Calculator/SvgTextSizeCalculator.php +++ b/src/Calculator/SvgTextSizeCalculator.php @@ -15,27 +15,44 @@ use Cog\Unicode\UnicodeString; /** + * SVG-based text size calculator using SVG fonts. + * + * This calculator provides more accurate text measurements using SVG fonts + * but requires additional dependencies (cog/svg-font and cog/unicode). + * * @author Anton Komarev */ class SvgTextSizeCalculator implements TextSizeCalculatorInterface { /** - * Calculate the width of the text box. + * Calculate the width of the text box using SVG fonts. + * + * @throws \RuntimeException If SVG font dependencies are not available */ public function calculateWidth(string $text, int $size = self::TEXT_SIZE): float { - $font = FontList::ofFile(__DIR__ . '/Font/DejaVuSans.svg')->getById('DejaVuSansBook'); - - $letterSpacing = 0.0; - - $width = $font->computeStringWidth( - UnicodeString::of($text), - $size, - $letterSpacing, - ); - - $shieldPaddingX = self::SHIELD_PADDING_EXTERNAL + self::SHIELD_PADDING_INTERNAL; - - return \round($width + $shieldPaddingX, 1); + if (!class_exists(FontList::class)) { + throw new \RuntimeException( + 'SVG font dependencies not available. Please install cog/svg-font and cog/unicode packages, ' . + 'or use GDTextSizeCalculator instead.' + ); + } + + try { + $font = FontList::ofFile(__DIR__ . '/Font/DejaVuSans.svg')->getById('DejaVuSansBook'); + $letterSpacing = 0.0; + + $width = $font->computeStringWidth( + UnicodeString::of($text), + $size, + $letterSpacing, + ); + + $shieldPaddingX = self::SHIELD_PADDING_EXTERNAL + self::SHIELD_PADDING_INTERNAL; + + return \round($width + $shieldPaddingX, 1); + } catch (\Exception $e) { + throw new \RuntimeException('Failed to calculate text width with SVG fonts: ' . $e->getMessage(), 0, $e); + } } } diff --git a/src/Poser.php b/src/Poser.php index fe03a674..f20f21b2 100644 --- a/src/Poser.php +++ b/src/Poser.php @@ -54,7 +54,7 @@ private function addStyleRender(RenderInterface $render): void $this->renders[$render->getBadgeStyle()] = $render; } - private function getRenderFor(string $style): RenderInterface + public function getRenderFor(string $style): RenderInterface { if (!isset($this->renders[$style])) { throw new \InvalidArgumentException(\sprintf('No render founds for this style [%s]', $style)); diff --git a/src/Render/LocalSvgRenderer.php b/src/Render/LocalSvgRenderer.php index b2e40127..d6fb7df1 100644 --- a/src/Render/LocalSvgRenderer.php +++ b/src/Render/LocalSvgRenderer.php @@ -26,11 +26,37 @@ abstract class LocalSvgRenderer implements RenderInterface { public const VENDOR_COLOR = '#555'; + /** + * Horizontal padding on each side of the text (px). + */ + protected const PADDING_H = 2; + + /** + * Gap between logo right edge and text start (px). + */ + protected const LOGO_TEXT_GAP = 3; + + /** + * Logo size (px). + */ + protected const LOGO_WIDTH = 14; + + /** + * Logo vertical offset for 20px-tall badges. + * Override in subclasses for different badge heights. + */ + protected function logoY(): int + { + return 3; + } + private TextSizeCalculatorInterface $textSizeCalculator; private string $templatesDirectory; - public function __construct(?TextSizeCalculatorInterface $textSizeCalculator = null, ?string $templatesDirectory = null) - { + public function __construct( + ?TextSizeCalculatorInterface $textSizeCalculator = null, + ?string $templatesDirectory = null + ) { $this->textSizeCalculator = $textSizeCalculator ?? new GDTextSizeCalculator(); $this->templatesDirectory = $templatesDirectory ?? (__DIR__ . '/../Resources/templates'); } @@ -45,9 +71,6 @@ public function render(Badge $badge): Image abstract protected function getTemplateName(): string; - /** - * @return string SVG content of the template - */ private function getTemplate(string $style): string { if (null === $this->templatesDirectory) { @@ -63,26 +86,26 @@ private function getTemplate(string $style): string return \file_get_contents($filepath); } - private function stringWidth(string $text): float + protected function stringWidth(string $text): float { - if (null === $this->textSizeCalculator) { - throw new \InvalidArgumentException('TextSizeCalculator cannot be null'); - } - return $this->textSizeCalculator->calculateWidth($text); } private function renderSvg(string $render, array $parameters, string $style): Image { foreach ($parameters as $key => $variable) { - $render = \str_replace(\sprintf('{{ %s }}', $key), $variable, $render); + $render = \str_replace(\sprintf('{{ %s }}', $key), (string) $variable, $render); } + $render = preg_replace('/\s+/', ' ', $render); + $render = str_replace('> <', '><', $render); + try { $xml = new \SimpleXMLElement($render); } catch (\Exception $e) { - throw new \RuntimeException('Generated string is not a valid XML'); + throw new \RuntimeException('Generated string is not a valid XML: ' . $e->getMessage()); } + if ('svg' !== $xml->getName()) { throw new \RuntimeException('Generated xml is not a SVG'); } @@ -92,18 +115,75 @@ private function renderSvg(string $render, array $parameters, string $style): Im protected function buildParameters(Badge $badge): array { - $parameters = []; - - $parameters['vendorWidth'] = $this->stringWidth($badge->getSubject()); - $parameters['valueWidth'] = $this->stringWidth($badge->getStatus()); - $parameters['totalWidth'] = $parameters['valueWidth'] + $parameters['vendorWidth']; - $parameters['vendorColor'] = static::VENDOR_COLOR; - $parameters['valueColor'] = $badge->getHexColor(); - $parameters['vendor'] = $badge->getSubject(); - $parameters['value'] = $badge->getStatus(); - $parameters['vendorStartPosition'] = \round($parameters['vendorWidth'] / 2, 1) + 1; - $parameters['valueStartPosition'] = $parameters['vendorWidth'] + \round($parameters['valueWidth'] / 2, 1) - 1; - - return $parameters; + $hasLogo = (bool) $badge->getLogo(); + $logoOffset = $hasLogo ? (self::LOGO_WIDTH + self::LOGO_TEXT_GAP) : 0; + + $subject = $badge->getSubject(); + $status = $badge->getStatus(); + + $subjectW = (int) round($this->stringWidth($subject)); + $statusW = (int) round($this->stringWidth($status)); + $vendorWidth = self::PADDING_H + $logoOffset + $subjectW + self::PADDING_H; + $valueWidth = self::PADDING_H + $statusW + self::PADDING_H; + $totalWidth = $vendorWidth + $valueWidth; + $vendorCenter = $logoOffset + ($subjectW / 2) + self::PADDING_H; + $valueCenter = ($statusW / 2) + self::PADDING_H; + $vendorStartX = (int) round($vendorCenter * 10); + $valueStartX = (int) round(($vendorWidth + $valueCenter) * 10); + $vendorTextLen = $subjectW * 10; + $valueTextLen = $statusW * 10; + + return [ + 'vendorWidth' => $vendorWidth, + 'valueWidth' => $valueWidth, + 'totalWidth' => $totalWidth, + 'vendorColor' => $badge->getLabelColor() ?: static::VENDOR_COLOR, + 'valueColor' => $badge->getHexColor(), + 'vendor' => $subject, + 'value' => $status, + 'vendorUpper' => strtoupper($subject), + 'valueUpper' => strtoupper($status), + 'vendorStartX' => $vendorStartX, + 'valueStartX' => $valueStartX, + 'vendorTextLength' => $vendorTextLen, + 'valueTextLength' => $valueTextLen, + 'vendorWidthMinus1' => $vendorWidth - 1, + 'valueWidthMinus1' => $valueWidth - 1, + 'valueRectX' => $vendorWidth + 6 + 0.5, + 'separatorX' => $vendorWidth + 0.5, + 'logoElement' => $hasLogo ? $this->buildLogoElement($badge) : '', + ]; + } + + private function buildLogoElement(Badge $badge): string + { + $y = $this->logoY(); + $logoColor = $badge->getLogoColor() ?: '#fff'; + $logo = $badge->getLogo(); + + if (strpos($logo, 'data:image/svg+xml;base64,') === 0) { + $b64 = preg_replace('/\s+/', '', substr($logo, strlen('data:image/svg+xml;base64,'))); + $href = 'data:image/svg+xml;base64,' . $b64; + return sprintf('', $y, $href); + } + + if (strpos($logo, 'data:image/') === 0) { + $href = str_replace(' ', '+', $logo); + return sprintf('', $y, htmlspecialchars($href)); + } + + if (strpos($logo, 'http') === 0) { + return sprintf('', $y, htmlspecialchars($logo)); + } + + $svg = sprintf( + '', + $logoColor, + $logo + ); + + $href = 'data:image/svg+xml;base64,' . base64_encode($svg); + + return sprintf('', $y, $href); } } diff --git a/src/Render/SvgBaseRenderer.php b/src/Render/SvgBaseRenderer.php new file mode 100644 index 00000000..fd4d9e19 --- /dev/null +++ b/src/Render/SvgBaseRenderer.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PUGX\Poser\Render; + +/** + * Base class for simple SVG renderers that follow the standard pattern. + * + * @author Refactored for better maintainability + */ +abstract class SvgBaseRenderer extends LocalSvgRenderer +{ + /** + * Get the template name which should match the badge style. + */ + protected function getTemplateName(): string + { + return $this->getBadgeStyle(); + } +} diff --git a/src/Render/SvgFlatRender.php b/src/Render/SvgFlatRender.php index b3875662..261e06f5 100644 --- a/src/Render/SvgFlatRender.php +++ b/src/Render/SvgFlatRender.php @@ -16,15 +16,10 @@ * * @author Giulio De Donato */ -class SvgFlatRender extends LocalSvgRenderer +class SvgFlatRender extends SvgBaseRenderer { public function getBadgeStyle(): string { return 'flat'; } - - protected function getTemplateName(): string - { - return $this->getBadgeStyle(); - } } diff --git a/src/Render/SvgFlatSquareRender.php b/src/Render/SvgFlatSquareRender.php index 979bb412..10c49149 100644 --- a/src/Render/SvgFlatSquareRender.php +++ b/src/Render/SvgFlatSquareRender.php @@ -16,15 +16,10 @@ * * @author Giulio De Donato */ -class SvgFlatSquareRender extends LocalSvgRenderer +class SvgFlatSquareRender extends SvgBaseRenderer { public function getBadgeStyle(): string { return 'flat-square'; } - - protected function getTemplateName(): string - { - return $this->getBadgeStyle(); - } } diff --git a/src/Render/SvgForTheBadgeRenderer.php b/src/Render/SvgForTheBadgeRenderer.php index f8e24588..0b5c832c 100644 --- a/src/Render/SvgForTheBadgeRenderer.php +++ b/src/Render/SvgForTheBadgeRenderer.php @@ -14,29 +14,28 @@ use PUGX\Poser\Badge; use PUGX\Poser\Calculator\TextSizeCalculatorInterface; +/** + * Renderer for the "for-the-badge" style. + * + * Differences vs. base: + * • Badge height = 28px → logo sits at y=7 (vertically centred). + * • Both labels are uppercased. + * • Wider padding: 10px each side instead of 5px. + * • No EasySVG dependency – uses the same GDTextSizeCalculator as every + * other renderer. The uppercased string is measured directly. + */ class SvgForTheBadgeRenderer extends LocalSvgRenderer { - public const VENDOR_TEXT_FONT = __DIR__ . '/../Calculator/Font/Verdana.svg'; - public const VALUE_TEXT_FONT = __DIR__ . '/../Calculator/Font/Verdana-Bold.svg'; - public const TEXT_FONT_SIZE = 10; - public const TEXT_FONT_COLOR = '#FFFFFF'; - public const TEXT_LETTER_SPACING = 0.1; - public const PADDING_X = 10; - - private \EasySVG $easy; + /** + * Wider padding for this style. + */ + private const FTB_PADDING = 0; public function __construct( - ?\EasySVG $easySVG = null, ?TextSizeCalculatorInterface $textSizeCalculator = null, ?string $templatesDirectory = null ) { parent::__construct($textSizeCalculator, $templatesDirectory); - - if (null === $easySVG) { - $easySVG = new \EasySVG(); - } - - $this->easy = $easySVG; } public function getBadgeStyle(): string @@ -49,29 +48,45 @@ protected function getTemplateName(): string return $this->getBadgeStyle(); } + /** + * for-the-badge badges are 28px tall; logo must be at y=7 to be centred. + */ + protected function logoY(): int + { + return 7; + } + protected function buildParameters(Badge $badge): array { $parameters = parent::buildParameters($badge); - $parameters['vendor'] = \mb_strtoupper($parameters['vendor']); - $parameters['value'] = \mb_strtoupper($parameters['value']); - - $this->easy->clearSVG(); - $this->easy->setLetterSpacing(self::TEXT_LETTER_SPACING); - $this->easy->setFont(self::VENDOR_TEXT_FONT, self::TEXT_FONT_SIZE, self::TEXT_FONT_COLOR); - $vendorDimensions = $this->easy->textDimensions($parameters['vendor']); - $parameters['vendorWidth'] = $vendorDimensions[0] + 2 * self::PADDING_X; - $parameters['vendorStartPosition'] = \round($parameters['vendorWidth'] / 2, 1) + 1; - - $this->easy->clearSVG(); - $this->easy->setLetterSpacing(self::TEXT_LETTER_SPACING); - $this->easy->setFont(self::VALUE_TEXT_FONT, self::TEXT_FONT_SIZE, self::TEXT_FONT_COLOR); - $valueDimensions = $this->easy->textDimensions($parameters['value']); - $parameters['valueWidth'] = $valueDimensions[0] + 2 * self::PADDING_X; - $parameters['valueStartPosition'] = $parameters['vendorWidth'] + \round($parameters['valueWidth'] / 2, 1) - 1; - - $parameters['totalWidth'] = $parameters['valueWidth'] + $parameters['vendorWidth']; + $hasLogo = (bool) $badge->getLogo(); + $logoOffset = $hasLogo ? (self::LOGO_WIDTH + self::LOGO_TEXT_GAP) : 0; + $vendorText = mb_strtoupper($badge->getSubject()); + $valueText = mb_strtoupper($badge->getStatus()); + $subjectW = (int) round($this->stringWidth($vendorText)); + $statusW = (int) round($this->stringWidth($valueText)); + $vendorWidth = self::FTB_PADDING + $logoOffset + $subjectW + self::FTB_PADDING; + $valueWidth = self::FTB_PADDING + $statusW + self::FTB_PADDING; + $vendorCenter = self::FTB_PADDING + $logoOffset + ($subjectW / 2); + $valueCenter = $vendorWidth + self::FTB_PADDING + ($statusW / 2); + $vendorStartX = (int) round($vendorCenter * 10); + $valueStartX = (int) round($valueCenter * 10); + $vendorTextLen = $subjectW * 10; + $valueTextLen = $statusW * 10; + + $parameters['vendor'] = $vendorText; + $parameters['value'] = $valueText; + $parameters['vendorUpper'] = $vendorText; + $parameters['valueUpper'] = $valueText; + $parameters['vendorWidth'] = $vendorWidth; + $parameters['valueWidth'] = $valueWidth; + $parameters['totalWidth'] = $vendorWidth + $valueWidth; + $parameters['vendorStartX'] = $vendorStartX; + $parameters['valueStartX'] = $valueStartX; + $parameters['vendorTextLength'] = $vendorTextLen; + $parameters['valueTextLength'] = $valueTextLen; return $parameters; } -} +} \ No newline at end of file diff --git a/src/Render/SvgPlasticRender.php b/src/Render/SvgPlasticRender.php index 69b71122..142a5a74 100644 --- a/src/Render/SvgPlasticRender.php +++ b/src/Render/SvgPlasticRender.php @@ -17,15 +17,10 @@ * @author Claudio D'Alicandro * @author Giulio De Donato */ -class SvgPlasticRender extends LocalSvgRenderer +class SvgPlasticRender extends SvgBaseRenderer { public function getBadgeStyle(): string { return 'plastic'; } - - protected function getTemplateName(): string - { - return $this->getBadgeStyle(); - } } diff --git a/src/Render/SvgSocialRender.php b/src/Render/SvgSocialRender.php new file mode 100644 index 00000000..3f1ffc88 --- /dev/null +++ b/src/Render/SvgSocialRender.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PUGX\Poser\Render; + +use PUGX\Poser\Badge; + +/** + * Renderer for the "social" style. + * + * The social badge has a 6px gap between the left (vendor) pill and the right + * (value) pill. That gap widens totalWidth and shifts the value text centre. + */ +class SvgSocialRender extends SvgBaseRenderer +{ + private const PILL_GAP = 6; + + public function getBadgeStyle(): string + { + return 'social'; + } + + protected function buildParameters(Badge $badge): array + { + $parameters = parent::buildParameters($badge); + + $vendorWidth = $parameters['vendorWidth']; + $valueWidth = $parameters['valueWidth']; + $statusW = (int) round($parameters['valueTextLength'] / 10); + $parameters['totalWidth'] = $vendorWidth + self::PILL_GAP + $valueWidth; + $parameters['valueRectX'] = $vendorWidth + self::PILL_GAP + 0.5; + $parameters['vendorWidthMinus1'] = $vendorWidth - 1; + $parameters['valueWidthMinus1'] = $valueWidth - 1; + $parameters['separatorX'] = $vendorWidth + 0.5; + $valueCenter = $vendorWidth + self::PILL_GAP + self::PADDING_H + ($statusW / 2); + $parameters['valueStartX'] = (int) round($valueCenter * 10); + + return $parameters; + } +} \ No newline at end of file diff --git a/src/Resources/templates/flat-square.svg b/src/Resources/templates/flat-square.svg index 8bb37724..c24aa4d2 100644 --- a/src/Resources/templates/flat-square.svg +++ b/src/Resources/templates/flat-square.svg @@ -1,20 +1,12 @@ - - - - - - - - - - + + {{ vendor }}: {{ value }} + + - - - {{ vendor }} - {{ vendor }} - {{ value }} - {{ value }} + {{ logoElement }} + + {{ vendor }} + {{ value }} - + \ No newline at end of file diff --git a/src/Resources/templates/flat.svg b/src/Resources/templates/flat.svg index bf8eb641..a994cff5 100644 --- a/src/Resources/templates/flat.svg +++ b/src/Resources/templates/flat.svg @@ -1,20 +1,22 @@ - - + + {{ vendor }}: {{ value }} + - + - - - + + + - + - - {{ vendor }} - {{ vendor }} - {{ value }} - {{ value }} + {{ logoElement }} + + + {{ vendor }} + + {{ value }} - + \ No newline at end of file diff --git a/src/Resources/templates/for-the-badge.svg b/src/Resources/templates/for-the-badge.svg index e334c7ff..25fa77ef 100644 --- a/src/Resources/templates/for-the-badge.svg +++ b/src/Resources/templates/for-the-badge.svg @@ -1,11 +1,12 @@ - - {{ vendor }}: {{ value }} - - - - - - {{ vendor }} - {{ value }} - - + + {{ vendorUpper }}: {{ valueUpper }} + + + + + {{ logoElement }} + + {{ vendorUpper }} + {{ valueUpper }} + + \ No newline at end of file diff --git a/src/Resources/templates/plastic.svg b/src/Resources/templates/plastic.svg index fc36097e..f27cec6d 100644 --- a/src/Resources/templates/plastic.svg +++ b/src/Resources/templates/plastic.svg @@ -1,18 +1,24 @@ - - - + + {{ vendor }}: {{ value }} + + - + - - - - - - {{ vendor }} - {{ vendor }} - {{ value }} - {{ value }} + + + + + + + - + {{ logoElement }} + + + {{ vendor }} + + {{ value }} + + \ No newline at end of file diff --git a/src/Resources/templates/social.svg b/src/Resources/templates/social.svg new file mode 100644 index 00000000..378f2436 --- /dev/null +++ b/src/Resources/templates/social.svg @@ -0,0 +1,27 @@ + + {{ vendor }}: {{ value }} + + + + + + + + + + + + + + + + + + {{ logoElement }} + + \ No newline at end of file diff --git a/src/UI/Command.php b/src/UI/Command.php index 262f4a15..233cf6fa 100644 --- a/src/UI/Command.php +++ b/src/UI/Command.php @@ -8,6 +8,7 @@ use PUGX\Poser\Render\SvgFlatSquareRender; use PUGX\Poser\Render\SvgForTheBadgeRenderer; use PUGX\Poser\Render\SvgPlasticRender; +use PUGX\Poser\Render\SvgSocialRender; use PUGX\Poser\ValueObject\InputRequest; use Symfony\Component\Console\Command\Command as BaseCommand; use Symfony\Component\Console\Input\InputArgument; @@ -38,24 +39,13 @@ public function __construct(?string $name = null) new SvgFlatRender(), new SvgFlatSquareRender(), new SvgForTheBadgeRenderer(), - ]); - $this->header = self::HEADER; - } - - private function init(): void - { - $this->poser = new Poser([ - new SvgPlasticRender(), - new SvgFlatRender(), - new SvgFlatSquareRender(), - new SvgForTheBadgeRenderer(), + new SvgSocialRender(), ]); $this->header = self::HEADER; } protected function configure(): void { - $this->init(); $this ->setName('generate') From ac0d5fe421143a4e142be4ece2978809918c216b Mon Sep 17 00:00:00 2001 From: aptyp4uk1337 <44950042+ApTyp4uK1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:49:36 +0300 Subject: [PATCH 2/4] fix: Fixing CI errors --- src/Badge.php | 40 ++++++------- src/Calculator/SvgTextSizeCalculator.php | 15 ++--- src/Render/LocalSvgRenderer.php | 75 ++++++++++++------------ src/Render/SvgForTheBadgeRenderer.php | 75 ++++++++++++------------ src/Render/SvgSocialRender.php | 30 ++++++---- src/UI/Command.php | 5 +- tests/Fixtures/flat-square.svg | 21 +------ tests/Fixtures/flat.svg | 21 +------ 8 files changed, 125 insertions(+), 157 deletions(-) diff --git a/src/Badge.php b/src/Badge.php index 7c2b9901..6eb5fee3 100644 --- a/src/Badge.php +++ b/src/Badge.php @@ -38,14 +38,14 @@ class Badge public function __construct(string $subject, string $status, string $color, string $style = self::DEFAULT_STYLE, string $format = self::DEFAULT_FORMAT, ?string $labelColor = null, ?string $logo = null, ?string $logoColor = null) { - $this->subject = $this->escapeValue($subject); - $this->status = $this->escapeValue($status); - $this->color = $this->getColorHex($color); - $this->style = $this->escapeValue($style); - $this->format = $this->escapeValue($format); + $this->subject = $this->escapeValue($subject); + $this->status = $this->escapeValue($status); + $this->color = $this->getColorHex($color); + $this->style = $this->escapeValue($style); + $this->format = $this->escapeValue($format); $this->labelColor = $labelColor ? $this->getColorHex($labelColor) : null; - $this->logo = $logo ? $this->escapeValue($logo) : null; - $this->logoColor = $logoColor ? $this->getColorHex($logoColor) : null; + $this->logo = $logo ? $this->escapeValue($logo) : null; + $this->logoColor = $logoColor ? $this->getColorHex($logoColor) : null; if (!$this->isValidColorHex($this->color)) { throw new \InvalidArgumentException(\sprintf('Color not valid %s', $this->color)); @@ -89,14 +89,14 @@ public static function fromURI(string $URI): self if (1 !== \preg_match($regex, $path, $match) && (6 > \count($match))) { throw new \InvalidArgumentException('The URI given is not a valid URI' . $URI); } - $subject = $match[1]; - $status = $match[3]; - $color = $match[5]; - $style = isset($query['style']) && '' !== $query['style'] ? $query['style'] : self::DEFAULT_STYLE; - $format = $match[8] ?? self::DEFAULT_FORMAT; + $subject = $match[1]; + $status = $match[3]; + $color = $match[5]; + $style = isset($query['style']) && '' !== $query['style'] ? $query['style'] : self::DEFAULT_STYLE; + $format = $match[8] ?? self::DEFAULT_FORMAT; $labelColor = $query['labelColor'] ?? null; - $logo = $query['logo'] ?? null; - $logoColor = $query['logoColor'] ?? null; + $logo = $query['logo'] ?? null; + $logoColor = $query['logoColor'] ?? null; return new self($subject, $status, $color, $style, $format, $labelColor, $logo, $logoColor); } @@ -188,15 +188,15 @@ private function getColorHex(string $color): string { $color = \array_key_exists($color, self::$colorScheme) ? self::$colorScheme[$color] : $color; $color = \ltrim($color, '#'); - + // Convert 3-digit hex to 6-digit hex - if (\strlen($color) === 3) { - $r = $color[0] . $color[0]; - $g = $color[1] . $color[1]; - $b = $color[2] . $color[2]; + if (3 === \strlen($color)) { + $r = $color[0] . $color[0]; + $g = $color[1] . $color[1]; + $b = $color[2] . $color[2]; $color = $r . $g . $b; } - + return $color; } diff --git a/src/Calculator/SvgTextSizeCalculator.php b/src/Calculator/SvgTextSizeCalculator.php index 0b0f1334..81a0b86b 100644 --- a/src/Calculator/SvgTextSizeCalculator.php +++ b/src/Calculator/SvgTextSizeCalculator.php @@ -16,30 +16,27 @@ /** * SVG-based text size calculator using SVG fonts. - * + * * This calculator provides more accurate text measurements using SVG fonts * but requires additional dependencies (cog/svg-font and cog/unicode). - * + * * @author Anton Komarev */ class SvgTextSizeCalculator implements TextSizeCalculatorInterface { /** * Calculate the width of the text box using SVG fonts. - * + * * @throws \RuntimeException If SVG font dependencies are not available */ public function calculateWidth(string $text, int $size = self::TEXT_SIZE): float { - if (!class_exists(FontList::class)) { - throw new \RuntimeException( - 'SVG font dependencies not available. Please install cog/svg-font and cog/unicode packages, ' . - 'or use GDTextSizeCalculator instead.' - ); + if (!\class_exists(FontList::class)) { + throw new \RuntimeException('SVG font dependencies not available. Please install cog/svg-font and cog/unicode packages, or use GDTextSizeCalculator instead.'); } try { - $font = FontList::ofFile(__DIR__ . '/Font/DejaVuSans.svg')->getById('DejaVuSansBook'); + $font = FontList::ofFile(__DIR__ . '/Font/DejaVuSans.svg')->getById('DejaVuSansBook'); $letterSpacing = 0.0; $width = $font->computeStringWidth( diff --git a/src/Render/LocalSvgRenderer.php b/src/Render/LocalSvgRenderer.php index d6fb7df1..a689cdde 100644 --- a/src/Render/LocalSvgRenderer.php +++ b/src/Render/LocalSvgRenderer.php @@ -27,7 +27,8 @@ abstract class LocalSvgRenderer implements RenderInterface public const VENDOR_COLOR = '#555'; /** - * Horizontal padding on each side of the text (px). + * Horizontal padding on each side of a text section (px). + * Shields.io uses 5 px per side; we match that for visual parity. */ protected const PADDING_H = 2; @@ -42,7 +43,7 @@ abstract class LocalSvgRenderer implements RenderInterface protected const LOGO_WIDTH = 14; /** - * Logo vertical offset for 20px-tall badges. + * Logo vertical offset for 20 px-tall badges. * Override in subclasses for different badge heights. */ protected function logoY(): int @@ -97,8 +98,8 @@ private function renderSvg(string $render, array $parameters, string $style): Im $render = \str_replace(\sprintf('{{ %s }}', $key), (string) $variable, $render); } - $render = preg_replace('/\s+/', ' ', $render); - $render = str_replace('> <', '><', $render); + $render = \preg_replace('/\s+/', ' ', $render); + $render = \str_replace('> <', '><', $render); try { $xml = new \SimpleXMLElement($render); @@ -115,21 +116,19 @@ private function renderSvg(string $render, array $parameters, string $style): Im protected function buildParameters(Badge $badge): array { - $hasLogo = (bool) $badge->getLogo(); - $logoOffset = $hasLogo ? (self::LOGO_WIDTH + self::LOGO_TEXT_GAP) : 0; - - $subject = $badge->getSubject(); - $status = $badge->getStatus(); - - $subjectW = (int) round($this->stringWidth($subject)); - $statusW = (int) round($this->stringWidth($status)); - $vendorWidth = self::PADDING_H + $logoOffset + $subjectW + self::PADDING_H; - $valueWidth = self::PADDING_H + $statusW + self::PADDING_H; - $totalWidth = $vendorWidth + $valueWidth; - $vendorCenter = $logoOffset + ($subjectW / 2) + self::PADDING_H; - $valueCenter = ($statusW / 2) + self::PADDING_H; - $vendorStartX = (int) round($vendorCenter * 10); - $valueStartX = (int) round(($vendorWidth + $valueCenter) * 10); + $hasLogo = (bool) $badge->getLogo(); + $logoOffset = $hasLogo ? (self::LOGO_WIDTH + self::LOGO_TEXT_GAP) : 0; + $subject = $badge->getSubject(); + $status = $badge->getStatus(); + $subjectW = (int) \round($this->stringWidth($subject)); + $statusW = (int) \round($this->stringWidth($status)); + $vendorWidth = self::PADDING_H + $logoOffset + $subjectW + self::PADDING_H; + $valueWidth = self::PADDING_H + $statusW + self::PADDING_H; + $totalWidth = $vendorWidth + $valueWidth; + $vendorCenter = self::PADDING_H + $logoOffset + ($subjectW / 2); + $valueCenter = self::PADDING_H + ($statusW / 2); + $vendorStartX = (int) \round($vendorCenter * 10); + $valueStartX = (int) \round(($vendorWidth + $valueCenter) * 10); $vendorTextLen = $subjectW * 10; $valueTextLen = $statusW * 10; @@ -141,15 +140,15 @@ protected function buildParameters(Badge $badge): array 'valueColor' => $badge->getHexColor(), 'vendor' => $subject, 'value' => $status, - 'vendorUpper' => strtoupper($subject), - 'valueUpper' => strtoupper($status), + 'vendorUpper' => \strtoupper($subject), + 'valueUpper' => \strtoupper($status), 'vendorStartX' => $vendorStartX, 'valueStartX' => $valueStartX, 'vendorTextLength' => $vendorTextLen, 'valueTextLength' => $valueTextLen, 'vendorWidthMinus1' => $vendorWidth - 1, 'valueWidthMinus1' => $valueWidth - 1, - 'valueRectX' => $vendorWidth + 6 + 0.5, + 'valueRectX' => $vendorWidth + 0.5, 'separatorX' => $vendorWidth + 0.5, 'logoElement' => $hasLogo ? $this->buildLogoElement($badge) : '', ]; @@ -160,30 +159,32 @@ private function buildLogoElement(Badge $badge): string $y = $this->logoY(); $logoColor = $badge->getLogoColor() ?: '#fff'; $logo = $badge->getLogo(); - - if (strpos($logo, 'data:image/svg+xml;base64,') === 0) { - $b64 = preg_replace('/\s+/', '', substr($logo, strlen('data:image/svg+xml;base64,'))); + + if (\str_starts_with($logo, 'data:image/svg+xml;base64,')) { + $b64 = \preg_replace('/\s+/', '', \substr($logo, \strlen('data:image/svg+xml;base64,'))); $href = 'data:image/svg+xml;base64,' . $b64; - return sprintf('', $y, $href); + + return \sprintf('', $y, $href); } - - if (strpos($logo, 'data:image/') === 0) { - $href = str_replace(' ', '+', $logo); - return sprintf('', $y, htmlspecialchars($href)); + + if (\str_starts_with($logo, 'data:image/')) { + $href = \str_replace(' ', '+', $logo); + + return \sprintf('', $y, \htmlspecialchars($href)); } - - if (strpos($logo, 'http') === 0) { - return sprintf('', $y, htmlspecialchars($logo)); + + if (\str_starts_with($logo, 'http')) { + return \sprintf('', $y, \htmlspecialchars($logo)); } - $svg = sprintf( + $svg = \sprintf( '', $logoColor, $logo ); - $href = 'data:image/svg+xml;base64,' . base64_encode($svg); - - return sprintf('', $y, $href); + $href = 'data:image/svg+xml;base64,' . \base64_encode($svg); + + return \sprintf('', $y, $href); } } diff --git a/src/Render/SvgForTheBadgeRenderer.php b/src/Render/SvgForTheBadgeRenderer.php index 0b5c832c..66068039 100644 --- a/src/Render/SvgForTheBadgeRenderer.php +++ b/src/Render/SvgForTheBadgeRenderer.php @@ -18,18 +18,18 @@ * Renderer for the "for-the-badge" style. * * Differences vs. base: - * • Badge height = 28px → logo sits at y=7 (vertically centred). - * • Both labels are uppercased. - * • Wider padding: 10px each side instead of 5px. - * • No EasySVG dependency – uses the same GDTextSizeCalculator as every - * other renderer. The uppercased string is measured directly. + * • Badge height = 28 px → logo sits at y = 7 (vertically centred). + * • Both labels are upper-cased before measuring and rendering. + * • Uses wider per-section padding (10 px each side) to match the + * reference Shields.io implementation for this style. */ class SvgForTheBadgeRenderer extends LocalSvgRenderer { /** - * Wider padding for this style. + * Padding per side for the for-the-badge style. + * Shields.io uses 10 px to give the uppercased text more breathing room. */ - private const FTB_PADDING = 0; + private const FTB_PADDING = 3; public function __construct( ?TextSizeCalculatorInterface $textSizeCalculator = null, @@ -48,9 +48,6 @@ protected function getTemplateName(): string return $this->getBadgeStyle(); } - /** - * for-the-badge badges are 28px tall; logo must be at y=7 to be centred. - */ protected function logoY(): int { return 7; @@ -58,35 +55,39 @@ protected function logoY(): int protected function buildParameters(Badge $badge): array { - $parameters = parent::buildParameters($badge); - - $hasLogo = (bool) $badge->getLogo(); - $logoOffset = $hasLogo ? (self::LOGO_WIDTH + self::LOGO_TEXT_GAP) : 0; - $vendorText = mb_strtoupper($badge->getSubject()); - $valueText = mb_strtoupper($badge->getStatus()); - $subjectW = (int) round($this->stringWidth($vendorText)); - $statusW = (int) round($this->stringWidth($valueText)); - $vendorWidth = self::FTB_PADDING + $logoOffset + $subjectW + self::FTB_PADDING; - $valueWidth = self::FTB_PADDING + $statusW + self::FTB_PADDING; - $vendorCenter = self::FTB_PADDING + $logoOffset + ($subjectW / 2); - $valueCenter = $vendorWidth + self::FTB_PADDING + ($statusW / 2); - $vendorStartX = (int) round($vendorCenter * 10); - $valueStartX = (int) round($valueCenter * 10); + $parameters = parent::buildParameters($badge); + $hasLogo = (bool) $badge->getLogo(); + $logoOffset = $hasLogo ? (self::LOGO_WIDTH + self::LOGO_TEXT_GAP) : 0; + $vendorText = \mb_strtoupper($badge->getSubject()); + $valueText = \mb_strtoupper($badge->getStatus()); + $subjectW = (int) \round($this->stringWidth($vendorText)); + $statusW = (int) \round($this->stringWidth($valueText)); + $vendorWidth = self::FTB_PADDING + $logoOffset + $subjectW + self::FTB_PADDING; + $valueWidth = self::FTB_PADDING + $statusW + self::FTB_PADDING; + $totalWidth = $vendorWidth + $valueWidth; + $vendorCenter = self::FTB_PADDING + $logoOffset + ($subjectW / 2); + $valueCenter = self::FTB_PADDING + ($statusW / 2); + $vendorStartX = (int) \round($vendorCenter * 10); + $valueStartX = (int) \round(($vendorWidth + $valueCenter) * 10); $vendorTextLen = $subjectW * 10; - $valueTextLen = $statusW * 10; + $valueTextLen = $statusW * 10; - $parameters['vendor'] = $vendorText; - $parameters['value'] = $valueText; - $parameters['vendorUpper'] = $vendorText; - $parameters['valueUpper'] = $valueText; - $parameters['vendorWidth'] = $vendorWidth; - $parameters['valueWidth'] = $valueWidth; - $parameters['totalWidth'] = $vendorWidth + $valueWidth; - $parameters['vendorStartX'] = $vendorStartX; - $parameters['valueStartX'] = $valueStartX; - $parameters['vendorTextLength'] = $vendorTextLen; - $parameters['valueTextLength'] = $valueTextLen; + $parameters['vendor'] = $vendorText; + $parameters['value'] = $valueText; + $parameters['vendorUpper'] = $vendorText; + $parameters['valueUpper'] = $valueText; + $parameters['vendorWidth'] = $vendorWidth; + $parameters['valueWidth'] = $valueWidth; + $parameters['totalWidth'] = $totalWidth; + $parameters['vendorStartX'] = $vendorStartX; + $parameters['valueStartX'] = $valueStartX; + $parameters['vendorTextLength'] = $vendorTextLen; + $parameters['valueTextLength'] = $valueTextLen; + $parameters['vendorWidthMinus1'] = $vendorWidth - 1; + $parameters['valueWidthMinus1'] = $valueWidth - 1; + $parameters['valueRectX'] = $vendorWidth + 0.5; + $parameters['separatorX'] = $vendorWidth + 0.5; return $parameters; } -} \ No newline at end of file +} diff --git a/src/Render/SvgSocialRender.php b/src/Render/SvgSocialRender.php index 3f1ffc88..bb4e5edd 100644 --- a/src/Render/SvgSocialRender.php +++ b/src/Render/SvgSocialRender.php @@ -16,12 +16,20 @@ /** * Renderer for the "social" style. * - * The social badge has a 6px gap between the left (vendor) pill and the right - * (value) pill. That gap widens totalWidth and shifts the value text centre. + * The social badge has a 6 px gap between the left (vendor) pill and the + * right (value) pill. That gap widens totalWidth and shifts the value + * text centre, but the individual pill widths are unchanged. + * + * Template variables specific to social.svg: + * • valueRectX – x origin of the right pill rect (vendorWidth + GAP + 0.5) + * • vendorWidthMinus1 – vendorWidth - 1 (used for the left pill stroke rect) + * • valueWidthMinus1 – valueWidth - 1 (used for the right pill stroke rect) + * • separatorX – x of the small chevron separator (vendorWidth + 0.5) + * • valueStartX – 10× text centre of the right pill (accounts for the gap) */ class SvgSocialRender extends SvgBaseRenderer { - private const PILL_GAP = 6; + private const PILL_GAP = 3; public function getBadgeStyle(): string { @@ -32,17 +40,17 @@ protected function buildParameters(Badge $badge): array { $parameters = parent::buildParameters($badge); - $vendorWidth = $parameters['vendorWidth']; - $valueWidth = $parameters['valueWidth']; - $statusW = (int) round($parameters['valueTextLength'] / 10); - $parameters['totalWidth'] = $vendorWidth + self::PILL_GAP + $valueWidth; + $vendorWidth = $parameters['vendorWidth']; + $valueWidth = $parameters['valueWidth']; + $statusW = (int) \round((int) $parameters['valueTextLength'] / 10); + $parameters['totalWidth'] = $vendorWidth + self::PILL_GAP + $valueWidth; $parameters['valueRectX'] = $vendorWidth + self::PILL_GAP + 0.5; $parameters['vendorWidthMinus1'] = $vendorWidth - 1; - $parameters['valueWidthMinus1'] = $valueWidth - 1; + $parameters['valueWidthMinus1'] = $valueWidth - 1; $parameters['separatorX'] = $vendorWidth + 0.5; - $valueCenter = $vendorWidth + self::PILL_GAP + self::PADDING_H + ($statusW / 2); - $parameters['valueStartX'] = (int) round($valueCenter * 10); + $valueCenter = $vendorWidth + self::PILL_GAP + self::PADDING_H + ($statusW / 2); + $parameters['valueStartX'] = (int) \round($valueCenter * 10); return $parameters; } -} \ No newline at end of file +} diff --git a/src/UI/Command.php b/src/UI/Command.php index 233cf6fa..7c9df0ef 100644 --- a/src/UI/Command.php +++ b/src/UI/Command.php @@ -32,8 +32,6 @@ class Command extends BaseCommand public function __construct(?string $name = null) { - parent::__construct($name); - $this->poser = new Poser([ new SvgPlasticRender(), new SvgFlatRender(), @@ -42,11 +40,12 @@ public function __construct(?string $name = null) new SvgSocialRender(), ]); $this->header = self::HEADER; + + parent::__construct($name); } protected function configure(): void { - $this ->setName('generate') ->setDescription('Create a badge you are a Poser.') diff --git a/tests/Fixtures/flat-square.svg b/tests/Fixtures/flat-square.svg index 6e7903c5..2118a20e 100644 --- a/tests/Fixtures/flat-square.svg +++ b/tests/Fixtures/flat-square.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - - license - license - MIT - MIT - - +license: MITlicenseMIT \ No newline at end of file diff --git a/tests/Fixtures/flat.svg b/tests/Fixtures/flat.svg index a86cc245..ffc1e052 100644 --- a/tests/Fixtures/flat.svg +++ b/tests/Fixtures/flat.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - - license - license - MIT - MIT - - +license: MITlicenseMIT \ No newline at end of file From 50e32372f1d92c3bce617faf801752e1d3265290 Mon Sep 17 00:00:00 2001 From: aptyp4uk1337 <44950042+ApTyp4uK1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:32:14 +0300 Subject: [PATCH 3/4] ci: CI tests passing --- .github/workflows/php.yml | 112 +++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 43 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 93b4e62c..076743a0 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -10,75 +10,101 @@ jobs: cs-fix: runs-on: ubuntu-24.04 name: PHP-CS-Fixer + container: + image: php:8.1-cli + steps: - name: Checkout uses: actions/checkout@v4 - - name: Fix CS - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --diff --dry-run + + - name: Install dependencies + run: | + apt-get update && apt-get install -y \ + git unzip zip \ + libzip-dev libpng-dev libjpeg-dev libfreetype6-dev + + docker-php-ext-configure gd --with-freetype --with-jpeg + docker-php-ext-install gd zip + + curl -sS https://getcomposer.org/installer | php \ + -- --install-dir=/usr/local/bin --filename=composer + + composer install --prefer-dist --no-progress + + - name: Run PHP CS Fixer + run: bin/php-cs-fixer fix --diff --dry-run + tests: + runs-on: ubuntu-24.04 + strategy: matrix: - container: [ "php81", "php82", "php83" ] + php-version: ['8.1', '8.2', '8.3'] + + container: + image: php:${{ matrix.php-version }}-cli - runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - name: Build Docker image - run: docker compose build ${{ matrix.container }} + - name: Install system dependencies + run: | + apt-get update && apt-get install -y \ + git unzip zip \ + libpng-dev libjpeg-dev libfreetype6-dev libzip-dev - - name: Check PHP version + - name: Install PHP extensions run: | - VERSION=$(echo "${{ matrix.container }}" | sed 's/php//') - MAJOR=${VERSION:0:1} - MINOR=${VERSION:1:1} - MIN_VERSION=$((MAJOR * 10000 + MINOR * 100)) - MAX_VERSION=$(((MAJOR * 10000 + (MINOR + 1) * 100))) - docker compose run --rm ${{ matrix.container }} php -r "if (PHP_VERSION_ID < $MIN_VERSION || PHP_VERSION_ID >= $MAX_VERSION) { echo 'Expected PHP $MAJOR.$MINOR but got ' . PHP_VERSION . PHP_EOL; exit(1); }" + docker-php-ext-configure gd --with-freetype --with-jpeg + docker-php-ext-install gd zip - - name: Validate composer.json - run: docker compose run -u $(id -u):$(id -g) --rm ${{ matrix.container }} composer validate + - name: Install Composer + run: | + curl -sS https://getcomposer.org/installer | php \ + -- --install-dir=/usr/local/bin --filename=composer - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-${{ matrix.container }}-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.container }} + - name: Validate composer.json + run: composer validate - name: Install dependencies - run: docker compose run -u $(id -u):$(id -g) --rm ${{ matrix.container }} composer install --prefer-dist --no-progress + run: composer install --prefer-dist --no-progress - name: Run tests - run: CONTAINER=${{ matrix.container }} make tests + run: bin/phpunit --display-warnings --do-not-fail-on-warning + tests-php84: runs-on: ubuntu-24.04 + container: + image: php:8.4-cli + steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - name: Build Docker image - run: docker compose build php84 + - name: Install system dependencies + run: | + apt-get update && apt-get install -y \ + git unzip zip \ + libpng-dev libjpeg-dev libfreetype6-dev libzip-dev - - name: Validate composer.json - run: docker compose run -u $(id -u):$(id -g) --rm php84 composer validate + - name: Install PHP extensions + run: | + docker-php-ext-configure gd --with-freetype --with-jpeg + docker-php-ext-install gd zip - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php84-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php84 + - name: Install Composer + run: | + curl -sS https://getcomposer.org/installer | php \ + -- --install-dir=/usr/local/bin --filename=composer + + - name: Validate composer.json + run: composer validate - name: Install dependencies - run: docker compose run -u $(id -u):$(id -g) --rm php84 composer install --prefer-dist --no-progress + run: composer install --prefer-dist --no-progress - name: Run tests - run: make tests + run: bin/phpunit --display-warnings --do-not-fail-on-warning \ No newline at end of file From 116ac30c59ed58bf189adfb5aa765002116bb52e Mon Sep 17 00:00:00 2001 From: aptyp4uk1337 <44950042+ApTyp4uK1337@users.noreply.github.com> Date: Fri, 1 May 2026 12:34:38 +0300 Subject: [PATCH 4/4] ci: SvgForTheBadgeRendererTest.php & SvgSocialRenderTest.php --- tests/Fixtures/for-the-badge-with-logo.svg | 1 + tests/Fixtures/for-the-badge.svg | 1 + tests/Fixtures/social-with-logo.svg | 1 + tests/Fixtures/social.svg | 1 + tests/Render/SvgForTheBadgeRendererTest.php | 96 ++++++++++++++++ tests/Render/SvgSocialRenderTest.php | 115 ++++++++++++++++++++ 6 files changed, 215 insertions(+) create mode 100644 tests/Fixtures/for-the-badge-with-logo.svg create mode 100644 tests/Fixtures/for-the-badge.svg create mode 100644 tests/Fixtures/social-with-logo.svg create mode 100644 tests/Fixtures/social.svg create mode 100644 tests/Render/SvgForTheBadgeRendererTest.php create mode 100644 tests/Render/SvgSocialRenderTest.php diff --git a/tests/Fixtures/for-the-badge-with-logo.svg b/tests/Fixtures/for-the-badge-with-logo.svg new file mode 100644 index 00000000..e49b47c9 --- /dev/null +++ b/tests/Fixtures/for-the-badge-with-logo.svg @@ -0,0 +1 @@ +BUILD: PASSINGBUILDPASSING \ No newline at end of file diff --git a/tests/Fixtures/for-the-badge.svg b/tests/Fixtures/for-the-badge.svg new file mode 100644 index 00000000..d5859aa5 --- /dev/null +++ b/tests/Fixtures/for-the-badge.svg @@ -0,0 +1 @@ +LICENSE: MITLICENSEMIT \ No newline at end of file diff --git a/tests/Fixtures/social-with-logo.svg b/tests/Fixtures/social-with-logo.svg new file mode 100644 index 00000000..64fed060 --- /dev/null +++ b/tests/Fixtures/social-with-logo.svg @@ -0,0 +1 @@ +github: stars \ No newline at end of file diff --git a/tests/Fixtures/social.svg b/tests/Fixtures/social.svg new file mode 100644 index 00000000..df3be1be --- /dev/null +++ b/tests/Fixtures/social.svg @@ -0,0 +1 @@ +twitter: follow \ No newline at end of file diff --git a/tests/Render/SvgForTheBadgeRendererTest.php b/tests/Render/SvgForTheBadgeRendererTest.php new file mode 100644 index 00000000..72f3a2c4 --- /dev/null +++ b/tests/Render/SvgForTheBadgeRendererTest.php @@ -0,0 +1,96 @@ +calculator = new GDTextSizeCalculator(); + $this->render = new SvgForTheBadgeRenderer($this->calculator); + } + + #[Test] + public function itShouldRenderASvg(): void + { + $badge = Badge::fromURI('version-stable-97CA00.svg?style=for-the-badge'); + $image = $this->render->render($badge); + + $this->assertValidSVGImage((string) $image); + } + + #[Test] + public function itShouldRenderALicenseMitExactlyLikeThisSvg(): void + { + $fixture = __DIR__ . '/../Fixtures/for-the-badge.svg'; + $template = \file_get_contents($fixture); + $badge = Badge::fromURI('license-MIT-blue.svg?style=for-the-badge'); + $image = $this->render->render($badge); + + $this->assertEquals($template, (string) $image); + } + + #[Test] + public function itShouldRenderWithLogoExactlyLikeThisSvg(): void + { + $fixture = __DIR__ . '/../Fixtures/for-the-badge-with-logo.svg'; + $template = \file_get_contents($fixture); + $badge = Badge::fromURI('build-passing-brightgreen.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0xIDE3SDl2LTJoMnYyeiIvPjwvc3ZnPg==&style=for-the-badge'); + $image = $this->render->render($badge); + + $this->assertEquals($template, (string) $image); + } + + #[Test] + public function itShouldReturnCorrectBadgeStyle(): void + { + $this->assertEquals('for-the-badge', $this->render->getBadgeStyle()); + } + + #[Test] + public function itShouldUseCorrectLogoYPosition(): void + { + $badge = Badge::fromURI('test-test-000000.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0xIDE3SDl2LTJoMnYyeiIvPjwvc3ZnPg==&style=for-the-badge'); + $image = (string) $this->render->render($badge); + + $this->assertStringContainsString('y="7"', $image); + } + + #[Test] + public function itShouldUppercaseText(): void + { + $badge = Badge::fromURI('test-lowercase-000000.svg?style=for-the-badge'); + $image = (string) $this->render->render($badge); + + $this->assertStringContainsString('TEST', $image); + $this->assertStringContainsString('LOWERCASE', $image); + } + + #[Test] + public function itShouldUseCorrectPadding(): void + { + $badge = Badge::fromURI('test-test-000000.svg?style=for-the-badge'); + $image = (string) $this->render->render($badge); + + $this->assertValidSVGImage($image); + $this->assertStringContainsString('assertStringContainsString('assertMatchesRegularExpression($regex, $svg); + } +} diff --git a/tests/Render/SvgSocialRenderTest.php b/tests/Render/SvgSocialRenderTest.php new file mode 100644 index 00000000..7189f013 --- /dev/null +++ b/tests/Render/SvgSocialRenderTest.php @@ -0,0 +1,115 @@ +calculator = new GDTextSizeCalculator(); + $this->render = new SvgSocialRender($this->calculator); + } + + #[Test] + public function itShouldRenderASvg(): void + { + $badge = Badge::fromURI('twitter-follow-1da1f2.svg?style=social&logo=twitter'); + $image = $this->render->render($badge); + + $this->assertValidSVGImage((string) $image); + } + + #[Test] + public function itShouldRenderTwitterSocialExactlyLikeThisSvg(): void + { + $fixture = __DIR__ . '/../Fixtures/social.svg'; + $template = \file_get_contents($fixture); + $badge = Badge::fromURI('twitter-follow-1da1f2.svg?style=social&logo=twitter'); + $image = $this->render->render($badge); + + $this->assertEquals($template, (string) $image); + } + + #[Test] + public function itShouldRenderGithubSocialExactlyLikeThisSvg(): void + { + $fixture = __DIR__ . '/../Fixtures/social-with-logo.svg'; + $template = \file_get_contents($fixture); + $badge = Badge::fromURI('github-stars-333.svg?style=social&logo=github'); + $image = $this->render->render($badge); + + $this->assertEquals($template, (string) $image); + } + + #[Test] + public function itShouldReturnCorrectBadgeStyle(): void + { + $this->assertEquals('social', $this->render->getBadgeStyle()); + } + + #[Test] + public function itShouldApplyPillGapToTotalWidth(): void + { + $badge = Badge::fromURI('test-test-000000.svg?style=social'); + $image = (string) $this->render->render($badge); + + $this->assertValidSVGImage($image); + $this->assertStringContainsString('assertStringContainsString('render->render($badge); + + $this->assertValidSVGImage($image); + } + + #[Test] + public function itShouldShiftValueStartXForGap(): void + { + $badge = Badge::fromURI('test-test-000000.svg?style=social'); + $image = (string) $this->render->render($badge); + + $this->assertValidSVGImage($image); + $this->assertStringContainsString('transform="scale(.1)"', $image); + } + + #[Test] + public function itShouldRenderWithLogo(): void + { + $badge = Badge::fromURI('test-test-000000.svg?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0xIDE3SDl2LTJoMnYyeiIvPjwvc3ZnPg=='); + $image = (string) $this->render->render($badge); + + $this->assertValidSVGImage($image); + $this->assertStringContainsString('render->render($badge); + + $this->assertValidSVGImage($image); + } + + private function assertValidSVGImage(string $svg): void + { + $regex = '/^$/'; + $this->assertMatchesRegularExpression($regex, $svg); + } +}