mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-01 21:00:58 +00:00
* refactor: stabilize model identities for domain migration * refactor: extract module platform context * refactor: assign models to domain contexts * refactor: extract ai platform context * refactor: extract storage platform context * refactor: extract mail platform context * refactor: extract pdf platform context * refactor: extract operations platform context * refactor: move installation into operations platform * refactor: extract money domain context * refactor: extract taxation domain context * refactor: extract catalog domain context * refactor: extract metadata domain context * refactor: extract reporting domain context * refactor: extract purchases domain context * refactor: extract receivables domain context * refactor: extract accounts domain context * refactor: complete reporting statement boundary * refactor: extract contacts domain context * refactor: extract sales domain context * refactor: remove legacy application layers * fix: migrate legacy bouncer role identities
206 lines
7.4 KiB
PHP
206 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Platform\Pdf\Rendering;
|
|
|
|
/**
|
|
* The page geometry both drivers render to, resolved once and translated per driver.
|
|
*
|
|
* Paper size used to be a Gotenberg-only setting stored as "210mm 297mm", while
|
|
* dompdf was pinned to whatever `config/dompdf.php` said and had no admin control
|
|
* at all. The two also disagreed about margins: dompdf falls back to its own
|
|
* stylesheet default of 1.2cm, Gotenberg was hardcoded to zero, so the same
|
|
* template came out differently depending on the driver. Both now default to
|
|
* nothing: the stock templates carry their own insets, and invoice2/estimate2
|
|
* are built around a header band that only reaches the paper edge at margin 0.
|
|
*
|
|
* The reports are the exception, which is what forReports() is for. Their
|
|
* templates never carried an inset of their own and were drawn against dompdf's
|
|
* built-in 1.2cm, so they need a real page margin put back, from their own
|
|
* config key rather than the document one.
|
|
*
|
|
* Dimensions are stored as CSS lengths because that is the only representation
|
|
* both drivers take without loss. Gotenberg has no notion of named sizes, only
|
|
* dimensions; dompdf accepts either a name from its own 66-entry table or a
|
|
* points array, and the points array is the branch that can express anything.
|
|
* Named presets are a convenience in the UI that resolve to a pair of lengths.
|
|
*/
|
|
final class PdfPageSetup
|
|
{
|
|
/** Points per unit. CSS px is 1/96in, PDF points are 1/72in. */
|
|
private const POINTS_PER_UNIT = [
|
|
'pt' => 1.0,
|
|
'px' => 0.75,
|
|
'pc' => 12.0,
|
|
'mm' => 72 / 25.4,
|
|
'cm' => 720 / 25.4,
|
|
'in' => 72.0,
|
|
];
|
|
|
|
private function __construct(
|
|
public readonly string $width,
|
|
public readonly string $height,
|
|
public readonly string $orientation,
|
|
public readonly string $marginTop,
|
|
public readonly string $marginRight,
|
|
public readonly string $marginBottom,
|
|
public readonly string $marginLeft,
|
|
) {}
|
|
|
|
public static function fromConfig(): self
|
|
{
|
|
return new self(
|
|
width: self::length('pdf.page.paper_width', '210mm'),
|
|
height: self::length('pdf.page.paper_height', '297mm'),
|
|
orientation: config('pdf.page.orientation') === 'landscape' ? 'landscape' : 'portrait',
|
|
marginTop: self::length('pdf.page.margin_top', '0'),
|
|
marginRight: self::length('pdf.page.margin_right', '0'),
|
|
marginBottom: self::length('pdf.page.margin_bottom', '0'),
|
|
marginLeft: self::length('pdf.page.margin_left', '0'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The configured page, but with the report margin on all four sides.
|
|
*
|
|
* Reports are the one family of templates that carries no inset of its own:
|
|
* they only set `.sub-container { padding: 0px 20px }` and relied on dompdf's
|
|
* built-in 1.2cm page margin, which stopped applying once DompdfDriver began
|
|
* injecting an @page rule from config. The document margins default to zero
|
|
* and must stay that way (invoice2 and estimate2 bleed a header band to the
|
|
* paper edge), so the reports get their own margin instead of inheriting
|
|
* those. Paper size and orientation still come from config: only the margins
|
|
* are overridden.
|
|
*
|
|
* A page margin rather than body padding because reports run to several
|
|
* pages, and padding only insets the first one.
|
|
*/
|
|
public static function forReports(): self
|
|
{
|
|
return self::fromConfig()->withUniformMargin(self::length('pdf.page.report_margin', '1.2cm'));
|
|
}
|
|
|
|
/**
|
|
* A copy of this page with the same paper and orientation, and the given
|
|
* margin on all four sides.
|
|
*/
|
|
public function withUniformMargin(string $margin): self
|
|
{
|
|
$margin = self::assertLength($margin, "Invalid PDF page margin: \"{$margin}\".");
|
|
|
|
return new self(
|
|
width: $this->width,
|
|
height: $this->height,
|
|
orientation: $this->orientation,
|
|
marginTop: $margin,
|
|
marginRight: $margin,
|
|
marginBottom: $margin,
|
|
marginLeft: $margin,
|
|
);
|
|
}
|
|
|
|
public function isLandscape(): bool
|
|
{
|
|
return $this->orientation === 'landscape';
|
|
}
|
|
|
|
/**
|
|
* Portrait dimensions for Gotenberg's paperSize(). Orientation is applied
|
|
* separately via landscape(), which does the swap itself.
|
|
*
|
|
* @return array{0: string, 1: string}
|
|
*/
|
|
public function gotenbergPaper(): array
|
|
{
|
|
return [$this->width, $this->height];
|
|
}
|
|
|
|
/**
|
|
* Gotenberg's margins() takes top, bottom, left, right — note the order,
|
|
* which is not the CSS one.
|
|
*
|
|
* @return array{0: string, 1: string, 2: string, 3: string}
|
|
*/
|
|
public function gotenbergMargins(): array
|
|
{
|
|
return [$this->marginTop, $this->marginBottom, $this->marginLeft, $this->marginRight];
|
|
}
|
|
|
|
/**
|
|
* Points array for dompdf's setPaper(). Always portrait: Dompdf::getPaperSize()
|
|
* swaps the axes itself when the orientation argument says landscape, so
|
|
* pre-swapping here would cancel out.
|
|
*
|
|
* @return array{0: float, 1: float, 2: float, 3: float}
|
|
*/
|
|
public function dompdfPaper(): array
|
|
{
|
|
return [0.0, 0.0, self::toPoints($this->width), self::toPoints($this->height)];
|
|
}
|
|
|
|
/**
|
|
* dompdf has no margin API at all — margins come from the `@page` box, so
|
|
* the only lever is CSS. See DompdfDriver, which injects this.
|
|
*/
|
|
public function marginCss(): string
|
|
{
|
|
return "{$this->marginTop} {$this->marginRight} {$this->marginBottom} {$this->marginLeft}";
|
|
}
|
|
|
|
public static function toPoints(string $length): float
|
|
{
|
|
$length = trim($length);
|
|
|
|
if ($length === '0') {
|
|
return 0.0;
|
|
}
|
|
|
|
if (! preg_match('/^(\d+(?:\.\d+)?)(pt|px|pc|mm|cm|in)$/', $length, $m)) {
|
|
throw new \InvalidArgumentException("Invalid PDF page length: {$length}");
|
|
}
|
|
|
|
return (float) $m[1] * self::POINTS_PER_UNIT[$m[2]];
|
|
}
|
|
|
|
/**
|
|
* Unset or blank falls back to the default; anything set but malformed
|
|
* throws.
|
|
*
|
|
* Values are validated on save, but config can also come from the
|
|
* environment, and the drivers would fail differently otherwise: dompdf
|
|
* throws while converting to points, whereas Gotenberg would forward the
|
|
* garbage and render at some other size. Failing here keeps them consistent
|
|
* and names the offending key.
|
|
*/
|
|
private static function length(string $key, string $fallback): string
|
|
{
|
|
$value = config($key);
|
|
|
|
if (! is_string($value) || trim($value) === '') {
|
|
return $fallback;
|
|
}
|
|
|
|
$value = trim($value);
|
|
|
|
return self::assertLength($value, "Invalid PDF page length for {$key}: \"{$value}\".");
|
|
}
|
|
|
|
/**
|
|
* A bare 0 needs no unit; anything else is a number and one of the units
|
|
* both drivers understand. Shared by length() and withUniformMargin() so
|
|
* there is one definition of what a valid length is, with the caller
|
|
* supplying the part of the message that says where the bad value came from.
|
|
*/
|
|
private static function assertLength(string $value, string $context): string
|
|
{
|
|
$value = trim($value);
|
|
|
|
if (! preg_match('/^(0|\d+(\.\d+)?(pt|px|pc|mm|cm|in))$/', $value)) {
|
|
throw new \InvalidArgumentException(
|
|
$context.' Expected 0, or a number and a unit, e.g. "210mm".'
|
|
);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|