Skip to content

Commit 49bcd2a

Browse files
committed
feat: detect transparency-bearing ExtGStates; fix gradient and arc output
Add getTransparencyExtGStateNames() to report the resource names of the ExtGStates that carry actual transparency — a constant alpha below 1, a non-Normal blend mode, or a soft mask — so callers can tell whether a content stream really triggers transparency rather than merely resetting it. Correctness fixes found while reviewing the gradient and drawing code: - Base: axial (type 2) and radial (type 3) shading dictionaries emitted a literal "\n" from a single-quoted sprintf instead of a newline before endobj, producing malformed objects. Transparent-gradient soft-mask ExtGStates now record their SMask so they are reported as transparent, and the missing-pattern guard no longer leaves a partial XObject in the stream. - Raw: getRawEllipticalArc() no longer divides by zero when the vertical radius is 0 (now treated as a circle, per its docblock); the duplicated arc-point/Bezier computation is extracted into a helper. - Gradient: getAlpha() no longer emits a warning on an empty blend mode. - Style/Draw/Transform: minor cleanups (redundant return, dead assignment, corrected docblock).
1 parent 52aee12 commit 49bcd2a

10 files changed

Lines changed: 329 additions & 36 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.12.0
1+
2.13.0

src/Base.php

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,58 @@ public function getLastExtGStateID(): ?int
320320
return \array_key_last($this->extgstates);
321321
}
322322

