Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/pdfx4-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
#
# A veraPDF policy check over the features of mPDF's PDF/X-4 output. veraPDF has no PDF/X validation
# profile, so this is NOT a conformance verdict - see utils/verapdf/pdfx4_policy.php.

name: "PDF/X-4 Policy"

on:
pull_request:
push:
branches:
- "master"
- "development"
- "test"
- "gravitypdf"

jobs:

policy:

name: "veraPDF policy"

runs-on: ubuntu-latest

steps:
- name: "Checkout"
uses: "actions/checkout@v7"

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.5"
extensions: "mbstring, gd, bcmath, bz2"
tools: composer:v2
ini-values: error_reporting=-1

- name: "Install Java"
uses: "actions/setup-java@v5"
with:
distribution: "temurin"
java-version: "21"

- name: "Install veraPDF"
run: |
curl -fsSL -o verapdf-installer.zip https://software.verapdf.org/rel/verapdf-installer.zip
unzip -q verapdf-installer.zip -d "$RUNNER_TEMP"
cat > "$RUNNER_TEMP/auto-install.xml" <<XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<AutomatedInstallation langpack="eng">
<com.izforge.izpack.panels.htmlhello.HTMLHelloPanel id="welcome"/>
<com.izforge.izpack.panels.target.TargetPanel id="install_dir">
<installpath>$RUNNER_TEMP/verapdf</installpath>
</com.izforge.izpack.panels.target.TargetPanel>
<com.izforge.izpack.panels.packs.PacksPanel id="sdk_pack_select">
<pack index="0" name="veraPDF Mac and *nix Scripts" selected="true"/>
<pack index="1" name="veraPDF Validation model" selected="true"/>
<pack index="2" name="veraPDF Documentation" selected="false"/>
<pack index="3" name="veraPDF Sample Plugins" selected="false"/>
</com.izforge.izpack.panels.packs.PacksPanel>
<com.izforge.izpack.panels.install.InstallPanel id="install"/>
<com.izforge.izpack.panels.finish.FinishPanel id="finish"/>
</AutomatedInstallation>
XML
java -jar "$RUNNER_TEMP"/verapdf-greenfield-*/verapdf-izpack-installer-*.jar "$RUNNER_TEMP/auto-install.xml"
"$RUNNER_TEMP/verapdf/verapdf" --version
echo "VERAPDF_BIN=$RUNNER_TEMP/verapdf/verapdf" >> "$GITHUB_ENV"

- name: "Install dependencies"
run: "composer install --no-interaction --no-progress"

- name: "Policy check"
run: php utils/verapdf/pdfx4_policy.php "$VERAPDF_BIN"

- name: "Upload the documents"
if: failure()
uses: "actions/upload-artifact@v4"
with:
name: "pdfx4-policy-documents"
path: "tmp/verapdf"
if-no-files-found: "ignore"
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"arabicjoining:update": "@php utils/arabicjoining_update.php",
"emoji:update": "@php utils/emoji_update.php",
"fontcache:update": "@php utils/fontcache_update.php",
"grayprofile:update": "@php utils/grayprofile_update.php",
"otlanguages:update": "@php utils/otlanguages_update.php",
"otldump:update": "@php utils/otldump_update.php",
"shaping:update": "@php utils/shaping_update.php",
Expand Down
Binary file added data/iccprofiles/Gray_sRGB_TRC.icc
Binary file not shown.
18 changes: 12 additions & 6 deletions data/iccprofiles/NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ licence — they are aggregated data assets, not part of mPDF's source code, and
are redistributed here under the terms their publishers grant. Do not alter these
files; ship them byte-for-byte.

To use a different output condition (e.g. your house print profile), set the
`ICCProfile` config key to the path of your own `.icc` file — it overrides the
bundled default and nothing here is embedded.
To use a different output condition (e.g. your house print profile, or an RGB
output intent for PDF/X-4), set the `ICCProfile` config key to the path of your
own `.icc` file — it overrides the bundled default. A PDF/X output intent must be
an output device (`prtr`) profile.

---

## sRGB_IEC61966-2-1.icc

