Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 69 additions & 43 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
71 changes: 56 additions & 15 deletions src/Badge.php
Original file line number Diff line number Diff line change
Expand Up @@ -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->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));
}
}

/**
Expand Down Expand Up @@ -77,13 +89,16 @@ 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;

return new self($subject, $status, $color, $style, $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;

return new self($subject, $status, $color, $style, $format, $labelColor, $logo, $logoColor);
}

/**
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 (3 === \strlen($color)) {
$r = $color[0] . $color[0];
$g = $color[1] . $color[1];
$b = $color[2] . $color[2];
$color = $r . $g . $b;
}

return $color;
}

/**
Expand All @@ -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);
}
Expand Down
42 changes: 28 additions & 14 deletions src/Calculator/SvgTextSizeCalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,41 @@
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 <anton@komarev.com>
*/
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);
}
}
}
2 changes: 1 addition & 1 deletion src/Poser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading
Loading