dompdf/LICENSE.LGPL 0000644 00000057440 15024772104 0007573 0 ustar 00 GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. dompdf/composer.json 0000644 00000002435 15024772104 0010545 0 ustar 00 {
"name": "dompdf/dompdf",
"type": "library",
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"license": "LGPL-2.1",
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"autoload-dev": {
"psr-4": {
"Dompdf\\Tests\\": "tests/"
}
},
"require": {
"php": "^7.1 || ^8.0",
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"phenx/php-font-lib": ">=0.5.4 <1.0.0",
"phenx/php-svg-lib": ">=0.3.3 <1.0.0"
},
"require-dev": {
"ext-json": "*",
"ext-zip": "*",
"phpunit/phpunit": "^7.5 || ^8 || ^9",
"squizlabs/php_codesniffer": "^3.5",
"mockery/mockery": "^1.3"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-imagick": "Improves image processing performance",
"ext-gmagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
}
}
dompdf/src/Options.php 0000644 00000076311 15024772104 0010762 0 ustar 00 ["rules" => []],
"http://" => ["rules" => []],
"https://" => ["rules" => []]
];
/**
* @var string
*/
private $logOutputFile;
/**
* Styles targeted to this media type are applied to the document.
* This is on top of the media types that are always applied:
* all, static, visual, bitmap, paged, dompdf
*
* @var string
*/
private $defaultMediaType = "screen";
/**
* The default paper size.
*
* North America standard is "letter"; other countries generally "a4"
* @see \Dompdf\Adapter\CPDF::PAPER_SIZES for valid sizes
*
* @var string|float[]
*/
private $defaultPaperSize = "letter";
/**
* The default paper orientation.
*
* The orientation of the page (portrait or landscape).
*
* @var string
*/
private $defaultPaperOrientation = "portrait";
/**
* The default font family
*
* Used if no suitable fonts can be found. This must exist in the font folder.
*
* @var string
*/
private $defaultFont = "serif";
/**
* Image DPI setting
*
* This setting determines the default DPI setting for images and fonts. The
* DPI may be overridden for inline images by explicitly setting the
* image's width & height style attributes (i.e. if the image's native
* width is 600 pixels and you specify the image's width as 72 points,
* the image will have a DPI of 600 in the rendered PDF. The DPI of
* background images can not be overridden and is controlled entirely
* via this parameter.
*
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
* If a size in html is given as px (or without unit as image size),
* this tells the corresponding size in pt at 72 DPI.
* This adjusts the relative sizes to be similar to the rendering of the
* html page in a reference browser.
*
* In pdf, always 1 pt = 1/72 inch
*
* @var int
*/
private $dpi = 96;
/**
* A ratio applied to the fonts height to be more like browsers' line height
*
* @var float
*/
private $fontHeightRatio = 1.1;
/**
* Enable embedded PHP
*
* If this setting is set to true then DOMPDF will automatically evaluate
* embedded PHP contained within tags.
*
* ==== IMPORTANT ====
* Enabling this for documents you do not trust (e.g. arbitrary remote html
* pages) is a security risk. Embedded scripts are run with the same level of
* system access available to dompdf. Set this option to false (recommended)
* if you wish to process untrusted documents.
*
* This setting may increase the risk of system exploit. Do not change
* this settings without understanding the consequences. Additional
* documentation is available on the dompdf wiki at:
* https://github.com/dompdf/dompdf/wiki
*
* @var bool
*/
private $isPhpEnabled = false;
/**
* Enable remote file access
*
* If this setting is set to true, DOMPDF will access remote sites for
* images and CSS files as required.
*
* ==== IMPORTANT ====
* This can be a security risk, in particular in combination with isPhpEnabled and
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
* This allows anonymous users to download legally doubtful internet content which on
* tracing back appears to being downloaded by your server, or allows malicious php code
* in remote html pages to be executed by your server with your account privileges.
*
* This setting may increase the risk of system exploit. Do not change
* this settings without understanding the consequences. Additional
* documentation is available on the dompdf wiki at:
* https://github.com/dompdf/dompdf/wiki
*
* @var bool
*/
private $isRemoteEnabled = false;
/**
* Enable inline JavaScript
*
* If this setting is set to true then DOMPDF will automatically insert
* JavaScript code contained within
* tags as written into the PDF.
*
* NOTE: This is PDF-based JavaScript to be executed by the PDF viewer,
* not browser-based JavaScript executed by Dompdf.
*
* @var bool
*/
private $isJavascriptEnabled = true;
/**
* Use the HTML5 Lib parser
*
* @deprecated
* @var bool
*/
private $isHtml5ParserEnabled = true;
/**
* Whether to enable font subsetting or not.
*
* @var bool
*/
private $isFontSubsettingEnabled = true;
/**
* @var bool
*/
private $debugPng = false;
/**
* @var bool
*/
private $debugKeepTemp = false;
/**
* @var bool
*/
private $debugCss = false;
/**
* @var bool
*/
private $debugLayout = false;
/**
* @var bool
*/
private $debugLayoutLines = true;
/**
* @var bool
*/
private $debugLayoutBlocks = true;
/**
* @var bool
*/
private $debugLayoutInline = true;
/**
* @var bool
*/
private $debugLayoutPaddingBox = true;
/**
* The PDF rendering backend to use
*
* Valid settings are 'PDFLib', 'CPDF', 'GD', and 'auto'. 'auto' will
* look for PDFLib and use it if found, or if not it will fall back on
* CPDF. 'GD' renders PDFs to graphic files. {@link Dompdf\CanvasFactory}
* ultimately determines which rendering class to instantiate
* based on this setting.
*
* @var string
*/
private $pdfBackend = "CPDF";
/**
* PDFlib license key
*
* If you are using a licensed, commercial version of PDFlib, specify
* your license key here. If you are using PDFlib-Lite or are evaluating
* the commercial version of PDFlib, comment out this setting.
*
* @link http://www.pdflib.com
*
* If pdflib present in web server and auto or selected explicitly above,
* a real license code must exist!
*
* @var string
*/
private $pdflibLicense = "";
/**
* HTTP context created with stream_context_create()
* Will be used for file_get_contents
*
* @link https://www.php.net/manual/context.php
*
* @var resource
*/
private $httpContext;
/**
* @param array $attributes
*/
public function __construct(array $attributes = null)
{
$rootDir = realpath(__DIR__ . "/../");
$this->setChroot(array($rootDir));
$this->setRootDir($rootDir);
$this->setTempDir(sys_get_temp_dir());
$this->setFontDir($rootDir . "/lib/fonts");
$this->setFontCache($this->getFontDir());
$ver = "";
$versionFile = realpath(__DIR__ . '/../VERSION');
if (($version = file_get_contents($versionFile)) !== false) {
$version = trim($version);
if ($version !== '$Format:<%h>$') {
$ver = "/$version";
}
}
$this->setHttpContext([
"http" => [
"follow_location" => false,
"user_agent" => "Dompdf$ver https://github.com/dompdf/dompdf"
]
]);
$this->setAllowedProtocols(["file://", "http://", "https://"]);
if (null !== $attributes) {
$this->set($attributes);
}
}
/**
* @param array|string $attributes
* @param null|mixed $value
* @return $this
*/
public function set($attributes, $value = null)
{
if (!is_array($attributes)) {
$attributes = [$attributes => $value];
}
foreach ($attributes as $key => $value) {
if ($key === 'tempDir' || $key === 'temp_dir') {
$this->setTempDir($value);
} elseif ($key === 'fontDir' || $key === 'font_dir') {
$this->setFontDir($value);
} elseif ($key === 'fontCache' || $key === 'font_cache') {
$this->setFontCache($value);
} elseif ($key === 'chroot') {
$this->setChroot($value);
} elseif ($key === 'allowedProtocols') {
$this->setAllowedProtocols($value);
} elseif ($key === 'logOutputFile' || $key === 'log_output_file') {
$this->setLogOutputFile($value);
} elseif ($key === 'defaultMediaType' || $key === 'default_media_type') {
$this->setDefaultMediaType($value);
} elseif ($key === 'defaultPaperSize' || $key === 'default_paper_size') {
$this->setDefaultPaperSize($value);
} elseif ($key === 'defaultPaperOrientation' || $key === 'default_paper_orientation') {
$this->setDefaultPaperOrientation($value);
} elseif ($key === 'defaultFont' || $key === 'default_font') {
$this->setDefaultFont($value);
} elseif ($key === 'dpi') {
$this->setDpi($value);
} elseif ($key === 'fontHeightRatio' || $key === 'font_height_ratio') {
$this->setFontHeightRatio($value);
} elseif ($key === 'isPhpEnabled' || $key === 'is_php_enabled' || $key === 'enable_php') {
$this->setIsPhpEnabled($value);
} elseif ($key === 'isRemoteEnabled' || $key === 'is_remote_enabled' || $key === 'enable_remote') {
$this->setIsRemoteEnabled($value);
} elseif ($key === 'isJavascriptEnabled' || $key === 'is_javascript_enabled' || $key === 'enable_javascript') {
$this->setIsJavascriptEnabled($value);
} elseif ($key === 'isHtml5ParserEnabled' || $key === 'is_html5_parser_enabled' || $key === 'enable_html5_parser') {
$this->setIsHtml5ParserEnabled($value);
} elseif ($key === 'isFontSubsettingEnabled' || $key === 'is_font_subsetting_enabled' || $key === 'enable_font_subsetting') {
$this->setIsFontSubsettingEnabled($value);
} elseif ($key === 'debugPng' || $key === 'debug_png') {
$this->setDebugPng($value);
} elseif ($key === 'debugKeepTemp' || $key === 'debug_keep_temp') {
$this->setDebugKeepTemp($value);
} elseif ($key === 'debugCss' || $key === 'debug_css') {
$this->setDebugCss($value);
} elseif ($key === 'debugLayout' || $key === 'debug_layout') {
$this->setDebugLayout($value);
} elseif ($key === 'debugLayoutLines' || $key === 'debug_layout_lines') {
$this->setDebugLayoutLines($value);
} elseif ($key === 'debugLayoutBlocks' || $key === 'debug_layout_blocks') {
$this->setDebugLayoutBlocks($value);
} elseif ($key === 'debugLayoutInline' || $key === 'debug_layout_inline') {
$this->setDebugLayoutInline($value);
} elseif ($key === 'debugLayoutPaddingBox' || $key === 'debug_layout_padding_box') {
$this->setDebugLayoutPaddingBox($value);
} elseif ($key === 'pdfBackend' || $key === 'pdf_backend') {
$this->setPdfBackend($value);
} elseif ($key === 'pdflibLicense' || $key === 'pdflib_license') {
$this->setPdflibLicense($value);
} elseif ($key === 'httpContext' || $key === 'http_context') {
$this->setHttpContext($value);
}
}
return $this;
}
/**
* @param string $key
* @return mixed
*/
public function get($key)
{
if ($key === 'tempDir' || $key === 'temp_dir') {
return $this->getTempDir();
} elseif ($key === 'fontDir' || $key === 'font_dir') {
return $this->getFontDir();
} elseif ($key === 'fontCache' || $key === 'font_cache') {
return $this->getFontCache();
} elseif ($key === 'chroot') {
return $this->getChroot();
} elseif ($key === 'allowedProtocols') {
return $this->getAllowedProtocols();
} elseif ($key === 'logOutputFile' || $key === 'log_output_file') {
return $this->getLogOutputFile();
} elseif ($key === 'defaultMediaType' || $key === 'default_media_type') {
return $this->getDefaultMediaType();
} elseif ($key === 'defaultPaperSize' || $key === 'default_paper_size') {
return $this->getDefaultPaperSize();
} elseif ($key === 'defaultPaperOrientation' || $key === 'default_paper_orientation') {
return $this->getDefaultPaperOrientation();
} elseif ($key === 'defaultFont' || $key === 'default_font') {
return $this->getDefaultFont();
} elseif ($key === 'dpi') {
return $this->getDpi();
} elseif ($key === 'fontHeightRatio' || $key === 'font_height_ratio') {
return $this->getFontHeightRatio();
} elseif ($key === 'isPhpEnabled' || $key === 'is_php_enabled' || $key === 'enable_php') {
return $this->getIsPhpEnabled();
} elseif ($key === 'isRemoteEnabled' || $key === 'is_remote_enabled' || $key === 'enable_remote') {
return $this->getIsRemoteEnabled();
} elseif ($key === 'isJavascriptEnabled' || $key === 'is_javascript_enabled' || $key === 'enable_javascript') {
return $this->getIsJavascriptEnabled();
} elseif ($key === 'isHtml5ParserEnabled' || $key === 'is_html5_parser_enabled' || $key === 'enable_html5_parser') {
return $this->getIsHtml5ParserEnabled();
} elseif ($key === 'isFontSubsettingEnabled' || $key === 'is_font_subsetting_enabled' || $key === 'enable_font_subsetting') {
return $this->getIsFontSubsettingEnabled();
} elseif ($key === 'debugPng' || $key === 'debug_png') {
return $this->getDebugPng();
} elseif ($key === 'debugKeepTemp' || $key === 'debug_keep_temp') {
return $this->getDebugKeepTemp();
} elseif ($key === 'debugCss' || $key === 'debug_css') {
return $this->getDebugCss();
} elseif ($key === 'debugLayout' || $key === 'debug_layout') {
return $this->getDebugLayout();
} elseif ($key === 'debugLayoutLines' || $key === 'debug_layout_lines') {
return $this->getDebugLayoutLines();
} elseif ($key === 'debugLayoutBlocks' || $key === 'debug_layout_blocks') {
return $this->getDebugLayoutBlocks();
} elseif ($key === 'debugLayoutInline' || $key === 'debug_layout_inline') {
return $this->getDebugLayoutInline();
} elseif ($key === 'debugLayoutPaddingBox' || $key === 'debug_layout_padding_box') {
return $this->getDebugLayoutPaddingBox();
} elseif ($key === 'pdfBackend' || $key === 'pdf_backend') {
return $this->getPdfBackend();
} elseif ($key === 'pdflibLicense' || $key === 'pdflib_license') {
return $this->getPdflibLicense();
} elseif ($key === 'httpContext' || $key === 'http_context') {
return $this->getHttpContext();
}
return null;
}
/**
* @param string $pdfBackend
* @return $this
*/
public function setPdfBackend($pdfBackend)
{
$this->pdfBackend = $pdfBackend;
return $this;
}
/**
* @return string
*/
public function getPdfBackend()
{
return $this->pdfBackend;
}
/**
* @param string $pdflibLicense
* @return $this
*/
public function setPdflibLicense($pdflibLicense)
{
$this->pdflibLicense = $pdflibLicense;
return $this;
}
/**
* @return string
*/
public function getPdflibLicense()
{
return $this->pdflibLicense;
}
/**
* @param array|string $chroot
* @return $this
*/
public function setChroot($chroot, $delimiter = ',')
{
if (is_string($chroot)) {
$this->chroot = explode($delimiter, $chroot);
} elseif (is_array($chroot)) {
$this->chroot = $chroot;
}
return $this;
}
/**
* @return array
*/
public function getAllowedProtocols()
{
return $this->allowedProtocols;
}
/**
* @param array $allowedProtocols The protocols to allow, as an array
* formatted as ["protocol://" => ["rules" => [callable]], ...]
* or ["protocol://", ...]
*
* @return $this
*/
public function setAllowedProtocols(array $allowedProtocols)
{
$protocols = [];
foreach ($allowedProtocols as $protocol => $config) {
if (is_string($protocol)) {
$protocols[$protocol] = [];
if (is_array($config)) {
$protocols[$protocol] = $config;
}
} elseif (is_string($config)) {
$protocols[$config] = [];
}
}
$this->allowedProtocols = [];
foreach ($protocols as $protocol => $config) {
$this->addAllowedProtocol($protocol, ...($config["rules"] ?? []));
}
return $this;
}
/**
* Adds a new protocol to the allowed protocols collection
*
* @param string $protocol The scheme to add (e.g. "http://")
* @param callable $rule A callable that validates the protocol
* @return $this
*/
public function addAllowedProtocol(string $protocol, callable ...$rules)
{
$protocol = strtolower($protocol);
if (empty($rules)) {
$rules = [];
switch ($protocol) {
case "file://":
$rules[] = [$this, "validateLocalUri"];
break;
case "http://":
case "https://":
$rules[] = [$this, "validateRemoteUri"];
break;
case "phar://":
$rules[] = [$this, "validatePharUri"];
break;
}
}
$this->allowedProtocols[$protocol] = ["rules" => $rules];
return $this;
}
/**
* @return array
*/
public function getChroot()
{
$chroot = [];
if (is_array($this->chroot)) {
$chroot = $this->chroot;
}
return $chroot;
}
/**
* @param boolean $debugCss
* @return $this
*/
public function setDebugCss($debugCss)
{
$this->debugCss = $debugCss;
return $this;
}
/**
* @return boolean
*/
public function getDebugCss()
{
return $this->debugCss;
}
/**
* @param boolean $debugKeepTemp
* @return $this
*/
public function setDebugKeepTemp($debugKeepTemp)
{
$this->debugKeepTemp = $debugKeepTemp;
return $this;
}
/**
* @return boolean
*/
public function getDebugKeepTemp()
{
return $this->debugKeepTemp;
}
/**
* @param boolean $debugLayout
* @return $this
*/
public function setDebugLayout($debugLayout)
{
$this->debugLayout = $debugLayout;
return $this;
}
/**
* @return boolean
*/
public function getDebugLayout()
{
return $this->debugLayout;
}
/**
* @param boolean $debugLayoutBlocks
* @return $this
*/
public function setDebugLayoutBlocks($debugLayoutBlocks)
{
$this->debugLayoutBlocks = $debugLayoutBlocks;
return $this;
}
/**
* @return boolean
*/
public function getDebugLayoutBlocks()
{
return $this->debugLayoutBlocks;
}
/**
* @param boolean $debugLayoutInline
* @return $this
*/
public function setDebugLayoutInline($debugLayoutInline)
{
$this->debugLayoutInline = $debugLayoutInline;
return $this;
}
/**
* @return boolean
*/
public function getDebugLayoutInline()
{
return $this->debugLayoutInline;
}
/**
* @param boolean $debugLayoutLines
* @return $this
*/
public function setDebugLayoutLines($debugLayoutLines)
{
$this->debugLayoutLines = $debugLayoutLines;
return $this;
}
/**
* @return boolean
*/
public function getDebugLayoutLines()
{
return $this->debugLayoutLines;
}
/**
* @param boolean $debugLayoutPaddingBox
* @return $this
*/
public function setDebugLayoutPaddingBox($debugLayoutPaddingBox)
{
$this->debugLayoutPaddingBox = $debugLayoutPaddingBox;
return $this;
}
/**
* @return boolean
*/
public function getDebugLayoutPaddingBox()
{
return $this->debugLayoutPaddingBox;
}
/**
* @param boolean $debugPng
* @return $this
*/
public function setDebugPng($debugPng)
{
$this->debugPng = $debugPng;
return $this;
}
/**
* @return boolean
*/
public function getDebugPng()
{
return $this->debugPng;
}
/**
* @param string $defaultFont
* @return $this
*/
public function setDefaultFont($defaultFont)
{
if (!($defaultFont === null || trim($defaultFont) === "")) {
$this->defaultFont = $defaultFont;
} else {
$this->defaultFont = "serif";
}
return $this;
}
/**
* @return string
*/
public function getDefaultFont()
{
return $this->defaultFont;
}
/**
* @param string $defaultMediaType
* @return $this
*/
public function setDefaultMediaType($defaultMediaType)
{
$this->defaultMediaType = $defaultMediaType;
return $this;
}
/**
* @return string
*/
public function getDefaultMediaType()
{
return $this->defaultMediaType;
}
/**
* @param string|float[] $defaultPaperSize
* @return $this
*/
public function setDefaultPaperSize($defaultPaperSize): self
{
$this->defaultPaperSize = $defaultPaperSize;
return $this;
}
/**
* @param string $defaultPaperOrientation
* @return $this
*/
public function setDefaultPaperOrientation(string $defaultPaperOrientation): self
{
$this->defaultPaperOrientation = $defaultPaperOrientation;
return $this;
}
/**
* @return string|float[]
*/
public function getDefaultPaperSize()
{
return $this->defaultPaperSize;
}
/**
* @return string
*/
public function getDefaultPaperOrientation(): string
{
return $this->defaultPaperOrientation;
}
/**
* @param int $dpi
* @return $this
*/
public function setDpi($dpi)
{
$this->dpi = $dpi;
return $this;
}
/**
* @return int
*/
public function getDpi()
{
return $this->dpi;
}
/**
* @param string $fontCache
* @return $this
*/
public function setFontCache($fontCache)
{
$this->fontCache = $fontCache;
return $this;
}
/**
* @return string
*/
public function getFontCache()
{
return $this->fontCache;
}
/**
* @param string $fontDir
* @return $this
*/
public function setFontDir($fontDir)
{
$this->fontDir = $fontDir;
return $this;
}
/**
* @return string
*/
public function getFontDir()
{
return $this->fontDir;
}
/**
* @param float $fontHeightRatio
* @return $this
*/
public function setFontHeightRatio($fontHeightRatio)
{
$this->fontHeightRatio = $fontHeightRatio;
return $this;
}
/**
* @return float
*/
public function getFontHeightRatio()
{
return $this->fontHeightRatio;
}
/**
* @param boolean $isFontSubsettingEnabled
* @return $this
*/
public function setIsFontSubsettingEnabled($isFontSubsettingEnabled)
{
$this->isFontSubsettingEnabled = $isFontSubsettingEnabled;
return $this;
}
/**
* @return boolean
*/
public function getIsFontSubsettingEnabled()
{
return $this->isFontSubsettingEnabled;
}
/**
* @return boolean
*/
public function isFontSubsettingEnabled()
{
return $this->getIsFontSubsettingEnabled();
}
/**
* @deprecated
* @param boolean $isHtml5ParserEnabled
* @return $this
*/
public function setIsHtml5ParserEnabled($isHtml5ParserEnabled)
{
$this->isHtml5ParserEnabled = $isHtml5ParserEnabled;
return $this;
}
/**
* @deprecated
* @return boolean
*/
public function getIsHtml5ParserEnabled()
{
return $this->isHtml5ParserEnabled;
}
/**
* @deprecated
* @return boolean
*/
public function isHtml5ParserEnabled()
{
return $this->getIsHtml5ParserEnabled();
}
/**
* @param boolean $isJavascriptEnabled
* @return $this
*/
public function setIsJavascriptEnabled($isJavascriptEnabled)
{
$this->isJavascriptEnabled = $isJavascriptEnabled;
return $this;
}
/**
* @return boolean
*/
public function getIsJavascriptEnabled()
{
return $this->isJavascriptEnabled;
}
/**
* @return boolean
*/
public function isJavascriptEnabled()
{
return $this->getIsJavascriptEnabled();
}
/**
* @param boolean $isPhpEnabled
* @return $this
*/
public function setIsPhpEnabled($isPhpEnabled)
{
$this->isPhpEnabled = $isPhpEnabled;
return $this;
}
/**
* @return boolean
*/
public function getIsPhpEnabled()
{
return $this->isPhpEnabled;
}
/**
* @return boolean
*/
public function isPhpEnabled()
{
return $this->getIsPhpEnabled();
}
/**
* @param boolean $isRemoteEnabled
* @return $this
*/
public function setIsRemoteEnabled($isRemoteEnabled)
{
$this->isRemoteEnabled = $isRemoteEnabled;
return $this;
}
/**
* @return boolean
*/
public function getIsRemoteEnabled()
{
return $this->isRemoteEnabled;
}
/**
* @return boolean
*/
public function isRemoteEnabled()
{
return $this->getIsRemoteEnabled();
}
/**
* @param string $logOutputFile
* @return $this
*/
public function setLogOutputFile($logOutputFile)
{
$this->logOutputFile = $logOutputFile;
return $this;
}
/**
* @return string
*/
public function getLogOutputFile()
{
return $this->logOutputFile;
}
/**
* @param string $tempDir
* @return $this
*/
public function setTempDir($tempDir)
{
$this->tempDir = $tempDir;
return $this;
}
/**
* @return string
*/
public function getTempDir()
{
return $this->tempDir;
}
/**
* @param string $rootDir
* @return $this
*/
public function setRootDir($rootDir)
{
$this->rootDir = $rootDir;
return $this;
}
/**
* @return string
*/
public function getRootDir()
{
return $this->rootDir;
}
/**
* Sets the HTTP context
*
* @param resource|array $httpContext
* @return $this
*/
public function setHttpContext($httpContext)
{
$this->httpContext = is_array($httpContext) ? stream_context_create($httpContext) : $httpContext;
return $this;
}
/**
* Returns the HTTP context
*
* @return resource
*/
public function getHttpContext()
{
return $this->httpContext;
}
public function validateLocalUri(string $uri)
{
if ($uri === null || strlen($uri) === 0) {
return [false, "The URI must not be empty."];
}
$realfile = realpath(str_replace("file://", "", $uri));
$dirs = $this->chroot;
$dirs[] = $this->rootDir;
$chrootValid = false;
foreach ($dirs as $chrootPath) {
$chrootPath = realpath($chrootPath);
if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
$chrootValid = true;
break;
}
}
if ($chrootValid !== true) {
return [false, "Permission denied. The file could not be found under the paths specified by Options::chroot."];
}
if (!$realfile) {
return [false, "File not found."];
}
return [true, null];
}
public function validatePharUri(string $uri)
{
if ($uri === null || strlen($uri) === 0) {
return [false, "The URI must not be empty."];
}
$file = substr(substr($uri, 0, strpos($uri, ".phar") + 5), 7);
return $this->validateLocalUri($file);
}
public function validateRemoteUri(string $uri)
{
if ($uri === null || strlen($uri) === 0) {
return [false, "The URI must not be empty."];
}
if (!$this->isRemoteEnabled) {
return [false, "Remote file requested, but remote file download is disabled."];
}
return [true, null];
}
}
dompdf/src/Positioner/Fixed.php 0000644 00000007054 15024772104 0012517 0 ustar 00 get_reflower() instanceof Block) {
parent::position($frame);
} else {
// Legacy positioning logic for image and table frames
// TODO: Resolve dimensions, margins, and offsets similar to the
// block case in the reflowers and use the simplified logic above
$style = $frame->get_style();
$root = $frame->get_root();
$initialcb = $root->get_containing_block();
$initialcb_style = $root->get_style();
$p = $frame->find_block_parent();
if ($p) {
$p->add_line();
}
// Compute the margins of the @page style
$margin_top = (float)$initialcb_style->length_in_pt($initialcb_style->margin_top, $initialcb["h"]);
$margin_right = (float)$initialcb_style->length_in_pt($initialcb_style->margin_right, $initialcb["w"]);
$margin_bottom = (float)$initialcb_style->length_in_pt($initialcb_style->margin_bottom, $initialcb["h"]);
$margin_left = (float)$initialcb_style->length_in_pt($initialcb_style->margin_left, $initialcb["w"]);
// The needed computed style of the element
$height = (float)$style->length_in_pt($style->get_specified("height"), $initialcb["h"]);
$width = (float)$style->length_in_pt($style->get_specified("width"), $initialcb["w"]);
$top = $style->length_in_pt($style->get_specified("top"), $initialcb["h"]);
$right = $style->length_in_pt($style->get_specified("right"), $initialcb["w"]);
$bottom = $style->length_in_pt($style->get_specified("bottom"), $initialcb["h"]);
$left = $style->length_in_pt($style->get_specified("left"), $initialcb["w"]);
$y = $margin_top;
if (isset($top)) {
$y = (float)$top + $margin_top;
if ($top === "auto") {
$y = $margin_top;
if (isset($bottom) && $bottom !== "auto") {
$y = $initialcb["h"] - $bottom - $margin_bottom;
if ($frame->is_auto_height()) {
$y -= $height;
} else {
$y -= $frame->get_margin_height();
}
}
}
}
$x = $margin_left;
if (isset($left)) {
$x = (float)$left + $margin_left;
if ($left === "auto") {
$x = $margin_left;
if (isset($right) && $right !== "auto") {
$x = $initialcb["w"] - $right - $margin_right;
if ($frame->is_auto_width()) {
$x -= $width;
} else {
$x -= $frame->get_margin_width();
}
}
}
}
$frame->set_position($x, $y);
foreach ($frame->get_children() as $child) {
$child->set_position($x, $y);
}
}
}
}
dompdf/src/Positioner/AbstractPositioner.php 0000644 00000002163 15024772104 0015273 0 ustar 00 get_position();
if (!$ignore_self) {
$frame->set_position($x + $offset_x, $y + $offset_y);
}
foreach ($frame->get_children() as $child) {
$child->move($offset_x, $offset_y);
}
}
}
dompdf/src/Positioner/ListBullet.php 0000644 00000002464 15024772104 0013543 0 ustar 00 get_parent();
$style = $parent->get_style();
$cbw = $parent->get_containing_block("w");
$margin_left = (float) $style->length_in_pt($style->margin_left, $cbw);
$border_edge = $parent->get_position("x") + $margin_left;
// This includes the marker indentation
$x = $border_edge - $frame->get_margin_width();
// The marker is later vertically aligned with the corresponding line
// box and its vertical position is fine-tuned in the renderer
$p = $frame->find_block_parent();
$y = $p->get_current_line_box()->y;
$frame->set_position($x, $y);
}
}
dompdf/src/Positioner/TableCell.php 0000644 00000001241 15024772104 0013277 0 ustar 00 get_cellmap();
$frame->set_position($cellmap->get_frame_position($frame));
}
}
dompdf/src/Positioner/Absolute.php 0000644 00000011074 15024772104 0013233 0 ustar 00 get_reflower() instanceof Block) {
$style = $frame->get_style();
[$cbx, $cby, $cbw, $cbh] = $frame->get_containing_block();
// If the `top` value is `auto`, the frame will be repositioned
// after its height has been resolved
$left = (float) $style->length_in_pt($style->left, $cbw);
$top = (float) $style->length_in_pt($style->top, $cbh);
$frame->set_position($cbx + $left, $cby + $top);
} else {
// Legacy positioning logic for image and table frames
// TODO: Resolve dimensions, margins, and offsets similar to the
// block case in the reflowers and use the simplified logic above
$style = $frame->get_style();
$block_parent = $frame->find_block_parent();
$current_line = $block_parent->get_current_line_box();
list($x, $y, $w, $h) = $frame->get_containing_block();
$inflow_x = $block_parent->get_content_box()["x"] + $current_line->left + $current_line->w;
$inflow_y = $current_line->y;
$top = $style->length_in_pt($style->top, $h);
$right = $style->length_in_pt($style->right, $w);
$bottom = $style->length_in_pt($style->bottom, $h);
$left = $style->length_in_pt($style->left, $w);
list($width, $height) = [$frame->get_margin_width(), $frame->get_margin_height()];
$orig_width = $style->get_specified("width");
$orig_height = $style->get_specified("height");
/****************************
*
* Width auto:
* ____________| left=auto | left=fixed |
* right=auto | A | B |
* right=fixed | C | D |
*
* Width fixed:
* ____________| left=auto | left=fixed |
* right=auto | E | F |
* right=fixed | G | H |
*****************************/
if ($left === "auto") {
if ($right === "auto") {
// A or E - Keep the frame at the same position
$x = $inflow_x;
} else {
if ($orig_width === "auto") {
// C
$x += $w - $width - $right;
} else {
// G
$x += $w - $width - $right;
}
}
} else {
if ($right === "auto") {
// B or F
$x += (float)$left;
} else {
if ($orig_width === "auto") {
// D - TODO change width
$x += (float)$left;
} else {
// H - Everything is fixed: left + width win
$x += (float)$left;
}
}
}
// The same vertically
if ($top === "auto") {
if ($bottom === "auto") {
// A or E - Keep the frame at the same position
$y = $inflow_y;
} else {
if ($orig_height === "auto") {
// C
$y += (float)$h - $height - (float)$bottom;
} else {
// G
$y += (float)$h - $height - (float)$bottom;
}
}
} else {
if ($bottom === "auto") {
// B or F
$y += (float)$top;
} else {
if ($orig_height === "auto") {
// D - TODO change height
$y += (float)$top;
} else {
// H - Everything is fixed: top + height win
$y += (float)$top;
}
}
}
$frame->set_position($x, $y);
}
}
}
dompdf/src/Positioner/TableRow.php 0000644 00000001353 15024772104 0013173 0 ustar 00 get_containing_block();
$p = $frame->get_prev_sibling();
if ($p) {
$y = $p->get_position("y") + $p->get_margin_height();
} else {
$y = $cb["y"];
}
$frame->set_position($cb["x"], $y);
}
}
dompdf/src/Positioner/NullPositioner.php 0000644 00000000757 15024772104 0014451 0 ustar 00 find_block_parent();
if (!$block) {
throw new Exception("No block-level parent found. Not good.");
}
$cb = $frame->get_containing_block();
$line = $block->get_current_line_box();
if (!$frame->is_text_node() && !($frame instanceof InlineFrameDecorator)) {
// Atomic inline boxes and replaced inline elements
// (inline-block, inline-table, img etc.)
$width = $frame->get_margin_width();
$available_width = $cb["w"] - $line->left - $line->w - $line->right;
if (Helpers::lengthGreater($width, $available_width)) {
$block->add_line();
$line = $block->get_current_line_box();
}
}
$frame->set_position($cb["x"] + $line->w, $line->y);
}
}
dompdf/src/Positioner/Block.php 0000644 00000001535 15024772104 0012510 0 ustar 00 get_style();
$cb = $frame->get_containing_block();
$p = $frame->find_block_parent();
if ($p) {
$float = $style->float;
if (!$float || $float === "none") {
$p->add_line(true);
}
$y = $p->get_current_line_box()->y;
} else {
$y = $cb["y"];
}
$x = $cb["x"];
$frame->set_position($x, $y);
}
}
dompdf/src/Renderer/Image.php 0000644 00000005104 15024772104 0012107 0 ustar 00 get_style();
$border_box = $frame->get_border_box();
$this->_set_opacity($frame->get_opacity($style->opacity));
// Render background & borders
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
$content_box = $frame->get_content_box();
[$x, $y, $w, $h] = $content_box;
$src = $frame->get_image_url();
$alt = null;
if (Cache::is_broken($src) &&
$alt = $frame->get_node()->getAttribute("alt")
) {
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$this->_canvas->text(
$x,
$y,
$alt,
$font,
$size,
$style->color,
$word_spacing,
$letter_spacing
);
} elseif ($w > 0 && $h > 0) {
if ($style->has_border_radius()) {
[$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $content_box);
$this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl);
}
$this->_canvas->image($src, $x, $y, $w, $h, $style->image_resolution);
if ($style->has_border_radius()) {
$this->_canvas->clipping_end();
}
}
if ($msg = $frame->get_image_msg()) {
$parts = preg_split("/\s*\n\s*/", $msg);
$font = $style->font_family;
$height = 10;
$_y = $alt ? $y + $h - count($parts) * $height : $y;
foreach ($parts as $i => $_part) {
$this->_canvas->text($x, $_y + $i * $height, $_part, $font, $height * 0.8, [0.5, 0.5, 0.5]);
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
$this->debugBlockLayout($frame, "blue");
}
}
dompdf/src/Renderer/ListBullet.php 0000644 00000015674 15024772104 0013165 0 ustar 00 get_parent();
$style = $frame->get_style();
$this->_set_opacity($frame->get_opacity($style->opacity));
// Don't render bullets twice if the list item was split
if ($li->is_split_off) {
return;
}
$font_family = $style->font_family;
$font_size = $style->font_size;
$baseline = $this->_canvas->get_font_baseline($font_family, $font_size);
// Handle list-style-image
// If list style image is requested but missing, fall back to predefined types
if ($frame instanceof ListBulletImage && !Cache::is_broken($img = $frame->get_image_url())) {
[$x, $y] = $frame->get_position();
$w = $frame->get_width();
$h = $frame->get_height();
$y += $baseline - $h;
$this->_canvas->image($img, $x, $y, $w, $h);
} else {
$bullet_style = $style->list_style_type;
switch ($bullet_style) {
default:
case "disc":
case "circle":
[$x, $y] = $frame->get_position();
$offset = $font_size * ListBulletFrameDecorator::BULLET_OFFSET;
$r = ($font_size * ListBulletFrameDecorator::BULLET_SIZE) / 2;
$x += $r;
$y += $baseline - $r - $offset;
$o = $font_size * ListBulletFrameDecorator::BULLET_THICKNESS;
$this->_canvas->circle($x, $y, $r, $style->color, $o, null, $bullet_style !== "circle");
break;
case "square":
[$x, $y] = $frame->get_position();
$offset = $font_size * ListBulletFrameDecorator::BULLET_OFFSET;
$w = $font_size * ListBulletFrameDecorator::BULLET_SIZE;
$y += $baseline - $w - $offset;
$this->_canvas->filled_rectangle($x, $y, $w, $w, $style->color);
break;
case "decimal-leading-zero":
case "decimal":
case "lower-alpha":
case "lower-latin":
case "lower-roman":
case "lower-greek":
case "upper-alpha":
case "upper-latin":
case "upper-roman":
case "1": // HTML 4.0 compatibility
case "a":
case "i":
case "A":
case "I":
$pad = null;
if ($bullet_style === "decimal-leading-zero") {
$pad = strlen($li->get_parent()->get_node()->getAttribute("dompdf-children-count"));
}
$node = $frame->get_node();
if (!$node->hasAttribute("dompdf-counter")) {
return;
}
$index = $node->getAttribute("dompdf-counter");
$text = $this->make_counter($index, $bullet_style, $pad);
if (trim($text) === "") {
return;
}
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$text_width = $this->_dompdf->getFontMetrics()->getTextWidth($text, $font_family, $font_size, $word_spacing, $letter_spacing);
[$x, $y] = $frame->get_position();
// Correct for static frame width applied by positioner
$x += $frame->get_width() - $text_width;
$this->_canvas->text($x, $y, $text,
$font_family, $font_size,
$style->color, $word_spacing, $letter_spacing);
case "none":
break;
}
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
}
}
dompdf/src/Renderer/TableCell.php 0000644 00000014373 15024772104 0012724 0 ustar 00 get_style();
if (trim($frame->get_node()->nodeValue) === "" && $style->empty_cells === "hide") {
return;
}
$this->_set_opacity($frame->get_opacity($style->opacity));
$border_box = $frame->get_border_box();
$table = Table::find_parent_table($frame);
if ($table->get_style()->border_collapse !== "collapse") {
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
} else {
// The collapsed case is slightly complicated...
$cells = $table->get_cellmap()->get_spanned_cells($frame);
if (is_null($cells)) {
return;
}
// Render the background to the padding box, as the cells are
// rendered individually one after another, and we don't want the
// background to overlap an adjacent border
$padding_box = $frame->get_padding_box();
$this->_render_background($frame, $padding_box);
$this->_render_collapsed_border($frame, $table);
// FIXME: Outline should be drawn over other cells
$this->_render_outline($frame, $border_box);
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
// $this->debugBlockLayout($frame, "red", false);
}
/**
* @param Frame $frame
* @param Table $table
*/
protected function _render_collapsed_border(Frame $frame, Table $table): void
{
$cellmap = $table->get_cellmap();
$cells = $cellmap->get_spanned_cells($frame);
$num_rows = $cellmap->get_num_rows();
$num_cols = $cellmap->get_num_cols();
[$table_x, $table_y] = $table->get_position();
// Determine the top row spanned by this cell
$i = $cells["rows"][0];
$top_row = $cellmap->get_row($i);
// Determine if this cell borders on the bottom of the table. If so,
// then we draw its bottom border. Otherwise the next row down will
// draw its top border instead.
if (in_array($num_rows - 1, $cells["rows"])) {
$draw_bottom = true;
$bottom_row = $cellmap->get_row($num_rows - 1);
} else {
$draw_bottom = false;
}
// Draw the horizontal borders
foreach ($cells["columns"] as $j) {
$bp = $cellmap->get_border_properties($i, $j);
$col = $cellmap->get_column($j);
$x = $table_x + $col["x"] - $bp["left"]["width"] / 2;
$y = $table_y + $top_row["y"] - $bp["top"]["width"] / 2;
$w = $col["used-width"] + ($bp["left"]["width"] + $bp["right"]["width"]) / 2;
if ($bp["top"]["width"] > 0) {
$widths = [
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
];
$method = "_border_" . $bp["top"]["style"];
$this->$method($x, $y, $w, $bp["top"]["color"], $widths, "top", "square");
}
if ($draw_bottom) {
$bp = $cellmap->get_border_properties($num_rows - 1, $j);
if ($bp["bottom"]["width"] <= 0) {
continue;
}
$widths = [
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
];
$y = $table_y + $bottom_row["y"] + $bottom_row["height"] + $bp["bottom"]["width"] / 2;
$method = "_border_" . $bp["bottom"]["style"];
$this->$method($x, $y, $w, $bp["bottom"]["color"], $widths, "bottom", "square");
}
}
$j = $cells["columns"][0];
$left_col = $cellmap->get_column($j);
if (in_array($num_cols - 1, $cells["columns"])) {
$draw_right = true;
$right_col = $cellmap->get_column($num_cols - 1);
} else {
$draw_right = false;
}
// Draw the vertical borders
foreach ($cells["rows"] as $i) {
$bp = $cellmap->get_border_properties($i, $j);
$row = $cellmap->get_row($i);
$x = $table_x + $left_col["x"] - $bp["left"]["width"] / 2;
$y = $table_y + $row["y"] - $bp["top"]["width"] / 2;
$h = $row["height"] + ($bp["top"]["width"] + $bp["bottom"]["width"]) / 2;
if ($bp["left"]["width"] > 0) {
$widths = [
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
];
$method = "_border_" . $bp["left"]["style"];
$this->$method($x, $y, $h, $bp["left"]["color"], $widths, "left", "square");
}
if ($draw_right) {
$bp = $cellmap->get_border_properties($i, $num_cols - 1);
if ($bp["right"]["width"] <= 0) {
continue;
}
$widths = [
(float)$bp["top"]["width"],
(float)$bp["right"]["width"],
(float)$bp["bottom"]["width"],
(float)$bp["left"]["width"]
];
$x = $table_x + $right_col["x"] + $right_col["used-width"] + $bp["right"]["width"] / 2;
$method = "_border_" . $bp["right"]["style"];
$this->$method($x, $y, $h, $bp["right"]["color"], $widths, "right", "square");
}
}
}
}
dompdf/src/Renderer/AbstractRenderer.php 0000644 00000121240 15024772104 0014317 0 ustar 00 _dompdf = $dompdf;
$this->_canvas = $dompdf->getCanvas();
}
/**
* Render a frame.
*
* Specialized in child classes
*
* @param Frame $frame The frame to render
*/
abstract function render(Frame $frame);
/**
* @param Frame $frame
* @param float[] $border_box
*/
protected function _render_background(Frame $frame, array $border_box): void
{
$style = $frame->get_style();
$color = $style->background_color;
$image = $style->background_image;
[$x, $y, $w, $h] = $border_box;
if ($color === "transparent" && $image === "none") {
return;
}
if ($style->has_border_radius()) {
[$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box);
$this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl);
}
if ($color !== "transparent") {
$this->_canvas->filled_rectangle($x, $y, $w, $h, $color);
}
if ($image !== "none") {
$this->_background_image($image, $x, $y, $w, $h, $style);
}
if ($style->has_border_radius()) {
$this->_canvas->clipping_end();
}
}
/**
* @param Frame $frame
* @param float[] $border_box
* @param string $corner_style
*/
protected function _render_border(Frame $frame, array $border_box, string $corner_style = "bevel"): void
{
$style = $frame->get_style();
$bp = $style->get_border_properties();
[$x, $y, $w, $h] = $border_box;
[$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box);
// Short-cut: If all the borders are "solid" with the same color and
// style, and no radius, we'd better draw a rectangle
if ($bp["top"]["style"] === "solid" &&
$bp["top"] === $bp["right"] &&
$bp["right"] === $bp["bottom"] &&
$bp["bottom"] === $bp["left"] &&
!$style->has_border_radius()
) {
$props = $bp["top"];
if ($props["color"] === "transparent" || $props["width"] <= 0) {
return;
}
$width = (float)$style->length_in_pt($props["width"]);
$this->_canvas->rectangle($x + $width / 2, $y + $width / 2, $w - $width, $h - $width, $props["color"], $width);
return;
}
// Do it the long way
$widths = [
(float)$style->length_in_pt($bp["top"]["width"]),
(float)$style->length_in_pt($bp["right"]["width"]),
(float)$style->length_in_pt($bp["bottom"]["width"]),
(float)$style->length_in_pt($bp["left"]["width"])
];
foreach ($bp as $side => $props) {
if ($props["style"] === "none" ||
$props["style"] === "hidden" ||
$props["color"] === "transparent" ||
$props["width"] <= 0
) {
continue;
}
[$x, $y, $w, $h] = $border_box;
$method = "_border_" . $props["style"];
switch ($side) {
case "top":
$length = $w;
$r1 = $tl;
$r2 = $tr;
break;
case "bottom":
$length = $w;
$y += $h;
$r1 = $bl;
$r2 = $br;
break;
case "left":
$length = $h;
$r1 = $tl;
$r2 = $bl;
break;
case "right":
$length = $h;
$x += $w;
$r1 = $tr;
$r2 = $br;
break;
default:
break;
}
// draw rounded corners
$this->$method($x, $y, $length, $props["color"], $widths, $side, $corner_style, $r1, $r2);
}
}
/**
* @param Frame $frame
* @param float[] $border_box
* @param string $corner_style
*/
protected function _render_outline(Frame $frame, array $border_box, string $corner_style = "bevel"): void
{
$style = $frame->get_style();
$width = $style->outline_width;
$outline_style = $style->outline_style;
$color = $style->outline_color;
if ($outline_style === "none" || $color === "transparent" || $width <= 0) {
return;
}
$offset = $style->outline_offset;
[$x, $y, $w, $h] = $border_box;
$d = $width + $offset;
$outline_box = [$x - $d, $y - $d, $w + $d * 2, $h + $d * 2];
[$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $outline_box);
$x -= $offset;
$y -= $offset;
$w += $offset * 2;
$h += $offset * 2;
// For a simple outline, we can draw a rectangle
if ($outline_style === "solid" && !$style->has_border_radius()) {
$x -= $width / 2;
$y -= $width / 2;
$w += $width;
$h += $width;
$this->_canvas->rectangle($x, $y, $w, $h, $color, $width);
return;
}
$x -= $width;
$y -= $width;
$w += $width * 2;
$h += $width * 2;
$method = "_border_" . $outline_style;
$widths = array_fill(0, 4, $width);
$sides = ["top", "right", "left", "bottom"];
foreach ($sides as $side) {
switch ($side) {
case "top":
$length = $w;
$side_x = $x;
$side_y = $y;
$r1 = $tl;
$r2 = $tr;
break;
case "bottom":
$length = $w;
$side_x = $x;
$side_y = $y + $h;
$r1 = $bl;
$r2 = $br;
break;
case "left":
$length = $h;
$side_x = $x;
$side_y = $y;
$r1 = $tl;
$r2 = $bl;
break;
case "right":
$length = $h;
$side_x = $x + $w;
$side_y = $y;
$r1 = $tr;
$r2 = $br;
break;
default:
break;
}
$this->$method($side_x, $side_y, $length, $color, $widths, $side, $corner_style, $r1, $r2);
}
}
/**
* Render a background image over a rectangular area
*
* @param string $url The background image to load
* @param float $x The left edge of the rectangular area
* @param float $y The top edge of the rectangular area
* @param float $width The width of the rectangular area
* @param float $height The height of the rectangular area
* @param Style $style The associated Style object
*
* @throws \Exception
*/
protected function _background_image($url, $x, $y, $width, $height, $style)
{
if (!function_exists("imagecreatetruecolor")) {
throw new \Exception("The PHP GD extension is required, but is not installed.");
}
$sheet = $style->get_stylesheet();
// Skip degenerate cases
if ($width == 0 || $height == 0) {
return;
}
$box_width = $width;
$box_height = $height;
//debugpng
if ($this->_dompdf->getOptions()->getDebugPng()) {
print '[_background_image ' . $url . ']';
}
list($img, $type, /*$msg*/) = Cache::resolve_url(
$url,
$sheet->get_protocol(),
$sheet->get_host(),
$sheet->get_base_path(),
$this->_dompdf->getOptions()
);
// Bail if the image is no good
if (Cache::is_broken($img)) {
return;
}
//Try to optimize away reading and composing of same background multiple times
//Postponing read with imagecreatefrom ...()
//final composition parameters and name not known yet
//Therefore read dimension directly from file, instead of creating gd object first.
//$img_w = imagesx($src); $img_h = imagesy($src);
list($img_w, $img_h) = Helpers::dompdf_getimagesize($img, $this->_dompdf->getHttpContext());
if ($img_w == 0 || $img_h == 0) {
return;
}
// save for later check if file needs to be resized.
$org_img_w = $img_w;
$org_img_h = $img_h;
$repeat = $style->background_repeat;
$dpi = $this->_dompdf->getOptions()->getDpi();
//Increase background resolution and dependent box size according to image resolution to be placed in
//Then image can be copied in without resize
$bg_width = round((float)($width * $dpi) / 72);
$bg_height = round((float)($height * $dpi) / 72);
list($img_w, $img_h) = $this->_resize_background_image(
$img_w,
$img_h,
$bg_width,
$bg_height,
$style->background_size,
$dpi
);
//Need %bg_x, $bg_y as background pos, where img starts, converted to pixel
list($bg_x, $bg_y) = $style->background_position;
if (Helpers::is_percent($bg_x)) {
// The point $bg_x % from the left edge of the image is placed
// $bg_x % from the left edge of the background rectangle
$p = ((float)$bg_x) / 100.0;
$x1 = $p * $img_w;
$x2 = $p * $bg_width;
$bg_x = $x2 - $x1;
} else {
$bg_x = (float)($style->length_in_pt($bg_x) * $dpi) / 72;
}
$bg_x = round($bg_x + (float)$style->length_in_pt($style->border_left_width) * $dpi / 72);
if (Helpers::is_percent($bg_y)) {
// The point $bg_y % from the left edge of the image is placed
// $bg_y % from the left edge of the background rectangle
$p = ((float)$bg_y) / 100.0;
$y1 = $p * $img_h;
$y2 = $p * $bg_height;
$bg_y = $y2 - $y1;
} else {
$bg_y = (float)($style->length_in_pt($bg_y) * $dpi) / 72;
}
$bg_y = round($bg_y + (float)$style->length_in_pt($style->border_top_width) * $dpi / 72);
//clip background to the image area on partial repeat. Nothing to do if img off area
//On repeat, normalize start position to the tile at immediate left/top or 0/0 of area
//On no repeat with positive offset: move size/start to have offset==0
//Handle x/y Dimensions separately
if ($repeat !== "repeat" && $repeat !== "repeat-x") {
//No repeat x
if ($bg_x < 0) {
$bg_width = $img_w + $bg_x;
} else {
$x += ($bg_x * 72) / $dpi;
$bg_width = $bg_width - $bg_x;
if ($bg_width > $img_w) {
$bg_width = $img_w;
}
$bg_x = 0;
}
if ($bg_width <= 0) {
return;
}
$width = (float)($bg_width * 72) / $dpi;
} else {
//repeat x
if ($bg_x < 0) {
$bg_x = -((-$bg_x) % $img_w);
} else {
$bg_x = $bg_x % $img_w;
if ($bg_x > 0) {
$bg_x -= $img_w;
}
}
}
if ($repeat !== "repeat" && $repeat !== "repeat-y") {
//no repeat y
if ($bg_y < 0) {
$bg_height = $img_h + $bg_y;
} else {
$y += ($bg_y * 72) / $dpi;
$bg_height = $bg_height - $bg_y;
if ($bg_height > $img_h) {
$bg_height = $img_h;
}
$bg_y = 0;
}
if ($bg_height <= 0) {
return;
}
$height = (float)($bg_height * 72) / $dpi;
} else {
//repeat y
if ($bg_y < 0) {
$bg_y = -((-$bg_y) % $img_h);
} else {
$bg_y = $bg_y % $img_h;
if ($bg_y > 0) {
$bg_y -= $img_h;
}
}
}
//Optimization, if repeat has no effect
if ($repeat === "repeat" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) {
$repeat = "repeat-x";
}
if ($repeat === "repeat" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) {
$repeat = "repeat-y";
}
if (($repeat === "repeat-x" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) ||
($repeat === "repeat-y" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height)
) {
$repeat = "no-repeat";
}
// Avoid rendering identical background-image variants multiple times
// This is not dependent of background color of box! .'_'.(is_array($bg_color) ? $bg_color["hex"] : $bg_color)
// Note: Here, bg_* are the start values, not end values after going through the tile loops!
$key = implode("_", [$bg_width, $bg_height, $img_w, $img_h, $bg_x, $bg_y, $repeat]);
// FIXME: This will fail when a file with that exact name exists in the
// same directory, included in the document as regular image
$cpdfKey = $img . "_" . $key;
$tmpFile = Cache::getTempImage($img, $key);
$cached = ($this->_canvas instanceof CPDF && $this->_canvas->get_cpdf()->image_iscached($cpdfKey))
|| ($tmpFile !== null && file_exists($tmpFile));
if (!$cached) {
// img: image url string
// img_w, img_h: original image size in px
// width, height: box size in pt
// bg_width, bg_height: box size in px
// x, y: left/top edge of box on page in pt
// start_x, start_y: placement of image relative to pattern
// $repeat: repeat mode
// $bg: GD object of result image
// $src: GD object of original image
// Create a new image to fit over the background rectangle
$bg = imagecreatetruecolor($bg_width, $bg_height);
$cpdfFromGd = true;
switch (strtolower($type)) {
case "png":
$cpdfFromGd = false;
imagesavealpha($bg, true);
imagealphablending($bg, false);
$src = @imagecreatefrompng($img);
break;
case "jpeg":
$src = @imagecreatefromjpeg($img);
break;
case "webp":
$src = @imagecreatefromwebp($img);
break;
case "gif":
$src = @imagecreatefromgif($img);
break;
case "bmp":
$src = @Helpers::imagecreatefrombmp($img);
break;
default:
return; // Unsupported image type
}
if ($src == null) {
return;
}
if ($img_w != $org_img_w || $img_h != $org_img_h) {
$newSrc = imagescale($src, $img_w, $img_h);
imagedestroy($src);
$src = $newSrc;
}
if ($src == null) {
return;
}
//Background color if box is not relevant here
//Non transparent image: box clipped to real size. Background non relevant.
//Transparent image: The image controls the transparency and lets shine through whatever background.
//However on transparent image preset the composed image with the transparency color,
//to keep the transparency when copying over the non transparent parts of the tiles.
$ti = imagecolortransparent($src);
$palletsize = imagecolorstotal($src);
if ($ti >= 0 && $ti < $palletsize) {
$tc = imagecolorsforindex($src, $ti);
$ti = imagecolorallocate($bg, $tc['red'], $tc['green'], $tc['blue']);
imagefill($bg, 0, 0, $ti);
imagecolortransparent($bg, $ti);
}
//This has only an effect for the non repeatable dimension.
//compute start of src and dest coordinates of the single copy
if ($bg_x < 0) {
$dst_x = 0;
$src_x = -$bg_x;
} else {
$src_x = 0;
$dst_x = $bg_x;
}
if ($bg_y < 0) {
$dst_y = 0;
$src_y = -$bg_y;
} else {
$src_y = 0;
$dst_y = $bg_y;
}
//For historical reasons exchange meanings of variables:
//start_* will be the start values, while bg_* will be the temporary start values in the loops
$start_x = $bg_x;
$start_y = $bg_y;
// Copy regions from the source image to the background
if ($repeat === "no-repeat") {
// Simply place the image on the background
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $img_h);
} elseif ($repeat === "repeat-x") {
for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) {
if ($bg_x < 0) {
$dst_x = 0;
$src_x = -$bg_x;
$w = $img_w + $bg_x;
} else {
$dst_x = $bg_x;
$src_x = 0;
$w = $img_w;
}
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $img_h);
}
} elseif ($repeat === "repeat-y") {
for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) {
if ($bg_y < 0) {
$dst_y = 0;
$src_y = -$bg_y;
$h = $img_h + $bg_y;
} else {
$dst_y = $bg_y;
$src_y = 0;
$h = $img_h;
}
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $h);
}
} elseif ($repeat === "repeat") {
for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) {
for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) {
if ($bg_x < 0) {
$dst_x = 0;
$src_x = -$bg_x;
$w = $img_w + $bg_x;
} else {
$dst_x = $bg_x;
$src_x = 0;
$w = $img_w;
}
if ($bg_y < 0) {
$dst_y = 0;
$src_y = -$bg_y;
$h = $img_h + $bg_y;
} else {
$dst_y = $bg_y;
$src_y = 0;
$h = $img_h;
}
imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $h);
}
}
} else {
print 'Unknown repeat!';
}
imagedestroy($src);
if ($cpdfFromGd && $this->_canvas instanceof CPDF) {
// Skip writing temp file as the GD object is added directly
} else {
$tmpDir = $this->_dompdf->getOptions()->getTempDir();
$tmpName = @tempnam($tmpDir, "bg_dompdf_img_");
@unlink($tmpName);
$tmpFile = "$tmpName.png";
imagepng($bg, $tmpFile);
imagedestroy($bg);
Cache::addTempImage($img, $tmpFile, $key);
}
} else {
$bg = null;
$cpdfFromGd = $tmpFile === null;
}
if ($this->_dompdf->getOptions()->getDebugPng()) {
print '[_background_image ' . $tmpFile . ']';
}
$this->_canvas->clipping_rectangle($x, $y, $box_width, $box_height);
// When using cpdf and optimization to direct png creation from gd object is available,
// don't create temp file, but place gd object directly into the pdf
if ($cpdfFromGd && $this->_canvas instanceof CPDF) {
// Note: CPDF_Adapter image converts y position
$this->_canvas->get_cpdf()->addImagePng($bg, $cpdfKey, $x, $this->_canvas->get_height() - $y - $height, $width, $height);
if (isset($bg)) {
imagedestroy($bg);
}
} else {
$this->_canvas->image($tmpFile, $x, $y, $width, $height);
}
$this->_canvas->clipping_end();
}
// Border rendering functions
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_dotted($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
$this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dotted", $r1, $r2);
}
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_dashed($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
$this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dashed", $r1, $r2);
}
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_solid($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
$this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "solid", $r1, $r2);
}
/**
* @param string $side
* @param float $ratio
* @param float $top
* @param float $right
* @param float $bottom
* @param float $left
* @param float $x
* @param float $y
* @param float $length
* @param float $r1
* @param float $r2
*/
protected function _apply_ratio($side, $ratio, $top, $right, $bottom, $left, &$x, &$y, &$length, &$r1, &$r2)
{
switch ($side) {
case "top":
$r1 -= $left * $ratio;
$r2 -= $right * $ratio;
$x += $left * $ratio;
$y += $top * $ratio;
$length -= $left * $ratio + $right * $ratio;
break;
case "bottom":
$r1 -= $right * $ratio;
$r2 -= $left * $ratio;
$x += $left * $ratio;
$y -= $bottom * $ratio;
$length -= $left * $ratio + $right * $ratio;
break;
case "left":
$r1 -= $top * $ratio;
$r2 -= $bottom * $ratio;
$x += $left * $ratio;
$y += $top * $ratio;
$length -= $top * $ratio + $bottom * $ratio;
break;
case "right":
$r1 -= $bottom * $ratio;
$r2 -= $top * $ratio;
$x -= $right * $ratio;
$y += $top * $ratio;
$length -= $top * $ratio + $bottom * $ratio;
break;
default:
return;
}
}
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_double($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
list($top, $right, $bottom, $left) = $widths;
$third_widths = [$top / 3, $right / 3, $bottom / 3, $left / 3];
// draw the outer border
$this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2);
$this->_apply_ratio($side, 2 / 3, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2);
$this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2);
}
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_groove($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
list($top, $right, $bottom, $left) = $widths;
$half_widths = [$top / 2, $right / 2, $bottom / 2, $left / 2];
$this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
$this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2);
$this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
}
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_ridge($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
list($top, $right, $bottom, $left) = $widths;
$half_widths = [$top / 2, $right / 2, $bottom / 2, $left / 2];
$this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
$this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2);
$this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2);
}
/**
* @param $c
* @return mixed
*/
protected function _tint($c)
{
if (!is_numeric($c)) {
return $c;
}
return min(1, $c + 0.16);
}
/**
* @param $c
* @return mixed
*/
protected function _shade($c)
{
if (!is_numeric($c)) {
return $c;
}
return max(0, $c - 0.33);
}
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_inset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
switch ($side) {
case "top":
case "left":
$shade = array_map([$this, "_shade"], $color);
$this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2);
break;
case "bottom":
case "right":
$tint = array_map([$this, "_tint"], $color);
$this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2);
break;
default:
return;
}
}
/**
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param float $r1
* @param float $r2
*/
protected function _border_outset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0)
{
switch ($side) {
case "top":
case "left":
$tint = array_map([$this, "_tint"], $color);
$this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2);
break;
case "bottom":
case "right":
$shade = array_map([$this, "_shade"], $color);
$this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2);
break;
default:
return;
}
}
/**
* Get the dash pattern and cap style for the given border style, width, and
* line length.
*
* The base pattern is adjusted so that it fits the given line length
* symmetrically.
*
* @param string $style
* @param float $width
* @param float $length
*
* @return array
*/
protected function dashPattern(string $style, float $width, float $length): array
{
if ($style === "dashed") {
$w = 3 * $width;
if ($length < $w) {
$s = $w;
} else {
// Scale dashes and gaps
$r = round($length / $w);
$r = $r % 2 === 0 ? $r + 1 : $r;
$s = $length / $r;
}
return [[$s], "butt"];
}
if ($style === "dotted") {
// Draw circles along the line
// Round caps extend outwards by half line width, so a zero dash
// width results in a circle
$gap = $width <= 1 ? 2 : 1;
$w = ($gap + 1) * $width;
if ($length < $w) {
$s = $w;
} else {
// Only scale gaps
$l = $length - $width;
$r = max(round($l / $w), 1);
$s = $l / $r;
}
return [[0, $s], "round"];
}
return [[], "butt"];
}
/**
* Draws a solid, dotted, or dashed line, observing the border radius
*
* @param float $x
* @param float $y
* @param float $length
* @param array $color
* @param float[] $widths
* @param string $side
* @param string $corner_style
* @param string $pattern_name
* @param float $r1
* @param float $r2
*/
protected function _border_line($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $pattern_name = "none", $r1 = 0, $r2 = 0)
{
/** used by $$side */
[$top, $right, $bottom, $left] = $widths;
$width = $$side;
// No need to clip corners if border radius is large enough
$cornerClip = $corner_style === "bevel" && ($r1 < $width || $r2 < $width);
$lineLength = $length - $r1 - $r2;
[$pattern, $cap] = $this->dashPattern($pattern_name, $width, $lineLength);
// Determine arc border radius for corner arcs
$halfWidth = $width / 2;
$ar1 = max($r1 - $halfWidth, 0);
$ar2 = max($r2 - $halfWidth, 0);
// Small angle adjustments to prevent the background from shining through
$adj1 = $ar1 / 80;
$adj2 = $ar2 / 80;
// Adjust line width and corner angles to account for the fact that
// round caps extend outwards. The line is actually only shifted below,
// not shortened, as otherwise the end dash (circle) will vanish
// occasionally
$dl = $cap === "round" ? $halfWidth : 0;
if ($cap === "round" && $ar1 > 0) {
$adj1 -= rad2deg(asin($halfWidth / $ar1));
}
if ($cap === "round" && $ar2 > 0) {
$adj2 -= rad2deg(asin($halfWidth / $ar2));
}
switch ($side) {
case "top":
if ($cornerClip) {
$points = [
$x, $y,
$x, $y - 1, // Extend outwards to avoid gaps
$x + $length, $y - 1, // Extend outwards to avoid gaps
$x + $length, $y,
$x + $length - max($right, $r2), $y + max($width, $r2),
$x + max($left, $r1), $y + max($width, $r1)
];
$this->_canvas->clipping_polygon($points);
}
$y += $halfWidth;
if ($ar1 > 0 && $adj1 > -22.5) {
$this->_canvas->arc($x + $r1, $y + $ar1, $ar1, $ar1, 90 - $adj1, 135 + $adj1, $color, $width, $pattern, $cap);
}
if ($lineLength > 0) {
$this->_canvas->line($x + $dl + $r1, $y, $x + $dl + $length - $r2, $y, $color, $width, $pattern, $cap);
}
if ($ar2 > 0 && $adj2 > -22.5) {
$this->_canvas->arc($x + $length - $r2, $y + $ar2, $ar2, $ar2, 45 - $adj2, 90 + $adj2, $color, $width, $pattern, $cap);
}
break;
case "bottom":
if ($cornerClip) {
$points = [
$x, $y,
$x, $y + 1, // Extend outwards to avoid gaps
$x + $length, $y + 1, // Extend outwards to avoid gaps
$x + $length, $y,
$x + $length - max($right, $r2), $y - max($width, $r2),
$x + max($left, $r1), $y - max($width, $r1)
];
$this->_canvas->clipping_polygon($points);
}
$y -= $halfWidth;
if ($ar1 > 0 && $adj1 > -22.5) {
$this->_canvas->arc($x + $r1, $y - $ar1, $ar1, $ar1, 225 - $adj1, 270 + $adj1, $color, $width, $pattern, $cap);
}
if ($lineLength > 0) {
$this->_canvas->line($x + $dl + $r1, $y, $x + $dl + $length - $r2, $y, $color, $width, $pattern, $cap);
}
if ($ar2 > 0 && $adj2 > -22.5) {
$this->_canvas->arc($x + $length - $r2, $y - $ar2, $ar2, $ar2, 270 - $adj2, 315 + $adj2, $color, $width, $pattern, $cap);
}
break;
case "left":
if ($cornerClip) {
$points = [
$x, $y,
$x - 1, $y, // Extend outwards to avoid gaps
$x - 1, $y + $length, // Extend outwards to avoid gaps
$x, $y + $length,
$x + max($width, $r2), $y + $length - max($bottom, $r2),
$x + max($width, $r1), $y + max($top, $r1)
];
$this->_canvas->clipping_polygon($points);
}
$x += $halfWidth;
if ($ar1 > 0 && $adj1 > -22.5) {
$this->_canvas->arc($x + $ar1, $y + $r1, $ar1, $ar1, 135 - $adj1, 180 + $adj1, $color, $width, $pattern, $cap);
}
if ($lineLength > 0) {
$this->_canvas->line($x, $y + $dl + $r1, $x, $y + $dl + $length - $r2, $color, $width, $pattern, $cap);
}
if ($ar2 > 0 && $adj2 > -22.5) {
$this->_canvas->arc($x + $ar2, $y + $length - $r2, $ar2, $ar2, 180 - $adj2, 225 + $adj2, $color, $width, $pattern, $cap);
}
break;
case "right":
if ($cornerClip) {
$points = [
$x, $y,
$x + 1, $y, // Extend outwards to avoid gaps
$x + 1, $y + $length, // Extend outwards to avoid gaps
$x, $y + $length,
$x - max($width, $r2), $y + $length - max($bottom, $r2),
$x - max($width, $r1), $y + max($top, $r1)
];
$this->_canvas->clipping_polygon($points);
}
$x -= $halfWidth;
if ($ar1 > 0 && $adj1 > -22.5) {
$this->_canvas->arc($x - $ar1, $y + $r1, $ar1, $ar1, 0 - $adj1, 45 + $adj1, $color, $width, $pattern, $cap);
}
if ($lineLength > 0) {
$this->_canvas->line($x, $y + $dl + $r1, $x, $y + $dl + $length - $r2, $color, $width, $pattern, $cap);
}
if ($ar2 > 0 && $adj2 > -22.5) {
$this->_canvas->arc($x - $ar2, $y + $length - $r2, $ar2, $ar2, 315 - $adj2, 360 + $adj2, $color, $width, $pattern, $cap);
}
break;
}
if ($cornerClip) {
$this->_canvas->clipping_end();
}
}
/**
* @param float $opacity
*/
protected function _set_opacity(float $opacity): void
{
if ($opacity >= 0.0 && $opacity <= 1.0) {
$this->_canvas->set_opacity($opacity);
}
}
/**
* @param float[] $box
* @param string $color
* @param array $style
*/
protected function _debug_layout($box, $color = "red", $style = [])
{
$this->_canvas->rectangle($box[0], $box[1], $box[2], $box[3], Color::parse($color), 0.1, $style);
}
/**
* @param float $img_width
* @param float $img_height
* @param float $container_width
* @param float $container_height
* @param array|string $bg_resize
* @param int $dpi
*
* @return array
*/
protected function _resize_background_image(
$img_width,
$img_height,
$container_width,
$container_height,
$bg_resize,
$dpi
) {
// We got two some specific numbers and/or auto definitions
if (is_array($bg_resize)) {
$is_auto_width = $bg_resize[0] === 'auto';
if ($is_auto_width) {
$new_img_width = $img_width;
} else {
$new_img_width = $bg_resize[0];
if (Helpers::is_percent($new_img_width)) {
$new_img_width = round(($container_width / 100) * (float)$new_img_width);
} else {
$new_img_width = round($new_img_width * $dpi / 72);
}
}
$is_auto_height = $bg_resize[1] === 'auto';
if ($is_auto_height) {
$new_img_height = $img_height;
} else {
$new_img_height = $bg_resize[1];
if (Helpers::is_percent($new_img_height)) {
$new_img_height = round(($container_height / 100) * (float)$new_img_height);
} else {
$new_img_height = round($new_img_height * $dpi / 72);
}
}
// if one of both was set to auto the other one needs to scale proportionally
if ($is_auto_width !== $is_auto_height) {
if ($is_auto_height) {
$new_img_height = round($new_img_width * ($img_height / $img_width));
} else {
$new_img_width = round($new_img_height * ($img_width / $img_height));
}
}
} else {
$container_ratio = $container_height / $container_width;
if ($bg_resize === 'cover' || $bg_resize === 'contain') {
$img_ratio = $img_height / $img_width;
if (
($bg_resize === 'cover' && $container_ratio > $img_ratio) ||
($bg_resize === 'contain' && $container_ratio < $img_ratio)
) {
$new_img_height = $container_height;
$new_img_width = round($container_height / $img_ratio);
} else {
$new_img_width = $container_width;
$new_img_height = round($container_width * $img_ratio);
}
} else {
$new_img_width = $img_width;
$new_img_height = $img_height;
}
}
return [$new_img_width, $new_img_height];
}
}
dompdf/src/Renderer/TableRowGroup.php 0000644 00000001520 15024772104 0013617 0 ustar 00 get_style();
$this->_set_opacity($frame->get_opacity($style->opacity));
$border_box = $frame->get_border_box();
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
$this->debugBlockLayout($frame, "red");
}
}
dompdf/src/Renderer/Inline.php 0000644 00000010357 15024772104 0012311 0 ustar 00 get_first_child()) {
return; // No children, no service
}
$style = $frame->get_style();
$dompdf = $this->_dompdf;
$this->_set_opacity($frame->get_opacity($style->opacity));
$do_debug_layout_line = $dompdf->getOptions()->getDebugLayout()
&& $dompdf->getOptions()->getDebugLayoutInline();
// Draw the background & border behind each child. To do this we need
// to figure out just how much space each child takes:
[$x, $y] = $frame->get_first_child()->get_position();
[$w, $h] = $this->get_child_size($frame, $do_debug_layout_line);
[, , $cbw] = $frame->get_containing_block();
$margin_left = $style->length_in_pt($style->margin_left, $cbw);
$pt = $style->length_in_pt($style->padding_top, $cbw);
$pb = $style->length_in_pt($style->padding_bottom, $cbw);
// Make sure that border and background start inside the left margin
// Extend the drawn box by border and padding in vertical direction, as
// these do not affect layout
// FIXME: Using a small vertical offset of a fraction of the height here
// to work around the vertical position being slightly off in general
$x += $margin_left;
$y -= $style->border_top_width + $pt - ($h * 0.1);
$w += $style->border_left_width + $style->border_right_width;
$h += $style->border_top_width + $pt + $style->border_bottom_width + $pb;
$border_box = [$x, $y, $w, $h];
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
$node = $frame->get_node();
$id = $node->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
// Only two levels of links frames
$is_link_node = $node->nodeName === "a";
if ($is_link_node) {
if (($name = $node->getAttribute("name"))) {
$this->_canvas->add_named_dest($name);
}
}
if ($frame->get_parent() && $frame->get_parent()->get_node()->nodeName === "a") {
$link_node = $frame->get_parent()->get_node();
}
// Handle anchors & links
if ($is_link_node) {
if ($href = $node->getAttribute("href")) {
$href = Helpers::build_url($dompdf->getProtocol(), $dompdf->getBaseHost(), $dompdf->getBasePath(), $href) ?? $href;
$this->_canvas->add_link($href, $x, $y, $w, $h);
}
}
}
protected function get_child_size(Frame $frame, bool $do_debug_layout_line): array
{
$w = 0.0;
$h = 0.0;
foreach ($frame->get_children() as $child) {
if ($child->get_node()->nodeValue === " " && $child->get_prev_sibling() && !$child->get_next_sibling()) {
break;
}
$style = $child->get_style();
$auto_width = $style->width === "auto";
$auto_height = $style->height === "auto";
[, , $child_w, $child_h] = $child->get_padding_box();
if ($auto_width || $auto_height) {
[$child_w2, $child_h2] = $this->get_child_size($child, $do_debug_layout_line);
if ($auto_width) {
$child_w = $child_w2;
}
if ($auto_height) {
$child_h = $child_h2;
}
}
$w += $child_w;
$h = max($h, $child_h);
if ($do_debug_layout_line) {
$this->_debug_layout($child->get_border_box(), "blue");
if ($this->_dompdf->getOptions()->getDebugLayoutPaddingBox()) {
$this->_debug_layout($child->get_padding_box(), "blue", [0.5, 0.5]);
}
}
}
return [$w, $h];
}
}
dompdf/src/Renderer/Block.php 0000644 00000005164 15024772104 0012125 0 ustar 00 get_style();
$node = $frame->get_node();
$dompdf = $this->_dompdf;
$this->_set_opacity($frame->get_opacity($style->opacity));
[$x, $y, $w, $h] = $frame->get_border_box();
if ($node->nodeName === "body") {
// Margins should be fully resolved at this point
$mt = $style->margin_top;
$mb = $style->margin_bottom;
$h = $frame->get_containing_block("h") - $mt - $mb;
}
$border_box = [$x, $y, $w, $h];
// Draw our background, border and content
$this->_render_background($frame, $border_box);
$this->_render_border($frame, $border_box);
$this->_render_outline($frame, $border_box);
// Handle anchors & links
if ($node->nodeName === "a" && $href = $node->getAttribute("href")) {
$href = Helpers::build_url($dompdf->getProtocol(), $dompdf->getBaseHost(), $dompdf->getBasePath(), $href) ?? $href;
$this->_canvas->add_link($href, $x, $y, $w, $h);
}
$id = $frame->get_node()->getAttribute("id");
if (strlen($id) > 0) {
$this->_canvas->add_named_dest($id);
}
$this->debugBlockLayout($frame, "red", false);
}
protected function debugBlockLayout(Frame $frame, ?string $color, bool $lines = false): void
{
$options = $this->_dompdf->getOptions();
$debugLayout = $options->getDebugLayout();
if (!$debugLayout) {
return;
}
if ($color && $options->getDebugLayoutBlocks()) {
$this->_debug_layout($frame->get_border_box(), $color);
if ($options->getDebugLayoutPaddingBox()) {
$this->_debug_layout($frame->get_padding_box(), $color, [0.5, 0.5]);
}
}
if ($lines && $options->getDebugLayoutLines() && $frame instanceof BlockFrameDecorator) {
[$cx, , $cw] = $frame->get_content_box();
foreach ($frame->get_line_boxes() as $line) {
$lw = $cw - $line->left - $line->right;
$this->_debug_layout([$cx + $line->left, $line->y, $lw, $line->h], "orange");
}
}
}
}
dompdf/src/Renderer/Text.php 0000644 00000012100 15024772104 0012003 0 ustar 00 _canvas, "get_cpdf" )
//- For cpdf these can and must stay 0, because font metrics are used directly.
//- For other renderers, if different values are wanted, separate the parameter sets.
// But $size and $size-$height seem to be accurate enough
/** Relative to bottom of text, as fraction of height */
const UNDERLINE_OFFSET = 0.0;
/** Relative to top of text */
const OVERLINE_OFFSET = 0.0;
/** Relative to centre of text. */
const LINETHROUGH_OFFSET = 0.0;
/** How far to extend lines past either end, in pt */
const DECO_EXTENSION = 0.0;
/**
* @param \Dompdf\FrameDecorator\Text $frame
*/
function render(Frame $frame)
{
$style = $frame->get_style();
$text = $frame->get_text();
if ($text === "") {
return;
}
$this->_set_opacity($frame->get_opacity($style->opacity));
list($x, $y) = $frame->get_position();
$cb = $frame->get_containing_block();
$ml = $style->margin_left;
$pl = $style->padding_left;
$bl = $style->border_left_width;
$x += (float) $style->length_in_pt([$ml, $pl, $bl], $cb["w"]);
$font = $style->font_family;
$size = $style->font_size;
$frame_font_size = $frame->get_dompdf()->getFontMetrics()->getFontHeight($font, $size);
$word_spacing = $frame->get_text_spacing() + $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$width = (float) $style->width;
/*$text = str_replace(
array("{PAGE_NUM}"),
array($this->_canvas->get_page_number()),
$text
);*/
$this->_canvas->text($x, $y, $text,
$font, $size,
$style->color, $word_spacing, $letter_spacing);
$line = $frame->get_containing_line();
// FIXME Instead of using the tallest frame to position,
// the decoration, the text should be well placed
if (false && $line->tallest_frame) {
$base_frame = $line->tallest_frame;
$style = $base_frame->get_style();
$size = $style->font_size;
}
$line_thickness = $size * self::DECO_THICKNESS;
$underline_offset = $size * self::UNDERLINE_OFFSET;
$overline_offset = $size * self::OVERLINE_OFFSET;
$linethrough_offset = $size * self::LINETHROUGH_OFFSET;
$underline_position = -0.08;
if ($this->_canvas instanceof CPDF) {
$cpdf_font = $this->_canvas->get_cpdf()->fonts[$style->font_family];
if (isset($cpdf_font["UnderlinePosition"])) {
$underline_position = $cpdf_font["UnderlinePosition"] / 1000;
}
if (isset($cpdf_font["UnderlineThickness"])) {
$line_thickness = $size * ($cpdf_font["UnderlineThickness"] / 1000);
}
}
$descent = $size * $underline_position;
$base = $frame_font_size;
// Handle text decoration:
// http://www.w3.org/TR/CSS21/text.html#propdef-text-decoration
// Draw all applicable text-decorations. Start with the root and work our way down.
$p = $frame;
$stack = [];
while ($p = $p->get_parent()) {
$stack[] = $p;
}
while (isset($stack[0])) {
$f = array_pop($stack);
if (($text_deco = $f->get_style()->text_decoration) === "none") {
continue;
}
$deco_y = $y; //$line->y;
$color = $f->get_style()->color;
switch ($text_deco) {
default:
continue 2;
case "underline":
$deco_y += $base - $descent + $underline_offset + $line_thickness / 2;
break;
case "overline":
$deco_y += $overline_offset + $line_thickness / 2;
break;
case "line-through":
$deco_y += $base * 0.7 + $linethrough_offset;
break;
}
$dx = 0;
$x1 = $x - self::DECO_EXTENSION;
$x2 = $x + $width + $dx + self::DECO_EXTENSION;
$this->_canvas->line($x1, $deco_y, $x2, $deco_y, $color, $line_thickness);
}
if ($this->_dompdf->getOptions()->getDebugLayout() && $this->_dompdf->getOptions()->getDebugLayoutLines()) {
$text_width = $this->_dompdf->getFontMetrics()->getTextWidth($text, $font, $size, $word_spacing, $letter_spacing);
$this->_debug_layout([$x, $y, $text_width, $frame_font_size], "orange", [0.5, 0.5]);
}
}
}
dompdf/src/FrameReflower/AbstractFrameReflower.php 0000644 00000054032 15024772104 0016307 0 ustar 00 _frame = $frame;
$this->_min_max_child_cache = null;
$this->_min_max_cache = null;
}
/**
* @return Dompdf
*/
function get_dompdf()
{
return $this->_frame->get_dompdf();
}
public function reset(): void
{
$this->_min_max_child_cache = null;
$this->_min_max_cache = null;
}
/**
* Determine the actual containing block for absolute and fixed position.
*
* https://www.w3.org/TR/CSS21/visudet.html#containing-block-details
*/
protected function determine_absolute_containing_block(): void
{
$frame = $this->_frame;
$style = $frame->get_style();
switch ($style->position) {
case "absolute":
$parent = $frame->find_positioned_parent();
if ($parent !== $frame->get_root()) {
$parent_style = $parent->get_style();
$parent_padding_box = $parent->get_padding_box();
//FIXME: an accurate measure of the positioned parent height
// is not possible until reflow has completed;
// we'll fall back to the parent's containing block,
// which is wrong for auto-height parents
if ($parent_style->height === "auto") {
$parent_containing_block = $parent->get_containing_block();
$containing_block_height = $parent_containing_block["h"] -
(float)$parent_style->length_in_pt([
$parent_style->margin_top,
$parent_style->margin_bottom,
$parent_style->border_top_width,
$parent_style->border_bottom_width
], $parent_containing_block["w"]);
} else {
$containing_block_height = $parent_padding_box["h"];
}
$frame->set_containing_block($parent_padding_box["x"], $parent_padding_box["y"], $parent_padding_box["w"], $containing_block_height);
break;
}
case "fixed":
$initial_cb = $frame->get_root()->get_first_child()->get_containing_block();
$frame->set_containing_block($initial_cb["x"], $initial_cb["y"], $initial_cb["w"], $initial_cb["h"]);
break;
default:
// Nothing to do, containing block already set via parent
break;
}
}
/**
* Collapse frames margins
* http://www.w3.org/TR/CSS21/box.html#collapsing-margins
*/
protected function _collapse_margins(): void
{
$frame = $this->_frame;
// Margins of float/absolutely positioned/inline-level elements do not collapse
if (!$frame->is_in_flow() || $frame->is_inline_level()
|| $frame->get_root() === $frame || $frame->get_parent() === $frame->get_root()
) {
return;
}
$cb = $frame->get_containing_block();
$style = $frame->get_style();
$t = $style->length_in_pt($style->margin_top, $cb["w"]);
$b = $style->length_in_pt($style->margin_bottom, $cb["w"]);
// Handle 'auto' values
if ($t === "auto") {
$style->set_used("margin_top", 0.0);
$t = 0.0;
}
if ($b === "auto") {
$style->set_used("margin_bottom", 0.0);
$b = 0.0;
}
// Collapse vertical margins:
$n = $frame->get_next_sibling();
if ( $n && !($n->is_block_level() && $n->is_in_flow()) ) {
while ($n = $n->get_next_sibling()) {
if ($n->is_block_level() && $n->is_in_flow()) {
break;
}
if (!$n->get_first_child()) {
$n = null;
break;
}
}
}
if ($n) {
$n_style = $n->get_style();
$n_t = (float)$n_style->length_in_pt($n_style->margin_top, $cb["w"]);
$b = $this->get_collapsed_margin_length($b, $n_t);
$style->set_used("margin_bottom", $b);
$n_style->set_used("margin_top", 0.0);
}
// Collapse our first child's margin, if there is no border or padding
if ($style->border_top_width == 0 && $style->length_in_pt($style->padding_top) == 0) {
$f = $this->_frame->get_first_child();
if ( $f && !($f->is_block_level() && $f->is_in_flow()) ) {
while ($f = $f->get_next_sibling()) {
if ($f->is_block_level() && $f->is_in_flow()) {
break;
}
if (!$f->get_first_child()) {
$f = null;
break;
}
}
}
// Margins are collapsed only between block-level boxes
if ($f) {
$f_style = $f->get_style();
$f_t = (float)$f_style->length_in_pt($f_style->margin_top, $cb["w"]);
$t = $this->get_collapsed_margin_length($t, $f_t);
$style->set_used("margin_top", $t);
$f_style->set_used("margin_top", 0.0);
}
}
// Collapse our last child's margin, if there is no border or padding
if ($style->border_bottom_width == 0 && $style->length_in_pt($style->padding_bottom) == 0) {
$l = $this->_frame->get_last_child();
if ( $l && !($l->is_block_level() && $l->is_in_flow()) ) {
while ($l = $l->get_prev_sibling()) {
if ($l->is_block_level() && $l->is_in_flow()) {
break;
}
if (!$l->get_last_child()) {
$l = null;
break;
}
}
}
// Margins are collapsed only between block-level boxes
if ($l) {
$l_style = $l->get_style();
$l_b = (float)$l_style->length_in_pt($l_style->margin_bottom, $cb["w"]);
$b = $this->get_collapsed_margin_length($b, $l_b);
$style->set_used("margin_bottom", $b);
$l_style->set_used("margin_bottom", 0.0);
}
}
}
/**
* Get the combined (collapsed) length of two adjoining margins.
*
* See http://www.w3.org/TR/CSS21/box.html#collapsing-margins.
*
* @param float $l1
* @param float $l2
*
* @return float
*/
private function get_collapsed_margin_length(float $l1, float $l2): float
{
if ($l1 < 0 && $l2 < 0) {
return min($l1, $l2); // min(x, y) = - max(abs(x), abs(y)), if x < 0 && y < 0
}
if ($l1 < 0 || $l2 < 0) {
return $l1 + $l2; // x + y = x - abs(y), if y < 0
}
return max($l1, $l2);
}
/**
* Handle relative positioning according to
* https://www.w3.org/TR/CSS21/visuren.html#relative-positioning.
*
* @param AbstractFrameDecorator $frame The frame to handle.
*/
protected function position_relative(AbstractFrameDecorator $frame): void
{
$style = $frame->get_style();
if ($style->position === "relative") {
$cb = $frame->get_containing_block();
$top = $style->length_in_pt($style->top, $cb["h"]);
$right = $style->length_in_pt($style->right, $cb["w"]);
$bottom = $style->length_in_pt($style->bottom, $cb["h"]);
$left = $style->length_in_pt($style->left, $cb["w"]);
// FIXME RTL case:
// if ($left !== "auto" && $right !== "auto") $left = -$right;
if ($left === "auto" && $right === "auto") {
$left = 0;
} elseif ($left === "auto") {
$left = -$right;
}
if ($top === "auto" && $bottom === "auto") {
$top = 0;
} elseif ($top === "auto") {
$top = -$bottom;
}
$frame->move($left, $top);
}
}
/**
* @param Block|null $block
*/
abstract function reflow(Block $block = null);
/**
* Resolve the `min-width` property.
*
* Resolves to 0 if not set or if a percentage and the containing-block
* width is not defined.
*
* @param float|null $cbw Width of the containing block.
*
* @return float
*/
protected function resolve_min_width(?float $cbw): float
{
$style = $this->_frame->get_style();
$min_width = $style->min_width;
return $min_width !== "auto"
? $style->length_in_pt($min_width, $cbw ?? 0)
: 0.0;
}
/**
* Resolve the `max-width` property.
*
* Resolves to `INF` if not set or if a percentage and the containing-block
* width is not defined.
*
* @param float|null $cbw Width of the containing block.
*
* @return float
*/
protected function resolve_max_width(?float $cbw): float
{
$style = $this->_frame->get_style();
$max_width = $style->max_width;
return $max_width !== "none"
? $style->length_in_pt($max_width, $cbw ?? INF)
: INF;
}
/**
* Resolve the `min-height` property.
*
* Resolves to 0 if not set or if a percentage and the containing-block
* height is not defined.
*
* @param float|null $cbh Height of the containing block.
*
* @return float
*/
protected function resolve_min_height(?float $cbh): float
{
$style = $this->_frame->get_style();
$min_height = $style->min_height;
return $min_height !== "auto"
? $style->length_in_pt($min_height, $cbh ?? 0)
: 0.0;
}
/**
* Resolve the `max-height` property.
*
* Resolves to `INF` if not set or if a percentage and the containing-block
* height is not defined.
*
* @param float|null $cbh Height of the containing block.
*
* @return float
*/
protected function resolve_max_height(?float $cbh): float
{
$style = $this->_frame->get_style();
$max_height = $style->max_height;
return $max_height !== "none"
? $style->length_in_pt($style->max_height, $cbh ?? INF)
: INF;
}
/**
* Get the minimum and maximum preferred width of the contents of the frame,
* as requested by its children.
*
* @return array A two-element array of min and max width.
*/
public function get_min_max_child_width(): array
{
if (!is_null($this->_min_max_child_cache)) {
return $this->_min_max_child_cache;
}
$low = [];
$high = [];
for ($iter = $this->_frame->get_children(); $iter->valid(); $iter->next()) {
$inline_min = 0;
$inline_max = 0;
// Add all adjacent inline widths together to calculate max width
while ($iter->valid() && ($iter->current()->is_inline_level() || $iter->current()->get_style()->display === "-dompdf-image")) {
/** @var AbstractFrameDecorator */
$child = $iter->current();
$child->get_reflower()->_set_content();
$minmax = $child->get_min_max_width();
if (in_array($child->get_style()->white_space, ["pre", "nowrap"], true)) {
$inline_min += $minmax["min"];
} else {
$low[] = $minmax["min"];
}
$inline_max += $minmax["max"];
$iter->next();
}
if ($inline_min > 0) {
$low[] = $inline_min;
}
if ($inline_max > 0) {
$high[] = $inline_max;
}
// Skip children with absolute position
if ($iter->valid() && !$iter->current()->is_absolute()) {
/** @var AbstractFrameDecorator */
$child = $iter->current();
$child->get_reflower()->_set_content();
list($low[], $high[]) = $child->get_min_max_width();
}
}
$min = count($low) ? max($low) : 0;
$max = count($high) ? max($high) : 0;
return $this->_min_max_child_cache = [$min, $max];
}
/**
* Get the minimum and maximum preferred content-box width of the frame.
*
* @return array A two-element array of min and max width.
*/
public function get_min_max_content_width(): array
{
return $this->get_min_max_child_width();
}
/**
* Get the minimum and maximum preferred border-box width of the frame.
*
* Required for shrink-to-fit width calculation, as used in automatic table
* layout, absolute positioning, float and inline-block. This provides a
* basic implementation. Child classes should override this or
* `get_min_max_content_width` as necessary.
*
* @return array An array `[0 => min, 1 => max, "min" => min, "max" => max]`
* of min and max width.
*/
public function get_min_max_width(): array
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
[$min, $max] = $this->get_min_max_content_width();
// Account for margins, borders, and padding
$dims = [
$style->padding_left,
$style->padding_right,
$style->border_left_width,
$style->border_right_width,
$style->margin_left,
$style->margin_right
];
// The containing block is not defined yet, treat percentages as 0
$delta = (float) $style->length_in_pt($dims, 0);
$min += $delta;
$max += $delta;
return $this->_min_max_cache = [$min, $max, "min" => $min, "max" => $max];
}
/**
* Parses a CSS string containing quotes and escaped hex characters
*
* @param $string string The CSS string to parse
* @param $single_trim
* @return string
*/
protected function _parse_string($string, $single_trim = false)
{
if ($single_trim) {
$string = preg_replace('/^[\"\']/', "", $string);
$string = preg_replace('/[\"\']$/', "", $string);
} else {
$string = trim($string, "'\"");
}
$string = str_replace(["\\\n", '\\"', "\\'"],
["", '"', "'"], $string);
// Convert escaped hex characters into ascii characters (e.g. \A => newline)
$string = preg_replace_callback("/\\\\([0-9a-fA-F]{0,6})/",
function ($matches) { return \Dompdf\Helpers::unichr(hexdec($matches[1])); },
$string);
return $string;
}
/**
* Parses a CSS "quotes" property
*
* https://www.w3.org/TR/css-content-3/#quotes
*
* @return array An array of pairs of quotes
*/
protected function _parse_quotes(): array
{
$quotes = $this->_frame->get_style()->quotes;
if ($quotes === "none") {
return [];
}
if ($quotes === "auto") {
// TODO: Use typographically appropriate quotes for the current
// language here
return [['"', '"'], ["'", "'"]];
}
// Matches quote types
$re = '/(\'[^\']*\')|(\"[^\"]*\")/';
// Split on spaces, except within quotes
if (!preg_match_all($re, $quotes, $matches, PREG_SET_ORDER)) {
return [];
}
$quotes_array = [];
foreach ($matches as $_quote) {
$quotes_array[] = $this->_parse_string($_quote[0], true);
}
return array_chunk($quotes_array, 2);
}
/**
* Parses the CSS "content" property
*
* https://www.w3.org/TR/CSS21/generate.html#content
*
* @return string The resulting string
*/
protected function _parse_content(): string
{
$style = $this->_frame->get_style();
$content = $style->content;
if ($content === "normal" || $content === "none") {
return "";
}
$quotes = $this->_parse_quotes();
$text = "";
foreach ($content as $val) {
// String
if (in_array(mb_substr($val, 0, 1), ['"', "'"], true)) {
$text .= $this->_parse_string($val);
continue;
}
$val = mb_strtolower($val);
// Keywords
if ($val === "open-quote") {
// FIXME: Take quotation depth into account
if (isset($quotes[0][0])) {
$text .= $quotes[0][0];
}
continue;
} elseif ($val === "close-quote") {
// FIXME: Take quotation depth into account
if (isset($quotes[0][1])) {
$text .= $quotes[0][1];
}
continue;
} elseif ($val === "no-open-quote") {
// FIXME: Increment quotation depth
continue;
} elseif ($val === "no-close-quote") {
// FIXME: Decrement quotation depth
continue;
}
// attr()
if (mb_substr($val, 0, 5) === "attr(") {
$i = mb_strpos($val, ")");
if ($i === false) {
continue;
}
$attr = trim(mb_substr($val, 5, $i - 5));
if ($attr === "") {
continue;
}
$text .= $this->_frame->get_parent()->get_node()->getAttribute($attr);
continue;
}
// counter()/counters()
if (mb_substr($val, 0, 7) === "counter") {
// Handle counter() references:
// http://www.w3.org/TR/CSS21/generate.html#content
$i = mb_strpos($val, ")");
if ($i === false) {
continue;
}
preg_match('/(counters?)(^\()*?\(\s*([^\s,]+)\s*(,\s*["\']?([^"\'\)]*)["\']?\s*(,\s*([^\s)]+)\s*)?)?\)/i', $val, $args);
$counter_id = $args[3];
if (strtolower($args[1]) === "counter") {
// counter(name [,style])
if (isset($args[5])) {
$type = trim($args[5]);
} else {
$type = "decimal";
}
$p = $this->_frame->lookup_counter_frame($counter_id);
$text .= $p->counter_value($counter_id, $type);
} elseif (strtolower($args[1]) === "counters") {
// counters(name, string [,style])
if (isset($args[5])) {
$string = $this->_parse_string($args[5]);
} else {
$string = "";
}
if (isset($args[7])) {
$type = trim($args[7]);
} else {
$type = "decimal";
}
$p = $this->_frame->lookup_counter_frame($counter_id);
$tmp = [];
while ($p) {
// We only want to use the counter values when they actually increment the counter
if (array_key_exists($counter_id, $p->_counters)) {
array_unshift($tmp, $p->counter_value($counter_id, $type));
}
$p = $p->lookup_counter_frame($counter_id);
}
$text .= implode($string, $tmp);
} else {
// countertops?
}
continue;
}
}
return $text;
}
/**
* Handle counters and set generated content if the frame is a
* generated-content frame.
*/
protected function _set_content(): void
{
$frame = $this->_frame;
if ($frame->content_set) {
return;
}
$style = $frame->get_style();
if (($reset = $style->counter_reset) !== "none") {
$frame->reset_counters($reset);
}
if (($increment = $style->counter_increment) !== "none") {
$frame->increment_counters($increment);
}
if ($frame->get_node()->nodeName === "dompdf_generated") {
$content = $this->_parse_content();
if ($content !== "") {
$node = $frame->get_node()->ownerDocument->createTextNode($content);
$new_style = $style->get_stylesheet()->create_style();
$new_style->inherit($style);
$new_frame = new Frame($node);
$new_frame->set_style($new_style);
Factory::decorate_frame($new_frame, $frame->get_dompdf(), $frame->get_root());
$frame->append_child($new_frame);
}
}
$frame->content_set = true;
}
}
dompdf/src/FrameReflower/Image.php 0000644 00000017444 15024772104 0013113 0 ustar 00 determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
//FLOAT
//$frame = $this->_frame;
//$page = $frame->get_root();
//if ($frame->get_style()->float !== "none" ) {
// $page->add_floating_frame($this);
//}
$this->resolve_dimensions();
$this->resolve_margins();
$frame = $this->_frame;
$frame->position();
if ($block && $frame->is_in_flow()) {
$block->add_frame_to_line($frame);
}
}
public function get_min_max_content_width(): array
{
// TODO: While the containing block is not set yet on the frame, it can
// already be determined in some cases due to fixed dimensions on the
// ancestor forming the containing block. In such cases, percentage
// values could be resolved here
$style = $this->_frame->get_style();
[$width] = $this->calculate_size(null, null);
$min_width = $this->resolve_min_width(null);
$percent_width = Helpers::is_percent($style->width)
|| Helpers::is_percent($style->max_width)
|| ($style->width === "auto"
&& (Helpers::is_percent($style->height) || Helpers::is_percent($style->max_height)));
// Use the specified min width as minimum when width or max width depend
// on the containing block and cannot be resolved yet. This mimics
// browser behavior
$min = $percent_width ? $min_width : $width;
$max = $width;
return [$min, $max];
}
/**
* Calculate width and height, accounting for min/max constraints.
*
* * https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
* * https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
* * https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
* * https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
*
* @param float|null $cbw Width of the containing block.
* @param float|null $cbh Height of the containing block.
*
* @return float[]
*/
protected function calculate_size(?float $cbw, ?float $cbh): array
{
/** @var ImageFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$computed_width = $style->width;
$computed_height = $style->height;
$width = $cbw === null && Helpers::is_percent($computed_width)
? "auto"
: $style->length_in_pt($computed_width, $cbw ?? 0);
$height = $cbh === null && Helpers::is_percent($computed_height)
? "auto"
: $style->length_in_pt($computed_height, $cbh ?? 0);
$min_width = $this->resolve_min_width($cbw);
$max_width = $this->resolve_max_width($cbw);
$min_height = $this->resolve_min_height($cbh);
$max_height = $this->resolve_max_height($cbh);
if ($width === "auto" && $height === "auto") {
// Use intrinsic dimensions, resampled to pt
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$w = $frame->resample($img_width);
$h = $frame->resample($img_height);
// Resolve min/max constraints according to the constraint-violation
// table in https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
$max_width = max($min_width, $max_width);
$max_height = max($min_height, $max_height);
if (($w > $max_width && $h <= $max_height)
|| ($w > $max_width && $h > $max_height && $max_width / $w <= $max_height / $h)
|| ($w < $min_width && $h > $min_height)
|| ($w < $min_width && $h < $min_height && $min_width / $w > $min_height / $h)
) {
$width = Helpers::clamp($w, $min_width, $max_width);
$height = $width * ($img_height / $img_width);
$height = Helpers::clamp($height, $min_height, $max_height);
} else {
$height = Helpers::clamp($h, $min_height, $max_height);
$width = $height * ($img_width / $img_height);
$width = Helpers::clamp($width, $min_width, $max_width);
}
} elseif ($height === "auto") {
// Width is fixed, scale height according to aspect ratio
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$width = Helpers::clamp((float) $width, $min_width, $max_width);
$height = $width * ($img_height / $img_width);
$height = Helpers::clamp($height, $min_height, $max_height);
} elseif ($width === "auto") {
// Height is fixed, scale width according to aspect ratio
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
$height = Helpers::clamp((float) $height, $min_height, $max_height);
$width = $height * ($img_width / $img_height);
$width = Helpers::clamp($width, $min_width, $max_width);
} else {
// Width and height are fixed
$width = Helpers::clamp((float) $width, $min_width, $max_width);
$height = Helpers::clamp((float) $height, $min_height, $max_height);
}
return [$width, $height];
}
protected function resolve_dimensions(): void
{
/** @var ImageFrameDecorator */
$frame = $this->_frame;
$style = $frame->get_style();
$debug_png = $this->get_dompdf()->getOptions()->getDebugPng();
if ($debug_png) {
[$img_width, $img_height] = $frame->get_intrinsic_dimensions();
print "resolve_dimensions() " .
$frame->get_style()->width . " " .
$frame->get_style()->height . ";" .
$frame->get_parent()->get_style()->width . " " .
$frame->get_parent()->get_style()->height . ";" .
$frame->get_parent()->get_parent()->get_style()->width . " " .
$frame->get_parent()->get_parent()->get_style()->height . ";" .
$img_width . " " .
$img_height . "|";
}
[, , $cbw, $cbh] = $frame->get_containing_block();
[$width, $height] = $this->calculate_size($cbw, $cbh);
if ($debug_png) {
print $width . " " . $height . ";";
}
$style->set_used("width", $width);
$style->set_used("height", $height);
}
protected function resolve_margins(): void
{
// Only handle the inline case for now
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
$style = $this->_frame->get_style();
if ($style->margin_left === "auto") {
$style->set_used("margin_left", 0.0);
}
if ($style->margin_right === "auto") {
$style->set_used("margin_right", 0.0);
}
if ($style->margin_top === "auto") {
$style->set_used("margin_top", 0.0);
}
if ($style->margin_bottom === "auto") {
$style->set_used("margin_bottom", 0.0);
}
}
}
dompdf/src/FrameReflower/ListBullet.php 0000644 00000002256 15024772104 0014147 0 ustar 00 _frame;
$style = $frame->get_style();
$style->set_used("width", $frame->get_width());
$frame->position();
if ($style->list_style_position === "inside") {
$block->add_frame_to_line($frame);
} else {
$block->add_dangling_marker($frame);
}
}
}
dompdf/src/FrameReflower/Table.php 0000644 00000041612 15024772104 0013112 0 ustar 00 _state = null;
parent::__construct($frame);
}
/**
* State is held here so it needs to be reset along with the decorator
*/
public function reset(): void
{
parent::reset();
$this->_state = null;
}
protected function _assign_widths()
{
$style = $this->_frame->get_style();
// Find the min/max width of the table and sort the columns into
// absolute/percent/auto arrays
$delta = $this->_state["width_delta"];
$min_width = $this->_state["min_width"];
$max_width = $this->_state["max_width"];
$percent_used = $this->_state["percent_used"];
$absolute_used = $this->_state["absolute_used"];
$auto_min = $this->_state["auto_min"];
$absolute =& $this->_state["absolute"];
$percent =& $this->_state["percent"];
$auto =& $this->_state["auto"];
// Determine the actual width of the table (excluding borders and
// padding)
$cb = $this->_frame->get_containing_block();
$columns =& $this->_frame->get_cellmap()->get_columns();
$width = $style->width;
$min_table_width = $this->resolve_min_width($cb["w"]) - $delta;
if ($width !== "auto") {
$preferred_width = (float) $style->length_in_pt($width, $cb["w"]) - $delta;
if ($preferred_width < $min_table_width) {
$preferred_width = $min_table_width;
}
if ($preferred_width > $min_width) {
$width = $preferred_width;
} else {
$width = $min_width;
}
} else {
if ($max_width + $delta < $cb["w"]) {
$width = $max_width;
} elseif ($cb["w"] - $delta > $min_width) {
$width = $cb["w"] - $delta;
} else {
$width = $min_width;
}
if ($width < $min_table_width) {
$width = $min_table_width;
}
}
// Store our resolved width
$style->set_used("width", $width);
$cellmap = $this->_frame->get_cellmap();
if ($cellmap->is_columns_locked()) {
return;
}
// If the whole table fits on the page, then assign each column it's max width
if ($width == $max_width) {
foreach ($columns as $i => $col) {
$cellmap->set_column_width($i, $col["max-width"]);
}
return;
}
// Determine leftover and assign it evenly to all columns
if ($width > $min_width) {
// We have three cases to deal with:
//
// 1. All columns are auto or absolute width. In this case we
// distribute extra space across all auto columns weighted by the
// difference between their max and min width, or by max width only
// if the width of the table is larger than the max width for all
// columns.
//
// 2. Only absolute widths have been specified, no auto columns. In
// this case we distribute extra space across all columns weighted
// by their absolute width.
//
// 3. Percentage widths have been specified. In this case we normalize
// the percentage values and try to assign widths as fractions of
// the table width. Absolute column widths are fully satisfied and
// any remaining space is evenly distributed among all auto columns.
// Case 1:
if ($percent_used == 0 && count($auto)) {
foreach ($absolute as $i) {
$w = $columns[$i]["min-width"];
$cellmap->set_column_width($i, $w);
}
if ($width < $max_width) {
$increment = $width - $min_width;
$table_delta = $max_width - $min_width;
foreach ($auto as $i) {
$min = $columns[$i]["min-width"];
$max = $columns[$i]["max-width"];
$col_delta = $max - $min;
$w = $min + $increment * ($col_delta / $table_delta);
$cellmap->set_column_width($i, $w);
}
} else {
$increment = $width - $max_width;
$auto_max = $max_width - $absolute_used;
foreach ($auto as $i) {
$max = $columns[$i]["max-width"];
$f = $auto_max > 0 ? $max / $auto_max : 1 / count($auto);
$w = $max + $increment * $f;
$cellmap->set_column_width($i, $w);
}
}
return;
}
// Case 2:
if ($percent_used == 0 && !count($auto)) {
$increment = $width - $absolute_used;
foreach ($absolute as $i) {
$abs = $columns[$i]["min-width"];
$f = $absolute_used > 0 ? $abs / $absolute_used : 1 / count($absolute);
$w = $abs + $increment * $f;
$cellmap->set_column_width($i, $w);
}
return;
}
// Case 3:
if ($percent_used > 0) {
// Scale percent values if the total percentage is > 100 or
// there are no auto values to take up slack
if ($percent_used > 100 || count($auto) == 0) {
$scale = 100 / $percent_used;
} else {
$scale = 1;
}
// Account for the minimum space used by the unassigned auto
// columns, by the columns with absolute widths, and the
// percentage columns following the current one
$used_width = $auto_min + $absolute_used;
foreach ($absolute as $i) {
$w = $columns[$i]["min-width"];
$cellmap->set_column_width($i, $w);
}
$percent_min = 0;
foreach ($percent as $i) {
$percent_min += $columns[$i]["min-width"];
}
// First-come, first served
foreach ($percent as $i) {
$min = $columns[$i]["min-width"];
$percent_min -= $min;
$slack = $width - $used_width - $percent_min;
$columns[$i]["percent"] *= $scale;
$w = min($columns[$i]["percent"] * $width / 100, $slack);
if ($w < $min) {
$w = $min;
}
$cellmap->set_column_width($i, $w);
$used_width += $w;
}
// This works because $used_width includes the min-width of each
// unassigned column
if (count($auto) > 0) {
$increment = ($width - $used_width) / count($auto);
foreach ($auto as $i) {
$w = $columns[$i]["min-width"] + $increment;
$cellmap->set_column_width($i, $w);
}
}
return;
}
} else {
// We are over-constrained:
// Each column gets its minimum width
foreach ($columns as $i => $col) {
$cellmap->set_column_width($i, $col["min-width"]);
}
}
}
/**
* Determine the frame's height based on min/max height
*
* @return float
*/
protected function _calculate_height()
{
$frame = $this->_frame;
$style = $frame->get_style();
$cb = $frame->get_containing_block();
$height = $style->length_in_pt($style->height, $cb["h"]);
$cellmap = $frame->get_cellmap();
$cellmap->assign_frame_heights();
$rows = $cellmap->get_rows();
// Determine our content height
$content_height = 0.0;
foreach ($rows as $r) {
$content_height += $r["height"];
}
if ($height === "auto") {
$height = $content_height;
}
// Handle min/max height
// https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
$min_height = $this->resolve_min_height($cb["h"]);
$max_height = $this->resolve_max_height($cb["h"]);
$height = Helpers::clamp($height, $min_height, $max_height);
// Use the content height or the height value, whichever is greater
if ($height <= $content_height) {
$height = $content_height;
} else {
// FIXME: Borders and row positions are not properly updated by this
// $cellmap->set_frame_heights($height, $content_height);
}
return $height;
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
/** @var TableFrameDecorator */
$frame = $this->_frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
// Let the page know that we're reflowing a table so that splits
// are suppressed (simply setting page-break-inside: avoid won't
// work because we may have an arbitrary number of block elements
// inside tds.)
$page->table_reflow_start();
$this->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
// Collapse vertical margins, if required
$this->_collapse_margins();
// Table layout algorithm:
// http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
if (is_null($this->_state)) {
$this->get_min_max_width();
}
$cb = $frame->get_containing_block();
$style = $frame->get_style();
// This is slightly inexact, but should be okay. Add half the
// border-spacing to the table as padding. The other half is added to
// the cells themselves.
if ($style->border_collapse === "separate") {
[$h, $v] = $style->border_spacing;
$v = $v / 2;
$h = $h / 2;
$style->set_used("padding_left", (float)$style->length_in_pt($style->padding_left, $cb["w"]) + $h);
$style->set_used("padding_right", (float)$style->length_in_pt($style->padding_right, $cb["w"]) + $h);
$style->set_used("padding_top", (float)$style->length_in_pt($style->padding_top, $cb["w"]) + $v);
$style->set_used("padding_bottom", (float)$style->length_in_pt($style->padding_bottom, $cb["w"]) + $v);
}
$this->_assign_widths();
// Adjust left & right margins, if they are auto
$delta = $this->_state["width_delta"];
$width = $style->width;
$left = $style->length_in_pt($style->margin_left, $cb["w"]);
$right = $style->length_in_pt($style->margin_right, $cb["w"]);
$diff = (float) $cb["w"] - (float) $width - $delta;
if ($left === "auto" && $right === "auto") {
if ($diff < 0) {
$left = 0;
$right = $diff;
} else {
$left = $right = $diff / 2;
}
} else {
if ($left === "auto") {
$left = max($diff - $right, 0);
}
if ($right === "auto") {
$right = max($diff - $left, 0);
}
}
$style->set_used("margin_left", $left);
$style->set_used("margin_right", $right);
$frame->position();
[$x, $y] = $frame->get_position();
// Determine the content edge
$offset_x = (float)$left + (float)$style->length_in_pt([
$style->padding_left,
$style->border_left_width
], $cb["w"]);
$offset_y = (float)$style->length_in_pt([
$style->margin_top,
$style->border_top_width,
$style->padding_top
], $cb["w"]);
$content_x = $x + $offset_x;
$content_y = $y + $offset_y;
if (isset($cb["h"])) {
$h = $cb["h"];
} else {
$h = null;
}
$cellmap = $frame->get_cellmap();
$col =& $cellmap->get_column(0);
$col["x"] = $offset_x;
$row =& $cellmap->get_row(0);
$row["y"] = $offset_y;
$cellmap->assign_x_positions();
// Set the containing block of each child & reflow
foreach ($frame->get_children() as $child) {
$child->set_containing_block($content_x, $content_y, $width, $h);
$child->reflow();
if (!$page->in_nested_table()) {
// Check if a split has occurred
$page->check_page_break($child);
if ($page->is_full()) {
break;
}
}
}
// Stop reflow if a page break has occurred before the frame, in which
// case it has been reset, including its position
if ($page->is_full() && $frame->get_position("x") === null) {
$page->table_reflow_end();
return;
}
// Assign heights to our cells:
$style->set_used("height", $this->_calculate_height());
$page->table_reflow_end();
if ($block && $frame->is_in_flow()) {
$block->add_frame_to_line($frame);
if ($frame->is_block_level()) {
$block->add_line();
}
}
}
public function get_min_max_width(): array
{
if (!is_null($this->_min_max_cache)) {
return $this->_min_max_cache;
}
$style = $this->_frame->get_style();
$cellmap = $this->_frame->get_cellmap();
$this->_frame->normalize();
// Add the cells to the cellmap (this will calculate column widths as
// frames are added)
$cellmap->add_frame($this->_frame);
// Find the min/max width of the table and sort the columns into
// absolute/percent/auto arrays
$this->_state = [];
$this->_state["min_width"] = 0;
$this->_state["max_width"] = 0;
$this->_state["percent_used"] = 0;
$this->_state["absolute_used"] = 0;
$this->_state["auto_min"] = 0;
$this->_state["absolute"] = [];
$this->_state["percent"] = [];
$this->_state["auto"] = [];
$columns =& $cellmap->get_columns();
foreach ($columns as $i => $col) {
$this->_state["min_width"] += $col["min-width"];
$this->_state["max_width"] += $col["max-width"];
if ($col["absolute"] > 0) {
$this->_state["absolute"][] = $i;
$this->_state["absolute_used"] += $col["min-width"];
} elseif ($col["percent"] > 0) {
$this->_state["percent"][] = $i;
$this->_state["percent_used"] += $col["percent"];
} else {
$this->_state["auto"][] = $i;
$this->_state["auto_min"] += $col["min-width"];
}
}
// Account for margins, borders, padding, and border spacing
$cb_w = $this->_frame->get_containing_block("w");
$lm = (float) $style->length_in_pt($style->margin_left, $cb_w);
$rm = (float) $style->length_in_pt($style->margin_right, $cb_w);
$dims = [
$style->border_left_width,
$style->border_right_width,
$style->padding_left,
$style->padding_right
];
if ($style->border_collapse !== "collapse") {
list($dims[]) = $style->border_spacing;
}
$delta = (float) $style->length_in_pt($dims, $cb_w);
$this->_state["width_delta"] = $delta;
$min_width = $this->_state["min_width"] + $delta + $lm + $rm;
$max_width = $this->_state["max_width"] + $delta + $lm + $rm;
return $this->_min_max_cache = [
$min_width,
$max_width,
"min" => $min_width,
"max" => $max_width
];
}
}
dompdf/src/FrameReflower/TableCell.php 0000644 00000011573 15024772104 0013715 0 ustar 00 _set_content();
$style = $this->_frame->get_style();
$table = TableFrameDecorator::find_parent_table($this->_frame);
$cellmap = $table->get_cellmap();
list($x, $y) = $cellmap->get_frame_position($this->_frame);
$this->_frame->set_position($x, $y);
$cells = $cellmap->get_spanned_cells($this->_frame);
$w = 0;
foreach ($cells["columns"] as $i) {
$col = $cellmap->get_column($i);
$w += $col["used-width"];
}
//FIXME?
$h = $this->_frame->get_containing_block("h");
$left_space = (float)$style->length_in_pt([$style->margin_left,
$style->padding_left,
$style->border_left_width],
$w);
$right_space = (float)$style->length_in_pt([$style->padding_right,
$style->margin_right,
$style->border_right_width],
$w);
$top_space = (float)$style->length_in_pt([$style->margin_top,
$style->padding_top,
$style->border_top_width],
$h);
$bottom_space = (float)$style->length_in_pt([$style->margin_bottom,
$style->padding_bottom,
$style->border_bottom_width],
$h);
$cb_w = $w - $left_space - $right_space;
$style->set_used("width", $cb_w);
$content_x = $x + $left_space;
$content_y = $line_y = $y + $top_space;
// Adjust the first line based on the text-indent property
$indent = (float)$style->length_in_pt($style->text_indent, $w);
$this->_frame->increase_line_width($indent);
$page = $this->_frame->get_root();
// Set the y position of the first line in the cell
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $line_y;
// Set the containing blocks and reflow each child
foreach ($this->_frame->get_children() as $child) {
$child->set_containing_block($content_x, $content_y, $cb_w, $h);
$this->process_clear($child);
$child->reflow($this->_frame);
$this->process_float($child, $content_x, $cb_w);
if ($page->is_full()) {
break;
}
}
// Determine our height
$style_height = (float)$style->length_in_pt($style->height, $h);
/** @var FrameDecorator\TableCell */
$frame = $this->_frame;
$frame->set_content_height($this->_calculate_content_height());
$height = max($style_height, (float)$frame->get_content_height());
// Let the cellmap know our height
$cell_height = $height / count($cells["rows"]);
if ($style_height <= $height) {
$cell_height += $top_space + $bottom_space;
}
foreach ($cells["rows"] as $i) {
$cellmap->set_row_height($i, $cell_height);
}
$style->set_used("height", $height);
$this->_text_align();
$this->vertical_align();
// Handle relative positioning
foreach ($this->_frame->get_children() as $child) {
$this->position_relative($child);
}
}
public function get_min_max_content_width(): array
{
// Ignore percentage values for a specified width here, as they are
// relative to the table width, which is not determined yet
$style = $this->_frame->get_style();
$width = $style->width;
$fixed_width = $width !== "auto" && !Helpers::is_percent($width);
[$min, $max] = $this->get_min_max_child_width();
// For table cells: Use specified width if it is greater than the
// minimum defined by the content
if ($fixed_width) {
$width = (float) $style->length_in_pt($width, 0);
$min = max($width, $min);
$max = $min;
}
// Handle min/max width style properties
$min_width = $this->resolve_min_width(null);
$max_width = $this->resolve_max_width(null);
$min = Helpers::clamp($min, $min_width, $max_width);
$max = Helpers::clamp($max, $min_width, $max_width);
return [$min, $max];
}
}
dompdf/src/FrameReflower/Page.php 0000644 00000013526 15024772104 0012742 0 ustar 00 get_style();
$page_styles = $style->get_stylesheet()->get_page_styles();
// http://www.w3.org/TR/CSS21/page.html#page-selectors
if (count($page_styles) > 1) {
$odd = $page_number % 2 == 1;
$first = $page_number == 1;
$style = clone $page_styles["base"];
// FIXME RTL
if ($odd && isset($page_styles[":right"])) {
$style->merge($page_styles[":right"]);
}
if ($odd && isset($page_styles[":odd"])) {
$style->merge($page_styles[":odd"]);
}
// FIXME RTL
if (!$odd && isset($page_styles[":left"])) {
$style->merge($page_styles[":left"]);
}
if (!$odd && isset($page_styles[":even"])) {
$style->merge($page_styles[":even"]);
}
if ($first && isset($page_styles[":first"])) {
$style->merge($page_styles[":first"]);
}
$frame->set_style($style);
}
$frame->calculate_bottom_page_edge();
}
/**
* Paged layout:
* http://www.w3.org/TR/CSS21/page.html
*
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
/** @var PageFrameDecorator $frame */
$frame = $this->_frame;
$child = $frame->get_first_child();
$fixed_children = [];
$prev_child = null;
$current_page = 0;
while ($child) {
$this->apply_page_style($frame, $current_page + 1);
$style = $frame->get_style();
// Pages are only concerned with margins
$cb = $frame->get_containing_block();
$left = (float)$style->length_in_pt($style->margin_left, $cb["w"]);
$right = (float)$style->length_in_pt($style->margin_right, $cb["w"]);
$top = (float)$style->length_in_pt($style->margin_top, $cb["h"]);
$bottom = (float)$style->length_in_pt($style->margin_bottom, $cb["h"]);
$content_x = $cb["x"] + $left;
$content_y = $cb["y"] + $top;
$content_width = $cb["w"] - $left - $right;
$content_height = $cb["h"] - $top - $bottom;
// Only if it's the first page, we save the nodes with a fixed position
if ($current_page == 0) {
foreach ($child->get_children() as $onechild) {
if ($onechild->get_style()->position === "fixed") {
$fixed_children[] = $onechild->deep_copy();
}
}
$fixed_children = array_reverse($fixed_children);
}
$child->set_containing_block($content_x, $content_y, $content_width, $content_height);
// Check for begin reflow callback
$this->_check_callbacks("begin_page_reflow", $child);
//Insert a copy of each node which have a fixed position
if ($current_page >= 1) {
foreach ($fixed_children as $fixed_child) {
$child->insert_child_before($fixed_child->deep_copy(), $child->get_first_child());
}
}
$child->reflow();
$next_child = $child->get_next_sibling();
// Check for begin render callback
$this->_check_callbacks("begin_page_render", $child);
// Render the page
$frame->get_renderer()->render($child);
// Check for end render callback
$this->_check_callbacks("end_page_render", $child);
if ($next_child) {
$frame->next_page();
}
// Wait to dispose of all frames on the previous page
// so callback will have access to them
if ($prev_child) {
$prev_child->dispose(true);
}
$prev_child = $child;
$child = $next_child;
$current_page++;
}
// Dispose of previous page if it still exists
if ($prev_child) {
$prev_child->dispose(true);
}
}
/**
* Check for callbacks that need to be performed when a given event
* gets triggered on a page
*
* @param string $event The type of event
* @param Frame $frame The frame that event is triggered on
*/
protected function _check_callbacks(string $event, Frame $frame): void
{
if (!isset($this->_callbacks)) {
$dompdf = $this->get_dompdf();
$this->_callbacks = $dompdf->getCallbacks();
$this->_canvas = $dompdf->getCanvas();
}
if (isset($this->_callbacks[$event])) {
$fs = $this->_callbacks[$event];
$canvas = $this->_canvas;
$fontMetrics = $this->get_dompdf()->getFontMetrics();
foreach ($fs as $f) {
$f($frame, $canvas, $fontMetrics);
}
}
}
}
dompdf/src/FrameReflower/TableRow.php 0000644 00000004137 15024772104 0013603 0 ustar 00 _frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
// Counters and generated content
$this->_set_content();
$this->_frame->position();
$style = $this->_frame->get_style();
$cb = $this->_frame->get_containing_block();
foreach ($this->_frame->get_children() as $child) {
$child->set_containing_block($cb);
$child->reflow();
if ($page->is_full()) {
break;
}
}
if ($page->is_full()) {
return;
}
$table = TableFrameDecorator::find_parent_table($this->_frame);
$cellmap = $table->get_cellmap();
$style->set_used("width", $cellmap->get_frame_width($this->_frame));
$style->set_used("height", $cellmap->get_frame_height($this->_frame));
$this->_frame->set_position($cellmap->get_frame_position($this->_frame));
}
/**
* @throws Exception
*/
public function get_min_max_width(): array
{
throw new Exception("Min/max width is undefined for table rows");
}
}
dompdf/src/FrameReflower/NullFrameReflower.php 0000644 00000001267 15024772104 0015460 0 ustar 00 _frame;
$page = $frame->get_root();
// Counters and generated content
$this->_set_content();
$style = $frame->get_style();
$cb = $frame->get_containing_block();
foreach ($frame->get_children() as $child) {
$child->set_containing_block($cb["x"], $cb["y"], $cb["w"], $cb["h"]);
$child->reflow();
// Check if a split has occurred
$page->check_page_break($child);
if ($page->is_full()) {
break;
}
}
$table = TableFrameDecorator::find_parent_table($frame);
$cellmap = $table->get_cellmap();
// Stop reflow if a page break has occurred before the frame, in which
// case it is not part of its parent table's cell map yet
if ($page->is_full() && !$cellmap->frame_exists_in_cellmap($frame)) {
return;
}
$style->set_used("width", $cellmap->get_frame_width($frame));
$style->set_used("height", $cellmap->get_frame_height($frame));
$frame->set_position($cellmap->get_frame_position($frame));
}
}
dompdf/src/FrameReflower/Inline.php 0000644 00000013660 15024772104 0013303 0 ustar 00 _frame;
$style = $frame->get_style();
// Resolve width, so the margin width can be checked
$style->set_used("width", 0.0);
$cb = $frame->get_containing_block();
$line = $block->get_current_line_box();
$width = $frame->get_margin_width();
if ($width > ($cb["w"] - $line->left - $line->w - $line->right)) {
$block->add_line();
// Find the appropriate inline ancestor to split
$child = $frame;
$p = $child->get_parent();
while ($p instanceof InlineFrameDecorator && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if ($p instanceof InlineFrameDecorator) {
// Split parent and stop current reflow. Reflow continues
// via child-reflow loop of split parent
$p->split($child);
return;
}
}
$frame->position();
$block->add_frame_to_line($frame);
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
/** @var InlineFrameDecorator */
$frame = $this->_frame;
// Check if a page break is forced
$page = $frame->get_root();
$page->check_forced_page_break($frame);
if ($page->is_full()) {
return;
}
// Counters and generated content
$this->_set_content();
$style = $frame->get_style();
// Resolve auto margins
// https://www.w3.org/TR/CSS21/visudet.html#inline-width
// https://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced
if ($style->margin_left === "auto") {
$style->set_used("margin_left", 0.0);
}
if ($style->margin_right === "auto") {
$style->set_used("margin_right", 0.0);
}
if ($style->margin_top === "auto") {
$style->set_used("margin_top", 0.0);
}
if ($style->margin_bottom === "auto") {
$style->set_used("margin_bottom", 0.0);
}
// Handle line breaks
if ($frame->get_node()->nodeName === "br") {
if ($block) {
$line = $block->get_current_line_box();
$frame->set_containing_line($line);
$block->maximize_line_height($frame->get_margin_height(), $frame);
$block->add_line(true);
$next = $frame->get_next_sibling();
$p = $frame->get_parent();
if ($next && $p instanceof InlineFrameDecorator) {
$p->split($next);
}
}
return;
}
// Handle empty inline frames
if (!$frame->get_first_child()) {
if ($block) {
$this->reflow_empty($block);
}
return;
}
// Add our margin, padding & border to the first and last children
if (($f = $frame->get_first_child()) && $f instanceof TextFrameDecorator) {
$f_style = $f->get_style();
$f_style->margin_left = $style->margin_left;
$f_style->padding_left = $style->padding_left;
$f_style->border_left_width = $style->border_left_width;
$f_style->border_left_style = $style->border_left_style;
$f_style->border_left_color = $style->border_left_color;
}
if (($l = $frame->get_last_child()) && $l instanceof TextFrameDecorator) {
$l_style = $l->get_style();
$l_style->margin_right = $style->margin_right;
$l_style->padding_right = $style->padding_right;
$l_style->border_right_width = $style->border_right_width;
$l_style->border_right_style = $style->border_right_style;
$l_style->border_right_color = $style->border_right_color;
}
$cb = $frame->get_containing_block();
// Set the containing blocks and reflow each child. The containing
// block is not changed by line boxes.
foreach ($frame->get_children() as $child) {
$child->set_containing_block($cb);
$child->reflow($block);
// Stop reflow if the frame has been reset by a line or page break
// due to child reflow
if (!$frame->content_set) {
return;
}
}
if (!$frame->get_first_child()) {
return;
}
// Assume the position of the first child
[$x, $y] = $frame->get_first_child()->get_position();
$frame->set_position($x, $y);
// Handle relative positioning
foreach ($frame->get_children() as $child) {
$this->position_relative($child);
}
if ($block) {
$block->add_frame_to_line($frame);
}
}
}
dompdf/src/FrameReflower/Block.php 0000644 00000103364 15024772104 0013120 0 ustar 00 _frame;
$style = $frame->get_style();
$absolute = $frame->is_absolute();
$cb = $frame->get_containing_block();
$w = $cb["w"];
$rm = $style->length_in_pt($style->margin_right, $w);
$lm = $style->length_in_pt($style->margin_left, $w);
$left = $style->length_in_pt($style->left, $w);
$right = $style->length_in_pt($style->right, $w);
// Handle 'auto' values
$dims = [$style->border_left_width,
$style->border_right_width,
$style->padding_left,
$style->padding_right,
$width !== "auto" ? $width : 0,
$rm !== "auto" ? $rm : 0,
$lm !== "auto" ? $lm : 0];
// absolutely positioned boxes take the 'left' and 'right' properties into account
if ($absolute) {
$dims[] = $left !== "auto" ? $left : 0;
$dims[] = $right !== "auto" ? $right : 0;
}
$sum = (float)$style->length_in_pt($dims, $w);
// Compare to the containing block
$diff = $w - $sum;
if ($absolute) {
// Absolutely positioned
// http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
if ($width === "auto" || $left === "auto" || $right === "auto") {
// "all of the three are 'auto'" logic + otherwise case
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
$block_parent = $frame->find_block_parent();
$parent_content = $block_parent->get_content_box();
$line = $block_parent->get_current_line_box();
// TODO: This is the in-flow inline position. Use the in-flow
// block position if the original display type is block-level
$inflow_x = $parent_content["x"] - $cb["x"] + $line->left + $line->w;
if ($width === "auto" && $left === "auto" && $right === "auto") {
// rule 3, per instruction preceding rule set
// shrink-to-fit width
$left = $inflow_x;
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff - $left), $max);
$right = $diff - $left - $width;
} elseif ($width === "auto" && $left === "auto") {
// rule 1
// shrink-to-fit width
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
$left = $diff - $width;
} elseif ($width === "auto" && $right === "auto") {
// rule 3
// shrink-to-fit width
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
$right = $diff - $width;
} elseif ($left === "auto" && $right === "auto") {
// rule 2
$left = $inflow_x;
$right = $diff - $left;
} elseif ($left === "auto") {
// rule 4
$left = $diff;
} elseif ($width === "auto") {
// rule 5
$width = max($diff, 0);
} else {
// $right === "auto"
// rule 6
$right = $diff;
}
} else {
// "none of the three are 'auto'" logic described in paragraph preceding the rules
if ($diff >= 0) {
if ($lm === "auto" && $rm === "auto") {
$lm = $rm = $diff / 2;
} elseif ($lm === "auto") {
$lm = $diff;
} elseif ($rm === "auto") {
$rm = $diff;
}
} else {
// over-constrained, solve for right
$right = $right + $diff;
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
}
}
} elseif ($style->float !== "none" || $style->display === "inline-block") {
// Shrink-to-fit width for float and inline block
// https://www.w3.org/TR/CSS21/visudet.html#float-width
// https://www.w3.org/TR/CSS21/visudet.html#inlineblock-width
if ($width === "auto") {
[$min, $max] = $this->get_min_max_child_width();
$width = min(max($min, $diff), $max);
}
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
} else {
// Block-level, normal flow
// https://www.w3.org/TR/CSS21/visudet.html#blockwidth
if ($diff >= 0) {
// Find auto properties and get them to take up the slack
if ($width === "auto") {
$width = $diff;
if ($lm === "auto") {
$lm = 0;
}
if ($rm === "auto") {
$rm = 0;
}
} elseif ($lm === "auto" && $rm === "auto") {
$lm = $rm = $diff / 2;
} elseif ($lm === "auto") {
$lm = $diff;
} elseif ($rm === "auto") {
$rm = $diff;
}
} else {
// We are over constrained--set margin-right to the difference
$rm = (float) $rm + $diff;
if ($width === "auto") {
$width = 0;
}
if ($lm === "auto") {
$lm = 0;
}
}
}
return [
"width" => $width,
"margin_left" => $lm,
"margin_right" => $rm,
"left" => $left,
"right" => $right,
];
}
/**
* Call the above function, but resolve max/min widths
*
* @throws Exception
* @return array
*/
protected function _calculate_restricted_width()
{
$frame = $this->_frame;
$style = $frame->get_style();
$cb = $frame->get_containing_block();
if (!isset($cb["w"])) {
throw new Exception("Box property calculation requires containing block width");
}
$width = $style->length_in_pt($style->width, $cb["w"]);
$values = $this->_calculate_width($width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
// Handle min/max width
// https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
$min_width = $this->resolve_min_width($cb["w"]);
$max_width = $this->resolve_max_width($cb["w"]);
if ($width > $max_width) {
$values = $this->_calculate_width($max_width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
}
if ($width < $min_width) {
$values = $this->_calculate_width($min_width);
$margin_left = $values["margin_left"];
$margin_right = $values["margin_right"];
$width = $values["width"];
$left = $values["left"];
$right = $values["right"];
}
return [$width, $margin_left, $margin_right, $left, $right];
}
/**
* Determine the unrestricted height of content within the block
* not by adding each line's height, but by getting the last line's position.
* This because lines could have been pushed lower by a clearing element.
*
* @return float
*/
protected function _calculate_content_height()
{
$height = 0;
$lines = $this->_frame->get_line_boxes();
if (count($lines) > 0) {
$last_line = end($lines);
$content_box = $this->_frame->get_content_box();
$height = $last_line->y + $last_line->h - $content_box["y"];
}
return $height;
}
/**
* Determine the frame's restricted height
*
* @return array
*/
protected function _calculate_restricted_height()
{
$frame = $this->_frame;
$style = $frame->get_style();
$content_height = $this->_calculate_content_height();
$cb = $frame->get_containing_block();
$height = $style->length_in_pt($style->height, $cb["h"]);
$margin_top = $style->length_in_pt($style->margin_top, $cb["w"]);
$margin_bottom = $style->length_in_pt($style->margin_bottom, $cb["w"]);
$top = $style->length_in_pt($style->top, $cb["h"]);
$bottom = $style->length_in_pt($style->bottom, $cb["h"]);
if ($frame->is_absolute()) {
// Absolutely positioned
// http://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-height
$h_dims = [
$top !== "auto" ? $top : 0,
$height !== "auto" ? $height : 0,
$bottom !== "auto" ? $bottom : 0
];
$w_dims = [
$style->margin_top !== "auto" ? $style->margin_top : 0,
$style->padding_top,
$style->border_top_width,
$style->border_bottom_width,
$style->padding_bottom,
$style->margin_bottom !== "auto" ? $style->margin_bottom : 0
];
$sum = (float)$style->length_in_pt($h_dims, $cb["h"])
+ (float)$style->length_in_pt($w_dims, $cb["w"]);
$diff = $cb["h"] - $sum;
if ($height === "auto" || $top === "auto" || $bottom === "auto") {
// "all of the three are 'auto'" logic + otherwise case
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
$block_parent = $frame->find_block_parent();
$current_line = $block_parent->get_current_line_box();
// TODO: This is the in-flow inline position. Use the in-flow
// block position if the original display type is block-level
$inflow_y = $current_line->y - $cb["y"];
if ($height === "auto" && $top === "auto" && $bottom === "auto") {
// rule 3, per instruction preceding rule set
$top = $inflow_y;
$height = $content_height;
$bottom = $diff - $top - $height;
} elseif ($height === "auto" && $top === "auto") {
// rule 1
$height = $content_height;
$top = $diff - $height;
} elseif ($height === "auto" && $bottom === "auto") {
// rule 3
$height = $content_height;
$bottom = $diff - $height;
} elseif ($top === "auto" && $bottom === "auto") {
// rule 2
$top = $inflow_y;
$bottom = $diff - $top;
} elseif ($top === "auto") {
// rule 4
$top = $diff;
} elseif ($height === "auto") {
// rule 5
$height = max($diff, 0);
} else {
// $bottom === "auto"
// rule 6
$bottom = $diff;
}
} else {
// "none of the three are 'auto'" logic described in paragraph preceding the rules
if ($diff >= 0) {
if ($margin_top === "auto" && $margin_bottom === "auto") {
$margin_top = $margin_bottom = $diff / 2;
} elseif ($margin_top === "auto") {
$margin_top = $diff;
} elseif ($margin_bottom === "auto") {
$margin_bottom = $diff;
}
} else {
// over-constrained, solve for bottom
$bottom = $bottom + $diff;
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
}
}
} else {
// https://www.w3.org/TR/CSS21/visudet.html#normal-block
// https://www.w3.org/TR/CSS21/visudet.html#block-root-margin
if ($height === "auto") {
$height = $content_height;
}
if ($margin_top === "auto") {
$margin_top = 0;
}
if ($margin_bottom === "auto") {
$margin_bottom = 0;
}
// Handle min/max height
// https://www.w3.org/TR/CSS21/visudet.html#min-max-heights
$min_height = $this->resolve_min_height($cb["h"]);
$max_height = $this->resolve_max_height($cb["h"]);
$height = Helpers::clamp($height, $min_height, $max_height);
}
// TODO: Need to also take min/max height into account for absolute
// positioning, using similar logic to the `_calculate_width`/
// `calculate_restricted_width` split above. The non-absolute case
// can simply clamp height within min/max, as margins and offsets are
// not affected
return [$height, $margin_top, $margin_bottom, $top, $bottom];
}
/**
* Adjust the justification of each of our lines.
* http://www.w3.org/TR/CSS21/text.html#propdef-text-align
*/
protected function _text_align()
{
$style = $this->_frame->get_style();
$w = $this->_frame->get_containing_block("w");
$width = (float)$style->length_in_pt($style->width, $w);
$text_indent = (float)$style->length_in_pt($style->text_indent, $w);
switch ($style->text_align) {
default:
case "left":
foreach ($this->_frame->get_line_boxes() as $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
if ($line->left) {
foreach ($line->frames_to_align() as $frame) {
$frame->move($line->left, 0);
}
}
}
break;
case "right":
foreach ($this->_frame->get_line_boxes() as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
$indent = $i === 0 ? $text_indent : 0;
$dx = $width - $line->w - $line->right - $indent;
foreach ($line->frames_to_align() as $frame) {
$frame->move($dx, 0);
}
}
break;
case "justify":
// We justify all lines except the last one, unless the frame
// has been split, in which case the actual last line is part of
// the split-off frame
$lines = $this->_frame->get_line_boxes();
$last_line_index = $this->_frame->is_split ? null : count($lines) - 1;
foreach ($lines as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
if ($line->left) {
foreach ($line->frames_to_align() as $frame) {
$frame->move($line->left, 0);
}
}
if ($line->br || $i === $last_line_index) {
continue;
}
$frames = $line->get_frames();
$other_frame_count = 0;
foreach ($frames as $frame) {
if (!($frame instanceof TextFrameDecorator)) {
$other_frame_count++;
}
}
$word_count = $line->wc + $other_frame_count;
// Set the spacing for each child
if ($word_count > 1) {
$indent = $i === 0 ? $text_indent : 0;
$spacing = ($width - $line->get_width() - $indent) / ($word_count - 1);
} else {
$spacing = 0;
}
$dx = 0;
foreach ($frames as $frame) {
if ($frame instanceof TextFrameDecorator) {
$text = $frame->get_text();
$spaces = mb_substr_count($text, " ");
$frame->move($dx, 0);
$frame->set_text_spacing($spacing);
$dx += $spaces * $spacing;
} else {
$frame->move($dx, 0);
}
}
// The line (should) now occupy the entire width
$line->w = $width;
}
break;
case "center":
case "centre":
foreach ($this->_frame->get_line_boxes() as $i => $line) {
if (!$line->inline) {
continue;
}
$line->trim_trailing_ws();
$indent = $i === 0 ? $text_indent : 0;
$dx = ($width + $line->left - $line->w - $line->right - $indent) / 2;
foreach ($line->frames_to_align() as $frame) {
$frame->move($dx, 0);
}
}
break;
}
}
/**
* Align inline children vertically.
* Aligns each child vertically after each line is reflowed
*/
function vertical_align()
{
$fontMetrics = $this->get_dompdf()->getFontMetrics();
foreach ($this->_frame->get_line_boxes() as $line) {
$height = $line->h;
// Move all markers to the top of the line box
foreach ($line->get_list_markers() as $marker) {
$x = $marker->get_position("x");
$marker->set_position($x, $line->y);
}
foreach ($line->frames_to_align() as $frame) {
$style = $frame->get_style();
$isInlineBlock = $style->display !== "inline"
&& $style->display !== "-dompdf-list-bullet";
$baseline = $fontMetrics->getFontBaseline($style->font_family, $style->font_size);
$y_offset = 0;
//FIXME: The 0.8 ratio applied to the height is arbitrary (used to accommodate descenders?)
if ($isInlineBlock) {
// Workaround: Skip vertical alignment if the frame is the
// only one one the line, excluding empty text frames, which
// may be the result of trailing white space
// FIXME: This special case should be removed once vertical
// alignment is properly fixed
$skip = true;
foreach ($line->get_frames() as $other) {
if ($other !== $frame
&& !($other->is_text_node() && $other->get_node()->nodeValue === "")
) {
$skip = false;
break;
}
}
if ($skip) {
continue;
}
$marginHeight = $frame->get_margin_height();
$imageHeightDiff = $height * 0.8 - $marginHeight;
$align = $frame->get_style()->vertical_align;
if (in_array($align, Style::VERTICAL_ALIGN_KEYWORDS, true)) {
switch ($align) {
case "middle":
$y_offset = $imageHeightDiff / 2;
break;
case "sub":
$y_offset = 0.3 * $height + $imageHeightDiff;
break;
case "super":
$y_offset = -0.2 * $height + $imageHeightDiff;
break;
case "text-top": // FIXME: this should be the height of the frame minus the height of the text
$y_offset = $height - $style->line_height;
break;
case "top":
break;
case "text-bottom": // FIXME: align bottom of image with the descender?
case "bottom":
$y_offset = 0.3 * $height + $imageHeightDiff;
break;
case "baseline":
default:
$y_offset = $imageHeightDiff;
break;
}
} else {
$y_offset = $baseline - (float)$style->length_in_pt($align, $style->font_size) - $marginHeight;
}
} else {
$parent = $frame->get_parent();
if ($parent instanceof TableCellFrameDecorator) {
$align = "baseline";
} else {
$align = $parent->get_style()->vertical_align;
}
if (in_array($align, Style::VERTICAL_ALIGN_KEYWORDS, true)) {
switch ($align) {
case "middle":
$y_offset = ($height * 0.8 - $baseline) / 2;
break;
case "sub":
$y_offset = $height * 0.8 - $baseline * 0.5;
break;
case "super":
$y_offset = $height * 0.8 - $baseline * 1.4;
break;
case "text-top":
case "top": // Not strictly accurate, but good enough for now
break;
case "text-bottom":
case "bottom":
$y_offset = $height * 0.8 - $baseline;
break;
case "baseline":
default:
$y_offset = $height * 0.8 - $baseline;
break;
}
} else {
$y_offset = $height * 0.8 - $baseline - (float)$style->length_in_pt($align, $style->font_size);
}
}
if ($y_offset !== 0) {
$frame->move(0, $y_offset);
}
}
}
}
/**
* @param AbstractFrameDecorator $child
*/
function process_clear(AbstractFrameDecorator $child)
{
$child_style = $child->get_style();
$root = $this->_frame->get_root();
// Handle "clear"
if ($child_style->clear !== "none") {
//TODO: this is a WIP for handling clear/float frames that are in between inline frames
if ($child->get_prev_sibling() !== null) {
$this->_frame->add_line();
}
if ($child_style->float !== "none" && $child->get_next_sibling()) {
$this->_frame->set_current_line_number($this->_frame->get_current_line_number() - 1);
}
$lowest_y = $root->get_lowest_float_offset($child);
// If a float is still applying, we handle it
if ($lowest_y) {
if ($child->is_in_flow()) {
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $lowest_y + $child->get_margin_height();
$line_box->left = 0;
$line_box->right = 0;
}
$child->move(0, $lowest_y - $child->get_position("y"));
}
}
}
/**
* @param AbstractFrameDecorator $child
* @param float $cb_x
* @param float $cb_w
*/
function process_float(AbstractFrameDecorator $child, $cb_x, $cb_w)
{
$child_style = $child->get_style();
$root = $this->_frame->get_root();
// Handle "float"
if ($child_style->float !== "none") {
$root->add_floating_frame($child);
// Remove next frame's beginning whitespace
$next = $child->get_next_sibling();
if ($next && $next instanceof TextFrameDecorator) {
$next->set_text(ltrim($next->get_text()));
}
$line_box = $this->_frame->get_current_line_box();
list($old_x, $old_y) = $child->get_position();
$float_x = $cb_x;
$float_y = $old_y;
$float_w = $child->get_margin_width();
if ($child_style->clear === "none") {
switch ($child_style->float) {
case "left":
$float_x += $line_box->left;
break;
case "right":
$float_x += ($cb_w - $line_box->right - $float_w);
break;
}
} else {
if ($child_style->float === "right") {
$float_x += ($cb_w - $float_w);
}
}
if ($cb_w < $float_x + $float_w - $old_x) {
// TODO handle when floating elements don't fit
}
$line_box->get_float_offsets();
if ($child->_float_next_line) {
$float_y += $line_box->h;
}
$child->set_position($float_x, $float_y);
$child->move($float_x - $old_x, $float_y - $old_y, true);
}
}
/**
* @param BlockFrameDecorator $block
*/
function reflow(BlockFrameDecorator $block = null)
{
// Check if a page break is forced
$page = $this->_frame->get_root();
$page->check_forced_page_break($this->_frame);
// Bail if the page is full
if ($page->is_full()) {
return;
}
$this->determine_absolute_containing_block();
// Counters and generated content
$this->_set_content();
// Inherit any dangling list markers
if ($block && $this->_frame->is_in_flow()) {
$this->_frame->inherit_dangling_markers($block);
}
// Collapse margins if required
$this->_collapse_margins();
$style = $this->_frame->get_style();
$cb = $this->_frame->get_containing_block();
// Determine the constraints imposed by this frame: calculate the width
// of the content area:
[$width, $margin_left, $margin_right, $left, $right] = $this->_calculate_restricted_width();
// Store the calculated properties
$style->set_used("width", $width);
$style->set_used("margin_left", $margin_left);
$style->set_used("margin_right", $margin_right);
$style->set_used("left", $left);
$style->set_used("right", $right);
$margin_top = $style->length_in_pt($style->margin_top, $cb["w"]);
$margin_bottom = $style->length_in_pt($style->margin_bottom, $cb["w"]);
$auto_top = $style->top === "auto";
$auto_margin_top = $margin_top === "auto";
// Update the position
$this->_frame->position();
[$x, $y] = $this->_frame->get_position();
// Adjust the first line based on the text-indent property
$indent = (float)$style->length_in_pt($style->text_indent, $cb["w"]);
$this->_frame->increase_line_width($indent);
// Determine the content edge
$top = (float)$style->length_in_pt([
$margin_top !== "auto" ? $margin_top : 0,
$style->border_top_width,
$style->padding_top
], $cb["w"]);
$bottom = (float)$style->length_in_pt([
$margin_bottom !== "auto" ? $margin_bottom : 0,
$style->border_bottom_width,
$style->padding_bottom
], $cb["w"]);
$cb_x = $x + (float)$margin_left + (float)$style->length_in_pt([$style->border_left_width,
$style->padding_left], $cb["w"]);
$cb_y = $y + $top;
$height = $style->length_in_pt($style->height, $cb["h"]);
if ($height === "auto") {
$height = ($cb["h"] + $cb["y"]) - $bottom - $cb_y;
}
// Set the y position of the first line in this block
$line_box = $this->_frame->get_current_line_box();
$line_box->y = $cb_y;
$line_box->get_float_offsets();
// Set the containing blocks and reflow each child
foreach ($this->_frame->get_children() as $child) {
$child->set_containing_block($cb_x, $cb_y, $width, $height);
$this->process_clear($child);
$child->reflow($this->_frame);
// Check for a page break before the child
$page->check_page_break($child);
// Don't add the child to the line if a page break has occurred
// before it (possibly via a descendant), in which case it has been
// reset, including its position
if ($page->is_full() && $child->get_position("x") === null) {
break;
}
$this->process_float($child, $cb_x, $width);
}
// Stop reflow if a page break has occurred before the frame, in which
// case it has been reset, including its position
if ($page->is_full() && $this->_frame->get_position("x") === null) {
return;
}
// Determine our height
[$height, $margin_top, $margin_bottom, $top, $bottom] = $this->_calculate_restricted_height();
$style->set_used("height", $height);
$style->set_used("margin_top", $margin_top);
$style->set_used("margin_bottom", $margin_bottom);
$style->set_used("top", $top);
$style->set_used("bottom", $bottom);
if ($this->_frame->is_absolute()) {
if ($auto_top) {
$this->_frame->move(0, $top);
}
if ($auto_margin_top) {
$this->_frame->move(0, $margin_top, true);
}
}
$this->_text_align();
$this->vertical_align();
// Handle relative positioning
foreach ($this->_frame->get_children() as $child) {
$this->position_relative($child);
}
if ($block && $this->_frame->is_in_flow()) {
$block->add_frame_to_line($this->_frame);
if ($this->_frame->is_block_level()) {
$block->add_line();
}
}
}
public function get_min_max_content_width(): array
{
// TODO: While the containing block is not set yet on the frame, it can
// already be determined in some cases due to fixed dimensions on the
// ancestor forming the containing block. In such cases, percentage
// values could be resolved here
$style = $this->_frame->get_style();
$width = $style->width;
$fixed_width = $width !== "auto" && !Helpers::is_percent($width);
// If the frame has a specified width, then we don't need to check
// its children
if ($fixed_width) {
$min = (float) $style->length_in_pt($width, 0);
$max = $min;
} else {
[$min, $max] = $this->get_min_max_child_width();
}
// Handle min/max width style properties
$min_width = $this->resolve_min_width(null);
$max_width = $this->resolve_max_width(null);
$min = Helpers::clamp($min, $min_width, $max_width);
$max = Helpers::clamp($max, $min_width, $max_width);
return [$min, $max];
}
}
dompdf/src/FrameReflower/Text.php 0000644 00000050262 15024772104 0013010 0 ustar 00
*/
const SOFT_HYPHEN = "\xC2\xAD";
/**
* The regex splits on everything that's a separator (^\S double negative),
* excluding the following non-breaking space characters:
* * nbsp (\xA0)
* * narrow nbsp (\x{202F})
* * figure space (\x{2007})
*/
public static $_whitespace_pattern = '/([^\S\xA0\x{202F}\x{2007}]+)/u';
/**
* The regex splits on everything that's a separator (^\S double negative)
* plus dashes, excluding the following non-breaking space characters:
* * nbsp (\xA0)
* * narrow nbsp (\x{202F})
* * figure space (\x{2007})
*/
public static $_wordbreak_pattern = '/([^\S\xA0\x{202F}\x{2007}\n]+|\R|\-+|\xAD+)/u';
/**
* Frame for this reflower
*
* @var TextFrameDecorator
*/
protected $_frame;
/**
* Saves trailing whitespace trimmed after a line break, so it can be
* restored when needed.
*
* @var string|null
*/
protected $trailingWs = null;
/**
* @var FontMetrics
*/
private $fontMetrics;
/**
* @param TextFrameDecorator $frame
* @param FontMetrics $fontMetrics
*/
public function __construct(TextFrameDecorator $frame, FontMetrics $fontMetrics)
{
parent::__construct($frame);
$this->setFontMetrics($fontMetrics);
}
/**
* Apply text transform and white-space collapse according to style.
*
* * http://www.w3.org/TR/CSS21/text.html#propdef-text-transform
* * http://www.w3.org/TR/CSS21/text.html#propdef-white-space
*
* @param string $text
* @return string
*/
protected function pre_process_text(string $text): string
{
$style = $this->_frame->get_style();
// Handle text transform
switch ($style->text_transform) {
case "capitalize":
$text = Helpers::mb_ucwords($text);
break;
case "uppercase":
$text = mb_convert_case($text, MB_CASE_UPPER);
break;
case "lowercase":
$text = mb_convert_case($text, MB_CASE_LOWER);
break;
default:
break;
}
// Handle white-space collapse
switch ($style->white_space) {
default:
case "normal":
case "nowrap":
$text = preg_replace(self::$_whitespace_pattern, " ", $text) ?? "";
break;
case "pre-line":
// Collapse white space except for line breaks
$text = preg_replace('/([^\S\xA0\x{202F}\x{2007}\n]+)/u', " ", $text) ?? "";
break;
case "pre":
case "pre-wrap":
break;
}
return $text;
}
/**
* @param string $text
* @param BlockFrameDecorator $block
* @param bool $nowrap
*
* @return bool|int
*/
protected function line_break(string $text, BlockFrameDecorator $block, bool $nowrap = false)
{
$fontMetrics = $this->getFontMetrics();
$frame = $this->_frame;
$style = $frame->get_style();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
// Determine the available width
$current_line = $block->get_current_line_box();
$line_width = $frame->get_containing_block("w");
$current_line_width = $current_line->left + $current_line->w + $current_line->right;
$available_width = $line_width - $current_line_width;
// Determine the frame width including margin, padding & border
$visible_text = preg_replace('/\xAD/u', "", $text);
$text_width = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
$mbp_width = (float) $style->length_in_pt([
$style->margin_left,
$style->border_left_width,
$style->padding_left,
$style->padding_right,
$style->border_right_width,
$style->margin_right
], $line_width);
$frame_width = $text_width + $mbp_width;
if (Helpers::lengthLessOrEqual($frame_width, $available_width)) {
return false;
}
if ($nowrap) {
return $current_line_width == 0 ? false : 0;
}
// Split the text into words
$words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$wc = count($words);
// Determine the split point
$width = 0.0;
$str = "";
$space_width = $fontMetrics->getTextWidth(" ", $font, $size, $word_spacing, $letter_spacing);
$shy_width = $fontMetrics->getTextWidth(self::SOFT_HYPHEN, $font, $size);
// @todo support
for ($i = 0; $i < $wc; $i += 2) {
// Allow trailing white space to overflow. White space is always
// collapsed to the standard space character currently, so only
// handle that for now
$sep = $words[$i + 1] ?? "";
$word = $sep === " " ? $words[$i] : $words[$i] . $sep;
$word_width = $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
$used_width = $width + $word_width + $mbp_width;
if (Helpers::lengthGreater($used_width, $available_width)) {
// If the previous split happened by soft hyphen, we have to
// append its width again because the last hyphen of a line
// won't be removed
if (isset($words[$i - 1]) && self::SOFT_HYPHEN === $words[$i - 1]) {
$width += $shy_width;
}
break;
}
// If the word is splitted by soft hyphen, but no line break is needed
// we have to reduce the width. But the str is not modified, otherwise
// the wrong offset is calculated at the end of this method.
if ($sep === self::SOFT_HYPHEN) {
$width += $word_width - $shy_width;
$str .= $word;
} elseif ($sep === " ") {
$width += $word_width + $space_width;
$str .= $word . $sep;
} else {
$width += $word_width;
$str .= $word;
}
}
// The first word has overflowed. Force it onto the line, or as many
// characters as fit if breaking words is allowed
if ($current_line_width == 0 && $width === 0.0) {
if ($sep === " ") {
$word .= $sep;
}
// https://www.w3.org/TR/css-text-3/#overflow-wrap-property
$wrap = $style->overflow_wrap;
$break_word = $wrap === "anywhere" || $wrap === "break-word";
if ($break_word) {
$s = "";
for ($j = 0; $j < mb_strlen($word); $j++) {
$c = mb_substr($word, $j, 1);
$w = $fontMetrics->getTextWidth($s . $c, $font, $size, $word_spacing, $letter_spacing);
if (Helpers::lengthGreater($w, $available_width)) {
break;
}
$s .= $c;
}
// Always force the first character onto the line
$str = $j === 0 ? $s . $c : $s;
} else {
$str = $word;
}
}
$offset = mb_strlen($str);
return $offset;
}
/**
* @param string $text
* @return bool|int
*/
protected function newline_break(string $text)
{
if (($i = mb_strpos($text, "\n")) === false) {
return false;
}
return $i + 1;
}
/**
* @param BlockFrameDecorator $block
* @return bool|null Whether to add a new line at the end. `null` if reflow
* should be stopped.
*/
protected function layout_line(BlockFrameDecorator $block): ?bool
{
$frame = $this->_frame;
$style = $frame->get_style();
$current_line = $block->get_current_line_box();
$text = $frame->get_text();
// Trim leading white space if this is the first text on the line
if ($current_line->w === 0.0 && !$frame->is_pre()) {
$text = ltrim($text, " ");
}
if ($text === "") {
$frame->set_text("");
$style->set_used("width", 0.0);
return false;
}
// Determine the next line break
// http://www.w3.org/TR/CSS21/text.html#propdef-white-space
$white_space = $style->white_space;
$nowrap = $white_space === "nowrap" || $white_space === "pre";
switch ($white_space) {
default:
case "normal":
case "nowrap":
$split = $this->line_break($text, $block, $nowrap);
$add_line = false;
break;
case "pre":
case "pre-line":
case "pre-wrap":
$hard_split = $this->newline_break($text);
$first_line = $hard_split !== false
? mb_substr($text, 0, $hard_split)
: $text;
$soft_split = $this->line_break($first_line, $block, $nowrap);
$split = $soft_split !== false ? $soft_split : $hard_split;
$add_line = $hard_split !== false;
break;
}
if ($split === 0) {
// Make sure to move text when floating frames leave no space to
// place anything onto the line
// TODO: Would probably be better to move just below the current
// floating frame instead of trying to place text in line-height
// increments
if ($current_line->h === 0.0) {
// Line height might be 0
$h = max($frame->get_margin_height(), 1.0);
$block->maximize_line_height($h, $frame);
}
// Break line and repeat layout
$block->add_line();
// Find the appropriate inline ancestor to split
$child = $frame;
$p = $child->get_parent();
while ($p instanceof InlineFrameDecorator && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if ($p instanceof InlineFrameDecorator) {
// Split parent and stop current reflow. Reflow continues
// via child-reflow loop of split parent
$p->split($child);
return null;
}
return $this->layout_line($block);
}
// Final split point is determined
if ($split !== false && $split < mb_strlen($text)) {
// Split the line
$frame->set_text($text);
$frame->split_text($split);
$add_line = true;
// Remove inner soft hyphens
$t = $frame->get_text();
$shyPosition = mb_strpos($t, self::SOFT_HYPHEN);
if (false !== $shyPosition && $shyPosition < mb_strlen($t) - 1) {
$t = str_replace(self::SOFT_HYPHEN, "", mb_substr($t, 0, -1)) . mb_substr($t, -1);
$frame->set_text($t);
}
} else {
// No split required
// Remove soft hyphens
$text = str_replace(self::SOFT_HYPHEN, "", $text);
$frame->set_text($text);
}
// Set our new width
$frame->recalculate_width();
return $add_line;
}
/**
* @param BlockFrameDecorator|null $block
*/
function reflow(BlockFrameDecorator $block = null)
{
$frame = $this->_frame;
$page = $frame->get_root();
$page->check_forced_page_break($frame);
if ($page->is_full()) {
return;
}
// Determine the text height
$style = $frame->get_style();
$size = $style->font_size;
$font = $style->font_family;
$font_height = $this->getFontMetrics()->getFontHeight($font, $size);
$style->set_used("height", $font_height);
// Handle text transform and white space
$text = $this->pre_process_text($frame->get_text());
$frame->set_text($text);
if ($block === null) {
return;
}
$add_line = $this->layout_line($block);
if ($add_line === null) {
return;
}
$frame->position();
// Skip wrapped white space between block-level elements in case white
// space is collapsed
if ($frame->get_text() === "" && $frame->get_margin_width() === 0.0) {
return;
}
$line = $block->add_frame_to_line($frame);
$trimmed = trim($frame->get_text());
// Split the text into words (used to determine spacing between
// words on justified lines)
if ($trimmed !== "") {
$words = preg_split(self::$_whitespace_pattern, $trimmed);
$line->wc += count($words);
}
if ($add_line) {
$block->add_line();
}
}
/**
* Trim trailing white space from the frame text.
*/
public function trim_trailing_ws(): void
{
$frame = $this->_frame;
$text = $frame->get_text();
$trailing = mb_substr($text, -1);
// White space is always collapsed to the standard space character
// currently, so only handle that for now
if ($trailing === " ") {
$this->trailingWs = $trailing;
$frame->set_text(mb_substr($text, 0, -1));
$frame->recalculate_width();
}
}
public function reset(): void
{
parent::reset();
// Restore trimmed trailing white space, as the frame will go through
// another reflow and line breaks might be different after a split
if ($this->trailingWs !== null) {
$text = $this->_frame->get_text();
$this->_frame->set_text($text . $this->trailingWs);
$this->trailingWs = null;
}
}
//........................................................................
public function get_min_max_width(): array
{
$fontMetrics = $this->getFontMetrics();
$frame = $this->_frame;
$style = $frame->get_style();
$text = $frame->get_text();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
// Handle text transform and white space
$text = $this->pre_process_text($frame->get_text());
if (!$frame->is_pre()) {
// Determine whether the frame is at the start of its parent block.
// Trim leading white space in that case
$child = $frame;
$p = $frame->get_parent();
while (!$p->is_block() && !$child->get_prev_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if (!$child->get_prev_sibling()) {
$text = ltrim($text, " ");
}
// Determine whether the frame is at the end of its parent block.
// Trim trailing white space in that case
$child = $frame;
$p = $frame->get_parent();
while (!$p->is_block() && !$child->get_next_sibling()) {
$child = $p;
$p = $p->get_parent();
}
if (!$child->get_next_sibling()) {
$text = rtrim($text, " ");
}
}
// Strip soft hyphens for max-line-width calculations
$visible_text = preg_replace('/\xAD/u', "", $text);
// Determine minimum text width
switch ($style->white_space) {
default:
case "normal":
case "pre-line":
case "pre-wrap":
// The min width is the longest word or, if breaking words is
// allowed with the `anywhere` keyword, the widest character.
// For performance reasons, we only check the first character in
// the latter case.
// https://www.w3.org/TR/css-text-3/#overflow-wrap-property
if ($style->overflow_wrap === "anywhere") {
$char = mb_substr($visible_text, 0, 1);
$min = $fontMetrics->getTextWidth($char, $font, $size, $word_spacing, $letter_spacing);
} else {
// Find the longest word
$words = preg_split(self::$_wordbreak_pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$lengths = array_map(function ($chunk) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
// Allow trailing white space to overflow. As in actual
// layout above, only handle a single space for now
$sep = $chunk[1] ?? "";
$word = $sep === " " ? $chunk[0] : $chunk[0] . $sep;
return $fontMetrics->getTextWidth($word, $font, $size, $word_spacing, $letter_spacing);
}, array_chunk($words, 2));
$min = max($lengths);
}
break;
case "pre":
// Find the longest line
$lines = array_flip(preg_split("/\R/u", $visible_text));
array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
$chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
});
arsort($lines);
$min = reset($lines);
break;
case "nowrap":
$min = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
break;
}
// Determine maximum text width
switch ($style->white_space) {
default:
case "normal":
$max = $fontMetrics->getTextWidth($visible_text, $font, $size, $word_spacing, $letter_spacing);
break;
case "pre-line":
case "pre-wrap":
// Find the longest line
$lines = array_flip(preg_split("/\R/u", $visible_text));
array_walk($lines, function (&$chunked_text_width, $chunked_text) use ($fontMetrics, $font, $size, $word_spacing, $letter_spacing) {
$chunked_text_width = $fontMetrics->getTextWidth($chunked_text, $font, $size, $word_spacing, $letter_spacing);
});
arsort($lines);
$max = reset($lines);
break;
case "pre":
case "nowrap":
$max = $min;
break;
}
// Account for margins, borders, and padding
$dims = [
$style->padding_left,
$style->padding_right,
$style->border_left_width,
$style->border_right_width,
$style->margin_left,
$style->margin_right
];
// The containing block is not defined yet, treat percentages as 0
$delta = (float) $style->length_in_pt($dims, 0);
$min += $delta;
$max += $delta;
return [$min, $max, "min" => $min, "max" => $max];
}
/**
* @param FontMetrics $fontMetrics
* @return $this
*/
public function setFontMetrics(FontMetrics $fontMetrics)
{
$this->fontMetrics = $fontMetrics;
return $this;
}
/**
* @return FontMetrics
*/
public function getFontMetrics()
{
return $this->fontMetrics;
}
}
dompdf/src/FrameDecorator/Image.php 0000644 00000005765 15024772104 0013253 0 ustar 00 get_node()->getAttribute("src");
$debug_png = $dompdf->getOptions()->getDebugPng();
if ($debug_png) {
print '[__construct ' . $url . ']';
}
list($this->_image_url, /*$type*/, $this->_image_msg) = Cache::resolve_url(
$url,
$dompdf->getProtocol(),
$dompdf->getBaseHost(),
$dompdf->getBasePath(),
$dompdf->getOptions()
);
if (Cache::is_broken($this->_image_url) &&
$alt = $frame->get_node()->getAttribute("alt")
) {
$fontMetrics = $dompdf->getFontMetrics();
$style = $frame->get_style();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$style->width = (4 / 3) * $fontMetrics->getTextWidth($alt, $font, $size, $word_spacing, $letter_spacing);
$style->height = $fontMetrics->getFontHeight($font, $size);
}
}
/**
* Get the intrinsic pixel dimensions of the image.
*
* @return array Width and height as `float|int`.
*/
public function get_intrinsic_dimensions(): array
{
[$width, $height] = Helpers::dompdf_getimagesize($this->_image_url, $this->_dompdf->getHttpContext());
return [$width, $height];
}
/**
* Resample the given pixel length according to dpi.
*
* @param float|int $length
* @return float
*/
public function resample($length): float
{
$dpi = $this->_dompdf->getOptions()->getDpi();
return ($length * 72) / $dpi;
}
/**
* Return the image's url
*
* @return string The url of this image
*/
function get_image_url()
{
return $this->_image_url;
}
/**
* Return the image's error message
*
* @return string The image's error message
*/
function get_image_msg()
{
return $this->_image_msg;
}
}
dompdf/src/FrameDecorator/ListBullet.php 0000644 00000005407 15024772104 0014305 0 ustar 00 _frame->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
return $style->font_size * self::BULLET_SIZE;
}
/**
* Get the height of the bullet symbol.
*
* @return float
*/
public function get_height(): float
{
$style = $this->_frame->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
return $style->font_size * self::BULLET_SIZE;
}
/**
* Get the width of the bullet, including indentation.
*/
public function get_margin_width(): float
{
$style = $this->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
return $style->font_size * (self::BULLET_SIZE + self::MARKER_INDENT);
}
/**
* Get the line height for the bullet.
*
* This increases the height of the corresponding line box when necessary.
*/
public function get_margin_height(): float
{
$style = $this->get_style();
if ($style->list_style_type === "none") {
return 0.0;
}
// TODO: This is a copy of `FrameDecorator\Text::get_margin_height()`
// Would be nice to properly refactor that at some point
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $this->_dompdf->getFontMetrics()->getFontHeight($font, $size);
return ($style->line_height / ($size > 0 ? $size : 1)) * $fontHeight;
}
}
dompdf/src/FrameDecorator/Table.php 0000644 00000023522 15024772104 0013247 0 ustar 00 _cellmap = new Cellmap($this);
if ($frame->get_style()->table_layout === "fixed") {
$this->_cellmap->set_layout_fixed(true);
}
$this->_headers = [];
$this->_footers = [];
}
public function reset()
{
parent::reset();
$this->_cellmap->reset();
$this->_headers = [];
$this->_footers = [];
$this->_reflower->reset();
}
//........................................................................
/**
* Split the table at $row. $row and all subsequent rows will be
* added to the clone. This method is overridden in order to remove
* frames from the cellmap properly.
*/
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
if (is_null($child)) {
parent::split($child, $page_break, $forced);
return;
}
// If $child is a header or if it is the first non-header row, do
// not duplicate headers, simply move the table to the next page.
if (count($this->_headers)
&& !in_array($child, $this->_headers, true)
&& !in_array($child->get_prev_sibling(), $this->_headers, true)
) {
$first_header = null;
// Insert copies of the table headers before $child
foreach ($this->_headers as $header) {
$new_header = $header->deep_copy();
if (is_null($first_header)) {
$first_header = $new_header;
}
$this->insert_child_before($new_header, $child);
}
parent::split($first_header, $page_break, $forced);
} elseif (in_array($child->get_style()->display, self::ROW_GROUPS, true)) {
// Individual rows should have already been handled
parent::split($child, $page_break, $forced);
} else {
$iter = $child;
while ($iter) {
$this->_cellmap->remove_row($iter);
$iter = $iter->get_next_sibling();
}
parent::split($child, $page_break, $forced);
}
}
public function copy(DOMNode $node)
{
$deco = parent::copy($node);
// In order to keep columns' widths through pages
$deco->_cellmap->set_columns($this->_cellmap->get_columns());
$deco->_cellmap->lock_columns();
return $deco;
}
/**
* Static function to locate the parent table of a frame
*
* @param Frame $frame
*
* @return Table the table that is an ancestor of $frame
*/
public static function find_parent_table(Frame $frame)
{
while ($frame = $frame->get_parent()) {
if ($frame->is_table()) {
break;
}
}
return $frame;
}
/**
* Return this table's Cellmap
*
* @return Cellmap
*/
public function get_cellmap()
{
return $this->_cellmap;
}
//........................................................................
/**
* Check for text nodes between valid table children that only contain white
* space, except if white space is to be preserved.
*
* @param AbstractFrameDecorator $frame
*
* @return bool
*/
private function isEmptyTextNode(AbstractFrameDecorator $frame): bool
{
// This is based on the white-space pattern in `FrameReflower\Text`,
// i.e. only match on collapsible white space
$wsPattern = '/^[^\S\xA0\x{202F}\x{2007}]*$/u';
$validChildOrNull = function ($frame) {
return $frame === null
|| in_array($frame->get_style()->display, self::VALID_CHILDREN, true);
};
return $frame instanceof Text
&& !$frame->is_pre()
&& preg_match($wsPattern, $frame->get_text())
&& $validChildOrNull($frame->get_prev_sibling())
&& $validChildOrNull($frame->get_next_sibling());
}
/**
* Restructure tree so that the table has the correct structure. Misplaced
* children are appropriately wrapped in anonymous row groups, rows, and
* cells.
*
* https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
*/
public function normalize(): void
{
$column_caption = ["table-column-group", "table-column", "table-caption"];
$children = iterator_to_array($this->get_children());
$tbody = null;
foreach ($children as $child) {
$display = $child->get_style()->display;
if (in_array($display, self::ROW_GROUPS, true)) {
// Reset anonymous tbody
$tbody = null;
// Add headers and footers
if ($display === "table-header-group") {
$this->_headers[] = $child;
} elseif ($display === "table-footer-group") {
$this->_footers[] = $child;
}
continue;
}
if (in_array($display, $column_caption, true)) {
continue;
}
// Remove empty text nodes between valid children
if ($this->isEmptyTextNode($child)) {
$this->remove_child($child);
continue;
}
// Catch consecutive misplaced frames within a single anonymous group
if ($tbody === null) {
$tbody = $this->create_anonymous_child("tbody", "table-row-group");
$this->insert_child_before($tbody, $child);
}
$tbody->append_child($child);
}
// Handle empty table: Make sure there is at least one row group
if (!$this->get_first_child()) {
$tbody = $this->create_anonymous_child("tbody", "table-row-group");
$this->append_child($tbody);
}
foreach ($this->get_children() as $child) {
$display = $child->get_style()->display;
if (in_array($display, self::ROW_GROUPS, true)) {
$this->normalizeRowGroup($child);
}
}
}
private function normalizeRowGroup(AbstractFrameDecorator $frame): void
{
$children = iterator_to_array($frame->get_children());
$tr = null;
foreach ($children as $child) {
$display = $child->get_style()->display;
if ($display === "table-row") {
// Reset anonymous tr
$tr = null;
continue;
}
// Remove empty text nodes between valid children
if ($this->isEmptyTextNode($child)) {
$frame->remove_child($child);
continue;
}
// Catch consecutive misplaced frames within a single anonymous row
if ($tr === null) {
$tr = $frame->create_anonymous_child("tr", "table-row");
$frame->insert_child_before($tr, $child);
}
$tr->append_child($child);
}
// Handle empty row group: Make sure there is at least one row
if (!$frame->get_first_child()) {
$tr = $frame->create_anonymous_child("tr", "table-row");
$frame->append_child($tr);
}
foreach ($frame->get_children() as $child) {
$this->normalizeRow($child);
}
}
private function normalizeRow(AbstractFrameDecorator $frame): void
{
$children = iterator_to_array($frame->get_children());
$td = null;
foreach ($children as $child) {
$display = $child->get_style()->display;
if ($display === "table-cell") {
// Reset anonymous td
$td = null;
continue;
}
// Remove empty text nodes between valid children
if ($this->isEmptyTextNode($child)) {
$frame->remove_child($child);
continue;
}
// Catch consecutive misplaced frames within a single anonymous cell
if ($td === null) {
$td = $frame->create_anonymous_child("td", "table-cell");
$frame->insert_child_before($td, $child);
}
$td->append_child($child);
}
// Handle empty row: Make sure there is at least one cell
if (!$frame->get_first_child()) {
$td = $frame->create_anonymous_child("td", "table-cell");
$frame->append_child($td);
}
}
}
dompdf/src/FrameDecorator/TableCell.php 0000644 00000006467 15024772104 0014060 0 ustar 00 _resolved_borders = [];
$this->_content_height = 0;
}
//........................................................................
function reset()
{
parent::reset();
$this->_resolved_borders = [];
$this->_content_height = 0;
$this->_frame->reset();
}
/**
* @return int
*/
function get_content_height()
{
return $this->_content_height;
}
/**
* @param $height
*/
function set_content_height($height)
{
$this->_content_height = $height;
}
/**
* @param $height
*/
function set_cell_height($height)
{
$style = $this->get_style();
$v_space = (float)$style->length_in_pt(
[
$style->margin_top,
$style->padding_top,
$style->border_top_width,
$style->border_bottom_width,
$style->padding_bottom,
$style->margin_bottom
],
(float)$style->length_in_pt($style->height)
);
$new_height = $height - $v_space;
$style->set_used("height", $new_height);
if ($new_height > $this->_content_height) {
$y_offset = 0;
// Adjust our vertical alignment
switch ($style->vertical_align) {
default:
case "baseline":
// FIXME: this isn't right
case "top":
// Don't need to do anything
return;
case "middle":
$y_offset = ($new_height - $this->_content_height) / 2;
break;
case "bottom":
$y_offset = $new_height - $this->_content_height;
break;
}
if ($y_offset) {
// Move our children
foreach ($this->get_line_boxes() as $line) {
foreach ($line->get_frames() as $frame) {
$frame->move(0, $y_offset);
}
}
}
}
}
/**
* @param $side
* @param $border_spec
*/
function set_resolved_border($side, $border_spec)
{
$this->_resolved_borders[$side] = $border_spec;
}
/**
* @param $side
* @return mixed
*/
function get_resolved_border($side)
{
return $this->_resolved_borders[$side];
}
/**
* @return array
*/
function get_resolved_borders()
{
return $this->_resolved_borders;
}
}
dompdf/src/FrameDecorator/NullFrameDecorator.php 0000644 00000001326 15024772104 0015746 0 ustar 00 _frame->get_style();
$style->width = 0;
$style->height = 0;
$style->margin = 0;
$style->padding = 0;
}
}
dompdf/src/FrameDecorator/Page.php 0000644 00000060444 15024772104 0013100 0 ustar 00 _page_full = false;
$this->_in_table = 0;
$this->bottom_page_edge = null;
}
/**
* Set the renderer used for this pdf
*
* @param Renderer $renderer the renderer to use
*/
function set_renderer($renderer)
{
$this->_renderer = $renderer;
}
/**
* Return the renderer used for this pdf
*
* @return Renderer
*/
function get_renderer()
{
return $this->_renderer;
}
/**
* Calculate the bottom edge of the page area after margins have been
* applied for the current page.
*/
public function calculate_bottom_page_edge(): void
{
[, , , $cbh] = $this->get_containing_block();
$style = $this->get_style();
$margin_bottom = (float) $style->length_in_pt($style->margin_bottom, $cbh);
$this->bottom_page_edge = $cbh - $margin_bottom;
}
/**
* Returns true if the page is full and is no longer accepting frames.
*
* @return bool
*/
function is_full()
{
return $this->_page_full;
}
/**
* Start a new page by resetting the full flag.
*/
function next_page()
{
$this->_floating_frames = [];
$this->_renderer->new_page();
$this->_page_full = false;
}
/**
* Indicate to the page that a table is currently being reflowed.
*/
function table_reflow_start()
{
$this->_in_table++;
}
/**
* Indicate to the page that table reflow is finished.
*/
function table_reflow_end()
{
$this->_in_table--;
}
/**
* Return whether we are currently in a nested table or not
*
* @return bool
*/
function in_nested_table()
{
return $this->_in_table > 1;
}
/**
* Check if a forced page break is required before $frame. This uses the
* frame's page_break_before property as well as the preceding frame's
* page_break_after property.
*
* @link http://www.w3.org/TR/CSS21/page.html#forced
*
* @param AbstractFrameDecorator $frame the frame to check
*
* @return bool true if a page break occurred
*/
function check_forced_page_break(Frame $frame)
{
// Skip check if page is already split and for the body
if ($this->_page_full || $frame->get_node()->nodeName === "body") {
return false;
}
$page_breaks = ["always", "left", "right"];
$style = $frame->get_style();
if (($frame->is_block_level() || $style->display === "table-row")
&& in_array($style->page_break_before, $page_breaks, true)
) {
// Prevent cascading splits
$frame->split(null, true, true);
$style->page_break_before = "auto";
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
// Find the preceding block-level sibling (or table row). Inline
// elements are treated as if wrapped in an anonymous block container
// here. See https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
$prev = $frame->get_prev_sibling();
while ($prev && (($prev->is_text_node() && $prev->get_node()->nodeValue === "")
|| $prev->get_node()->nodeName === "bullet")
) {
$prev = $prev->get_prev_sibling();
}
if ($prev && ($prev->is_block_level() || $prev->get_style()->display === "table-row")) {
if (in_array($prev->get_style()->page_break_after, $page_breaks, true)) {
// Prevent cascading splits
$frame->split(null, true, true);
$prev->get_style()->page_break_after = "auto";
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
$prev_last_child = $prev->get_last_child();
while ($prev_last_child && (($prev_last_child->is_text_node() && $prev_last_child->get_node()->nodeValue === "")
|| $prev_last_child->get_node()->nodeName === "bullet")
) {
$prev_last_child = $prev_last_child->get_prev_sibling();
}
if ($prev_last_child
&& $prev_last_child->is_block_level()
&& in_array($prev_last_child->get_style()->page_break_after, $page_breaks, true)
) {
$frame->split(null, true, true);
$prev_last_child->get_style()->page_break_after = "auto";
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
}
return false;
}
/**
* Check for a gap between the top content edge of a frame and its child
* content.
*
* Additionally, the top margin, border, and padding of the frame must fit
* on the current page.
*
* @param float $childPos The top margin or line-box edge of the child content.
* @param Frame $frame The parent frame to check.
* @return bool
*/
protected function hasGap(float $childPos, Frame $frame): bool
{
$style = $frame->get_style();
$cbw = $frame->get_containing_block("w");
$contentEdge = $frame->get_position("y") + (float) $style->length_in_pt([
$style->margin_top,
$style->border_top_width,
$style->padding_top
], $cbw);
return Helpers::lengthGreater($childPos, $contentEdge)
&& Helpers::lengthLessOrEqual($contentEdge, $this->bottom_page_edge);
}
/**
* Determine if a page break is allowed before $frame
* http://www.w3.org/TR/CSS21/page.html#allowed-page-breaks
*
* In the normal flow, page breaks can occur at the following places:
*
* 1. In the vertical margin between block boxes. When an
* unforced page break occurs here, the used values of the
* relevant 'margin-top' and 'margin-bottom' properties are set
* to '0'. When a forced page break occurs here, the used value
* of the relevant 'margin-bottom' property is set to '0'; the
* relevant 'margin-top' used value may either be set to '0' or
* retained.
* 2. Between line boxes inside a block container box.
* 3. Between the content edge of a block container box and the
* outer edges of its child content (margin edges of block-level
* children or line box edges for inline-level children) if there
* is a (non-zero) gap between them.
*
* These breaks are subject to the following rules:
*
* * Rule A: Breaking at (1) is allowed only if the
* 'page-break-after' and 'page-break-before' properties of all
* the elements generating boxes that meet at this margin allow
* it, which is when at least one of them has the value
* 'always', 'left', or 'right', or when all of them are 'auto'.
*
* * Rule B: However, if all of them are 'auto' and a common
* ancestor of all the elements has a 'page-break-inside' value
* of 'avoid', then breaking here is not allowed.
*
* * Rule C: Breaking at (2) is allowed only if the number of line
* boxes between the break and the start of the enclosing block
* box is the value of 'orphans' or more, and the number of line
* boxes between the break and the end of the box is the value
* of 'widows' or more.
*
* * Rule D: In addition, breaking at (2) or (3) is allowed only
* if the 'page-break-inside' property of the element and all
* its ancestors is 'auto'.
*
* If the above does not provide enough break points to keep content
* from overflowing the page boxes, then rules A, B and D are
* dropped in order to find additional breakpoints.
*
* If that still does not lead to sufficient break points, rule C is
* dropped as well, to find still more break points.
*
* We also allow breaks between table rows.
*
* @param AbstractFrameDecorator $frame the frame to check
*
* @return bool true if a break is allowed, false otherwise
*/
protected function _page_break_allowed(Frame $frame)
{
Helpers::dompdf_debug("page-break", "_page_break_allowed(" . $frame->get_node()->nodeName . ")");
$display = $frame->get_style()->display;
// Block Frames (1):
if ($frame->is_block_level() || $display === "-dompdf-image") {
// Avoid breaks within table-cells
if ($this->_in_table > ($display === "table" ? 1 : 0)) {
Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
return false;
}
// Rule A
if ($frame->get_style()->page_break_before === "avoid") {
Helpers::dompdf_debug("page-break", "before: avoid");
return false;
}
// Find the preceding block-level sibling. Inline elements are
// treated as if wrapped in an anonymous block container here. See
// https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
$prev = $frame->get_prev_sibling();
while ($prev && (($prev->is_text_node() && $prev->get_node()->nodeValue === "")
|| $prev->get_node()->nodeName === "bullet")
) {
$prev = $prev->get_prev_sibling();
}
// Does the previous element allow a page break after?
if ($prev && ($prev->is_block_level() || $prev->get_style()->display === "-dompdf-image")
&& $prev->get_style()->page_break_after === "avoid"
) {
Helpers::dompdf_debug("page-break", "after: avoid");
return false;
}
// Rules B & D
$parent = $frame->get_parent();
$p = $parent;
while ($p) {
if ($p->get_style()->page_break_inside === "avoid") {
Helpers::dompdf_debug("page-break", "parent->inside: avoid");
return false;
}
$p = $p->find_block_parent();
}
// To prevent cascading page breaks when a top-level element has
// page-break-inside: avoid, ensure that at least one frame is
// on the page before splitting.
if ($parent->get_node()->nodeName === "body" && !$prev) {
// We are the body's first child
Helpers::dompdf_debug("page-break", "Body's first child.");
return false;
}
// Check for a possible type (3) break
if (!$prev && $parent && !$this->hasGap($frame->get_position("y"), $parent)) {
Helpers::dompdf_debug("page-break", "First block-level frame, no gap");
return false;
}
Helpers::dompdf_debug("page-break", "block: break allowed");
return true;
} // Inline frames (2):
else {
if ($frame->is_inline_level()) {
// Avoid breaks within table-cells
if ($this->_in_table) {
Helpers::dompdf_debug("page-break", "In table: " . $this->_in_table);
return false;
}
// Rule C
$block_parent = $frame->find_block_parent();
$parent_style = $block_parent->get_style();
$line = $block_parent->get_current_line_box();
$line_count = count($block_parent->get_line_boxes());
$line_number = $frame->get_containing_line() && empty($line->get_frames())
? $line_count - 1
: $line_count;
// The line number of the frame can be less than the current
// number of line boxes, in case we are backtracking. As long as
// we are not checking for widows yet, just checking against the
// number of line boxes is sufficient in most cases, though.
if ($line_number <= $parent_style->orphans) {
Helpers::dompdf_debug("page-break", "orphans");
return false;
}
// FIXME: Checking widows is tricky without having laid out the
// remaining line boxes. Just ignore it for now...
// Rule D
$p = $block_parent;
while ($p) {
if ($p->get_style()->page_break_inside === "avoid") {
Helpers::dompdf_debug("page-break", "parent->inside: avoid");
return false;
}
$p = $p->find_block_parent();
}
// To prevent cascading page breaks when a top-level element has
// page-break-inside: avoid, ensure that at least one frame with
// some content is on the page before splitting.
$prev = $frame->get_prev_sibling();
while ($prev && ($prev->is_text_node() && trim($prev->get_node()->nodeValue) == "")) {
$prev = $prev->get_prev_sibling();
}
if ($block_parent->get_node()->nodeName === "body" && !$prev) {
// We are the body's first child
Helpers::dompdf_debug("page-break", "Body's first child.");
return false;
}
Helpers::dompdf_debug("page-break", "inline: break allowed");
return true;
// Table-rows
} else {
if ($display === "table-row") {
// If this is a nested table, prevent the page from breaking
if ($this->_in_table > 1) {
Helpers::dompdf_debug("page-break", "table: nested table");
return false;
}
// Rule A (table row)
if ($frame->get_style()->page_break_before === "avoid") {
Helpers::dompdf_debug("page-break", "before: avoid");
return false;
}
// Find the preceding row
$prev = $frame->get_prev_sibling();
if (!$prev) {
$prev_group = $frame->get_parent()->get_prev_sibling();
if ($prev_group
&& in_array($prev_group->get_style()->display, Table::ROW_GROUPS, true)
) {
$prev = $prev_group->get_last_child();
}
}
// Check if a page break is allowed after the preceding row
if ($prev && $prev->get_style()->page_break_after === "avoid") {
Helpers::dompdf_debug("page-break", "after: avoid");
return false;
}
// Avoid breaking before the first row of a table
if (!$prev) {
Helpers::dompdf_debug("page-break", "table: first-row");
return false;
}
// Rule B (table row)
// Check if the page_break_inside property is not 'avoid'
// for the parent table or any of its ancestors
$table = Table::find_parent_table($frame);
$p = $table;
while ($p) {
if ($p->get_style()->page_break_inside === "avoid") {
Helpers::dompdf_debug("page-break", "parent->inside: avoid");
return false;
}
$p = $p->find_block_parent();
}
Helpers::dompdf_debug("page-break", "table-row: break allowed");
return true;
} else {
if (in_array($display, Table::ROW_GROUPS, true)) {
// Disallow breaks at row-groups: only split at row boundaries
return false;
} else {
Helpers::dompdf_debug("page-break", "? " . $display);
return false;
}
}
}
}
}
/**
* Check if $frame will fit on the page. If the frame does not fit,
* the frame tree is modified so that a page break occurs in the
* correct location.
*
* @param AbstractFrameDecorator $frame the frame to check
*
* @return bool
*/
function check_page_break(Frame $frame)
{
if ($this->_page_full || $frame->_already_pushed
// Never check for breaks on empty text nodes
|| ($frame->is_text_node() && $frame->get_node()->nodeValue === "")
) {
return false;
}
$p = $frame;
do {
$display = $p->get_style()->display;
if ($display == "table-row") {
if ($p->_already_pushed) { return false; }
}
} while ($p = $p->get_parent());
// If the frame is absolute or fixed it shouldn't break
$p = $frame;
do {
if ($p->is_absolute()) {
return false;
}
} while ($p = $p->get_parent());
$margin_height = $frame->get_margin_height();
// Determine the frame's maximum y value
$max_y = (float)$frame->get_position("y") + $margin_height;
// If a split is to occur here, then the bottom margins & paddings of all
// parents of $frame must fit on the page as well:
$p = $frame->get_parent();
while ($p && $p !== $this) {
$cbw = $p->get_containing_block("w");
$max_y += (float) $p->get_style()->computed_bottom_spacing($cbw);
$p = $p->get_parent();
}
// Check if $frame flows off the page
if (Helpers::lengthLessOrEqual($max_y, $this->bottom_page_edge)) {
// no: do nothing
return false;
}
Helpers::dompdf_debug("page-break", "check_page_break");
Helpers::dompdf_debug("page-break", "in_table: " . $this->_in_table);
// yes: determine page break location
$iter = $frame;
$flg = false;
$pushed_flg = false;
$in_table = $this->_in_table;
Helpers::dompdf_debug("page-break", "Starting search");
while ($iter) {
// echo "\nbacktrack: " .$iter->get_node()->nodeName ." ".spl_object_hash($iter->get_node()). "";
if ($iter === $this) {
Helpers::dompdf_debug("page-break", "reached root.");
// We've reached the root in our search. Just split at $frame.
break;
}
if ($iter->_already_pushed) {
$pushed_flg = true;
} elseif ($this->_page_break_allowed($iter)) {
Helpers::dompdf_debug("page-break", "break allowed, splitting.");
$iter->split(null, true);
$this->_page_full = true;
$this->_in_table = $in_table;
$iter->_already_pushed = true;
$frame->_already_pushed = true;
return true;
}
if (!$flg && $next = $iter->get_last_child()) {
Helpers::dompdf_debug("page-break", "following last child.");
if ($next->is_table()) {
$this->_in_table++;
}
$iter = $next;
$pushed_flg = false;
continue;
}
if ($pushed_flg) {
// The frame was already pushed, avoid breaking on a previous page
break;
}
$next = $iter->get_prev_sibling();
// Skip empty text nodes
while ($next && $next->is_text_node() && $next->get_node()->nodeValue === "") {
$next = $next->get_prev_sibling();
}
if ($next) {
Helpers::dompdf_debug("page-break", "following prev sibling.");
if ($next->is_table() && !$iter->is_table()) {
$this->_in_table++;
} elseif (!$next->is_table() && $iter->is_table()) {
$this->_in_table--;
}
$iter = $next;
$flg = false;
continue;
}
if ($next = $iter->get_parent()) {
Helpers::dompdf_debug("page-break", "following parent.");
if ($iter->is_table()) {
$this->_in_table--;
}
$iter = $next;
$flg = true;
continue;
}
break;
}
$this->_in_table = $in_table;
// No valid page break found. Just break at $frame.
Helpers::dompdf_debug("page-break", "no valid break found, just splitting.");
// If we are in a table, backtrack to the nearest top-level table row
if ($this->_in_table) {
$iter = $frame;
while ($iter && $iter->get_style()->display !== "table-row" && $iter->get_style()->display !== 'table-row-group' && $iter->_already_pushed === false) {
$iter = $iter->get_parent();
}
if ($iter) {
$iter->split(null, true);
$iter->_already_pushed = true;
} else {
return false;
}
} else {
$frame->split(null, true);
}
$this->_page_full = true;
$frame->_already_pushed = true;
return true;
}
//........................................................................
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
// Do nothing
}
/**
* Add a floating frame
*
* @param Frame $frame
*
* @return void
*/
function add_floating_frame(Frame $frame)
{
array_unshift($this->_floating_frames, $frame);
}
/**
* @return Frame[]
*/
function get_floating_frames()
{
return $this->_floating_frames;
}
/**
* @param $key
*/
public function remove_floating_frame($key)
{
unset($this->_floating_frames[$key]);
}
/**
* @param Frame $child
* @return int|mixed
*/
public function get_lowest_float_offset(Frame $child)
{
$style = $child->get_style();
$side = $style->clear;
$float = $style->float;
$y = 0;
if ($float === "none") {
foreach ($this->_floating_frames as $key => $frame) {
if ($side === "both" || $frame->get_style()->float === $side) {
$y = max($y, $frame->get_position("y") + $frame->get_margin_height());
}
$this->remove_floating_frame($key);
}
}
if ($y > 0) {
$y++; // add 1px buffer from float
}
return $y;
}
}
dompdf/src/FrameDecorator/TableRow.php 0000644 00000001073 15024772104 0013734 0 ustar 00 get_parent();
$cellmap = $parent->get_cellmap();
$iter = $child;
while ($iter) {
$cellmap->remove_row($iter);
$iter = $iter->get_next_sibling();
}
// Remove all subsequent row groups from the cellmap
$iter = $this->get_next_sibling();
while ($iter) {
$cellmap->remove_row_group($iter);
$iter = $iter->get_next_sibling();
}
// If we are splitting at the first child remove the
// table-row-group from the cellmap as well
if ($child === $this->get_first_child()) {
$cellmap->remove_row_group($this);
parent::split(null, $page_break, $forced);
return;
}
$cellmap->update_row_group($this, $child->get_prev_sibling());
parent::split($child, $page_break, $forced);
}
}
dompdf/src/FrameDecorator/ListBulletImage.php 0000644 00000005346 15024772104 0015252 0 ustar 00 get_style();
$url = $style->list_style_image;
$frame->get_node()->setAttribute("src", $url);
$this->_img = new Image($frame, $dompdf);
parent::__construct($this->_img, $dompdf);
$url = $this->_img->get_image_url();
if (Cache::is_broken($url)) {
$this->_width = parent::get_width();
$this->_height = parent::get_height();
} else {
// Resample the bullet image to be consistent with 'auto' sized images
[$width, $height] = $this->_img->get_intrinsic_dimensions();
$this->_width = $this->_img->resample($width);
$this->_height = $this->_img->resample($height);
}
}
public function get_width(): float
{
return $this->_width;
}
public function get_height(): float
{
return $this->_height;
}
public function get_margin_width(): float
{
$style = $this->get_style();
return $this->_width + $style->font_size * self::MARKER_INDENT;
}
public function get_margin_height(): float
{
$fontMetrics = $this->_dompdf->getFontMetrics();
$style = $this->get_style();
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $fontMetrics->getFontHeight($font, $size);
$baseline = $fontMetrics->getFontBaseline($font, $size);
// This is the same factor as used in
// `FrameDecorator\Text::get_margin_height()`
$f = $style->line_height / ($size > 0 ? $size : 1);
// FIXME: Tries to approximate replacing the space above the font
// baseline with the image
return $f * ($fontHeight - $baseline) + $this->_height;
}
/**
* Return image url
*
* @return string
*/
function get_image_url()
{
return $this->_img->get_image_url();
}
}
dompdf/src/FrameDecorator/AbstractFrameDecorator.php 0000644 00000054035 15024772104 0016604 0 ustar 00 counter_value) (for generated content)
*
* @var array
*/
public $_counters = [];
/**
* The root node of the DOM tree
*
* @var Frame
*/
protected $_root;
/**
* The decorated frame
*
* @var Frame
*/
protected $_frame;
/**
* AbstractPositioner object used to position this frame (Strategy pattern)
*
* @var AbstractPositioner
*/
protected $_positioner;
/**
* Reflower object used to calculate frame dimensions (Strategy pattern)
*
* @var AbstractFrameReflower
*/
protected $_reflower;
/**
* Reference to the current dompdf instance
*
* @var Dompdf
*/
protected $_dompdf;
/**
* First block parent
*
* @var Block
*/
private $_block_parent;
/**
* First positioned parent (position: relative | absolute | fixed)
*
* @var AbstractFrameDecorator
*/
private $_positioned_parent;
/**
* Cache for the get_parent while loop results
*
* @var Frame
*/
private $_cached_parent;
/**
* Whether generated content and counters have been set.
*
* @var bool
*/
public $content_set = false;
/**
* Whether the frame has been split
*
* @var bool
*/
public $is_split = false;
/**
* Whether the frame is a split-off frame
*
* @var bool
*/
public $is_split_off = false;
/**
* Class constructor
*
* @param Frame $frame The decoration target
* @param Dompdf $dompdf The Dompdf object
*/
function __construct(Frame $frame, Dompdf $dompdf)
{
$this->_frame = $frame;
$this->_root = null;
$this->_dompdf = $dompdf;
$frame->set_decorator($this);
}
/**
* "Destructor": forcibly free all references held by this object
*
* @param bool $recursive if true, call dispose on all children
*/
function dispose($recursive = false)
{
if ($recursive) {
while ($child = $this->get_first_child()) {
$child->dispose(true);
}
}
$this->_root = null;
unset($this->_root);
$this->_frame->dispose(true);
$this->_frame = null;
unset($this->_frame);
$this->_positioner = null;
unset($this->_positioner);
$this->_reflower = null;
unset($this->_reflower);
}
/**
* Return a copy of this frame with $node as its node
*
* @param DOMNode $node
*
* @return AbstractFrameDecorator
*/
function copy(DOMNode $node)
{
$frame = new Frame($node);
$style = clone $this->_frame->get_style();
$style->reset();
$frame->set_style($style);
if ($node instanceof DOMElement && $node->hasAttribute("id")) {
$node->setAttribute("data-dompdf-original-id", $node->getAttribute("id"));
$node->removeAttribute("id");
}
return Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
}
/**
* Create a deep copy: copy this node and all children
*
* @return AbstractFrameDecorator
*/
function deep_copy()
{
$node = $this->_frame->get_node()->cloneNode();
$frame = new Frame($node);
$style = clone $this->_frame->get_style();
$style->reset();
$frame->set_style($style);
if ($node instanceof DOMElement && $node->hasAttribute("id")) {
$node->setAttribute("data-dompdf-original-id", $node->getAttribute("id"));
$node->removeAttribute("id");
}
$deco = Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
foreach ($this->get_children() as $child) {
$deco->append_child($child->deep_copy());
}
return $deco;
}
/**
* Create an anonymous child frame, inheriting styles from this frame.
*
* @param string $node_name
* @param string $display
*
* @return AbstractFrameDecorator
*/
public function create_anonymous_child(string $node_name, string $display): AbstractFrameDecorator
{
$style = $this->get_style();
$child_style = $style->get_stylesheet()->create_style();
$child_style->set_prop("display", $display);
$child_style->inherit($style);
$node = $this->get_node()->ownerDocument->createElement($node_name);
$frame = new Frame($node);
$frame->set_style($child_style);
return Factory::decorate_frame($frame, $this->_dompdf, $this->_root);
}
function reset()
{
$this->_frame->reset();
$this->_reflower->reset();
$this->reset_generated_content();
$this->revert_counter_increment();
$this->content_set = false;
$this->_counters = [];
// clear parent lookup caches
$this->_cached_parent = null;
$this->_block_parent = null;
$this->_positioned_parent = null;
// Reset all children
foreach ($this->get_children() as $child) {
$child->reset();
}
}
/**
* If this represents a generated node then child nodes represent generated
* content. Remove the children since the content will be generated next
* time this frame is reflowed.
*/
protected function reset_generated_content(): void
{
if ($this->content_set
&& $this->get_node()->nodeName === "dompdf_generated"
) {
$content = $this->get_style()->content;
if ($content !== "normal" && $content !== "none") {
foreach ($this->get_children() as $child) {
$this->remove_child($child);
}
}
}
}
/**
* Decrement any counters that were incremented on the current node, unless
* that node is the body.
*/
protected function revert_counter_increment(): void
{
if ($this->content_set
&& $this->get_node()->nodeName !== "body"
&& ($decrement = $this->get_style()->counter_increment) !== "none"
) {
$this->decrement_counters($decrement);
}
}
// Getters -----------
function get_id()
{
return $this->_frame->get_id();
}
/**
* @return Frame
*/
function get_frame()
{
return $this->_frame;
}
function get_node()
{
return $this->_frame->get_node();
}
function get_style()
{
return $this->_frame->get_style();
}
/**
* @deprecated
*/
function get_original_style()
{
return $this->_frame->get_style();
}
function get_containing_block($i = null)
{
return $this->_frame->get_containing_block($i);
}
function get_position($i = null)
{
return $this->_frame->get_position($i);
}
/**
* @return Dompdf
*/
function get_dompdf()
{
return $this->_dompdf;
}
public function get_margin_width(): float
{
return $this->_frame->get_margin_width();
}
public function get_margin_height(): float
{
return $this->_frame->get_margin_height();
}
public function get_content_box(): array
{
return $this->_frame->get_content_box();
}
public function get_padding_box(): array
{
return $this->_frame->get_padding_box();
}
public function get_border_box(): array
{
return $this->_frame->get_border_box();
}
function set_id($id)
{
$this->_frame->set_id($id);
}
public function set_style(Style $style): void
{
$this->_frame->set_style($style);
}
function set_containing_block($x = null, $y = null, $w = null, $h = null)
{
$this->_frame->set_containing_block($x, $y, $w, $h);
}
function set_position($x = null, $y = null)
{
$this->_frame->set_position($x, $y);
}
function is_auto_height()
{
return $this->_frame->is_auto_height();
}
function is_auto_width()
{
return $this->_frame->is_auto_width();
}
function __toString()
{
return $this->_frame->__toString();
}
function prepend_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
$this->_frame->prepend_child($child, $update_node);
}
function append_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
$this->_frame->append_child($child, $update_node);
}
function insert_child_before(Frame $new_child, Frame $ref, $update_node = true)
{
while ($new_child instanceof AbstractFrameDecorator) {
$new_child = $new_child->_frame;
}
if ($ref instanceof AbstractFrameDecorator) {
$ref = $ref->_frame;
}
$this->_frame->insert_child_before($new_child, $ref, $update_node);
}
function insert_child_after(Frame $new_child, Frame $ref, $update_node = true)
{
$insert_frame = $new_child;
while ($insert_frame instanceof AbstractFrameDecorator) {
$insert_frame = $insert_frame->_frame;
}
$reference_frame = $ref;
while ($reference_frame instanceof AbstractFrameDecorator) {
$reference_frame = $reference_frame->_frame;
}
$this->_frame->insert_child_after($insert_frame, $reference_frame, $update_node);
}
function remove_child(Frame $child, $update_node = true)
{
while ($child instanceof AbstractFrameDecorator) {
$child = $child->_frame;
}
return $this->_frame->remove_child($child, $update_node);
}
/**
* @param bool $use_cache
* @return AbstractFrameDecorator
*/
function get_parent($use_cache = true)
{
if ($use_cache && $this->_cached_parent) {
return $this->_cached_parent;
}
$p = $this->_frame->get_parent();
if ($p && $deco = $p->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $this->_cached_parent = $deco;
} else {
return $this->_cached_parent = $p;
}
}
/**
* @return AbstractFrameDecorator
*/
function get_first_child()
{
$c = $this->_frame->get_first_child();
if ($c && $deco = $c->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($c) {
return $c;
}
}
return null;
}
/**
* @return AbstractFrameDecorator
*/
function get_last_child()
{
$c = $this->_frame->get_last_child();
if ($c && $deco = $c->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($c) {
return $c;
}
}
return null;
}
/**
* @return AbstractFrameDecorator
*/
function get_prev_sibling()
{
$s = $this->_frame->get_prev_sibling();
if ($s && $deco = $s->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($s) {
return $s;
}
}
return null;
}
/**
* @return AbstractFrameDecorator
*/
function get_next_sibling()
{
$s = $this->_frame->get_next_sibling();
if ($s && $deco = $s->get_decorator()) {
while ($tmp = $deco->get_decorator()) {
$deco = $tmp;
}
return $deco;
} else {
if ($s) {
return $s;
}
}
return null;
}
/**
* @return FrameListIterator
*/
public function get_children(): FrameListIterator
{
return new FrameListIterator($this);
}
/**
* @return FrameTreeIterator
*/
function get_subtree(): FrameTreeIterator
{
return new FrameTreeIterator($this);
}
function set_positioner(AbstractPositioner $posn)
{
$this->_positioner = $posn;
if ($this->_frame instanceof AbstractFrameDecorator) {
$this->_frame->set_positioner($posn);
}
}
function set_reflower(AbstractFrameReflower $reflower)
{
$this->_reflower = $reflower;
if ($this->_frame instanceof AbstractFrameDecorator) {
$this->_frame->set_reflower($reflower);
}
}
/**
* @return AbstractPositioner
*/
function get_positioner()
{
return $this->_positioner;
}
/**
* @return AbstractFrameReflower
*/
function get_reflower()
{
return $this->_reflower;
}
/**
* @param Frame $root
*/
function set_root(Frame $root)
{
$this->_root = $root;
if ($this->_frame instanceof AbstractFrameDecorator) {
$this->_frame->set_root($root);
}
}
/**
* @return Page
*/
function get_root()
{
return $this->_root;
}
/**
* @return Block
*/
function find_block_parent()
{
// Find our nearest block level parent
if (isset($this->_block_parent)) {
return $this->_block_parent;
}
$p = $this->get_parent();
while ($p) {
if ($p->is_block()) {
break;
}
$p = $p->get_parent();
}
return $this->_block_parent = $p;
}
/**
* @return AbstractFrameDecorator
*/
function find_positioned_parent()
{
// Find our nearest relative positioned parent
if (isset($this->_positioned_parent)) {
return $this->_positioned_parent;
}
$p = $this->get_parent();
while ($p) {
if ($p->is_positioned()) {
break;
}
$p = $p->get_parent();
}
if (!$p) {
$p = $this->_root;
}
return $this->_positioned_parent = $p;
}
/**
* Split this frame at $child.
* The current frame is cloned and $child and all children following
* $child are added to the clone. The clone is then passed to the
* current frame's parent->split() method.
*
* @param Frame|null $child
* @param bool $page_break
* @param bool $forced Whether the page break is forced.
*
* @throws Exception
*/
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
if (is_null($child)) {
$this->get_parent()->split($this, $page_break, $forced);
return;
}
if ($child->get_parent() !== $this) {
throw new Exception("Unable to split: frame is not a child of this one.");
}
$this->revert_counter_increment();
$node = $this->_frame->get_node();
$split = $this->copy($node->cloneNode());
$style = $this->_frame->get_style();
$split_style = $split->get_style();
// Truncate the box decoration at the split, except for the body
if ($node->nodeName !== "body") {
// Clear bottom decoration of original frame
$style->margin_bottom = 0.0;
$style->padding_bottom = 0.0;
$style->border_bottom_width = 0.0;
$style->border_bottom_left_radius = 0.0;
$style->border_bottom_right_radius = 0.0;
// Clear top decoration of split frame
$split_style->margin_top = 0.0;
$split_style->padding_top = 0.0;
$split_style->border_top_width = 0.0;
$split_style->border_top_left_radius = 0.0;
$split_style->border_top_right_radius = 0.0;
$split_style->page_break_before = "auto";
}
$split_style->text_indent = 0.0;
$split_style->counter_reset = "none";
$this->is_split = true;
$split->is_split_off = true;
$split->_already_pushed = true;
$this->get_parent()->insert_child_after($split, $this);
if ($this instanceof Block) {
// Remove the frames that will be moved to the new split node from
// the line boxes
$this->remove_frames_from_line($child);
// recalculate the float offsets after paging
foreach ($this->get_line_boxes() as $line_box) {
$line_box->get_float_offsets();
}
}
if (!$forced) {
// Reset top margin in case of an unforced page break
// https://www.w3.org/TR/CSS21/page.html#allowed-page-breaks
$child->get_style()->margin_top = 0.0;
}
// Add $child and all following siblings to the new split node
$iter = $child;
while ($iter) {
$frame = $iter;
$iter = $iter->get_next_sibling();
$frame->reset();
$split->append_child($frame);
}
$this->get_parent()->split($split, $page_break, $forced);
// Preserve the current counter values. This must be done after the
// parent split, as counters get reset on frame reset
$split->_counters = $this->_counters;
}
/**
* @param array $counters
*/
public function reset_counters(array $counters): void
{
foreach ($counters as $id => $value) {
$this->reset_counter($id, $value);
}
}
/**
* @param string $id
* @param int $value
*/
public function reset_counter(string $id = self::DEFAULT_COUNTER, int $value = 0): void
{
$this->get_parent()->_counters[$id] = $value;
}
/**
* @param array $counters
*/
public function decrement_counters(array $counters): void
{
foreach ($counters as $id => $increment) {
$this->increment_counter($id, $increment * -1);
}
}
/**
* @param array $counters
*/
public function increment_counters(array $counters): void
{
foreach ($counters as $id => $increment) {
$this->increment_counter($id, $increment);
}
}
/**
* @param string $id
* @param int $increment
*/
public function increment_counter(string $id = self::DEFAULT_COUNTER, int $increment = 1): void
{
$counter_frame = $this->lookup_counter_frame($id);
if ($counter_frame) {
if (!isset($counter_frame->_counters[$id])) {
$counter_frame->_counters[$id] = 0;
}
$counter_frame->_counters[$id] += $increment;
}
}
/**
* @param string $id
* @return AbstractFrameDecorator|null
*/
function lookup_counter_frame($id = self::DEFAULT_COUNTER)
{
$f = $this->get_parent();
while ($f) {
if (isset($f->_counters[$id])) {
return $f;
}
$fp = $f->get_parent();
if (!$fp) {
return $f;
}
$f = $fp;
}
return null;
}
/**
* @param string $id
* @param string $type
* @return bool|string
*
* TODO: What version is the best : this one or the one in ListBullet ?
*/
function counter_value(string $id = self::DEFAULT_COUNTER, string $type = "decimal")
{
$type = mb_strtolower($type);
if (!isset($this->_counters[$id])) {
$this->_counters[$id] = 0;
}
$value = $this->_counters[$id];
switch ($type) {
default:
case "decimal":
return $value;
case "decimal-leading-zero":
return str_pad($value, 2, "0", STR_PAD_LEFT);
case "lower-roman":
return Helpers::dec2roman($value);
case "upper-roman":
return mb_strtoupper(Helpers::dec2roman($value));
case "lower-latin":
case "lower-alpha":
return chr((($value - 1) % 26) + ord('a'));
case "upper-latin":
case "upper-alpha":
return chr((($value - 1) % 26) + ord('A'));
case "lower-greek":
return Helpers::unichr($value + 944);
case "upper-greek":
return Helpers::unichr($value + 912);
}
}
final function position()
{
$this->_positioner->position($this);
}
/**
* @param float $offset_x
* @param float $offset_y
* @param bool $ignore_self
*/
final function move(float $offset_x, float $offset_y, bool $ignore_self = false): void
{
$this->_positioner->move($this, $offset_x, $offset_y, $ignore_self);
}
/**
* @param Block|null $block
*/
final function reflow(Block $block = null)
{
// Uncomment this to see the frames before they're laid out, instead of
// during rendering.
//echo $this->_frame; flush();
$this->_reflower->reflow($block);
}
/**
* @return array
*/
final public function get_min_max_width(): array
{
return $this->_reflower->get_min_max_width();
}
}
dompdf/src/FrameDecorator/Inline.php 0000644 00000007363 15024772104 0013443 0 ustar 00 get_style();
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $this->_dompdf->getFontMetrics()->getFontHeight($font, $size);
return ($style->line_height / ($size > 0 ? $size : 1)) * $fontHeight;
}
public function split(?Frame $child = null, bool $page_break = false, bool $forced = false): void
{
if (is_null($child)) {
$this->get_parent()->split($this, $page_break, $forced);
return;
}
if ($child->get_parent() !== $this) {
throw new Exception("Unable to split: frame is not a child of this one.");
}
$this->revert_counter_increment();
$node = $this->_frame->get_node();
$split = $this->copy($node->cloneNode());
$style = $this->_frame->get_style();
$split_style = $split->get_style();
// Unset the current node's right style properties
$style->margin_right = 0.0;
$style->padding_right = 0.0;
$style->border_right_width = 0.0;
$style->border_top_right_radius = 0.0;
$style->border_bottom_right_radius = 0.0;
// Unset the split node's left style properties since we don't want them
// to propagate
$split_style->margin_left = 0.0;
$split_style->padding_left = 0.0;
$split_style->border_left_width = 0.0;
$split_style->border_top_left_radius = 0.0;
$split_style->border_bottom_left_radius = 0.0;
// If this is a generated node don't propagate the content style
if ($split->get_node()->nodeName == "dompdf_generated") {
$split_style->content = "normal";
}
//On continuation of inline element on next line,
//don't repeat non-horizontally repeatable background images
//See e.g. in testcase image_variants, long descriptions
if (($url = $style->background_image) && $url !== "none"
&& ($repeat = $style->background_repeat) && $repeat !== "repeat" && $repeat !== "repeat-x"
) {
$split_style->background_image = "none";
}
$this->get_parent()->insert_child_after($split, $this);
// Add $child and all following siblings to the new split node
$iter = $child;
while ($iter) {
$frame = $iter;
$iter = $iter->get_next_sibling();
$frame->reset();
$split->append_child($frame);
}
$parent = $this->get_parent();
if ($page_break) {
$parent->split($split, $page_break, $forced);
} elseif ($parent instanceof Inline) {
$parent->split($split);
}
}
}
dompdf/src/FrameDecorator/Block.php 0000644 00000013753 15024772104 0013257 0 ustar 00 _line_boxes = [new LineBox($this)];
$this->_cl = 0;
$this->dangling_markers = [];
}
function reset()
{
parent::reset();
$this->_line_boxes = [new LineBox($this)];
$this->_cl = 0;
$this->dangling_markers = [];
}
/**
* @return LineBox
*/
function get_current_line_box()
{
return $this->_line_boxes[$this->_cl];
}
/**
* @return int
*/
function get_current_line_number()
{
return $this->_cl;
}
/**
* @return LineBox[]
*/
function get_line_boxes()
{
return $this->_line_boxes;
}
/**
* @param int $line_number
* @return int
*/
function set_current_line_number($line_number)
{
$line_boxes_count = count($this->_line_boxes);
$cl = max(min($line_number, $line_boxes_count), 0);
return ($this->_cl = $cl);
}
/**
* @param int $i
*/
function clear_line($i)
{
if (isset($this->_line_boxes[$i])) {
unset($this->_line_boxes[$i]);
}
}
/**
* @param Frame $frame
* @return LineBox|null
*/
public function add_frame_to_line(Frame $frame): ?LineBox
{
$current_line = $this->_line_boxes[$this->_cl];
$frame->set_containing_line($current_line);
// Inline frames are currently treated as wrappers, and are not actually
// added to the line
if ($frame instanceof Inline) {
return null;
}
$current_line->add_frame($frame);
$this->increase_line_width($frame->get_margin_width());
$this->maximize_line_height($frame->get_margin_height(), $frame);
// Add any dangling list markers to the first line box if it is inline
if ($this->_cl === 0 && $current_line->inline
&& $this->dangling_markers !== []
) {
foreach ($this->dangling_markers as $marker) {
$current_line->add_list_marker($marker);
$this->maximize_line_height($marker->get_margin_height(), $marker);
}
$this->dangling_markers = [];
}
return $current_line;
}
/**
* Remove the given frame and all following frames and lines from the block.
*
* @param Frame $frame
*/
public function remove_frames_from_line(Frame $frame): void
{
// Inline frames are not added to line boxes themselves, only their
// text frame children
$actualFrame = $frame;
while ($actualFrame !== null && $actualFrame instanceof Inline) {
$actualFrame = $actualFrame->get_first_child();
}
if ($actualFrame === null) {
return;
}
// Search backwards through the lines for $frame
$frame = $actualFrame;
$i = $this->_cl;
$j = null;
while ($i > 0) {
$line = $this->_line_boxes[$i];
foreach ($line->get_frames() as $index => $f) {
if ($frame === $f) {
$j = $index;
break 2;
}
}
$i--;
}
if ($j === null) {
return;
}
// Remove all lines that follow
for ($k = $this->_cl; $k > $i; $k--) {
unset($this->_line_boxes[$k]);
}
// Remove the line, if it is empty
if ($j > 0) {
$line->remove_frames($j);
} else {
unset($this->_line_boxes[$i]);
}
// Reset array indices
$this->_line_boxes = array_values($this->_line_boxes);
$this->_cl = count($this->_line_boxes) - 1;
}
/**
* @param float $w
*/
public function increase_line_width(float $w): void
{
$this->_line_boxes[$this->_cl]->w += $w;
}
/**
* @param float $val
* @param Frame $frame
*/
public function maximize_line_height(float $val, Frame $frame): void
{
if ($val > $this->_line_boxes[$this->_cl]->h) {
$this->_line_boxes[$this->_cl]->tallest_frame = $frame;
$this->_line_boxes[$this->_cl]->h = $val;
}
}
/**
* @param bool $br
*/
public function add_line(bool $br = false): void
{
$line = $this->_line_boxes[$this->_cl];
$line->br = $br;
$y = $line->y + $line->h;
$new_line = new LineBox($this, $y);
$this->_line_boxes[++$this->_cl] = $new_line;
}
/**
* @param ListBullet $marker
*/
public function add_dangling_marker(ListBullet $marker): void
{
$this->dangling_markers[] = $marker;
}
/**
* Inherit any dangling markers from the parent block.
*
* @param Block $block
*/
public function inherit_dangling_markers(self $block): void
{
if ($block->dangling_markers !== []) {
$this->dangling_markers = $block->dangling_markers;
$block->dangling_markers = [];
}
}
}
dompdf/src/FrameDecorator/Text.php 0000644 00000011714 15024772104 0013144 0 ustar 00 is_text_node()) {
throw new Exception("Text_Decorator can only be applied to #text nodes.");
}
parent::__construct($frame, $dompdf);
$this->text_spacing = 0.0;
}
function reset()
{
parent::reset();
$this->text_spacing = 0.0;
}
// Accessor methods
/**
* @return float
*/
public function get_text_spacing(): float
{
return $this->text_spacing;
}
/**
* @return string
*/
function get_text()
{
// FIXME: this should be in a child class (and is incorrect)
// if ( $this->_frame->get_style()->content !== "normal" ) {
// $this->_frame->get_node()->data = $this->_frame->get_style()->content;
// $this->_frame->get_style()->content = "normal";
// }
// Helpers::pre_r("---");
// $style = $this->_frame->get_style();
// var_dump($text = $this->_frame->get_node()->data);
// var_dump($asc = utf8_decode($text));
// for ($i = 0; $i < strlen($asc); $i++)
// Helpers::pre_r("$i: " . $asc[$i] . " - " . ord($asc[$i]));
// Helpers::pre_r("width: " . $this->_dompdf->getFontMetrics()->getTextWidth($text, $style->font_family, $style->font_size));
return $this->_frame->get_node()->data;
}
//........................................................................
/**
* Vertical padding, border, and margin do not apply when determining the
* height for inline frames.
*
* http://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced
*
* The vertical padding, border and margin of an inline, non-replaced box
* start at the top and bottom of the content area, not the
* 'line-height'. But only the 'line-height' is used to calculate the
* height of the line box.
*
* @return float
*/
public function get_margin_height(): float
{
// This function is also called in add_frame_to_line() and is used to
// determine the line height
$style = $this->get_style();
$font = $style->font_family;
$size = $style->font_size;
$fontHeight = $this->_dompdf->getFontMetrics()->getFontHeight($font, $size);
return ($style->line_height / ($size > 0 ? $size : 1)) * $fontHeight;
}
public function get_padding_box(): array
{
$style = $this->_frame->get_style();
$pb = $this->_frame->get_padding_box();
$pb[3] = $pb["h"] = (float) $style->length_in_pt($style->height);
return $pb;
}
/**
* @param float $spacing
*/
public function set_text_spacing(float $spacing): void
{
$this->text_spacing = $spacing;
$this->recalculate_width();
}
/**
* Recalculate the text width
*
* @return float
*/
public function recalculate_width(): float
{
$fontMetrics = $this->_dompdf->getFontMetrics();
$style = $this->get_style();
$text = $this->get_text();
$font = $style->font_family;
$size = $style->font_size;
$word_spacing = $this->text_spacing + $style->word_spacing;
$letter_spacing = $style->letter_spacing;
$text_width = $fontMetrics->getTextWidth($text, $font, $size, $word_spacing, $letter_spacing);
$style->set_used("width", $text_width);
return $text_width;
}
// Text manipulation methods
/**
* Split the text in this frame at the offset specified. The remaining
* text is added as a sibling frame following this one and is returned.
*
* @param int $offset
* @return Frame|null
*/
function split_text($offset)
{
if ($offset == 0) {
return null;
}
$split = $this->_frame->get_node()->splitText($offset);
if ($split === false) {
return null;
}
$deco = $this->copy($split);
$p = $this->get_parent();
$p->insert_child_after($deco, $this, false);
if ($p instanceof Inline) {
$p->split($deco);
}
return $deco;
}
/**
* @param int $offset
* @param int $count
*/
function delete_text($offset, $count)
{
$this->_frame->get_node()->deleteData($offset, $count);
}
/**
* @param string $text
*/
function set_text($text)
{
$this->_frame->get_node()->data = $text;
}
}
dompdf/src/Css/Color.php 0000644 00000024712 15024772104 0011133 0 ustar 00 "F0F8FF",
"antiquewhite" => "FAEBD7",
"aqua" => "00FFFF",
"aquamarine" => "7FFFD4",
"azure" => "F0FFFF",
"beige" => "F5F5DC",
"bisque" => "FFE4C4",
"black" => "000000",
"blanchedalmond" => "FFEBCD",
"blue" => "0000FF",
"blueviolet" => "8A2BE2",
"brown" => "A52A2A",
"burlywood" => "DEB887",
"cadetblue" => "5F9EA0",
"chartreuse" => "7FFF00",
"chocolate" => "D2691E",
"coral" => "FF7F50",
"cornflowerblue" => "6495ED",
"cornsilk" => "FFF8DC",
"crimson" => "DC143C",
"cyan" => "00FFFF",
"darkblue" => "00008B",
"darkcyan" => "008B8B",
"darkgoldenrod" => "B8860B",
"darkgray" => "A9A9A9",
"darkgreen" => "006400",
"darkgrey" => "A9A9A9",
"darkkhaki" => "BDB76B",
"darkmagenta" => "8B008B",
"darkolivegreen" => "556B2F",
"darkorange" => "FF8C00",
"darkorchid" => "9932CC",
"darkred" => "8B0000",
"darksalmon" => "E9967A",
"darkseagreen" => "8FBC8F",
"darkslateblue" => "483D8B",
"darkslategray" => "2F4F4F",
"darkslategrey" => "2F4F4F",
"darkturquoise" => "00CED1",
"darkviolet" => "9400D3",
"deeppink" => "FF1493",
"deepskyblue" => "00BFFF",
"dimgray" => "696969",
"dimgrey" => "696969",
"dodgerblue" => "1E90FF",
"firebrick" => "B22222",
"floralwhite" => "FFFAF0",
"forestgreen" => "228B22",
"fuchsia" => "FF00FF",
"gainsboro" => "DCDCDC",
"ghostwhite" => "F8F8FF",
"gold" => "FFD700",
"goldenrod" => "DAA520",
"gray" => "808080",
"green" => "008000",
"greenyellow" => "ADFF2F",
"grey" => "808080",
"honeydew" => "F0FFF0",
"hotpink" => "FF69B4",
"indianred" => "CD5C5C",
"indigo" => "4B0082",
"ivory" => "FFFFF0",
"khaki" => "F0E68C",
"lavender" => "E6E6FA",
"lavenderblush" => "FFF0F5",
"lawngreen" => "7CFC00",
"lemonchiffon" => "FFFACD",
"lightblue" => "ADD8E6",
"lightcoral" => "F08080",
"lightcyan" => "E0FFFF",
"lightgoldenrodyellow" => "FAFAD2",
"lightgray" => "D3D3D3",
"lightgreen" => "90EE90",
"lightgrey" => "D3D3D3",
"lightpink" => "FFB6C1",
"lightsalmon" => "FFA07A",
"lightseagreen" => "20B2AA",
"lightskyblue" => "87CEFA",
"lightslategray" => "778899",
"lightslategrey" => "778899",
"lightsteelblue" => "B0C4DE",
"lightyellow" => "FFFFE0",
"lime" => "00FF00",
"limegreen" => "32CD32",
"linen" => "FAF0E6",
"magenta" => "FF00FF",
"maroon" => "800000",
"mediumaquamarine" => "66CDAA",
"mediumblue" => "0000CD",
"mediumorchid" => "BA55D3",
"mediumpurple" => "9370DB",
"mediumseagreen" => "3CB371",
"mediumslateblue" => "7B68EE",
"mediumspringgreen" => "00FA9A",
"mediumturquoise" => "48D1CC",
"mediumvioletred" => "C71585",
"midnightblue" => "191970",
"mintcream" => "F5FFFA",
"mistyrose" => "FFE4E1",
"moccasin" => "FFE4B5",
"navajowhite" => "FFDEAD",
"navy" => "000080",
"oldlace" => "FDF5E6",
"olive" => "808000",
"olivedrab" => "6B8E23",
"orange" => "FFA500",
"orangered" => "FF4500",
"orchid" => "DA70D6",
"palegoldenrod" => "EEE8AA",
"palegreen" => "98FB98",
"paleturquoise" => "AFEEEE",
"palevioletred" => "DB7093",
"papayawhip" => "FFEFD5",
"peachpuff" => "FFDAB9",
"peru" => "CD853F",
"pink" => "FFC0CB",
"plum" => "DDA0DD",
"powderblue" => "B0E0E6",
"purple" => "800080",
"red" => "FF0000",
"rosybrown" => "BC8F8F",
"royalblue" => "4169E1",
"saddlebrown" => "8B4513",
"salmon" => "FA8072",
"sandybrown" => "F4A460",
"seagreen" => "2E8B57",
"seashell" => "FFF5EE",
"sienna" => "A0522D",
"silver" => "C0C0C0",
"skyblue" => "87CEEB",
"slateblue" => "6A5ACD",
"slategray" => "708090",
"slategrey" => "708090",
"snow" => "FFFAFA",
"springgreen" => "00FF7F",
"steelblue" => "4682B4",
"tan" => "D2B48C",
"teal" => "008080",
"thistle" => "D8BFD8",
"tomato" => "FF6347",
"turquoise" => "40E0D0",
"violet" => "EE82EE",
"wheat" => "F5DEB3",
"white" => "FFFFFF",
"whitesmoke" => "F5F5F5",
"yellow" => "FFFF00",
"yellowgreen" => "9ACD32",
];
/**
* @param array|string|null $color
* @return array|string|null
*/
static function parse($color)
{
if ($color === null) {
return null;
}
if (is_array($color)) {
// Assume the array has the right format...
// FIXME: should/could verify this.
return $color;
}
static $cache = [];
$color = strtolower($color);
if (isset($cache[$color])) {
return $cache[$color];
}
if ($color === "transparent") {
return $cache[$color] = $color;
}
if (isset(self::$cssColorNames[$color])) {
return $cache[$color] = self::getArray(self::$cssColorNames[$color]);
}
// https://www.w3.org/TR/css-color-4/#hex-notation
if (mb_substr($color, 0, 1) === "#") {
$length = mb_strlen($color);
$alpha = 1.0;
// #rgb format
if ($length === 4) {
return $cache[$color] = self::getArray($color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3]);
}
// #rgba format
if ($length === 5) {
if (ctype_xdigit($color[4])) {
$alpha = round(hexdec($color[4] . $color[4])/255, 2);
}
return $cache[$color] = self::getArray($color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3], $alpha);
}
// #rrggbb format
if ($length === 7) {
return $cache[$color] = self::getArray(mb_substr($color, 1, 6));
}
// #rrggbbaa format
if ($length === 9) {
if (ctype_xdigit(mb_substr($color, 7, 2))) {
$alpha = round(hexdec(mb_substr($color, 7, 2))/255, 2);
}
return $cache[$color] = self::getArray(mb_substr($color, 1, 6), $alpha);
}
return null;
}
// rgb( r g b [/α] ) / rgb( r,g,b[,α] ) format and alias rgba()
// https://www.w3.org/TR/css-color-4/#rgb-functions
if (mb_substr($color, 0, 4) === "rgb(" || mb_substr($color, 0, 5) === "rgba(") {
$i = mb_strpos($color, "(");
$j = mb_strpos($color, ")");
// Bad color value
if ($i === false || $j === false) {
return null;
}
$value_decl = trim(mb_substr($color, $i + 1, $j - $i - 1));
if (mb_strpos($value_decl, ",") === false) {
// Space-separated values syntax `r g b` or `r g b / α`
$parts = preg_split("/\s*\/\s*/", $value_decl);
$triplet = preg_split("/\s+/", $parts[0]);
$alpha = $parts[1] ?? 1.0;
} else {
// Comma-separated values syntax `r, g, b` or `r, g, b, α`
$parts = preg_split("/\s*,\s*/", $value_decl);
$triplet = array_slice($parts, 0, 3);
$alpha = $parts[3] ?? 1.0;
}
if (count($triplet) !== 3) {
return null;
}
// Parse alpha value
if (Helpers::is_percent($alpha)) {
$alpha = (float) $alpha / 100;
} else {
$alpha = (float) $alpha;
}
$alpha = max(0.0, min($alpha, 1.0));
foreach ($triplet as &$c) {
if (Helpers::is_percent($c)) {
$c = round((float) $c * 2.55);
}
}
return $cache[$color] = self::getArray(vsprintf("%02X%02X%02X", $triplet), $alpha);
}
// cmyk( c,m,y,k ) format
// http://www.w3.org/TR/css3-gcpm/#cmyk-colors
if (mb_substr($color, 0, 5) === "cmyk(") {
$i = mb_strpos($color, "(");
$j = mb_strpos($color, ")");
// Bad color value
if ($i === false || $j === false) {
return null;
}
$values = explode(",", mb_substr($color, $i + 1, $j - $i - 1));
if (count($values) != 4) {
return null;
}
$values = array_map(function ($c) {
return min(1.0, max(0.0, floatval(trim($c))));
}, $values);
return $cache[$color] = self::getArray($values);
}
// Invalid or unsupported color format
return null;
}
/**
* @param array|string $color
* @param float $alpha
* @return array
*/
static function getArray($color, $alpha = 1.0)
{
$c = [null, null, null, null, "alpha" => $alpha, "hex" => null];
if (is_array($color)) {
$c = $color;
$c["c"] = $c[0];
$c["m"] = $c[1];
$c["y"] = $c[2];
$c["k"] = $c[3];
$c["alpha"] = $alpha;
$c["hex"] = "cmyk($c[0],$c[1],$c[2],$c[3])";
} else {
if (ctype_xdigit($color) === false || mb_strlen($color) !== 6) {
// invalid color value ... expected 6-character hex
return $c;
}
$c[0] = hexdec(mb_substr($color, 0, 2)) / 0xff;
$c[1] = hexdec(mb_substr($color, 2, 2)) / 0xff;
$c[2] = hexdec(mb_substr($color, 4, 2)) / 0xff;
$c["r"] = $c[0];
$c["g"] = $c[1];
$c["b"] = $c[2];
$c["alpha"] = $alpha;
$c["hex"] = sprintf("#%s%02X", $color, round($alpha * 255));
}
return $c;
}
}
dompdf/src/Css/Style.php 0000644 00000340370 15024772104 0011156 0 ustar 00 margin_top = 10.0;
* echo $style->margin_top; // Returns `10.0`
* ```
*
* To declare a property from a string, use {@link Style::set_prop()}:
*
* ```
* $style->set_prop("margin_top", "1em");
* echo $style->get_specified("margin_top"); // Returns `1em`
* echo $style->margin_top; // Returns `12.0`, assuming the default font size
* ```
*
* Actual CSS parsing is performed in the {@link Stylesheet} class.
*
* @property string $azimuth
* @property string $background_attachment
* @property array|string $background_color
* @property string $background_image Image URL or `none`
* @property string $background_image_resolution
* @property array $background_position
* @property string $background_repeat
* @property array|string $background_size `cover`, `contain`, or `[width, height]`, each being a length, percentage, or `auto`
* @property string $border_collapse
* @property string $border_color Only use for setting all sides to the same color
* @property float[] $border_spacing Pair of `[horizontal, vertical]` spacing
* @property string $border_style Only use for setting all sides to the same style
* @property array|string $border_top_color
* @property array|string $border_right_color
* @property array|string $border_bottom_color
* @property array|string $border_left_color
* @property string $border_top_style Valid border style
* @property string $border_right_style Valid border style
* @property string $border_bottom_style Valid border style
* @property string $border_left_style Valid border style
* @property float $border_top_width Length in pt
* @property float $border_right_width Length in pt
* @property float $border_bottom_width Length in pt
* @property float $border_left_width Length in pt
* @property string $border_width Only use for setting all sides to the same width
* @property float|string $border_bottom_left_radius Radius in pt or a percentage value
* @property float|string $border_bottom_right_radius Radius in pt or a percentage value
* @property float|string $border_top_left_radius Radius in pt or a percentage value
* @property float|string $border_top_right_radius Radius in pt or a percentage value
* @property string $border_radius Only use for setting all corners to the same radius
* @property float|string $bottom Length in pt, a percentage value, or `auto`
* @property string $caption_side
* @property string $clear
* @property string $clip
* @property array|string $color
* @property string[]|string $content List of content components, `normal`, or `none`
* @property array|string $counter_increment Array defining the counters to increment or `none`
* @property array|string $counter_reset Array defining the counters to reset or `none`
* @property string $cue_after
* @property string $cue_before
* @property string $cue
* @property string $cursor
* @property string $direction
* @property string $display
* @property string $elevation
* @property string $empty_cells
* @property string $float
* @property string $font_family
* @property float $font_size Length in pt
* @property string $font_style
* @property string $font_variant
* @property string $font_weight
* @property float|string $height Length in pt, a percentage value, or `auto`
* @property string $image_resolution
* @property string $inset Only use for setting all box insets to the same length
* @property float|string $left Length in pt, a percentage value, or `auto`
* @property float $letter_spacing Length in pt
* @property float $line_height Length in pt
* @property string $list_style_image Image URL or `none`
* @property string $list_style_position
* @property string $list_style_type
* @property float|string $margin_right Length in pt, a percentage value, or `auto`
* @property float|string $margin_left Length in pt, a percentage value, or `auto`
* @property float|string $margin_top Length in pt, a percentage value, or `auto`
* @property float|string $margin_bottom Length in pt, a percentage value, or `auto`
* @property string $margin Only use for setting all sides to the same length
* @property float|string $max_height Length in pt, a percentage value, or `none`
* @property float|string $max_width Length in pt, a percentage value, or `none`
* @property float|string $min_height Length in pt, a percentage value, or `auto`
* @property float|string $min_width Length in pt, a percentage value, or `auto`
* @property float $opacity Number in the range [0, 1]
* @property int $orphans
* @property array|string $outline_color
* @property string $outline_style Valid border style, except for `hidden`
* @property float $outline_width Length in pt
* @property float $outline_offset Length in pt
* @property string $overflow
* @property string $overflow_wrap
* @property float|string $padding_top Length in pt or a percentage value
* @property float|string $padding_right Length in pt or a percentage value
* @property float|string $padding_bottom Length in pt or a percentage value
* @property float|string $padding_left Length in pt or a percentage value
* @property string $padding Only use for setting all sides to the same length
* @property string $page_break_after
* @property string $page_break_before
* @property string $page_break_inside
* @property string $pause_after
* @property string $pause_before
* @property string $pause
* @property string $pitch_range
* @property string $pitch
* @property string $play_during
* @property string $position
* @property string $quotes
* @property string $richness
* @property float|string $right Length in pt, a percentage value, or `auto`
* @property float[]|string $size Pair of `[width, height]` or `auto`
* @property string $speak_header
* @property string $speak_numeral
* @property string $speak_punctuation
* @property string $speak
* @property string $speech_rate
* @property string $src
* @property string $stress
* @property string $table_layout
* @property string $text_align
* @property string $text_decoration
* @property float|string $text_indent Length in pt or a percentage value
* @property string $text_transform
* @property float|string $top Length in pt, a percentage value, or `auto`
* @property array $transform List of transforms
* @property array $transform_origin
* @property string $unicode_bidi
* @property string $unicode_range
* @property string $vertical_align
* @property string $visibility
* @property string $voice_family
* @property string $volume
* @property string $white_space
* @property int $widows
* @property float|string $width Length in pt, a percentage value, or `auto`
* @property string $word_break
* @property float $word_spacing Length in pt
* @property int|string $z_index Integer value or `auto`
* @property string $_dompdf_keep
*
* @package dompdf
*/
class Style
{
protected const CSS_IDENTIFIER = "-?[_a-zA-Z]+[_a-zA-Z0-9-]*";
protected const CSS_INTEGER = "[+-]?\d+";
protected const CSS_NUMBER = "[+-]?\d*\.?\d+(?:[eE][+-]?\d+)?";
/**
* Default font size, in points.
*
* @var float
*/
public static $default_font_size = 12;
/**
* Default line height, as a fraction of the font size.
*
* @var float
*/
public static $default_line_height = 1.2;
/**
* Default "absolute" font sizes relative to the default font-size
* https://www.w3.org/TR/css-fonts-3/#absolute-size-value
*
* @var array
*/
public static $font_size_keywords = [
"xx-small" => 0.6, // 3/5
"x-small" => 0.75, // 3/4
"small" => 0.889, // 8/9
"medium" => 1, // 1
"large" => 1.2, // 6/5
"x-large" => 1.5, // 3/2
"xx-large" => 2.0, // 2/1
];
/**
* List of valid text-align keywords.
*/
public const TEXT_ALIGN_KEYWORDS = ["left", "right", "center", "justify"];
/**
* List of valid vertical-align keywords.
*/
public const VERTICAL_ALIGN_KEYWORDS = ["baseline", "bottom", "middle",
"sub", "super", "text-bottom", "text-top", "top"];
/**
* List of all block-level (outer) display types.
* * https://www.w3.org/TR/css-display-3/#display-type
* * https://www.w3.org/TR/css-display-3/#block-level
*/
public const BLOCK_LEVEL_TYPES = [
"block",
// "flow-root",
"list-item",
// "flex",
// "grid",
"table"
];
/**
* List of all inline-level (outer) display types.
* * https://www.w3.org/TR/css-display-3/#display-type
* * https://www.w3.org/TR/css-display-3/#inline-level
*/
public const INLINE_LEVEL_TYPES = [
"inline",
"inline-block",
// "inline-flex",
// "inline-grid",
"inline-table"
];
/**
* List of all table-internal (outer) display types.
* * https://www.w3.org/TR/css-display-3/#layout-specific-display
*/
public const TABLE_INTERNAL_TYPES = [
"table-row-group",
"table-header-group",
"table-footer-group",
"table-row",
"table-cell",
"table-column-group",
"table-column",
"table-caption"
];
/**
* List of all inline (inner) display types.
*/
public const INLINE_TYPES = ["inline"];
/**
* List of all block (inner) display types.
*/
public const BLOCK_TYPES = ["block", "inline-block", "table-cell", "list-item"];
/**
* List of all table (inner) display types.
*/
public const TABLE_TYPES = ["table", "inline-table"];
/**
* Lookup table for valid display types. Initially computed from the
* different constants.
*
* @var array
*/
protected static $valid_display_types = [];
/**
* List of all positioned types.
*/
public const POSITIONED_TYPES = ["relative", "absolute", "fixed"];
/**
* List of valid border styles.
*/
public const BORDER_STYLES = [
"none", "hidden",
"dotted", "dashed", "solid",
"double", "groove", "ridge", "inset", "outset"
];
/**
* List of valid outline-style values.
* Same as the border styles, except `auto` is allowed, `hidden` is not.
*
* @link https://www.w3.org/TR/css-ui-4/#typedef-outline-line-style
*/
protected const OUTLINE_STYLES = [
"auto", "none",
"dotted", "dashed", "solid",
"double", "groove", "ridge", "inset", "outset"
];
/**
* Map of CSS shorthand properties and their corresponding sub-properties.
* The order of the sub-properties is relevant for the fallback getter,
* which is used in case no specific getter method is defined.
*
* @var array
*/
protected static $_props_shorthand = [
"background" => [
"background_image",
"background_position",
"background_size",
"background_repeat",
// "background_origin",
// "background_clip",
"background_attachment",
"background_color"
],
"border" => [
"border_top_width",
"border_right_width",
"border_bottom_width",
"border_left_width",
"border_top_style",
"border_right_style",
"border_bottom_style",
"border_left_style",
"border_top_color",
"border_right_color",
"border_bottom_color",
"border_left_color"
],
"border_top" => [
"border_top_width",
"border_top_style",
"border_top_color"
],
"border_right" => [
"border_right_width",
"border_right_style",
"border_right_color"
],
"border_bottom" => [
"border_bottom_width",
"border_bottom_style",
"border_bottom_color"
],
"border_left" => [
"border_left_width",
"border_left_style",
"border_left_color"
],
"border_width" => [
"border_top_width",
"border_right_width",
"border_bottom_width",
"border_left_width"
],
"border_style" => [
"border_top_style",
"border_right_style",
"border_bottom_style",
"border_left_style"
],
"border_color" => [
"border_top_color",
"border_right_color",
"border_bottom_color",
"border_left_color"
],
"border_radius" => [
"border_top_left_radius",
"border_top_right_radius",
"border_bottom_right_radius",
"border_bottom_left_radius"
],
"font" => [
"font_family",
"font_size",
// "font_stretch",
"font_style",
"font_variant",
"font_weight",
"line_height"
],
"inset" => [
"top",
"right",
"bottom",
"left"
],
"list_style" => [
"list_style_image",
"list_style_position",
"list_style_type"
],
"margin" => [
"margin_top",
"margin_right",
"margin_bottom",
"margin_left"
],
"padding" => [
"padding_top",
"padding_right",
"padding_bottom",
"padding_left"
],
"outline" => [
"outline_width",
"outline_style",
"outline_color"
]
];
/**
* Maps legacy property names to actual property names.
*
* @var array
*/
protected static $_props_alias = [
"word_wrap" => "overflow_wrap",
"_dompdf_background_image_resolution" => "background_image_resolution",
"_dompdf_image_resolution" => "image_resolution",
"_webkit_transform" => "transform",
"_webkit_transform_origin" => "transform_origin"
];
/**
* Default style values.
*
* @link https://www.w3.org/TR/CSS21/propidx.html
*
* @var array
*/
protected static $_defaults = null;
/**
* List of inherited properties
*
* @link https://www.w3.org/TR/CSS21/propidx.html
*
* @var array
*/
protected static $_inherited = null;
/**
* Caches method_exists result
*
* @var array
*/
protected static $_methods_cache = [];
/**
* The stylesheet this style belongs to
*
* @var Stylesheet
*/
protected $_stylesheet;
/**
* Media queries attached to the style
*
* @var array
*/
protected $_media_queries;
/**
* Properties set by an `!important` declaration.
*
* @var array
*/
protected $_important_props = [];
/**
* Specified (or declared) values of the CSS properties.
*
* https://www.w3.org/TR/css-cascade-3/#value-stages
*
* @var array
*/
protected $_props = [];
/**
* Computed values of the CSS properties.
*
* @var array
*/
protected $_props_computed = [];
/**
* Used values of the CSS properties.
*
* @var array
*/
protected $_props_used = [];
/**
* Marks properties with non-final used values that should be cleared on
* style reset.
*
* @var array
*/
protected $non_final_used = [];
protected static $_dependency_map = [
"border_top_style" => [
"border_top_width"
],
"border_bottom_style" => [
"border_bottom_width"
],
"border_left_style" => [
"border_left_width"
],
"border_right_style" => [
"border_right_width"
],
"direction" => [
"text_align"
],
"font_size" => [
"background_position",
"background_size",
"border_top_width",
"border_right_width",
"border_bottom_width",
"border_left_width",
"border_top_left_radius",
"border_top_right_radius",
"border_bottom_right_radius",
"border_bottom_left_radius",
"letter_spacing",
"line_height",
"margin_top",
"margin_right",
"margin_bottom",
"margin_left",
"outline_width",
"outline_offset",
"padding_top",
"padding_right",
"padding_bottom",
"padding_left",
"word_spacing",
"width",
"height",
"min-width",
"min-height",
"max-width",
"max-height"
],
"float" => [
"display"
],
"position" => [
"display"
],
"outline_style" => [
"outline_width"
]
];
/**
* Lookup table for dependent properties. Initially computed from the
* dependency map.
*
* @var array
*/
protected static $_dependent_props = [];
/**
* Style of the parent element in document tree.
*
* @var Style
*/
protected $parent_style;
/**
* @var Frame|null
*/
protected $_frame;
/**
* The origin of the style
*
* @var int
*/
protected $_origin = Stylesheet::ORIG_AUTHOR;
/**
* The computed bottom spacing
*
* @var float|string|null
*/
private $_computed_bottom_spacing = null;
/**
* @var bool|null
*/
private $has_border_radius_cache = null;
/**
* @var array|null
*/
private $resolved_border_radius = null;
/**
* @var FontMetrics
*/
private $fontMetrics;
/**
* @param Stylesheet $stylesheet The stylesheet the style is associated with.
* @param int $origin
*/
public function __construct(Stylesheet $stylesheet, int $origin = Stylesheet::ORIG_AUTHOR)
{
$this->fontMetrics = $stylesheet->getFontMetrics();
$this->_stylesheet = $stylesheet;
$this->_media_queries = [];
$this->_origin = $origin;
$this->parent_style = null;
if (!isset(self::$_defaults)) {
// Shorthand
$d =& self::$_defaults;
// All CSS 2.1 properties, and their default values
// Some properties are specified with their computed value for
// efficiency; this only works if the computed value is not
// dependent on another property
$d["azimuth"] = "center";
$d["background_attachment"] = "scroll";
$d["background_color"] = "transparent";
$d["background_image"] = "none";
$d["background_image_resolution"] = "normal";
$d["background_position"] = ["0%", "0%"];
$d["background_repeat"] = "repeat";
$d["background"] = "";
$d["border_collapse"] = "separate";
$d["border_color"] = "";
$d["border_spacing"] = [0.0, 0.0];
$d["border_style"] = "";
$d["border_top"] = "";
$d["border_right"] = "";
$d["border_bottom"] = "";
$d["border_left"] = "";
$d["border_top_color"] = "currentcolor";
$d["border_right_color"] = "currentcolor";
$d["border_bottom_color"] = "currentcolor";
$d["border_left_color"] = "currentcolor";
$d["border_top_style"] = "none";
$d["border_right_style"] = "none";
$d["border_bottom_style"] = "none";
$d["border_left_style"] = "none";
$d["border_top_width"] = "medium";
$d["border_right_width"] = "medium";
$d["border_bottom_width"] = "medium";
$d["border_left_width"] = "medium";
$d["border_width"] = "";
$d["border_bottom_left_radius"] = 0.0;
$d["border_bottom_right_radius"] = 0.0;
$d["border_top_left_radius"] = 0.0;
$d["border_top_right_radius"] = 0.0;
$d["border_radius"] = "";
$d["border"] = "";
$d["bottom"] = "auto";
$d["caption_side"] = "top";
$d["clear"] = "none";
$d["clip"] = "auto";
$d["color"] = "#000000";
$d["content"] = "normal";
$d["counter_increment"] = "none";
$d["counter_reset"] = "none";
$d["cue_after"] = "none";
$d["cue_before"] = "none";
$d["cue"] = "";
$d["cursor"] = "auto";
$d["direction"] = "ltr";
$d["display"] = "inline";
$d["elevation"] = "level";
$d["empty_cells"] = "show";
$d["float"] = "none";
$d["font_family"] = $stylesheet->get_dompdf()->getOptions()->getDefaultFont();
$d["font_size"] = "medium";
$d["font_style"] = "normal";
$d["font_variant"] = "normal";
$d["font_weight"] = "normal";
$d["font"] = "";
$d["height"] = "auto";
$d["image_resolution"] = "normal";
$d["inset"] = "";
$d["left"] = "auto";
$d["letter_spacing"] = "normal";
$d["line_height"] = "normal";
$d["list_style_image"] = "none";
$d["list_style_position"] = "outside";
$d["list_style_type"] = "disc";
$d["list_style"] = "";
$d["margin_right"] = 0.0;
$d["margin_left"] = 0.0;
$d["margin_top"] = 0.0;
$d["margin_bottom"] = 0.0;
$d["margin"] = "";
$d["max_height"] = "none";
$d["max_width"] = "none";
$d["min_height"] = "auto";
$d["min_width"] = "auto";
$d["orphans"] = 2;
$d["outline_color"] = "currentcolor"; // "invert" special color is not supported
$d["outline_style"] = "none";
$d["outline_width"] = "medium";
$d["outline_offset"] = 0.0;
$d["outline"] = "";
$d["overflow"] = "visible";
$d["overflow_wrap"] = "normal";
$d["padding_top"] = 0.0;
$d["padding_right"] = 0.0;
$d["padding_bottom"] = 0.0;
$d["padding_left"] = 0.0;
$d["padding"] = "";
$d["page_break_after"] = "auto";
$d["page_break_before"] = "auto";
$d["page_break_inside"] = "auto";
$d["pause_after"] = "0";
$d["pause_before"] = "0";
$d["pause"] = "";
$d["pitch_range"] = "50";
$d["pitch"] = "medium";
$d["play_during"] = "auto";
$d["position"] = "static";
$d["quotes"] = "auto";
$d["richness"] = "50";
$d["right"] = "auto";
$d["size"] = "auto"; // @page
$d["speak_header"] = "once";
$d["speak_numeral"] = "continuous";
$d["speak_punctuation"] = "none";
$d["speak"] = "normal";
$d["speech_rate"] = "medium";
$d["stress"] = "50";
$d["table_layout"] = "auto";
$d["text_align"] = "";
$d["text_decoration"] = "none";
$d["text_indent"] = 0.0;
$d["text_transform"] = "none";
$d["top"] = "auto";
$d["unicode_bidi"] = "normal";
$d["vertical_align"] = "baseline";
$d["visibility"] = "visible";
$d["voice_family"] = "";
$d["volume"] = "medium";
$d["white_space"] = "normal";
$d["widows"] = 2;
$d["width"] = "auto";
$d["word_break"] = "normal";
$d["word_spacing"] = "normal";
$d["z_index"] = "auto";
// CSS3
$d["opacity"] = 1.0;
$d["background_size"] = ["auto", "auto"];
$d["transform"] = "none";
$d["transform_origin"] = "50% 50%";
// for @font-face
$d["src"] = "";
$d["unicode_range"] = "";
// vendor-prefixed properties
$d["_dompdf_keep"] = "";
// Properties that inherit by default
self::$_inherited = [
"azimuth",
"background_image_resolution",
"border_collapse",
"border_spacing",
"caption_side",
"color",
"cursor",
"direction",
"elevation",
"empty_cells",
"font_family",
"font_size",
"font_style",
"font_variant",
"font_weight",
"font",
"image_resolution",
"letter_spacing",
"line_height",
"list_style_image",
"list_style_position",
"list_style_type",
"list_style",
"orphans",
"overflow_wrap",
"pitch_range",
"pitch",
"quotes",
"richness",
"speak_header",
"speak_numeral",
"speak_punctuation",
"speak",
"speech_rate",
"stress",
"text_align",
"text_indent",
"text_transform",
"visibility",
"voice_family",
"volume",
"white_space",
"widows",
"word_break",
"word_spacing",
];
// Compute dependent props from dependency map
foreach (self::$_dependency_map as $props) {
foreach ($props as $prop) {
self::$_dependent_props[$prop] = true;
}
}
// Compute valid display-type lookup table
self::$valid_display_types = [
"none" => true,
"-dompdf-br" => true,
"-dompdf-image" => true,
"-dompdf-list-bullet" => true,
"-dompdf-page" => true
];
foreach (self::BLOCK_LEVEL_TYPES as $val) {
self::$valid_display_types[$val] = true;
}
foreach (self::INLINE_LEVEL_TYPES as $val) {
self::$valid_display_types[$val] = true;
}
foreach (self::TABLE_INTERNAL_TYPES as $val) {
self::$valid_display_types[$val] = true;
}
}
}
/**
* Clear all non-final used values.
*
* @return void
*/
public function reset(): void
{
foreach (array_keys($this->non_final_used) as $prop) {
unset($this->_props_used[$prop]);
}
$this->non_final_used = [];
}
/**
* @param array $media_queries
*/
public function set_media_queries(array $media_queries): void
{
$this->_media_queries = $media_queries;
}
/**
* @return array
*/
public function get_media_queries(): array
{
return $this->_media_queries;
}
/**
* @param Frame $frame
*/
public function set_frame(Frame $frame): void
{
$this->_frame = $frame;
}
/**
* @return Frame|null
*/
public function get_frame(): ?Frame
{
return $this->_frame;
}
/**
* @param int $origin
*/
public function set_origin(int $origin): void
{
$this->_origin = $origin;
}
/**
* @return int
*/
public function get_origin(): int
{
return $this->_origin;
}
/**
* Returns the {@link Stylesheet} the style is associated with.
*
* @return Stylesheet
*/
public function get_stylesheet(): Stylesheet
{
return $this->_stylesheet;
}
public function is_absolute(): bool
{
$position = $this->__get("position");
return $position === "absolute" || $position === "fixed";
}
public function is_in_flow(): bool
{
$float = $this->__get("float");
return $float === "none" && !$this->is_absolute();
}
/**
* Converts any CSS length value into an absolute length in points.
*
* length_in_pt() takes a single length (e.g. '1em') or an array of
* lengths and returns an absolute length. If an array is passed, then
* the return value is the sum of all elements. If any of the lengths
* provided are "auto" or "none" then that value is returned.
*
* If a reference size is not provided, the current font size is used.
*
* @param float|string|array $length The numeric length (or string measurement) or array of lengths to resolve.
* @param float|null $ref_size An absolute reference size to resolve percentage lengths.
*
* @return float|string
*/
public function length_in_pt($length, ?float $ref_size = null)
{
$font_size = $this->__get("font_size");
$ref_size = $ref_size ?? $font_size;
if (!\is_array($length)) {
$length = [$length];
}
$ret = 0.0;
foreach ($length as $l) {
if ($l === "auto" || $l === "none") {
return $l;
}
// Assume numeric values are already in points
if (is_numeric($l)) {
$ret += (float) $l;
continue;
}
$val = $this->single_length_in_pt((string) $l, $ref_size, $font_size);
$ret += $val ?? 0;
}
return $ret;
}
/**
* Convert a length declaration to pt.
*
* @param string $l The length declaration.
* @param float $ref_size Reference size for percentage declarations.
* @param float|null $font_size Font size for resolving font-size relative units.
*
* @return float|null The length in pt, or `null` for invalid declarations.
*/
protected function single_length_in_pt(string $l, float $ref_size = 0, ?float $font_size = null): ?float
{
static $cache = [];
$font_size = $font_size ?? $this->__get("font_size");
$key = "$l/$ref_size/$font_size";
if (\array_key_exists($key, $cache)) {
return $cache[$key];
}
$number = self::CSS_NUMBER;
$pattern = "/^($number)(.*)?$/";
if (!preg_match($pattern, $l, $matches)) {
return null;
}
$v = (float) $matches[1];
$unit = mb_strtolower($matches[2]);
if ($unit === "") {
// Legacy support for unitless values, not covered by spec. Might
// want to restrict this to unitless `0` in the future
$value = $v;
}
elseif ($unit === "%") {
$value = $v / 100 * $ref_size;
}
elseif ($unit === "px") {
$dpi = $this->_stylesheet->get_dompdf()->getOptions()->getDpi();
$value = ($v * 72) / $dpi;
}
elseif ($unit === "pt") {
$value = $v;
}
elseif ($unit === "rem") {
$tree = $this->_stylesheet->get_dompdf()->getTree();
$root_style = $tree !== null ? $tree->get_root()->get_style() : null;
$root_font_size = $root_style === null || $root_style === $this
? $font_size
: $root_style->__get("font_size");
$value = $v * $root_font_size;
// Skip caching if the root style is not available yet, as to avoid
// incorrectly cached values if the root font size is different from
// the default
if ($root_style === null) {
return $value;
}
}
elseif ($unit === "em") {
$value = $v * $font_size;
}
elseif ($unit === "cm") {
$value = $v * 72 / 2.54;
}
elseif ($unit === "mm") {
$value = $v * 72 / 25.4;
}
elseif ($unit === "ex") {
// FIXME: em:ex ratio?
$value = $v * $font_size / 2;
}
elseif ($unit === "in") {
$value = $v * 72;
}
elseif ($unit === "pc") {
$value = $v * 12;
}
else {
// Invalid or unsupported declaration
$value = null;
}
return $cache[$key] = $value;
}
/**
* Resolve inherited property values using the provided parent style or the
* default values, in case no parent style exists.
*
* https://www.w3.org/TR/css-cascade-3/#inheriting
*
* @param Style|null $parent
*/
public function inherit(?Style $parent = null): void
{
$this->parent_style = $parent;
// Clear the computed font size, as it might depend on the parent
// font size
unset($this->_props_computed["font_size"]);
unset($this->_props_used["font_size"]);
if ($parent) {
foreach (self::$_inherited as $prop) {
// For properties that inherit by default: When the cascade did
// not result in a value, inherit the parent value. Inheritance
// is handled via the specific sub-properties for shorthands
if (isset($this->_props[$prop]) || isset(self::$_props_shorthand[$prop])) {
continue;
}
if (isset($parent->_props[$prop])) {
$parent_val = $parent->computed($prop);
$this->_props[$prop] = $parent_val;
$this->_props_computed[$prop] = $parent_val;
$this->_props_used[$prop] = null;
}
}
}
foreach ($this->_props as $prop => $val) {
if ($val === "inherit") {
if ($parent && isset($parent->_props[$prop])) {
$parent_val = $parent->computed($prop);
$this->_props[$prop] = $parent_val;
$this->_props_computed[$prop] = $parent_val;
$this->_props_used[$prop] = null;
} else {
// Parent prop not set, use default
$this->_props[$prop] = self::$_defaults[$prop];
unset($this->_props_computed[$prop]);
unset($this->_props_used[$prop]);
}
}
}
}
/**
* Override properties in this style with those in $style
*
* @param Style $style
*/
public function merge(Style $style): void
{
foreach ($style->_props as $prop => $val) {
$important = isset($style->_important_props[$prop]);
// `!important` declarations take precedence over normal ones
if (!$important && isset($this->_important_props[$prop])) {
continue;
}
if ($important) {
$this->_important_props[$prop] = true;
}
$this->_props[$prop] = $val;
// Copy an existing computed value only for non-dependent
// properties; otherwise it may be invalid for the current style
if (!isset(self::$_dependent_props[$prop])
&& \array_key_exists($prop, $style->_props_computed)
) {
$this->_props_computed[$prop] = $style->_props_computed[$prop];
$this->_props_used[$prop] = null;
} else {
unset($this->_props_computed[$prop]);
unset($this->_props_used[$prop]);
}
}
}
/**
* Clear information about important declarations after the style has been
* finalized during stylesheet loading.
*/
public function clear_important(): void
{
$this->_important_props = [];
}
/**
* Clear border-radius and bottom-spacing cache as necessary when a given
* property is set.
*
* @param string $prop The property that is set.
*/
protected function clear_cache(string $prop): void
{
// Clear border-radius cache on setting any border-radius
// property
if ($prop === "border_top_left_radius"
|| $prop === "border_top_right_radius"
|| $prop === "border_bottom_left_radius"
|| $prop === "border_bottom_right_radius"
) {
$this->has_border_radius_cache = null;
$this->resolved_border_radius = null;
}
// Clear bottom-spacing cache if necessary. Border style can
// disable/enable border calculations
if ($prop === "margin_bottom"
|| $prop === "padding_bottom"
|| $prop === "border_bottom_width"
|| $prop === "border_bottom_style"
) {
$this->_computed_bottom_spacing = null;
}
}
/**
* Set a style property from a value declaration.
*
* Setting `$clear_dependencies` to `false` is useful for saving a bit of
* unnecessary work while loading stylesheets.
*
* @param string $prop The property to set.
* @param mixed $val The value declaration or computed value.
* @param bool $important Whether the declaration is important.
* @param bool $clear_dependencies Whether to clear computed values of dependent properties.
*/
public function set_prop(string $prop, $val, bool $important = false, bool $clear_dependencies = true): void
{
$prop = str_replace("-", "_", $prop);
// Legacy property aliases
if (isset(self::$_props_alias[$prop])) {
$prop = self::$_props_alias[$prop];
}
if (!isset(self::$_defaults[$prop])) {
global $_dompdf_warnings;
$_dompdf_warnings[] = "'$prop' is not a recognized CSS property.";
return;
}
if ($prop !== "content" && \is_string($val) && mb_strpos($val, "url") === false && mb_strlen($val) > 1) {
$val = mb_strtolower(trim(str_replace(["\n", "\t"], [" "], $val)));
}
if (isset(self::$_props_shorthand[$prop])) {
// Shorthand properties directly set their respective sub-properties
// https://www.w3.org/TR/css-cascade-3/#shorthand
if ($val === "initial" || $val === "inherit" || $val === "unset") {
foreach (self::$_props_shorthand[$prop] as $sub_prop) {
$this->set_prop($sub_prop, $val, $important, $clear_dependencies);
}
} else {
$method = "_set_$prop";
if (!isset(self::$_methods_cache[$method])) {
self::$_methods_cache[$method] = method_exists($this, $method);
}
if (self::$_methods_cache[$method]) {
$values = $this->$method($val);
if ($values === []) {
return;
}
// Each missing sub-property is assigned its initial value
// https://www.w3.org/TR/css-cascade-3/#shorthand
foreach (self::$_props_shorthand[$prop] as $sub_prop) {
$sub_val = $values[$sub_prop] ?? self::$_defaults[$sub_prop];
$this->set_prop($sub_prop, $sub_val, $important, $clear_dependencies);
}
}
}
} else {
// Legacy support for `word-break: break-word`
// https://www.w3.org/TR/css-text-3/#valdef-word-break-break-word
if ($prop === "word_break" && $val === "break-word") {
$val = "normal";
$this->set_prop("overflow_wrap", "anywhere", $important, $clear_dependencies);
}
// `!important` declarations take precedence over normal ones
if (!$important && isset($this->_important_props[$prop])) {
return;
}
if ($important) {
$this->_important_props[$prop] = true;
}
// https://www.w3.org/TR/css-cascade-3/#inherit-initial
if ($val === "unset") {
$val = \in_array($prop, self::$_inherited, true)
? "inherit"
: "initial";
}
// https://www.w3.org/TR/css-cascade-3/#valdef-all-initial
if ($val === "initial") {
$val = self::$_defaults[$prop];
}
$computed = $this->compute_prop($prop, $val);
// Skip invalid declarations
if ($computed === null) {
return;
}
$this->_props[$prop] = $val;
$this->_props_computed[$prop] = $computed;
$this->_props_used[$prop] = null;
if ($clear_dependencies) {
// Clear the computed values of any dependent properties, so
// they can be re-computed
if (isset(self::$_dependency_map[$prop])) {
foreach (self::$_dependency_map[$prop] as $dependent) {
unset($this->_props_computed[$dependent]);
unset($this->_props_used[$dependent]);
}
}
$this->clear_cache($prop);
}
}
}
/**
* Get the specified value of a style property.
*
* @param string $prop
*
* @return mixed
* @throws Exception
*/
public function get_specified(string $prop)
{
// Legacy property aliases
if (isset(self::$_props_alias[$prop])) {
$prop = self::$_props_alias[$prop];
}
if (!isset(self::$_defaults[$prop])) {
throw new Exception("'$prop' is not a recognized CSS property.");
}
return $this->_props[$prop] ?? self::$_defaults[$prop];
}
/**
* Set a style property to its final value.
*
* This sets the specified and used value of the style property to the given
* value, meaning the value is not parsed and thus should have a type
* compatible with the property.
*
* If a shorthand property is specified, all of its sub-properties are set
* to the given value.
*
* @param string $prop The property to set.
* @param mixed $val The final value of the property.
*
* @throws Exception
*/
public function __set(string $prop, $val)
{
// Legacy property aliases
if (isset(self::$_props_alias[$prop])) {
$prop = self::$_props_alias[$prop];
}
if (!isset(self::$_defaults[$prop])) {
throw new Exception("'$prop' is not a recognized CSS property.");
}
if (isset(self::$_props_shorthand[$prop])) {
foreach (self::$_props_shorthand[$prop] as $sub_prop) {
$this->__set($sub_prop, $val);
}
} else {
$this->_props[$prop] = $val;
$this->_props_computed[$prop] = $val;
$this->_props_used[$prop] = $val;
$this->clear_cache($prop);
}
}
/**
* Set the used value of a style property.
*
* Used values are cleared on style reset.
*
* If a shorthand property is specified, all of its sub-properties are set
* to the given value.
*
* @param string $prop The property to set.
* @param mixed $val The used value of the property.
*
* @throws Exception
*/
public function set_used(string $prop, $val): void
{
// Legacy property aliases
if (isset(self::$_props_alias[$prop])) {
$prop = self::$_props_alias[$prop];
}
if (!isset(self::$_defaults[$prop])) {
throw new Exception("'$prop' is not a recognized CSS property.");
}
if (isset(self::$_props_shorthand[$prop])) {
foreach (self::$_props_shorthand[$prop] as $sub_prop) {
$this->set_used($sub_prop, $val);
}
} else {
$this->_props_used[$prop] = $val;
$this->non_final_used[$prop] = true;
}
}
/**
* Get the used or computed value of a style property, depending on whether
* the used value has been determined yet.
*
* @param string $prop
*
* @return mixed
* @throws Exception
*/
public function __get(string $prop)
{
// Legacy property aliases
if (isset(self::$_props_alias[$prop])) {
$prop = self::$_props_alias[$prop];
}
if (!isset(self::$_defaults[$prop])) {
throw new Exception("'$prop' is not a recognized CSS property.");
}
if (isset($this->_props_used[$prop])) {
return $this->_props_used[$prop];
}
$method = "_get_$prop";
if (!isset(self::$_methods_cache[$method])) {
self::$_methods_cache[$method] = method_exists($this, $method);
}
if (isset(self::$_props_shorthand[$prop])) {
// Don't cache shorthand values, always use getter. If no dedicated
// getter exists, use a simple fallback getter concatenating all
// sub-property values
if (self::$_methods_cache[$method]) {
return $this->$method();
} else {
return implode(" ", array_map(function ($sub_prop) {
$val = $this->__get($sub_prop);
return \is_array($val) ? implode(" ", $val) : $val;
}, self::$_props_shorthand[$prop]));
}
} else {
$computed = $this->computed($prop);
$used = self::$_methods_cache[$method]
? $this->$method($computed)
: $computed;
$this->_props_used[$prop] = $used;
return $used;
}
}
/**
* @param string $prop The property to compute.
* @param mixed $val The value to compute. Non-string values are treated as already computed.
*
* @return mixed The computed value.
*/
protected function compute_prop(string $prop, $val)
{
// During style merge, the parent style is not available yet, so
// temporarily use the initial value for `inherit` properties. The
// keyword is properly resolved during inheritance
if ($val === "inherit") {
$val = self::$_defaults[$prop];
}
// Check for values which are already computed
if (!\is_string($val)) {
return $val;
}
$method = "_compute_$prop";
if (!isset(self::$_methods_cache[$method])) {
self::$_methods_cache[$method] = method_exists($this, $method);
}
if (self::$_methods_cache[$method]) {
return $this->$method($val);
} elseif ($val !== "") {
return $val;
} else {
return null;
}
}
/**
* Get the computed value for the given property.
*
* @param string $prop The property to get the computed value of.
*
* @return mixed The computed value.
*/
protected function computed(string $prop)
{
if (!\array_key_exists($prop, $this->_props_computed)) {
$val = $this->_props[$prop] ?? self::$_defaults[$prop];
$computed = $this->compute_prop($prop, $val);
$this->_props_computed[$prop] = $computed;
}
return $this->_props_computed[$prop];
}
/**
* @param float $cbw The width of the containing block.
* @return float|string|null
*/
public function computed_bottom_spacing(float $cbw)
{
// Caching the bottom spacing independently of the given width is a bit
// iffy, but should be okay, as the containing block should only
// potentially change after a page break, and the style is reset in that
// case
if ($this->_computed_bottom_spacing !== null) {
return $this->_computed_bottom_spacing;
}
return $this->_computed_bottom_spacing = $this->length_in_pt(
[
$this->margin_bottom,
$this->padding_bottom,
$this->border_bottom_width
],
$cbw
);
}
/**
* Returns an `array(r, g, b, "r" => r, "g" => g, "b" => b, "alpha" => alpha, "hex" => "#rrggbb")`
* based on the provided CSS color value.
*
* @param string|null $color
* @return array|string|null
*/
public function munge_color($color)
{
return Color::parse($color);
}
/**
* @return string
*/
public function get_font_family_raw(): string
{
return trim($this->_props["font_family"], " \t\n\r\x0B\"'");
}
/**
* Getter for the `font-family` CSS property.
*
* Uses the {@link FontMetrics} class to resolve the font family into an
* actual font file.
*
* @param string $computed
* @return string
* @throws Exception
*
* @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-family
*/
protected function _get_font_family($computed): string
{
//TODO: we should be using the calculated prop rather than perform the entire family parsing operation again
$fontMetrics = $this->getFontMetrics();
$DEBUGCSS = $this->_stylesheet->get_dompdf()->getOptions()->getDebugCss();
// Select the appropriate font. First determine the subtype, then check
// the specified font-families for a candidate.
// Resolve font-weight
$weight = $this->__get("font_weight");
if ($weight === 'bold') {
$weight = 700;
} elseif (preg_match('/^[0-9]+$/', $weight, $match)) {
$weight = (int)$match[0];
} else {
$weight = 400;
}
// Resolve font-style
$font_style = $this->__get("font_style");
$subtype = $fontMetrics->getType($weight . ' ' . $font_style);
$families = preg_split("/\s*,\s*/", $computed);
$font = null;
foreach ($families as $family) {
//remove leading and trailing string delimiters, e.g. on font names with spaces;
//remove leading and trailing whitespace
$family = trim($family, " \t\n\r\x0B\"'");
if ($DEBUGCSS) {
print '(' . $family . ')';
}
$font = $fontMetrics->getFont($family, $subtype);
if ($font) {
if ($DEBUGCSS) {
print "