new NumericType($value); $rgb = new DeviceColourSpace(ColourSpaceFamily::DeviceRGB); $gray = new DeviceColourSpace(ColourSpaceFamily::DeviceGray); $colour = static fn (int|float $r, int|float $g, int|float $b): ColourComponents => new ColourComponents($rgb, $n($r), $n($g), $n($b)); $box = static fn (int|float $width, int|float $height): Rectangle => new Rectangle($n(0), $n(0), $n($width), $n($height)); $packer = new X\SamplePacker(); $dimensions = new ImageDimensions(16, 16); $maskValues = $indexedValues = $grayValues = $rgbValues = $spriteValues = $blueValues = $preblendedValues = $opacityValues = []; $matte = new X\MatteColour($colour(1, 1, 1)); // These are normalized PDF samples without PNG/JPEG or file readers. // Each pixel supplies its components in order; SamplePacker aligns // every row to a byte and preserves depths of 1/2/4/8/16 bits. for ($y = 0; $y < 16; ++$y) { for ($x = 0; $x < 16; ++$x) { $maskValues[] = ($x + $y) % 2; $indexedValues[] = intdiv($x, 4); $grayValues[] = $x; array_push($rgbValues, (int) round($x / 15 * 65535), (int) round($y / 15 * 65535), 32768); array_push($spriteValues, ...($x > 3 && $x < 12 && $y > 3 && $y < 12 ? [240, 70, 85] : [255, 255, 255])); array_push($blueValues, 20, 130, 230); $alpha = $x / 15; $opacityValues[] = (int) round($alpha * 255); $preblended = $matte->preblend($colour(20 / 255, 130 / 255, 230 / 255), $n($alpha)); foreach ($preblended->numbers as $component) { $preblendedValues[] = (int) round($component->value * 255); } } } $indexed = new Indexed($rgb, new IntegerType(3), new HexadecimalString("\xEC\x45\x59\xF2\xAC\x35\x34\xAB\x87\x46\x72\xCE")); $mask = new X\ImageMask($packer->pack($dimensions, new SampleLayout(DeviceColorSpace::Gray, 1), $maskValues)); $imageIndexed = new X\ImageXObject($packer->pack($dimensions, new SampleLayout($indexed, 2), $indexedValues)); $imageGray = new X\ImageXObject($packer->pack($dimensions, new SampleLayout($gray, 4), $grayValues)); $imageRgb = new X\ImageXObject($packer->pack($dimensions, new SampleLayout($rgb, 16), $rgbValues)); // The key compares integers BEFORE Decode: white here means (255,255,255). $key = new X\ColourKeyMask(...array_fill(0, 3, new NumberInterval(new IntegerType(255), new IntegerType(255)))); $sprite = new X\ImageXObject($packer->pack($dimensions, new SampleLayout($rgb), $spriteValues), mask: $key); // Without Matte, the mask has its own dimensions and is also mapped to the // unit square. With Matte, dimensions must match: c' = alpha*c + // (1-alpha)*matte. The bytes above are actually preblended with white. $smallAlpha = new X\SoftMaskImage($packer->pack(new ImageDimensions(4, 1), new SampleLayout(DeviceColorSpace::Gray), [0, 85, 170, 255])); $soft = new X\ImageXObject($packer->pack($dimensions, new SampleLayout($rgb), $blueValues), softMask: $smallAlpha); $matteAlpha = new X\SoftMaskImage($packer->pack($dimensions, new SampleLayout(DeviceColorSpace::Gray), $opacityValues), $matte); $preblendedImage = new X\ImageXObject($packer->pack($dimensions, new SampleLayout($rgb), $preblendedValues), softMask: $matteAlpha); $images = ['Stencil1' => $mask, 'Indexed2' => $imageIndexed, 'Gray4' => $imageGray, 'Rgb16' => $imageRgb, 'ColourKey' => $sprite, 'SoftMask' => $soft, 'Matte' => $preblendedImage]; // ExtGState contains no drawing. Rectangles then select mode and // alpha; the Form has its own box, matrix and resources. $multiply = new G\GraphicsStateParameters(new Entry(new NameType('BM'), (new G\BlendModes(G\BlendMode::Multiply))->value()), new Entry(new NameType('ca'), $n(0.75))); $screen = new G\GraphicsStateParameters(new Entry(new NameType('BM'), new NameType('Screen')), new Entry(new NameType('ca'), $n(0.75))); $groupResources = new ResourceDictionary(new Entry(new NameType('ExtGState'), new DictionaryType(new Entry(new NameType('Multiply'), new IndirectReference($multiply)), new Entry(new NameType('Screen'), new IndirectReference($screen))))); $groupDrawing = static fn (NameType $mode): ContentStreams => new ContentStreams(new Op\SetNonstrokingRGBColor($n(0.94), $n(0.24), $n(0.32)), new Op\AppendRectangle($n(0), $n(0), $n(0.72), $n(0.85)), new Op\FillPathNonzero(), new Op\SetGraphicsStateParameters($mode), new Op\SetNonstrokingRGBColor($n(0.2), $n(0.45), $n(0.95)), new Op\AppendRectangle($n(0.28), $n(0.15), $n(0.72), $n(0.85)), new Op\FillPathNonzero()); $group = new X\TransparencyGroup(new G\BlendingColourSpace($rgb), new BooleanType(true)); $formMultiply = new X\FormXObject($groupDrawing(new NameType('Multiply')), $box(1, 1), $groupResources, group: $group); $formScreen = new X\FormXObject($groupDrawing(new NameType('Screen')), $box(1, 1), $groupResources, group: $group); // The luminosity mask is another Form in DeviceGray. A gradient of // black/white rectangles supplies luminosity here without an image or codec. $maskOperators = []; for ($step = 0; $step < 16; ++$step) { array_push($maskOperators, new Op\SetNonstrokingGray($n($step / 15)), new Op\AppendRectangle($n($step / 16), $n(0), $n(1 / 16), $n(1)), new Op\FillPathNonzero()); } $maskGroup = new X\FormXObject(new ContentStreams(...$maskOperators), $box(1, 1), new ResourceDictionary(), group: new X\TransparencyGroup(new G\BlendingColourSpace($gray), new BooleanType(true))); $luminosity = new G\SoftMask(G\SoftMaskSubtype::Luminosity, $maskGroup); $maskedState = new G\GraphicsStateParameters(new Entry(new NameType('SMask'), $luminosity)); $formMasked = new X\FormXObject(new ContentStreams(new Op\SetGraphicsStateParameters(new NameType('Mask')), new Op\SetNonstrokingRGBColor($n(0.14), $n(0.62), $n(0.48)), new Op\AppendRectangle($n(0), $n(0), $n(1), $n(1)), new Op\FillPathNonzero()), $box(1, 1), new ResourceDictionary(new Entry(new NameType('ExtGState'), new DictionaryType(new Entry(new NameType('Mask'), new IndirectReference($maskedState)))))); // Halftones are output PDF objects, not textures simulated by the // core. These actual small arrays remain available in the resources. $thresholds = new H\RectangularHalftone(new H\ThresholdRectangle(new ImageDimensions(2, 2), new H\ThresholdArray(8, 4, [0, 85, 170, 255]))); $thresholds16 = new H\SixteenBitHalftone(new H\ThresholdRectangle(new ImageDimensions(2, 1), new H\ThresholdArray(16, 2, [0x1234, 0xFFFF]))); $halftoneState = new G\GraphicsStateParameters(new Entry(new NameType('HT'), new IndirectReference($thresholds))); $halftone16State = new G\GraphicsStateParameters(new Entry(new NameType('HT'), new IndirectReference($thresholds16))); // Groups are declared once in OCProperties. Their states are distinct // from dictionaries: here Visible is ON and Hidden is OFF. $visible = new OC\OptionalContentGroup(new TextString('Couche visible'), usage: new OC\GroupUsage(new OC\EventUsage(OC\UsageEvent::View, new BooleanType(true)))); $hidden = new OC\OptionalContentGroup(new TextString('Couche masquee')); $visibleAssociation = new OC\OptionalContentAssociation(new NameType('Visible'), $visible); $hiddenAssociation = new OC\OptionalContentAssociation(new NameType('Hidden'), $hidden); $membership = new OC\OptionalContentMembership(new OC\GroupCollection($visible, $hidden), OC\VisibilityPolicy::AnyOn, new OC\VisibilityExpression(OC\VisibilityOperator::And, $visible, new OC\VisibilityExpression(OC\VisibilityOperator::Not, $hidden))); $membershipAssociation = new OC\OptionalContentAssociation(new NameType('Condition'), $membership); $properties = new OC\OptionalContentProperties(new OC\GroupCollection($visible, $hidden), new OC\Configuration(off: new OC\GroupCollection($hidden), order: new OC\GroupOrder(new OC\OrderBranch(new TextString('Demonstration'), $visible, $hidden)))); $forms = ['Multiply' => $formMultiply, 'Screen' => $formScreen, 'Luminosity' => $formMasked]; $resources = new ResourceDictionary( new Entry(new NameType('Font'), new DictionaryType(new Entry(new NameType('F1'), new IndirectReference(new Type1Font(StandardFont::Helvetica))))), new Entry(new NameType('XObject'), new DictionaryType(...array_map(static fn (string $name): Entry => new Entry(new NameType($name), new IndirectReference($images[$name])), array_keys($images)), ...array_map(static fn (string $name): Entry => new Entry(new NameType($name), new IndirectReference($forms[$name])), array_keys($forms)))), new Entry(new NameType('Properties'), new DictionaryType($visibleAssociation->propertyEntry(), $hiddenAssociation->propertyEntry(), $membershipAssociation->propertyEntry())), new Entry(new NameType('ExtGState'), new DictionaryType(new Entry(new NameType('Halftone8'), new IndirectReference($halftoneState)), new Entry(new NameType('Halftone16'), new IndirectReference($halftone16State)))), ); /** @return list */ $text = static fn (string $label, int $x, int $y, int $size = 11): array => [new Op\SetNonstrokingRGBColor($n(0.12), $n(0.16), $n(0.22)), new Op\BeginTextObject(), new Op\SetTextFontAndSize(new NameType('F1'), $n($size)), new Op\MoveTextPosition($n($x), $n($y)), new Op\ShowText(new LiteralString($label)), new Op\EndTextObject()]; /** @return list */ $invoke = static fn (string $name, int $x, int $y, int $width, int $height): array => [new Op\SaveGraphicsState(), new Op\ConcatenateMatrix(new Matrix($n($width), $n(0), $n(0), $n($height), $n($x), $n($y))), new Op\SetNonstrokingRGBColor($n(0.68), $n(0.22), $n(0.72)), new Op\InvokeXObject(new NameType($name)), new Op\RestoreGraphicsState()]; $first = [...$text('Images PDF natives', 42, 742, 25), ...$text('Samples, Decode et masques : aucun fichier image externe.', 42, 712)]; $labels = ['Stencil1' => '1 bit - stencil, couleur courante', 'Indexed2' => '2 bits - palette Indexed', 'Gray4' => '4 bits - DeviceGray', 'Rgb16' => '16 bits - DeviceRGB', 'ColourKey' => '8 bits - blanc masque par cle', 'SoftMask' => 'SMask - dimensions propres', 'Matte' => 'SMask + Matte - preblend blanc']; foreach (array_keys($images) as $index => $name) { $x = 42 + $index % 3 * 176; $y = 532 - intdiv($index, 3) * 176; array_push($first, new Op\SetNonstrokingRGBColor($n(0.94), $n(0.96), $n(0.98)), new Op\AppendRectangle($n($x), $n($y), $n(152), $n(132)), new Op\FillPathNonzero(), ...$invoke($name, $x, $y, 152, 132), ...$text($labels[$name], $x, $y - 19, 10)); } array_push($first, ...$text('Chaque ligne est alignee a l octet ; les valeurs sont intercalees par pixel.', 42, 105), ...$text('La geometrie de placement est independante des dimensions en samples.', 42, 84)); $second = [...$text('Formes, transparence et couches', 42, 742, 23), ...$text('Les relations sont explicites et verifiees dans le graphe du document.', 42, 712)]; foreach (['Multiply' => 'Groupe isole - Multiply / 0.75', 'Screen' => 'Groupe isole - Screen / 0.75', 'Luminosity' => 'SMask - groupe de luminosite'] as $name => $label) { $index = array_search($name, array_keys($forms), true); $x = 42 + $index * 176; array_push($second, ...$invoke($name, $x, 506, 152, 158), ...$text($label, $x, 486, 10)); } $second = [...$second, ...$text('OptionalContent : deux groupes et une expression VE', 42, 428, 16), $visibleAssociation->begin(), new Op\SetNonstrokingRGBColor($n(0.15), $n(0.6), $n(0.47)), new Op\AppendRectangle($n(42), $n(290), $n(240), $n(108)), new Op\FillPathNonzero(), ...$text('Visible : ON', 61, 335, 18), new Op\EndMarkedContent()]; $second = [...$second, $hiddenAssociation->begin(), new Op\SetNonstrokingRGBColor($n(0.95), $n(0.25), $n(0.32)), new Op\AppendRectangle($n(312), $n(290), $n(240), $n(108)), new Op\FillPathNonzero(), ...$text('Hidden : OFF', 329, 335, 18), new Op\EndMarkedContent()]; $second = [...$second, ...$text('La zone de droite est cachee par OCProperties / D / OFF.', 42, 260), $membershipAssociation->begin(), ...$text('VE : Visible AND NOT Hidden => contenu visible', 42, 222, 15), new Op\EndMarkedContent(), ...$text('Le panneau des couches peut changer ces etats dans un lecteur PDF.', 42, 188)]; array_push($second, ...$text('Halftones : les tables 8/16 bits sont embarquees dans ExtGState.', 42, 116), ...$text('Leur aspect depend du dispositif de sortie, qui effectue le screening.', 42, 94)); $pageBox = $box(612, 792); $pages = (new PageTreeNode())->addKid(new PageObject($pageBox, $resources, new StreamObject(new ContentStreams(...$first))))->addKid(new PageObject($pageBox, $resources, new StreamObject(new ContentStreams(...$second)))); $catalog = new DocumentCatalog($pages, $properties->catalogEntry()); $file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog)); $path = $argv[1] ?? __DIR__ . '/native-graphics.pdf'; $file->writeTo($path); echo 'PDF written to ' . $path . PHP_EOL;