- **Colour space:** RGB (used as the PDF/A and default output-intent profile).
- **Colour space:** RGB, device class `mntr` (display). Used as the PDF/A
output-intent profile. Also the ICC-based colour space RGB is written in where
a PDF/X-4 document prints to a CMYK or grey output intent, which permits no
DeviceRGB. Being a display profile, it is not a PDF/X output intent.
- **Publisher / copyright:** International Color Consortium.
- **Embedded copyright string:** `Copyright International Color Consortium, 2009`.
- **Terms:** Distributed by the ICC as a freely redistributable reference
Expand All @@ -24,8 +28,10 @@ bundled default and nothing here is embedded.
## SWOP2006_Coated3v2.icc

- **Colour space:** CMYK, device class `prtr` (printer), ICC v2.
A CMYK output intent: point `ICCProfile` at it, with `restrictColorSpace` set
to 3, to write a CMYK PDF/A document. Nothing embeds it unless asked.
The output intent a **PDF/X-4** document embeds where it names no
`ICCProfile`, identified by the CGATS TR 003 characterisation it is built on.
Point `ICCProfile` at it, with `restrictColorSpace` set to 3, to write a CMYK
PDF/A document.
Represents U.S. web-coated (SWOP) publication printing, 2006, grade 3 coated.
- **Publisher:** IDEAlliance, with permission of X-Rite, Inc.
- **Copyright:** X-Rite, Inc.
Expand Down
12 changes: 0 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,6 @@ parameters:
count: 2
path: src/Image/ImageProcessor.php

-
message: '#^Variable \$info might not be defined\.$#'
identifier: variable.undefined
count: 1
path: src/Image/ImageProcessor.php

-
message: '#^Binary operation "\*" between 0\.3333333333333333 and string results in an error\.$#'
identifier: binaryOp.invalid
Expand Down Expand Up @@ -216,12 +210,6 @@ parameters:
count: 1
path: src/Image/Svg.php

-
message: '#^Variable \$color_final might not be defined\.$#'
identifier: variable.undefined
count: 1
path: src/Image/Svg.php

-
message: '#^Variable \$inners might not be defined\.$#'
identifier: variable.undefined
Expand Down
57 changes: 57 additions & 0 deletions src/Color/ColorConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,63 @@ public function colAtoString($c)
return '';
}

/**
* A shading function gives every stop of a gradient the same number of components, so the stops are
* written in one colour space: grey where every stop is grey, CMYK where any is CMYK, keeping the
* inks the document names, and RGB otherwise
*
* @param string[] $colors Binary color strings
*
* @return string 'Gray', 'RGB' or 'CMYK'
*/
public function gradientColorSpace(array $colors)
{
$space = 'Gray';

foreach ($colors as $c) {
if ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
return 'CMYK';
}

if ($c[0] != static::MODE_GRAYSCALE) {
$space = 'RGB';
}
}

return $space;
}

/**
* @param string $c Binary color string
* @param string $space The colour space gradientColorSpace() settled on for the stops
*
* @return string The colour's components in that space, as a shading function writes them
*/
public function gradientComponents($c, $space)
{
if ($c[0] == static::MODE_CMYK || $c[0] == static::MODE_CMYKA) {
return sprintf('%.3F %.3F %.3F %.3F', ord($c[1]) / 100, ord($c[2]) / 100, ord($c[3]) / 100, ord($c[4]) / 100);
}

if ($c[0] == static::MODE_GRAYSCALE) {
$gray = ord($c[1]) / 255;

if ($space === 'CMYK') {
return sprintf('0.000 0.000 0.000 %.3F', 1 - $gray);
}

return $space === 'RGB' ? sprintf('%.3F %.3F %.3F', $gray, $gray, $gray) : sprintf('%.3F', $gray);
}

if ($space === 'CMYK') {
$cmyk = $this->colorModeConverter->rgb2cmyk([static::MODE_RGB, ord($c[1]), ord($c[2]), ord($c[3])]);

return sprintf('%.3F %.3F %.3F %.3F', $cmyk[1] / 100, $cmyk[2] / 100, $cmyk[3] / 100, $cmyk[4] / 100);
}

return sprintf('%.3F %.3F %.3F', ord($c[1]) / 255, ord($c[2]) / 255, ord($c[3]) / 255);
}

