tecnickcom/tc-lib-color
PHP color toolkit for parsing, converting, and normalizing GRAY, RGB/RGBA, HSL, CMYK, CIE Lab, and spot colors, with named web color lookup. Provides CSS-ready output and PDF-oriented helpers, keeping browser previews and final rendered PDFs visually consistent. Part of the TCPDF ecosystem.
composer require tecnickcom/tc-lib-color
There is no Laravel service provider, no config file, and no auto-discovery — it's a plain PHP library. You bind it yourself:
// app/Providers/AppServiceProvider.php
use Com\Tecnick\Color\Web;
public function register(): void
{
$this->app->singleton(Web::class, fn () => new Web());
}
The realistic day-one job is "a hex string comes in from the DB/admin panel, and CSS or a PDF has to come out."
use Com\Tecnick\Color\Web;
$web = app(Web::class);
$rgb = $web->getRgbObjFromHex('#336699');
echo $rgb->getCssColor(); // rgb(51, 102, 153) / rgba(...) depending on alpha
Then wire it into Blade without leaking the library into templates:
// app/Support/Color/ColorService.php
namespace App\Support\Color;
use Com\Tecnick\Color\Web;
final class ColorService
{
public function __construct(private readonly Web $web) {}
public function css(string $hex): string
{
return $this->web->getRgbObjFromHex($hex)->getCssColor();
}
/** @return array{0:int,1:int,2:int} */
public function rgb(string $hex): array
{
$rgb = $this->web->getRgbObjFromHex($hex);
return [$rgb->getRed(), $rgb->getGreen(), $rgb->getBlue()];
}
}
<div style="background-color: {{ app(\App\Support\Color\ColorService::class)->css($theme->primary) }}">
example/index.php in the repo — a full conversion showcase you can run locally with make server.Web for web/CSS/named-color work, Pdf for print/CMYK/Lab work, plus the per-model objects (RGB, HSL, CMYK, Lab) that carry the cross-model to*() helpers.Start with Web even if your end target is a PDF. It's the most forgiving entry point (hex, named colors, CSS strings) and everything else chains from an RGB object.
Confidence note: only
getRgbObjFromHex()andgetCssColor()are shown in the README. Accessors likegetRed()/getHex()and converters liketoCmyk()/toHsl()are consistent with the documented "cross-model conversion helpers on all color models", but confirm the exact signatures against the API docs before building on them.
Don't scatter new Web() and getRgbObjFromHex() across controllers. One ColorService (or a small Color facade over it) means you can swap implementations later — and, importantly, you can stub it in tests without touching the vendor package.
// app/Facades/Color.php
namespace App\Facades;
use App\Support\Color\ColorService;
use Illuminate\Support\Facades\Facade;
class Color extends Facade
{
protected static function getFacadeAccessor(): string
{
return ColorService::class;
}
}
Laravel ships no first-class hex-color rule, and the library is not a validator — feed it garbage and you'll get exceptions or nonsense. Use a rule object:
// app/Rules/HexColor.php
namespace App\Rules;
use Closure;
use Com\Tecnick\Color\Web;
use Illuminate\Contracts\Validation\ValidationRule;
class HexColor implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || ! preg_match('/^#?(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i', $value)) {
$fail('The :attribute must be a hex color (3, 4, 6, or 8 digits).');
return;
}
try {
app(Web::class)->getRgbObjFromHex($value);
} catch (\Throwable) {
$fail('The :attribute is not a parseable color.');
}
}
}
Store the normalized form (lowercase, with #) after validation so equality checks, cache keys, and unique indexes behave predictably.
Theming is the highest-value use: take one brand color and generate hover/pressed/surface variants. Do the conversion work once per color and cache the result — not per request, and definitely not inside a loop over 500 PDF rows.
public function palette(string $hex): array
{
return Cache::remember("color:palette:{$hex}", now()->addDay(), function () use ($hex) {
$rgb = app(Web::class)->getRgbObjFromHex($hex);
$hsl = $rgb->toHsl(); // verify method name in API docs
return [
'base' => $rgb->getCssColor(),
'dark' => $hsl->toRgb()->getCssColor(),
'accent' => $rgb->toCmyk()->toRgb()->getCssColor(),
];
});
}
Invalidate that cache key in the same listener/handler that saves the theme record.
This is the package's home turf (it's the color layer of the TCPDF / tc-lib-pdf ecosystem). Pattern: store brand colors as hex in the DB, convert at render time for the PDF builder, and keep the conversion in one method so preview and document agree.
$cmyk = app(Web::class)->getRgbObjFromHex($settings->brand_color)->toCmyk();
// feed $cmyk components into your PDF generator's fill/stroke setters
$spot = app(\Com\Tecnick\Color\Pdf::class)->getSpotColorObj('PANTONE 185 C');
If you use dompdf/Snappy instead of TCPDF, this package still helps for the math (CMYK/Lab conversion, named-color lookup) but not for the PDF writing — keep the two concerns separate.
Use the library to get channels, then compute WCAG contrast yourself — the library does color, not contrast:
private function channel(int $c): float
{
$s = $c / 255;
return $s <= 0.03928 ? $s / 12.92 : (($s + 0.055) / 1.055) ** 2.4;
}
private function luminance(array $rgb): float
{
return 0.2126 * $this->channel($rgb[0])
+ 0.7152 * $this->channel($rgb[1])
+ 0.0722 * $this->channel($rgb[2]);
}
public function contrastRatio(string $a, string $b): float
{
$la = $this->luminance($this->rgb($a));
$lb = $this->luminance($this->rgb($b));
return (max($la, $lb) + 0.05) / (min($la, $lb) + 0.05);
}
Enforce a minimum ratio (4.5 for body text) in your HexColor validation or in an admin UI warning.
Prefer a plain string column plus an accessor that returns CSS-ready values. Don't serialize library objects into the DB or into queue payloads — pass hex strings or [r, g, b] arrays across job boundaries.
protected function primaryColorCss(): Attribute
{
return Attribute::get(fn () => app(ColorService::class)->css($this->primary_color));
}
You're not testing tc-lib-color; you're testing that your assumptions hold across versions.
it('round-trips a brand hex through hsl', function () {
$rgb = (new \Com\Tecnick\Color\Web())->getRgbObjFromHex('#336699');
expect($rgb->toHsl()->toRgb()->getHex())->toBe('#336699');
});
Keep a handful of these fixtures (pure black, white, a saturated mid-tone, an 8-digit hex) and run them on dependency upgrades.
License is not what the metadata says. Packagist/GitHub metadata reports NOASSERTION, but the README and LICENSE file state GNU LGPL v3. For a hosted SaaS this is generally a non-issue; for a distributed Laravel product, LGPL means you can link to it dynamically without releasing your application code, but modifications to the library itself must be published. Get a human to sign off before shipping closed-source binaries, and prefer depending on the upstream package over vendoring and patching it.
PHP 8.2+ is a hard floor. That rules out older Laravel apps still on PHP 8.0/8.1. Check composer.json before you plan the integration into a legacy codebase.
Sparse documentation is the real friction. There's one README example and generated API docs. Budget an hour to read the source for the classes you need; don't guess at toXyz() signatures in a PR review.
Alpha is a float in the object model, an integer in hex. An 8-digit hex (#33669980) carries alpha, and getCssColor() may emit rgba(r, g, b, 1) even when you wanted terse rgb(...). If your design tokens or tests compare exact strings, format the output yourself from the channel accessors rather than relying on the helper.
Rounding drift compounds across chained conversions. RGB → CMYK → Lab → RGB will not return you to the exact starting triple. Always convert from the original source value, never from an already-converted one, and never treat a converted value as canonical storage. Round only at the presentation boundary.
Spot colors are name-lookup, not a PANTONE license. getSpotColorObj() resolves against the library's built-in table (with DeviceCMYK/Lab alternates). Unknown names fail — don't assume the whole Pantone book is present, and don't let an admin free-text a spot name without a fallback path. Store the CMYK/Lab alternate you used alongside the name so a later library version can't silently shift your brand color.
No ICC profiles, no gamut mapping, no soft proofing. This library is arithmetic between color models. If someone asks for real prepress color management, the answer is Little CMS, not this package. Say so early.
Cache keys must include the normalized hex. #FFF, #ffffff, and ffffff are the same color and three different cache keys. Normalize on write.
Don't compute palettes inside loops. Constructing a Web instance per iteration is cheap, but conversion chains over hundreds of invoice rows aren't free. Resolve the singleton once, convert once, reuse the strings.
Keep a debugging helper. When output looks off, dump the intermediate values before blaming the renderer:
$rgb = app(Web::class)->getRgbObjFromHex($hex);
dump(['red' => $rgb->getRed(), 'green' => $rgb->getGreen(), 'blue' => $rgb->getBlue(), 'hex' => $rgb->getHex()]);
Extension points are thin by design. There's no plugin registry or config. If you need a model the library lacks (HSB, OKLCH), don't subclass its internals — implement it in your own ColorService and use this package only for the models it covers. If you need a Laravel-native API, your facade is the extension point; the package won't grow one.
How can I help you explore Laravel packages today?