mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-19 01:55:08 +00:00
479dc61d2d
Logo and watermark belonged in the private package for one reason: that is where they were written. Nothing about them needs a hosted platform, and an installation wanting its own mark on the pages it serves is the ordinary case rather than the exotic one. They are core's now, and every installation has them. Hiding "Powered by ProjectSend" did not come. That is what a hosted customer pays for, and its gate is not a capability key but the absence of the code: cloud-modules keeps the listener, so an installation without that package holds the column and has nothing able to read it. Flipping an edition variable buys nothing, which was true before and stays true. Core renders the switch where Capability::AttributionHide is held and has no route that can save it -- there is a test asserting exactly that, which fails the day white-labelling quietly becomes free. The migrations move with their original filenames on purpose. A Cloud tenant already ran them under those names, so Laravel skips them there and the table and its data are untouched; a fresh install or a community one runs them from here for the first time. What got better on the way rather than merely moving: The watermark listeners take core's real RenderingImage and ResolvingImageRendering instead of duck-typed `object` payloads, and the tests construct the genuine events rather than anonymous stand-ins that imitated their shape. The package had to do it that way -- it builds with no host present -- so three PHPStan ignore entries existed to describe what the type system could not see. They are gone. ModuleBoundaryTest asserted "branding is cloud-only, and the suite runs as community", which was never what it was testing. It now reads the capability off the route and subtracts it, so the invariant holds for whichever module is installed. The 43 branding strings arrived in all sixteen locales from the package's own catalogues rather than being retranslated, and the package's are pruned to the one string it still uses. A hosted plan without branding subtracts branding.customize and attribution.hide from the instance's environment. The row is never deleted by that: a downgrade is usually an expired card rather than a decision, and wiping somebody's artwork over a billing event is a loss they would find weeks later with no way to know what it used to be. Hiding reverses; deleting does not.
95 lines
3.5 KiB
PHP
95 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Platform\Branding\Watermark;
|
|
|
|
use claviska\SimpleImage;
|
|
|
|
/**
|
|
* The stand-in photograph the settings screen draws the mark on, so an
|
|
* administrator can judge placement, scale and opacity without going to
|
|
* find a client account and a real file.
|
|
*
|
|
* Drawn rather than shipped as an asset: a stock photograph would be a
|
|
* licensing question and a binary in a git repository, and would only
|
|
* ever exercise whatever tones that one picture happens to contain. This
|
|
* is built to answer the question the sample exists for — "will my mark
|
|
* still read?" — with a full dark-to-light ramp under it, plus a couple
|
|
* of hard edges, so a too-transparent or too-small mark is obvious
|
|
* against at least one part of it.
|
|
*/
|
|
class WatermarkSample
|
|
{
|
|
/**
|
|
* Roughly the proportions of a landscape photograph, and about the
|
|
* size the settings screen shows it at — big enough to judge, small
|
|
* enough to re-render on every keystroke.
|
|
*/
|
|
private const WIDTH = 480;
|
|
|
|
private const HEIGHT = 300;
|
|
|
|
public function __construct(
|
|
private readonly WatermarkPainter $painter,
|
|
) {}
|
|
|
|
/**
|
|
* @param string $markPath an absolute local path to the artwork
|
|
*/
|
|
public function render(string $markPath, WatermarkPosition $position, int $size, int $opacity): SimpleImage
|
|
{
|
|
$canvas = $this->backdrop();
|
|
|
|
$this->painter->paint($canvas, $markPath, $position, $size, $opacity);
|
|
|
|
return $canvas;
|
|
}
|
|
|
|
private function backdrop(): SimpleImage
|
|
{
|
|
$canvas = (new SimpleImage())->fromNew(self::WIDTH, self::HEIGHT, '#1f2937');
|
|
|
|
// A left-to-right ramp, one column at a time — GD has no gradient
|
|
// primitive, and 480 lines is imperceptible next to the encode
|
|
// that follows.
|
|
for ($x = 0; $x < self::WIDTH; $x++) {
|
|
$shade = (int) round(24 + ($x / self::WIDTH) * 210);
|
|
|
|
// alpha 1 is *opaque* in SimpleImage's vocabulary — 0 is the
|
|
// fully transparent one ('transparent' normalizes to alpha 0).
|
|
// Getting that backwards draws the whole ramp invisibly, which
|
|
// no assertion about the mark itself would ever have caught.
|
|
$canvas->line($x, 0, $x, self::HEIGHT, [
|
|
'red' => $shade, 'green' => $shade, 'blue' => $shade, 'alpha' => 1,
|
|
]);
|
|
}
|
|
|
|
// Two blocks at the extremes of the ramp, so every corner and edge
|
|
// the position picker offers has both a light and a dark
|
|
// neighbourhood somewhere near it.
|
|
$this->fill($canvas, 0, 0, (int) (self::WIDTH * 0.28), (int) (self::HEIGHT * 0.34), '#f8fafc');
|
|
$this->fill($canvas, (int) (self::WIDTH * 0.68), (int) (self::HEIGHT * 0.62), self::WIDTH, self::HEIGHT, '#0b1120');
|
|
|
|
return $canvas;
|
|
}
|
|
|
|
/**
|
|
* A filled rectangle, drawn as a run of vertical lines.
|
|
*
|
|
* `rectangle(..., 'filled')` does exist and would be the obvious call,
|
|
* but SimpleImage's own docblock types that parameter `integer|array`,
|
|
* so passing its documented magic string fails static analysis. Lines
|
|
* cost nothing here and keep the analyser honest instead of teaching
|
|
* it to ignore a whole category of argument-type error in this file.
|
|
*
|
|
* @param string|array<string, int> $color
|
|
*/
|
|
private function fill(SimpleImage $canvas, int $x1, int $y1, int $x2, int $y2, string|array $color): void
|
|
{
|
|
for ($x = $x1; $x <= $x2; $x++) {
|
|
$canvas->line($x, $y1, $x, $y2, $color);
|
|
}
|
|
}
|
|
}
|