/**
* @param string $color
* @param string[] $PDFAXwarnings
Expand Down
11 changes: 6 additions & 5 deletions src/Color/ColorModeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public function cmyk2gray($c)
}

/**
* Black is the black ink alone. Painting it as all four inks at once - registration black - is 400%
* total area coverage, past what any press allows, and would be a jump away from every near-black
* value: rgb(1, 1, 1) is already K 99.6% and nothing else.
*
* @param float[] $c
*
* @return float[]
Expand All @@ -44,13 +48,10 @@ public function rgb2cmyk($c)

if ($min == 1) {
if ($c[0] == 5) {
return [6, 100, 100, 100, 100, $c[4]];
return [6, 0, 0, 0, 100, $c[4]];
}

return [4, 100, 100, 100, 100];
// For K-Black
//if ($c[0]==5) { return array (6,0,0,0,100, $c[4]); }
//else { return array (4,0,0,0,100); }
return [4, 0, 0, 0, 100];
}

$K = $min;
Expand Down
52 changes: 49 additions & 3 deletions src/Color/ColorSpaceRestrictor.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,20 @@ private function restrictSpotColorSpace($c, &$PDFAXwarnings = [])
*/
private function restrictRgbColorSpace($c, $color, &$PDFAXwarnings = [])
{
if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) {
// PDF/X-4 keeps RGB as it is: printing to an RGB condition it is DeviceRGB, and printing to any
// other it is written in the ICC-based sRGB colour space, so that the press converts the colours
// of the content and of the images together - see Mpdf::writesCalibratedRgb()
if ($this->mpdf->isPdfx4()) {
if ($this->mpdf->pdfxRgbIntent()) {
return $c;
}

$gray = $this->neutralToGray($c);

return $gray === null ? $c : $gray;
}

if (($this->mpdf->PDFX && !$this->mpdf->pdfxRgbIntent()) || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) {
if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) {
$PDFAXwarnings[] = "RGB color specified '" . $color . "' (converted to CMYK)";
}
Expand All @@ -136,8 +149,8 @@ private function restrictRgbColorSpace($c, $color, &$PDFAXwarnings = [])
*/
private function restrictCmykColorSpace($c, $color, &$PDFAXwarnings = [])
{
if ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) {
if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) {
if (($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) || $this->mpdf->pdfxConvertsCmyk()) {
if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) {
$PDFAXwarnings[] = "CMYK color specified '" . $color . "' (converted to RGB)";
}
$c = $this->colorModeConverter->cmyk2rgb($c);
Expand All @@ -159,6 +172,11 @@ private function restrictCmykColorSpace($c, $color, &$PDFAXwarnings = [])
*/
private function restrictRgbaColorSpace($c, $color, &$PDFAXwarnings = [])
{
// PDF/X-4 keeps the transparency, which the colour space it settles on carries
if ($this->mpdf->isPdfx4()) {
return $this->restrictRgbColorSpace($c, $color, $PDFAXwarnings);
}

if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) {
if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) {
$PDFAXwarnings[] = "RGB color with transparency specified '" . $color . "' (converted to CMYK" . ($this->mpdf->transparencyAllowed() ? ')' : ' without transparency)');
Expand All @@ -179,6 +197,30 @@ private function restrictRgbaColorSpace($c, $color, &$PDFAXwarnings = [])
return $c;
}

/**
* Tagging black sRGB would have the press make it out of all four inks, which fringes text where the
* plates are a hair out of register. A neutral colour goes to DeviceGray instead, which ISO 15930-7
* permits where the output condition is CMYK or grey, and which such a condition takes as its black
* separation. A colour that is neutral but translucent keeps its RGB, since DeviceGray carries no
* alpha for mPDF to put it back into.
*
* @param float[] $c A colour in RGB or RGBA
*
* @return float[]|null That colour in DeviceGray, or null where it is not neutral and opaque
*/
private function neutralToGray($c)
{
if ($c[1] != $c[2] || $c[2] != $c[3]) {
return null;
}

if ($c[0] == 5 && $c[4] < 100) {
return null;
}

return [1, $c[1]];
}

/**
* @param mixed $c
* @param string $color
Expand All @@ -188,6 +230,10 @@ private function restrictRgbaColorSpace($c, $color, &$PDFAXwarnings = [])
*/
private function restrictCmykaColorSpace($c, $color, &$PDFAXwarnings = [])
{
if ($this->mpdf->isPdfx4()) {
return $this->restrictCmykColorSpace($c, $color, $PDFAXwarnings);
}

if ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) {
if (!$this->mpdf->PDFAauto) {
$PDFAXwarnings[] = "CMYK color with transparency specified '" . $color . "' (converted to RGB" . ($this->mpdf->transparencyAllowed() ? ')' : ' without transparency)');
Expand Down
16 changes: 12 additions & 4 deletions src/Config/ConfigVariables.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,19 @@ public function __construct()
// 3 - allow CMYK / SPOT COLOR / Grayscale [convert RGB->CMYK]
'restrictColorSpace' => 0,

// PDFX/1-a Compliant files
// true=Forces compliance with PDFX-1a spec
// PDF/X Compliant files
// true=Forces compliance with the PDF/X version PDFXversion names
// Cannot be used with 'restrictColorSpace' (i.e. no RGB)
'PDFX' => false,
// Overrides warnings making changes when possible to force PDFX1-a compliance
// Overrides warnings making changes when possible to force PDF/X compliance
'PDFXauto' => false,
// The PDF/X version to write
// 4 PDF/X-4 (ISO 15930-7, PDF 1.6). The general choice for print, and the version print workflows
// and the Ghent Workgroup specifications expect. Keeps transparency and layers, and prints to
// the CMYK, RGB or grey condition ICCProfile names (SWOP where it names none)
// 1a PDF/X-1a:2003 (ISO 15930-4, PDF 1.4). For printers and publications that accept only it:
// CMYK and spot colour only, and translucent content is drawn opaque
'PDFXversion' => '4',

// PDF/A Compliant files
// true=Forces compliance with the PDF/A part and level PDFAversion names
Expand All @@ -183,7 +190,8 @@ public function __construct()

// Colour profile OutputIntent
// sRGB_IEC61966-2-1 (=default if blank and PDFA), or other added .icc profile
// Must be CMYK for PDFX, or appropriate type for PDFA(RGB or CMYK)
// Must be CMYK for PDF/X-1a, or appropriate type for PDFA(RGB or CMYK) or PDF/X-4
// PDF/X-4 embeds SWOP2006_Coated3v2 (CMYK) where blank. PDF/X takes a printer (prtr) profile only
'ICCProfile' => '',

'spotColors' => [],
Expand Down
5 changes: 3 additions & 2 deletions src/Fonts/Color/ColorFormats.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public static function drawable(array $fontFormats)
/**
* Whether a document may draw colour fonts in colour: not where PDF/A-1b or PDF/X-1a forbid the
* transparency every colour format is drawn with, nor where restrictColorSpace holds the document to
* colours an RGB bitmap is not.
* colours an RGB bitmap is not. PDF/X-4 allows the transparency, and its RGB is written calibrated
* where the output intent is not RGB - see Type3FontWriter.
*
* Asked whenever it matters rather than once, since each of those settings can be changed on the
* document after the fonts it starts with are added.
Expand All @@ -53,7 +54,7 @@ public static function drawable(array $fontFormats)
*/
public static function inColor(Mpdf $mpdf)
{
return !$mpdf->PDFA && !$mpdf->PDFX && !$mpdf->restrictColorSpace;
return !$mpdf->PDFA && !$mpdf->isPdfx1a() && !$mpdf->restrictColorSpace;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Fonts/Color/FillsInColour.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private function filled(array $colour, $fill, GlyphResources $resources)
$operators[] = $resources->alpha($alpha);
}
if ($rgb !== null) {
$operators[] = vsprintf('%.3F %.3F %.3F rg', $rgb);
$operators[] = $resources->rgb($rgb);
}

return $operators ? 'q ' . implode(' ', $operators) . "\n" . $fill . "\nQ\n" : $fill . "\n";
Expand Down
Loading
Loading