323+
/**
324+
* Returns the resource names of the registered ExtGState objects that carry
325+
* actual (non-trivial) transparency: a fill or stroke alpha below 1, a blend
326+
* mode other than Normal, or a soft mask.
327+
*
328+
* The returned names match the tokens used by the `gs` operator in content
329+
* streams (for example "GS3"), so callers can detect whether a given content
330+
* stream actually triggers transparency (as opposed to merely resetting the
331+
* graphics state back to fully opaque).
332+
*
333+
* @return array<int, string> List of ExtGState resource names.
334+
*/
335+
public function getTransparencyExtGStateNames(): array
336+
{
337+
$names = [];
338+
foreach ($this->extgstates as $key => $ext) {
339+
if (!self::extGStateHasTransparency($ext['parms'])) {
340+
continue;
341+
}
342+
343+
$names[] = $ext['name'] !== '' ? $ext['name'] : 'GS' . $key;
344+
}
345+
346+
return $names;
347+
}
348+
349+
/**
350+
* Whether the given ExtGState parameters describe actual transparency:
351+
* a constant alpha below 1, a non-Normal blend mode, or a soft mask.
352+
*
353+
* @param array<string, int|float|bool|string> $parms ExtGState parameters.
354+
*/
355+
private static function extGStateHasTransparency(array $parms): bool
356+
{
357+
$belowOne = static fn(string $key): bool => (
358+
isset($parms[$key])
359+
&& \is_numeric($parms[$key])
360+
&& (float) $parms[$key] < 1.0
361+
);
362+
if ($belowOne('ca') || $belowOne('CA')) {
363+
return true;
364+
}
365+
366+
$blend = isset($parms['BM']) ? \ltrim((string) $parms['BM'], '/') : 'Normal';
367+
if ($blend !== '' && $blend !== 'Normal') {
368+
return true;
369+
}
370+
371+
$smask = isset($parms['SMask']) ? \ltrim((string) $parms['SMask'], '/') : 'None';
372+
return $smask !== '' && $smask !== 'None';
373+
}
374+
323375
/**
324376
* Get the PDF output string for ExtGState Resource Dictionary.
325377
*
@@ -591,7 +643,7 @@ protected function getOutPatternObj(array $grad, int $objref): string
591643

592644
if ($grad['type'] === 2) {
593645
$out .= \sprintf(
594-
' /Coords [%F %F %F %F] /Domain [0 1] /Function %d 0 R /Extend [true true] >>\n',
646+
' /Coords [%F %F %F %F] /Domain [0 1] /Function %d 0 R /Extend [true true] >>' . "\n",
595647
$coords[0] ?? 0.0,
596648
$coords[1] ?? 0.0,
597649
$coords[2] ?? 0.0,
@@ -602,7 +654,7 @@ protected function getOutPatternObj(array $grad, int $objref): string
602654
// x0, y0, r0, x1, y1, r1
603655
// the radius of the inner circle is 0
604656
$out .= \sprintf(
605-
' /Coords [%F %F 0 %F %F %F] /Domain [0 1] /Function %d 0 R /Extend [true true] >>\n',
657+
' /Coords [%F %F 0 %F %F %F] /Domain [0 1] /Function %d 0 R /Extend [true true] >>' . "\n",
606658
$coords[0] ?? 0.0,
607659
$coords[1] ?? 0.0,
608660
$coords[2] ?? 0.0,
@@ -686,6 +738,11 @@ public function getOutGradientShaders(int $pon): string
686738
}
687739

688740
if ($grad['transparency']) {
741+
if (!isset($this->gradients[$idgs]['pattern'])) {
742+
// no transparency pattern was generated: skip without emitting a partial object
743+
continue;
744+
}
745+
689746
$oid = ++$this->pon;
690747
$pwidth = $this->pagew * $this->kunit;
691748
$pheight = $this->pageh * $this->kunit;
@@ -701,10 +758,6 @@ public function getOutGradientShaders(int $pon): string
701758
}
702759
}
703760

704-
if (!isset($this->gradients[$idgs]['pattern'])) {
705-
continue;
706-
}
707-
708761
$stream = $this->encrypt->encryptString($stream, $oid);
709762
$out .=
710763
' /Length '
@@ -768,7 +821,11 @@ public function getOutGradientShaders(int $pon): string
768821
$this->extgstates[] = [
769822
'n' => $objext,
770823
'name' => 'TGS' . $idx,
771-
'parms' => [],
824+
// Record the soft mask so this entry is recognised as carrying
825+
// actual transparency (see getTransparencyExtGStateNames()).
826+
'parms' => [
827+
'SMask' => $objsm . ' 0 R',
828+
],
772829
];
773830
}
774831
}

src/Draw.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,8 +669,7 @@ public function getArrow(
669669
$sy1 = $posy1;
670670
if ($headmode > 0) {
671671
// calculate the stopping point for the arrow shaft
672-
$linewidth = 0;
673-
$linewidth = $style['lineWidth'] ?? (float) $this->getLastStyleProperty('lineWidth', $linewidth);
672+
$linewidth = $style['lineWidth'] ?? (float) $this->getLastStyleProperty('lineWidth', 0);
674673

675674
$sx1 = $posx1 + (($armsize - $linewidth) * \cos($dir_angle));
676675
$sy1 = $posy1 + (($armsize - $linewidth) * \sin($dir_angle));

src/Gradient.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,8 +941,8 @@ public function getAlpha(
941941
$nonstroking = $stroking;
942942
}
943943

944-
if ($bmv[0] === '/') {
945-
// remove trailing slash
944+
if ($bmv !== '' && $bmv[0] === '/') {
945+
// remove leading slash
946946
$bmv = \substr($bmv, 1);
947947
}
948948

src/Raw.php

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ public function getRawEllipticalArc(
250250
return '';
251251
}
252252

253+
if ($rdv === 0.0) {
254+
// a zero vertical radius means a circle: use the horizontal radius
255+
// (also avoids a division by zero when computing the arc angles)
256+
$rdv = $rdh;
257+
}
258+
253259
$bbox = [PHP_INT_MAX, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MIN];
254260
if ($pie) {
255261
$out .= $this->getRawPoint($posxc, $posyc); // center of the arc
@@ -264,24 +270,28 @@ public function getRawEllipticalArc(
264270
$ncv *= (2 * \abs($total_angle)) / self::MPI; // total arcs to draw
265271
$ncv = (int) (\round($ncv) + 1);
266272
$arcang = $total_angle / $ncv; // angle of each arc
273+
$pageh = $this->pageh;
267274
$posx0 = $posxc; // X center point in PDF coordinates
268-
$posy0 = $this->pageh - $posyc; // Y center point in PDF coordinates
275+
$posy0 = $pageh - $posyc; // Y center point in PDF coordinates
269276
$ang = $ags; // starting angle
270277
$alpha = \sin($arcang) * ((\sqrt(4 + (3 * (\tan($arcang / 2) ** 2))) - 1) / 3);
271278
$cos_xang = \cos($posxang);
272279
$sin_xang = \sin($posxang);
273-
$cos_ang = \cos($ang);
274-
$sin_ang = \sin($ang);
275-
// first arc point
276-
$px1 = $posx0 + ($rdh * $cos_xang * $cos_ang) - ($rdv * $sin_xang * $sin_ang);
277-
$py1 = $posy0 + ($rdh * $sin_xang * $cos_ang) + ($rdv * $cos_xang * $sin_ang);
278-
// first Bezier control point
279-
$qx1 = $alpha * ((-$rdh * $cos_xang * $sin_ang) - ($rdv * $sin_xang * $cos_ang));
280-
$qy1 = $alpha * ((-$rdh * $sin_xang * $sin_ang) + ($rdv * $cos_xang * $cos_ang));
280+
// first arc point and its Bezier control point
281+
[$px1, $py1, $qx1, $qy1] = $this->getRawEllipticalArcPoint(
282+
$posx0,
283+
$posy0,
284+
$rdh,
285+
$rdv,
286+
$cos_xang,
287+
$sin_xang,
288+
$alpha,
289+
$ang,
290+
);
281291
if ($pie) {
282-
$out .= $this->getRawLine($px1, $this->pageh - $py1); // line from center to arc starting point
292+
$out .= $this->getRawLine($px1, $pageh - $py1); // line from center to arc starting point
283293
} elseif ($startpoint) {
284-
$out .= $this->getRawPoint($px1, $this->pageh - $py1); // arc starting point
294+
$out .= $this->getRawPoint($px1, $pageh - $py1); // arc starting point
285295
}
286296

287297
// draw arcs
@@ -291,21 +301,24 @@ public function getRawEllipticalArc(
291301
$ang = $agf;
292302
}
293303

294-
$cos_ang = \cos($ang);
295-
$sin_ang = \sin($ang);
296-
// second arc point
297-
$px2 = $posx0 + ($rdh * $cos_xang * $cos_ang) - ($rdv * $sin_xang * $sin_ang);
298-
$py2 = $posy0 + ($rdh * $sin_xang * $cos_ang) + ($rdv * $cos_xang * $sin_ang);
299-
// second Bezier control point
300-
$qx2 = $alpha * ((-$rdh * $cos_xang * $sin_ang) - ($rdv * $sin_xang * $cos_ang));
301-
$qy2 = $alpha * ((-$rdh * $sin_xang * $sin_ang) + ($rdv * $cos_xang * $cos_ang));
304+
// second arc point and its Bezier control point
305+
[$px2, $py2, $qx2, $qy2] = $this->getRawEllipticalArcPoint(
306+
$posx0,
307+
$posy0,
308+
$rdh,
309+
$rdv,
310+
$cos_xang,
311+
$sin_xang,
312+
$alpha,
313+
$ang,
314+
);
302315
// draw arc
303316
$cx1 = $px1 + $qx1;
304-
$cy1 = $this->pageh - ($py1 + $qy1);
317+
$cy1 = $pageh - ($py1 + $qy1);
305318
$cx2 = $px2 - $qx2;
306-
$cy2 = $this->pageh - ($py2 - $qy2);
319+
$cy2 = $pageh - ($py2 - $qy2);
307320
$cx3 = $px2;
308-
$cy3 = $this->pageh - $py2;
321+
$cy3 = $pageh - $py2;
309322
$out .= $this->getRawCurve($cx1, $cy1, $cx2, $cy2, $cx3, $cy3);
310323
// get bounding box coordinates
311324
$bbox = [
@@ -335,6 +348,42 @@ public function getRawEllipticalArc(
335348
return $out;
336349
}
337350

351+
/**
352+
* Computes an elliptical arc point and its Bezier control-point offset.
353+
*
354+
* @param float $posx0 X center point in PDF coordinates.
355+
* @param float $posy0 Y center point in PDF coordinates.
356+
* @param float $rdh Horizontal radius.
357+
* @param float $rdv Vertical radius.
358+
* @param float $cosXang Cosine of the ellipse orientation angle.
359+
* @param float $sinXang Sine of the ellipse orientation angle.
360+
* @param float $alpha Bezier control-point scaling factor.
361+
* @param float $ang Arc angle in radians.
362+
*
363+
* @return array{float, float, float, float} Point and control-point offset: [px, py, qx, qy].
364+
*
365+
* @SuppressWarnings("PHPMD.ExcessiveParameterList")
366+
*/
367+
private function getRawEllipticalArcPoint(
368+
float $posx0,
369+
float $posy0,
370+
float $rdh,
371+
float $rdv,
372+
float $cosXang,
373+
float $sinXang,
374+
float $alpha,
375+
float $ang,
376+
): array {
377+
$cosAng = \cos($ang);
378+
$sinAng = \sin($ang);
379+
return [
380+
$posx0 + ($rdh * $cosXang * $cosAng) - ($rdv * $sinXang * $sinAng),
381+
$posy0 + ($rdh * $sinXang * $cosAng) + ($rdv * $cosXang * $sinAng),
382+
$alpha * ((-$rdh * $cosXang * $sinAng) - ($rdv * $sinXang * $cosAng)),
383+
$alpha * ((-$rdh * $sinXang * $sinAng) + ($rdv * $cosXang * $cosAng)),
384+
];
385+
}
386+
338387
/**
339388
* Returns the angle in radiants between two vectors with the same origin point.
340389
* Angles are counted counter-clock wise.

src/Style.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,6 @@ protected function getLineModeCmd(array $style = []): string
390390
}
391391

392392
$out .= \sprintf('[%s] %F d' . "\n", \implode(' ', $dash), $style['dashPhase']);
393-
return $out;
394393
}
395394

396395
return $out;

src/Transform.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ public function getCtmProduct(array $tma, array $tmb): array
381381

382382
/**
383383
* Converts the number in degrees to the radian equivalent.
384-
* We use this instead of $this->degToRad to avoid precision problems with hhvm.
384+
* We use this instead of the deg2rad() function to avoid precision problems with hhvm.
385385
*
386386
* @param float $deg Angular value in degrees.
387387
*

0 commit comments

Comments
 (0)