Skip to content

Commit fd370d2

Browse files
authored
fix: Use fill-opacity and stroke-opacity for hex colors with alpha (DenverCoder1#456)
1 parent 55ee1da commit fd370d2

File tree

2 files changed

+97
-12
lines changed

2 files changed

+97
-12
lines changed

src/card.php

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,67 @@ function removeAnimations(string $svg): string
492492
return $svg;
493493
}
494494

495+
/**
496+
* Convert a color from hex 3/4/6/8 digits to hex 6 digits and opacity (0-1)
497+
*
498+
* @param string $color The color to convert
499+
* @return array<string, string> The converted color
500+
*/
501+
function convertHexColor(string $color): array
502+
{
503+
$color = preg_replace("/[^0-9a-fA-F]/", "", $color);
504+
505+
// double each character if the color is in 3/4 digit format
506+
if (strlen($color) === 3) {
507+
$chars = str_split($color);
508+
$color = "{$chars[0]}{$chars[0]}{$chars[1]}{$chars[1]}{$chars[2]}{$chars[2]}";
509+
} elseif (strlen($color) === 4) {
510+
$chars = str_split($color);
511+
$color = "{$chars[0]}{$chars[0]}{$chars[1]}{$chars[1]}{$chars[2]}{$chars[2]}{$chars[3]}{$chars[3]}";
512+
}
513+
514+
// convert to 6 digit hex and opacity
515+
if (strlen($color) === 6) {
516+
return [
517+
"color" => "#{$color}",
518+
"opacity" => 1,
519+
];
520+
} elseif (strlen($color) === 8) {
521+
return [
522+
"color" => "#" . substr($color, 0, 6),
523+
"opacity" => hexdec(substr($color, 6, 2)) / 255,
524+
];
525+
}
526+
throw new AssertionError("Invalid color: " . $color);
527+
}
528+
529+
/**
530+
* Convert transparent hex colors (4/8 digits) in an SVG to hex 6 digits and corresponding opacity attribute (0-1)
531+
*
532+
* @param string $svg The SVG for the card as a string
533+
* @return string The SVG with converted colors
534+
*/
535+
function convertHexColors(string $svg): string
536+
{
537+
// convert "transparent" to "#0000"
538+
$svg = preg_replace("/(fill|stroke)=['\"]transparent['\"]/m", '\1="#0000"', $svg);
539+
540+
// convert hex colors to 6 digits and corresponding opacity attribute
541+
$svg = preg_replace_callback(
542+
"/(fill|stroke)=['\"]#([0-9a-fA-F]{4}|[0-9a-fA-F]{8})['\"]/m",
543+
function ($matches) {
544+
$attribute = $matches[1];
545+
$result = convertHexColor($matches[2]);
546+
$color = $result["color"];
547+
$opacity = $result["opacity"];
548+
return "{$attribute}='{$color}' {$attribute}-opacity='{$opacity}'";
549+
},
550+
$svg
551+
);
552+
553+
return $svg;
554+
}
555+
495556
/**
496557
* Converts an SVG card to a PNG image
497558
*
@@ -506,15 +567,6 @@ function convertSvgToPng(string $svg): string
506567
// remove style and animations
507568
$svg = removeAnimations($svg);
508569

509-
// replace all fully transparent colors in fill or stroke with "none"
510-
// this is a workaround for what seems to be a bug in inkscape where rgba alpha values are ignored
511-
// TODO: find a way to make partially transparent colors work (eg. #ffffff50)
512-
$svg = preg_replace(
513-
"/(fill|stroke)=['\"](?:#[0-9a-fA-F]{6}00|#[0-9a-fA-F]{3}0|transparent)['\"]/m",
514-
'\1="none"',
515-
$svg
516-
);
517-
518570
// escape svg for shell
519571
$svg = escapeshellarg($svg);
520572

@@ -560,8 +612,13 @@ function generateOutput(string|array $output, array $params = null): array
560612
"body" => json_encode($data),
561613
];
562614
}
563-
// Generate SVG card
564-
$svg = gettype($output) === "string" ? generateErrorCard($output) : generateCard($output);
615+
616+
// generate SVG card
617+
$svg = gettype($output) === "string" ? generateErrorCard($output, $params) : generateCard($output, $params);
618+
619+
// some renderers such as inkscape doesn't support transparent colors in hex format, so we need to convert them
620+
$svg = convertHexColors($svg);
621+
565622
// output PNG card
566623
if ($requestedType === "png") {
567624
try {
@@ -574,14 +631,16 @@ function generateOutput(string|array $output, array $params = null): array
574631
return [
575632
"contentType" => "image/svg+xml",
576633
"status" => 500,
577-
"body" => generateErrorCard($e->getMessage()),
634+
"body" => generateErrorCard($e->getMessage(), $params),
578635
];
579636
}
580637
}
638+
581639
// remove animations if disable_animations is set
582640
if (isset($params["disable_animations"]) && $params["disable_animations"] == "true") {
583641
$svg = removeAnimations($svg);
584642
}
643+
585644
// output SVG card
586645
return [
587646
"contentType" => "image/svg+xml",

tests/RenderTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,30 @@ public function testDisableAnimations(): void
150150
$this->assertStringNotContainsString("animation:", $render);
151151
$this->assertStringNotContainsString("<style>", $render);
152152
}
153+
154+
/**
155+
* Test alpha in hex colors
156+
*/
157+
public function testAlphaInHexColors(): void
158+
{
159+
// "tranparent" gets converted to "#0000"
160+
$this->testParams["background"] = "transparent";
161+
$render = generateOutput($this->testStats, $this->testParams)["body"];
162+
$this->assertStringContainsString("fill='#000000' fill-opacity='0'", $render);
163+
164+
// "#ff000080" gets converted to "#ff0000" and fill-opacity is set to 0.50196078431373
165+
$this->testParams["background"] = "ff000080";
166+
$render = generateOutput($this->testStats, $this->testParams)["body"];
167+
$this->assertStringContainsString("fill='#ff0000' fill-opacity='0.50196078431373'", $render);
168+
169+
// "#ff0000" gets converted to "#ff0000" and fill-opacity is not set
170+
$this->testParams["background"] = "ff0000ff";
171+
$render = generateOutput($this->testStats, $this->testParams)["body"];
172+
$this->assertStringContainsString("fill='#ff0000' fill-opacity='1'", $render);
173+
174+
// test stroke opacity
175+
$this->testParams["border"] = "00ff0080";
176+
$render = generateOutput($this->testStats, $this->testParams)["body"];
177+
$this->assertStringContainsString("stroke='#00ff00' stroke-opacity='0.50196078431373'", $render);
178+
}
153179
}

0 commit comments

Comments
 (0)