PKZ[ _ _ LICENSE.LGPLnuW+A 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.PKZPVERSIONnuW+A2.0.3 PKZܦ 88src/Canvas.phpnuW+A alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $width * @param array $style * @param string $cap `butt`, `round`, or `square` */ function line($x1, $y1, $x2, $y2, $color, $width, $style = [], $cap = "butt"); /** * Draws an arc * * See {@link Cpdf::setLineStyle()} for a description of the format of the * $style and $cap parameters (aka dash and cap). * * @param float $x X coordinate of the arc * @param float $y Y coordinate of the arc * @param float $r1 Radius 1 * @param float $r2 Radius 2 * @param float $astart Start angle in degrees * @param float $aend End angle in degrees * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $width * @param array $style * @param string $cap `butt`, `round`, or `square` */ function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt"); /** * Draws a rectangle at x1,y1 with width w and height h * * See {@link Cpdf::setLineStyle()} for a description of the format of the * $style and $cap parameters (aka dash and cap). * * @param float $x1 * @param float $y1 * @param float $w * @param float $h * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $width * @param array $style * @param string $cap `butt`, `round`, or `square` */ function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt"); /** * Draws a filled rectangle at x1,y1 with width w and height h * * @param float $x1 * @param float $y1 * @param float $w * @param float $h * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 */ function filled_rectangle($x1, $y1, $w, $h, $color); /** * Starts a clipping rectangle at x1,y1 with width w and height h * * @param float $x1 * @param float $y1 * @param float $w * @param float $h */ function clipping_rectangle($x1, $y1, $w, $h); /** * Starts a rounded clipping rectangle at x1,y1 with width w and height h * * @param float $x1 * @param float $y1 * @param float $w * @param float $h * @param float $tl * @param float $tr * @param float $br * @param float $bl */ function clipping_roundrectangle($x1, $y1, $w, $h, $tl, $tr, $br, $bl); /** * Starts a clipping polygon * * @param float[] $points */ public function clipping_polygon(array $points): void; /** * Ends the last clipping shape */ function clipping_end(); /** * Processes a callback on every page. * * The callback function receives the four parameters `int $pageNumber`, * `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics`, in * that order. * * This function can be used to add page numbers to all pages after the * first one, for example. * * @param callable $callback The callback function to process on every page */ public function page_script($callback): void; /** * Writes text at the specified x and y coordinates on every page. * * The strings '{PAGE_NUM}' and '{PAGE_COUNT}' are automatically replaced * with their current values. * * @param float $x * @param float $y * @param string $text The text to write * @param string $font The font file to use * @param float $size The font size, in points * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $word_space Word spacing adjustment * @param float $char_space Char spacing adjustment * @param float $angle Angle to write the text at, measured clockwise starting from the x-axis */ public function page_text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0); /** * Draws a line at the specified coordinates on every page. * * @param float $x1 * @param float $y1 * @param float $x2 * @param float $y2 * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $width * @param array $style */ public function page_line($x1, $y1, $x2, $y2, $color, $width, $style = []); /** * Save current state */ function save(); /** * Restore last state */ function restore(); /** * Rotate * * @param float $angle angle in degrees for counter-clockwise rotation * @param float $x Origin abscissa * @param float $y Origin ordinate */ function rotate($angle, $x, $y); /** * Skew * * @param float $angle_x * @param float $angle_y * @param float $x Origin abscissa * @param float $y Origin ordinate */ function skew($angle_x, $angle_y, $x, $y); /** * Scale * * @param float $s_x scaling factor for width as percent * @param float $s_y scaling factor for height as percent * @param float $x Origin abscissa * @param float $y Origin ordinate */ function scale($s_x, $s_y, $x, $y); /** * Translate * * @param float $t_x movement to the right * @param float $t_y movement to the bottom */ function translate($t_x, $t_y); /** * Transform * * @param float $a * @param float $b * @param float $c * @param float $d * @param float $e * @param float $f */ function transform($a, $b, $c, $d, $e, $f); /** * Draws a polygon * * The polygon is formed by joining all the points stored in the $points * array. $points has the following structure: * ``` * array(0 => x1, * 1 => y1, * 2 => x2, * 3 => y2, * ... * ); * ``` * * See {@link Cpdf::setLineStyle()} for a description of the format of the * $style parameter (aka dash). * * @param array $points * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $width * @param array $style * @param bool $fill Fills the polygon if true */ function polygon($points, $color, $width = null, $style = [], $fill = false); /** * Draws a circle at $x,$y with radius $r * * See {@link Cpdf::setLineStyle()} for a description of the format of the * $style parameter (aka dash). * * @param float $x * @param float $y * @param float $r * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $width * @param array $style * @param bool $fill Fills the circle if true */ function circle($x, $y, $r, $color, $width = null, $style = [], $fill = false); /** * Add an image to the pdf. * * The image is placed at the specified x and y coordinates with the * given width and height. * * @param string $img The path to the image * @param float $x X position * @param float $y Y position * @param float $w Width * @param float $h Height * @param string $resolution The resolution of the image */ function image($img, $x, $y, $w, $h, $resolution = "normal"); /** * Writes text at the specified x and y coordinates * * @param float $x * @param float $y * @param string $text The text to write * @param string $font The font file to use * @param float $size The font size, in points * @param array $color Color array in the format `[r, g, b, "alpha" => alpha]` * where r, g, b, and alpha are float values between 0 and 1 * @param float $word_space Word spacing adjustment * @param float $char_space Char spacing adjustment * @param float $angle Angle to write the text at, measured clockwise starting from the x-axis */ function text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0); /** * Add a named destination (similar to ... in html) * * @param string $anchorname The name of the named destination */ function add_named_dest($anchorname); /** * Add a link to the pdf * * @param string $url The url to link to * @param float $x The x position of the link * @param float $y The y position of the link * @param float $width The width of the link * @param float $height The height of the link */ function add_link($url, $x, $y, $width, $height); /** * Add meta information to the PDF. * * @param string $label Label of the value (Creator, Producer, etc.) * @param string $value The text to set */ public function add_info(string $label, string $value): void; /** * Calculates text size, in points * * @param string $text The text to be sized * @param string $font The font file to use * @param float $size The font size, in points * @param float $word_spacing Word spacing, if any * @param float $char_spacing Char spacing, if any * * @return float */ function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0); /** * Calculates font height, in points * * @param string $font The font file to use * @param float $size The font size, in points * * @return float */ function get_font_height($font, $size); /** * Returns the font x-height, in points * * @param string $font The font file to use * @param float $size The font size, in points * * @return float */ //function get_font_x_height($font, $size); /** * Calculates font baseline, in points * * @param string $font The font file to use * @param float $size The font size, in points * * @return float */ function get_font_baseline($font, $size); /** * Returns the PDF's width in points * * @return float */ function get_width(); /** * Returns the PDF's height in points * * @return float */ function get_height(); /** * Sets the opacity * * @param float $opacity * @param string $mode */ public function set_opacity(float $opacity, string $mode = "Normal"): void; /** * Sets the default view * * @param string $view * 'XYZ' left, top, zoom * 'Fit' * 'FitH' top * 'FitV' left * 'FitR' left,bottom,right * 'FitB' * 'FitBH' top * 'FitBV' left * @param array $options */ function set_default_view($view, $options = []); /** * @param string $code */ function javascript($code); /** * Starts a new page * * Subsequent drawing operations will appear on the new page. */ function new_page(); /** * Streams the PDF to the client. * * @param string $filename The filename to present to the client. * @param array $options Associative array: 'compress' => 1 or 0 (default 1); 'Attachment' => 1 or 0 (default 1). */ function stream($filename, $options = []); /** * Returns the PDF as a string. * * @param array $options Associative array: 'compress' => 1 or 0 (default 1). * * @return string */ function output($options = []); } PKZsrc/Dompdf.phpnuW+AsetOptions($options); } elseif (is_array($options)) { $this->setOptions(new Options($options)); } else { $this->setOptions(new Options()); } $versionFile = realpath(__DIR__ . '/../VERSION'); if (($version = file_get_contents($versionFile)) !== false) { $version = trim($version); if ($version !== '$Format:<%h>$') { $this->version = sprintf('dompdf %s', $version); } } $this->setPhpConfig(); $this->paperSize = $this->options->getDefaultPaperSize(); $this->paperOrientation = $this->options->getDefaultPaperOrientation(); $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation); $this->fontMetrics = new FontMetrics($this->canvas, $this->options); $this->css = new Stylesheet($this); $this->restorePhpConfig(); } /** * Save the system's existing locale, PCRE JIT, and MBString encoding * configuration and configure the system for Dompdf processing */ private function setPhpConfig() { if (sprintf('%.1f', 1.0) !== '1.0') { $this->systemLocale = setlocale(LC_NUMERIC, "0"); setlocale(LC_NUMERIC, "C"); } $this->pcreJit = @ini_get('pcre.jit'); @ini_set('pcre.jit', '0'); $this->mbstringEncoding = mb_internal_encoding(); mb_internal_encoding('UTF-8'); } /** * Restore the system's locale configuration */ private function restorePhpConfig() { if ($this->systemLocale !== null) { setlocale(LC_NUMERIC, $this->systemLocale); $this->systemLocale = null; } if ($this->pcreJit !== null) { @ini_set('pcre.jit', $this->pcreJit); $this->pcreJit = null; } if ($this->mbstringEncoding !== null) { mb_internal_encoding($this->mbstringEncoding); $this->mbstringEncoding = null; } } /** * @param $file * @deprecated */ public function load_html_file($file) { $this->loadHtmlFile($file); } /** * Loads an HTML file * Parse errors are stored in the global array _dompdf_warnings. * * @param string $file a filename or url to load * @param string $encoding Encoding of $file * * @throws Exception */ public function loadHtmlFile($file, $encoding = null) { $this->setPhpConfig(); if (!$this->protocol && !$this->baseHost && !$this->basePath) { [$this->protocol, $this->baseHost, $this->basePath] = Helpers::explode_url($file); } $protocol = strtolower($this->protocol); $uri = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $file); $allowed_protocols = $this->options->getAllowedProtocols(); if (!array_key_exists($protocol, $allowed_protocols)) { throw new Exception("Permission denied on $file. The communication protocol is not supported."); } if ($protocol === "file://") { $ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION)); if (!in_array($ext, $this->allowedLocalFileExtensions)) { throw new Exception("Permission denied on $file: The file extension is forbidden."); } } foreach ($allowed_protocols[$protocol]["rules"] as $rule) { [$result, $message] = $rule($uri); if (!$result) { throw new Exception("Error loading $file: $message"); } } [$contents, $http_response_header] = Helpers::getFileContent($uri, $this->options->getHttpContext()); if ($contents === null) { throw new Exception("File '$file' not found."); } // See http://the-stickman.com/web-development/php/getting-http-response-headers-when-using-file_get_contents/ if (isset($http_response_header)) { foreach ($http_response_header as $_header) { if (preg_match("@Content-Type:\s*[\w/]+;\s*?charset=([^\s]+)@i", $_header, $matches)) { $encoding = strtoupper($matches[1]); break; } } } $this->restorePhpConfig(); $this->loadHtml($contents, $encoding); } /** * @param string $str * @param string $encoding * @deprecated */ public function load_html($str, $encoding = null) { $this->loadHtml($str, $encoding); } public function loadDOM($doc, $quirksmode = false) { // Remove #text children nodes in nodes that shouldn't have $tag_names = ["html", "head", "table", "tbody", "thead", "tfoot", "tr"]; foreach ($tag_names as $tag_name) { $nodes = $doc->getElementsByTagName($tag_name); foreach ($nodes as $node) { self::removeTextNodes($node); } } $this->dom = $doc; $this->quirksmode = $quirksmode; $this->tree = new FrameTree($this->dom); } /** * Loads an HTML string * Parse errors are stored in the global array _dompdf_warnings. * * @param string $str HTML text to load * @param string $encoding Encoding of $str */ public function loadHtml($str, $encoding = null) { $this->setPhpConfig(); // Determine character encoding when $encoding parameter not used if ($encoding === null) { mb_detect_order('auto'); if (($encoding = mb_detect_encoding($str, null, true)) === false) { //"auto" is expanded to "ASCII,JIS,UTF-8,EUC-JP,SJIS" $encoding = "auto"; } } if (in_array(strtoupper($encoding), array('UTF-8','UTF8')) === false) { $str = mb_convert_encoding($str, 'UTF-8', $encoding); //Update encoding after converting $encoding = 'UTF-8'; } $metatags = [ '@]*charset\s*=\s*["\']?\s*([^"\' ]+)@i', ]; foreach ($metatags as $metatag) { if (preg_match($metatag, $str, $matches)) { if (isset($matches[1]) && in_array($matches[1], mb_list_encodings())) { $document_encoding = $matches[1]; break; } } } if (isset($document_encoding) && in_array(strtoupper($document_encoding), ['UTF-8','UTF8']) === false) { $str = preg_replace('/charset=([^\s"]+)/i', 'charset=UTF-8', $str); } elseif (isset($document_encoding) === false && strpos($str, '') !== false) { $str = str_replace('', '', $str); } elseif (isset($document_encoding) === false) { $str = '' . $str; } // remove BOM mark from UTF-8, it's treated as document text by DOMDocument // FIXME: roll this into the encoding detection using UTF-8/16/32 BOM (http://us2.php.net/manual/en/function.mb-detect-encoding.php#91051)? if (substr($str, 0, 3) == chr(0xEF) . chr(0xBB) . chr(0xBF)) { $str = substr($str, 3); } // Store parsing warnings as messages set_error_handler([Helpers::class, 'record_warnings']); try { // @todo Take the quirksmode into account // https://quirks.spec.whatwg.org/ // http://hsivonen.iki.fi/doctype/ $quirksmode = false; $html5 = new HTML5(['encoding' => $encoding, 'disable_html_ns' => true]); $dom = $html5->loadHTML($str); // extra step to normalize the HTML document structure // see Masterminds/html5-php#166 $doc = new DOMDocument("1.0", $encoding); $doc->preserveWhiteSpace = true; $doc->loadHTML($html5->saveHTML($dom), LIBXML_NOWARNING | LIBXML_NOERROR); $this->loadDOM($doc, $quirksmode); } finally { restore_error_handler(); $this->restorePhpConfig(); } } /** * @param DOMNode $node * @deprecated */ public static function remove_text_nodes(DOMNode $node) { self::removeTextNodes($node); } /** * @param DOMNode $node */ public static function removeTextNodes(DOMNode $node) { $children = []; for ($i = 0; $i < $node->childNodes->length; $i++) { $child = $node->childNodes->item($i); if ($child->nodeName === "#text") { $children[] = $child; } } foreach ($children as $child) { $node->removeChild($child); } } /** * Builds the {@link FrameTree}, loads any CSS and applies the styles to * the {@link FrameTree} */ private function processHtml() { $this->tree->build_tree(); $this->css->load_css_file($this->css->getDefaultStylesheet(), Stylesheet::ORIG_UA); $acceptedmedia = Stylesheet::$ACCEPTED_GENERIC_MEDIA_TYPES; $acceptedmedia[] = $this->options->getDefaultMediaType(); // /** @var \DOMElement|null */ $baseNode = $this->dom->getElementsByTagName("base")->item(0); $baseHref = $baseNode ? $baseNode->getAttribute("href") : ""; if ($baseHref !== "") { [$this->protocol, $this->baseHost, $this->basePath] = Helpers::explode_url($baseHref); } // Set the base path of the Stylesheet to that of the file being processed $this->css->set_protocol($this->protocol); $this->css->set_host($this->baseHost); $this->css->set_base_path($this->basePath); // Get all the stylesheets so that they are processed in document order $xpath = new DOMXPath($this->dom); $stylesheets = $xpath->query("//*[name() = 'link' or name() = 'style']"); /** @var \DOMElement $tag */ foreach ($stylesheets as $tag) { switch (strtolower($tag->nodeName)) { // load tags case "link": if (mb_strtolower(stripos($tag->getAttribute("rel"), "stylesheet") !== false) || // may be "appendix stylesheet" mb_strtolower($tag->getAttribute("type")) === "text/css" ) { //Check if the css file is for an accepted media type //media not given then always valid $formedialist = preg_split("/[\s\n,]/", $tag->getAttribute("media"), -1, PREG_SPLIT_NO_EMPTY); if (count($formedialist) > 0) { $accept = false; foreach ($formedialist as $type) { if (in_array(mb_strtolower(trim($type)), $acceptedmedia)) { $accept = true; break; } } if (!$accept) { //found at least one mediatype, but none of the accepted ones //Skip this css file. break; } } $url = $tag->getAttribute("href"); $url = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $url); if ($url !== null) { $this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR); } } break; // load $child = $child->nextSibling; } } else { $css = $tag->nodeValue; } // Set the base path of the Stylesheet to that of the file being processed $this->css->set_protocol($this->protocol); $this->css->set_host($this->baseHost); $this->css->set_base_path($this->basePath); $this->css->load_css($css, Stylesheet::ORIG_AUTHOR); break; } // Set the base path of the Stylesheet to that of the file being processed $this->css->set_protocol($this->protocol); $this->css->set_host($this->baseHost); $this->css->set_base_path($this->basePath); } } /** * @param string $cacheId * @deprecated */ public function enable_caching($cacheId) { $this->enableCaching($cacheId); } /** * Enable experimental caching capability * * @param string $cacheId */ public function enableCaching($cacheId) { $this->cacheId = $cacheId; } /** * @param string $value * @return bool * @deprecated */ public function parse_default_view($value) { return $this->parseDefaultView($value); } /** * @param string $value * @return bool */ public function parseDefaultView($value) { $valid = ["XYZ", "Fit", "FitH", "FitV", "FitR", "FitB", "FitBH", "FitBV"]; $options = preg_split("/\s*,\s*/", trim($value)); $defaultView = array_shift($options); if (!in_array($defaultView, $valid)) { return false; } $this->setDefaultView($defaultView, $options); return true; } /** * Renders the HTML to PDF */ public function render() { $this->setPhpConfig(); $logOutputFile = $this->options->getLogOutputFile(); if ($logOutputFile) { if (!file_exists($logOutputFile) && is_writable(dirname($logOutputFile))) { touch($logOutputFile); } $startTime = microtime(true); if (is_writable($logOutputFile)) { ob_start(); } } $this->processHtml(); $this->css->apply_styles($this->tree); // @page style rules : size, margins $pageStyles = $this->css->get_page_styles(); $basePageStyle = $pageStyles["base"]; unset($pageStyles["base"]); foreach ($pageStyles as $pageStyle) { $pageStyle->inherit($basePageStyle); } // Set paper size if defined via CSS if (is_array($basePageStyle->size)) { [$width, $height] = $basePageStyle->size; $this->setPaper([0, 0, $width, $height]); } // Create a new canvas instance if the current one does not match the // desired paper size $canvasWidth = $this->canvas->get_width(); $canvasHeight = $this->canvas->get_height(); $size = $this->getPaperSize(); if ($canvasWidth !== $size[2] || $canvasHeight !== $size[3]) { $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation); $this->fontMetrics->setCanvas($this->canvas); } $canvas = $this->canvas; $root_frame = $this->tree->get_root(); $root = Factory::decorate_root($root_frame, $this); foreach ($this->tree as $frame) { if ($frame === $root_frame) { continue; } Factory::decorate_frame($frame, $this, $root); } // Add meta information $title = $this->dom->getElementsByTagName("title"); if ($title->length) { $canvas->add_info("Title", trim($title->item(0)->nodeValue)); } $metas = $this->dom->getElementsByTagName("meta"); $labels = [ "author" => "Author", "keywords" => "Keywords", "description" => "Subject", ]; /** @var \DOMElement $meta */ foreach ($metas as $meta) { $name = mb_strtolower($meta->getAttribute("name")); $value = trim($meta->getAttribute("content")); if (isset($labels[$name])) { $canvas->add_info($labels[$name], $value); continue; } if ($name === "dompdf.view" && $this->parseDefaultView($value)) { $canvas->set_default_view($this->defaultView, $this->defaultViewOptions); } } $root->set_containing_block(0, 0, $canvas->get_width(), $canvas->get_height()); $root->set_renderer(new Renderer($this)); // This is where the magic happens: $root->reflow(); if (isset($this->callbacks["end_document"])) { $fs = $this->callbacks["end_document"]; foreach ($fs as $f) { $canvas->page_script($f); } } // Clean up cached images if (!$this->options->getDebugKeepTemp()) { Cache::clear($this->options->getDebugPng()); } global $_dompdf_warnings, $_dompdf_show_warnings; if ($_dompdf_show_warnings && isset($_dompdf_warnings)) { echo 'Dompdf Warnings
';
            foreach ($_dompdf_warnings as $msg) {
                echo $msg . "\n";
            }

            if ($canvas instanceof CPDF) {
                echo $canvas->get_cpdf()->messages;
            }
            echo '
'; flush(); } if ($logOutputFile && is_writable($logOutputFile)) { $this->writeLog($logOutputFile, $startTime); ob_end_clean(); } $this->restorePhpConfig(); } /** * Writes the output buffer in the log file * * @param string $logOutputFile * @param float $startTime */ private function writeLog(string $logOutputFile, float $startTime): void { $frames = Frame::$ID_COUNTER; $memory = memory_get_peak_usage(true) / 1024; $time = (microtime(true) - $startTime) * 1000; $out = sprintf( "%6d" . "%10.2f KB" . "%10.2f ms" . " " . ($this->quirksmode ? " ON" : "OFF") . "
", $frames, $memory, $time); $out .= ob_get_contents(); ob_clean(); file_put_contents($logOutputFile, $out); } /** * Add meta information to the PDF after rendering. * * @deprecated */ public function add_info($label, $value) { $this->addInfo($label, $value); } /** * Add meta information to the PDF after rendering. * * @param string $label Label of the value (Creator, Producer, etc.) * @param string $value The text to set */ public function addInfo(string $label, string $value): void { $this->canvas->add_info($label, $value); } /** * Streams the PDF to the client. * * The file will open a download dialog by default. The options * parameter controls the output. Accepted options (array keys) are: * * 'compress' = > 1 (=default) or 0: * Apply content stream compression * * 'Attachment' => 1 (=default) or 0: * Set the 'Content-Disposition:' HTTP header to 'attachment' * (thereby causing the browser to open a download dialog) * * @param string $filename the name of the streamed file * @param array $options header options (see above) */ public function stream($filename = "document.pdf", $options = []) { $this->setPhpConfig(); $this->canvas->stream($filename, $options); $this->restorePhpConfig(); } /** * Returns the PDF as a string. * * The options parameter controls the output. Accepted options are: * * 'compress' = > 1 or 0 - apply content stream compression, this is * on (1) by default * * @param array $options options (see above) * * @return string|null */ public function output($options = []) { $this->setPhpConfig(); $output = $this->canvas->output($options); $this->restorePhpConfig(); return $output; } /** * @return string * @deprecated */ public function output_html() { return $this->outputHtml(); } /** * Returns the underlying HTML document as a string * * @return string */ public function outputHtml() { return $this->dom->saveHTML(); } /** * Get the dompdf option value * * @param string $key * @return mixed * @deprecated */ public function get_option($key) { return $this->options->get($key); } /** * @param string $key * @param mixed $value * @return $this * @deprecated */ public function set_option($key, $value) { $this->options->set($key, $value); return $this; } /** * @param array $options * @return $this * @deprecated */ public function set_options(array $options) { $this->options->set($options); return $this; } /** * @param string $size * @param string $orientation * @deprecated */ public function set_paper($size, $orientation = "portrait") { $this->setPaper($size, $orientation); } /** * Sets the paper size & orientation * * @param string|float[] $size 'letter', 'legal', 'A4', etc. {@link Dompdf\Adapter\CPDF::$PAPER_SIZES} * @param string $orientation 'portrait' or 'landscape' * @return $this */ public function setPaper($size, string $orientation = "portrait"): self { $this->paperSize = $size; $this->paperOrientation = $orientation; return $this; } /** * Gets the paper size * * @return float[] A four-element float array */ public function getPaperSize(): array { $paper = $this->paperSize; $orientation = $this->paperOrientation; if (is_array($paper)) { $size = array_map("floatval", $paper); } else { $paper = strtolower($paper); $size = CPDF::$PAPER_SIZES[$paper] ?? CPDF::$PAPER_SIZES["letter"]; } if (strtolower($orientation) === "landscape") { [$size[2], $size[3]] = [$size[3], $size[2]]; } return $size; } /** * Gets the paper orientation * * @return string Either "portrait" or "landscape" */ public function getPaperOrientation(): string { return $this->paperOrientation; } /** * @param FrameTree $tree * @return $this */ public function setTree(FrameTree $tree) { $this->tree = $tree; return $this; } /** * @return FrameTree * @deprecated */ public function get_tree() { return $this->getTree(); } /** * Returns the underlying {@link FrameTree} object * * @return FrameTree */ public function getTree() { return $this->tree; } /** * @param string $protocol * @return $this * @deprecated */ public function set_protocol($protocol) { return $this->setProtocol($protocol); } /** * Sets the protocol to use * FIXME validate these * * @param string $protocol * @return $this */ public function setProtocol(string $protocol) { $this->protocol = $protocol; return $this; } /** * @return string * @deprecated */ public function get_protocol() { return $this->getProtocol(); } /** * Returns the protocol in use * * @return string */ public function getProtocol() { return $this->protocol; } /** * @param string $host * @deprecated */ public function set_host($host) { $this->setBaseHost($host); } /** * Sets the base hostname * * @param string $baseHost * @return $this */ public function setBaseHost(string $baseHost) { $this->baseHost = $baseHost; return $this; } /** * @return string * @deprecated */ public function get_host() { return $this->getBaseHost(); } /** * Returns the base hostname * * @return string */ public function getBaseHost() { return $this->baseHost; } /** * Sets the base path * * @param string $path * @deprecated */ public function set_base_path($path) { $this->setBasePath($path); } /** * Sets the base path * * @param string $basePath * @return $this */ public function setBasePath(string $basePath) { $this->basePath = $basePath; return $this; } /** * @return string * @deprecated */ public function get_base_path() { return $this->getBasePath(); } /** * Returns the base path * * @return string */ public function getBasePath() { return $this->basePath; } /** * @param string $default_view The default document view * @param array $options The view's options * @return $this * @deprecated */ public function set_default_view($default_view, $options) { return $this->setDefaultView($default_view, $options); } /** * Sets the default view * * @param string $defaultView The default document view * @param array $options The view's options * @return $this */ public function setDefaultView($defaultView, $options) { $this->defaultView = $defaultView; $this->defaultViewOptions = $options; return $this; } /** * @param resource $http_context * @return $this * @deprecated */ public function set_http_context($http_context) { return $this->setHttpContext($http_context); } /** * Sets the HTTP context * * @param resource|array $httpContext * @return $this */ public function setHttpContext($httpContext) { $this->options->setHttpContext($httpContext); return $this; } /** * @return resource * @deprecated */ public function get_http_context() { return $this->getHttpContext(); } /** * Returns the HTTP context * * @return resource */ public function getHttpContext() { return $this->options->getHttpContext(); } /** * Set a custom `Canvas` instance to render the document to. * * Be aware that the instance will be replaced on render if the document * defines a paper size different from the canvas. * * @param Canvas $canvas * @return $this */ public function setCanvas(Canvas $canvas) { $this->canvas = $canvas; return $this; } /** * @return Canvas * @deprecated */ public function get_canvas() { return $this->getCanvas(); } /** * Return the underlying Canvas instance (e.g. Dompdf\Adapter\CPDF, Dompdf\Adapter\GD) * * @return Canvas */ public function getCanvas() { return $this->canvas; } /** * @param Stylesheet $css * @return $this */ public function setCss(Stylesheet $css) { $this->css = $css; return $this; } /** * @return Stylesheet * @deprecated */ public function get_css() { return $this->getCss(); } /** * Returns the stylesheet * * @return Stylesheet */ public function getCss() { return $this->css; } /** * @param DOMDocument $dom * @return $this */ public function setDom(DOMDocument $dom) { $this->dom = $dom; return $this; } /** * @return DOMDocument * @deprecated */ public function get_dom() { return $this->getDom(); } /** * @return DOMDocument */ public function getDom() { return $this->dom; } /** * @param Options $options * @return $this */ public function setOptions(Options $options) { // For backwards compatibility if ($this->options && $this->options->getHttpContext() && !$options->getHttpContext()) { $options->setHttpContext($this->options->getHttpContext()); } $this->options = $options; $fontMetrics = $this->fontMetrics; if (isset($fontMetrics)) { $fontMetrics->setOptions($options); } return $this; } /** * @return Options */ public function getOptions() { return $this->options; } /** * @return array * @deprecated */ public function get_callbacks() { return $this->getCallbacks(); } /** * Returns the callbacks array * * @return array */ public function getCallbacks() { return $this->callbacks; } /** * @param array $callbacks the set of callbacks to set * @return $this * @deprecated */ public function set_callbacks($callbacks) { return $this->setCallbacks($callbacks); } /** * Define callbacks that allow modifying the document during render. * * The callbacks array should contain arrays with `event` set to a callback * event name and `f` set to a function or any other callable. * * The available callback events are: * * `begin_page_reflow`: called before page reflow * * `begin_frame`: called before a frame is rendered * * `end_frame`: called after frame rendering is complete * * `begin_page_render`: called before a page is rendered * * `end_page_render`: called after page rendering is complete * * `end_document`: called for every page after rendering is complete * * The function `f` receives three arguments `Frame $frame`, `Canvas $canvas`, * and `FontMetrics $fontMetrics` for all events but `end_document`. For * `end_document`, the function receives four arguments `int $pageNumber`, * `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics` instead. * * @param array $callbacks The set of callbacks to set. * @return $this */ public function setCallbacks(array $callbacks): self { $this->callbacks = []; foreach ($callbacks as $c) { if (is_array($c) && isset($c["event"]) && isset($c["f"])) { $event = $c["event"]; $f = $c["f"]; if (is_string($event) && is_callable($f)) { $this->callbacks[$event][] = $f; } } } return $this; } /** * @return boolean * @deprecated */ public function get_quirksmode() { return $this->getQuirksmode(); } /** * Get the quirks mode * * @return boolean true if quirks mode is active */ public function getQuirksmode() { return $this->quirksmode; } /** * @param FontMetrics $fontMetrics * @return $this */ public function setFontMetrics(FontMetrics $fontMetrics) { $this->fontMetrics = $fontMetrics; return $this; } /** * @return FontMetrics */ public function getFontMetrics() { return $this->fontMetrics; } /** * PHP5 overloaded getter * Along with {@link Dompdf::__set()} __get() provides access to all * properties directly. Typically __get() is not called directly outside * of this class. * * @param string $prop * * @throws Exception * @return mixed */ function __get($prop) { switch ($prop) { case 'version': return $this->version; default: throw new Exception('Invalid property: ' . $prop); } } } PKZ5wj;; src/Exception/ImageException.phpnuW+AgetAllowedProtocols(); if (!array_key_exists($protocol, $allowed_protocols)) { throw new ImageException("Permission denied on $url. The communication protocol is not supported.", E_WARNING); } foreach ($allowed_protocols[$protocol]["rules"] as $rule) { [$result, $message] = $rule($full_url); if (!$result) { throw new ImageException("Error loading $url: $message", E_WARNING); } } } if ($protocol === "file://") { $resolved_url = $full_url; } elseif (isset(self::$_cache[$full_url])) { $resolved_url = self::$_cache[$full_url]; } else { $tmp_dir = $options->getTempDir(); if (($resolved_url = @tempnam($tmp_dir, "ca_dompdf_img_")) === false) { throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING); } $tempfile = $resolved_url; $image = null; if ($is_data_uri) { if (($parsed_data_uri = Helpers::parse_data_uri($url)) !== false) { $image = $parsed_data_uri["data"]; } } else { list($image, $http_response_header) = Helpers::getFileContent($full_url, $options->getHttpContext()); } // Image not found or invalid if ($image === null) { $msg = ($is_data_uri ? "Data-URI could not be parsed" : "Image not found"); throw new ImageException($msg, E_WARNING); } if (@file_put_contents($resolved_url, $image) === false) { throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING); } self::$_cache[$full_url] = $resolved_url; } // Check if the local file is readable if (!is_readable($resolved_url) || !filesize($resolved_url)) { throw new ImageException("Image not readable or empty", E_WARNING); } list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext()); if (($width && $height && in_array($type, ["gif", "png", "jpeg", "bmp", "svg","webp"], true)) === false) { throw new ImageException("Image type unknown", E_WARNING); } if ($type === "svg") { $parser = xml_parser_create("utf-8"); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); xml_set_element_handler( $parser, function ($parser, $name, $attributes) use ($options, $parsed_url, $full_url) { if (strtolower($name) === "image") { $attributes = array_change_key_case($attributes, CASE_LOWER); $urls = []; $urls[] = $attributes["xlink:href"] ?? ""; $urls[] = $attributes["href"] ?? ""; foreach ($urls as $url) { if (!empty($url)) { $inner_full_url = Helpers::build_url($parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $url); if ($inner_full_url === $full_url) { throw new ImageException("SVG self-reference is not allowed", E_WARNING); } [$resolved_url, $type, $message] = self::resolve_url($url, $parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $options); if (!empty($message)) { throw new ImageException("This SVG document references a restricted resource. $message", E_WARNING); } } } } }, false ); if (($fp = fopen($resolved_url, "r")) !== false) { while ($line = fread($fp, 8192)) { xml_parse($parser, $line, false); } fclose($fp); xml_parse($parser, "", true); } xml_parser_free($parser); } } catch (ImageException $e) { if ($tempfile) { unlink($tempfile); } $resolved_url = self::$broken_image; list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext()); $message = self::$error_message; Helpers::record_warnings($e->getCode(), $e->getMessage() . " \n $url", $e->getFile(), $e->getLine()); self::$_cache[$full_url] = $resolved_url; } return [$resolved_url, $type, $message]; } /** * Register a temp file for the given original image file. * * @param string $filePath The path of the original image. * @param string $tempPath The path of the temp file to register. * @param string $key An optional key to register the temp file at. */ static function addTempImage(string $filePath, string $tempPath, string $key = "default"): void { if (!isset(self::$tempImages[$filePath])) { self::$tempImages[$filePath] = []; } self::$tempImages[$filePath][$key] = $tempPath; } /** * Get the path of a temp file registered for the given original image file. * * @param string $filePath The path of the original image. * @param string $key The key the temp file is registered at. */ static function getTempImage(string $filePath, string $key = "default"): ?string { return self::$tempImages[$filePath][$key] ?? null; } /** * Unlink all cached images (i.e. temporary images either downloaded * or converted) except for the bundled "broken image" */ static function clear(bool $debugPng = false) { foreach (self::$_cache as $file) { if ($file === self::$broken_image) { continue; } if ($debugPng) { print "[clear unlink $file]"; } if (file_exists($file)) { unlink($file); } } foreach (self::$tempImages as $versions) { foreach ($versions as $file) { if ($file === self::$broken_image) { continue; } if ($debugPng) { print "[unlink temp image $file]"; } if (file_exists($file)) { unlink($file); } } } self::$_cache = []; self::$tempImages = []; } static function detect_type($file, $context = null) { list(, , $type) = Helpers::dompdf_getimagesize($file, $context); return $type; } static function is_broken($url) { return $url === self::$broken_image; } } if (file_exists(realpath(__DIR__ . "/../../lib/res/broken_image.svg"))) { Cache::$broken_image = realpath(__DIR__ . "/../../lib/res/broken_image.svg"); } PKZmXX+src/FrameReflower/AbstractFrameReflower.phpnuW+A_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; } } PKZ #src/FrameReflower/TableRowGroup.phpnuW+A_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)); } } PKZ src/FrameReflower/ListBullet.phpnuW+A_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); } } } PKZLU'src/FrameReflower/NullFrameReflower.phpnuW+A */ 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; } } PKZAѰsrc/FrameReflower/Inline.phpnuW+A_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); } } } PKZYsrc/FrameReflower/Block.phpnuW+A_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]; } } PKZʼ$$src/FrameReflower/Image.phpnuW+Adetermine_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); } } } PKZ^#|{{src/FrameReflower/TableCell.phpnuW+A_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]; } } PKZRJVVsrc/FrameReflower/Page.phpnuW+Aget_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); } } } } PKZ__src/FrameReflower/TableRow.phpnuW+A_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"); } } PKZwCCsrc/FrameReflower/Table.phpnuW+A_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 ]; } } PKZAB!B!src/Renderer.phpnuW+A_canvas->new_page(); } /** * Render frames recursively * * @param Frame $frame the frame to render */ public function render(Frame $frame) { global $_dompdf_debug; $this->_check_callbacks("begin_frame", $frame); if ($_dompdf_debug) { echo $frame; flush(); } $style = $frame->get_style(); if (in_array($style->visibility, ["hidden", "collapse"], true)) { return; } $display = $style->display; $transformList = $style->transform; $hasTransform = $transformList !== []; // Starts the CSS transformation if ($hasTransform) { $this->_canvas->save(); list($x, $y) = $frame->get_padding_box(); $origin = $style->transform_origin; foreach ($transformList as $transform) { list($function, $values) = $transform; if ($function === "matrix") { $function = "transform"; } $values = array_map("floatval", $values); $values[] = $x + (float)$style->length_in_pt($origin[0], (float)$style->length_in_pt($style->width)); $values[] = $y + (float)$style->length_in_pt($origin[1], (float)$style->length_in_pt($style->height)); call_user_func_array([$this->_canvas, $function], $values); } } switch ($display) { case "block": case "list-item": case "inline-block": case "table": case "inline-table": $this->_render_frame("block", $frame); break; case "inline": if ($frame->is_text_node()) { $this->_render_frame("text", $frame); } else { $this->_render_frame("inline", $frame); } break; case "table-cell": $this->_render_frame("table-cell", $frame); break; case "table-row-group": case "table-header-group": case "table-footer-group": $this->_render_frame("table-row-group", $frame); break; case "-dompdf-list-bullet": $this->_render_frame("list-bullet", $frame); break; case "-dompdf-image": $this->_render_frame("image", $frame); break; case "none": $node = $frame->get_node(); if ($node->nodeName === "script") { if ($node->getAttribute("type") === "text/php" || $node->getAttribute("language") === "php" ) { // Evaluate embedded php scripts $this->_render_frame("php", $frame); } elseif ($node->getAttribute("type") === "text/javascript" || $node->getAttribute("language") === "javascript" ) { // Insert JavaScript $this->_render_frame("javascript", $frame); } } // Don't render children, so skip to next iter return; default: break; } // Starts the overflow: hidden box if ($style->overflow === "hidden") { $padding_box = $frame->get_padding_box(); [$x, $y, $w, $h] = $padding_box; $style = $frame->get_style(); if ($style->has_border_radius()) { $border_box = $frame->get_border_box(); [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $padding_box); $this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl); } else { $this->_canvas->clipping_rectangle($x, $y, $w, $h); } } $stack = []; foreach ($frame->get_children() as $child) { // < 0 : negative z-index // = 0 : no z-index, no stacking context // = 1 : stacking context without z-index // > 1 : z-index $child_style = $child->get_style(); $child_z_index = $child_style->z_index; $z_index = 0; if ($child_z_index !== "auto") { $z_index = $child_z_index + 1; } elseif ($child_style->float !== "none" || $child->is_positioned()) { $z_index = 1; } $stack[$z_index][] = $child; } ksort($stack); foreach ($stack as $by_index) { foreach ($by_index as $child) { $this->render($child); } } // Ends the overflow: hidden box if ($style->overflow === "hidden") { $this->_canvas->clipping_end(); } if ($hasTransform) { $this->_canvas->restore(); } // Check for end frame callback $this->_check_callbacks("end_frame", $frame); } /** * Check for callbacks that need to be performed when a given event * gets triggered on a frame * * @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)) { $this->_callbacks = $this->_dompdf->getCallbacks(); } if (isset($this->_callbacks[$event])) { $fs = $this->_callbacks[$event]; $canvas = $this->_canvas; $fontMetrics = $this->_dompdf->getFontMetrics(); foreach ($fs as $f) { $f($frame, $canvas, $fontMetrics); } } } /** * Render a single frame * * Creates Renderer objects on demand * * @param string $type type of renderer to use * @param Frame $frame the frame to render */ protected function _render_frame($type, $frame) { if (!isset($this->_renderers[$type])) { switch ($type) { case "block": $this->_renderers[$type] = new Block($this->_dompdf); break; case "inline": $this->_renderers[$type] = new Renderer\Inline($this->_dompdf); break; case "text": $this->_renderers[$type] = new Text($this->_dompdf); break; case "image": $this->_renderers[$type] = new Image($this->_dompdf); break; case "table-cell": $this->_renderers[$type] = new TableCell($this->_dompdf); break; case "table-row-group": $this->_renderers[$type] = new TableRowGroup($this->_dompdf); break; case "list-bullet": $this->_renderers[$type] = new ListBullet($this->_dompdf); break; case "php": $this->_renderers[$type] = new PhpEvaluator($this->_canvas); break; case "javascript": $this->_renderers[$type] = new JavascriptEmbedder($this->_dompdf); break; } } $this->_renderers[$type]->render($frame); } } PKZ!src/Positioner/NullPositioner.phpnuW+Aget_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); } } PKZTsrc/Positioner/Inline.phpnuW+Afind_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); } } PKZ)]]src/Positioner/Block.phpnuW+Aget_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); } } PKZ|<<src/Positioner/Absolute.phpnuW+Aget_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); } } } PKZGxss%src/Positioner/AbstractPositioner.phpnuW+Aget_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); } } } PKZ'7,,src/Positioner/Fixed.phpnuW+Aget_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); } } } } PKZ3src/Positioner/TableCell.phpnuW+Aget_cellmap(); $frame->set_position($cellmap->get_frame_position($frame)); } } PKZ뀦src/Positioner/TableRow.phpnuW+Aget_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); } } PKZ=N''src/LineBox.phpnuW+A_block_frame = $frame; $this->_frames = []; $this->y = $y; $this->get_float_offsets(); } /** * Returns the floating elements inside the first floating parent * * @param Page $root * * @return Frame[] */ public function get_floats_inside(Page $root) { $floating_frames = $root->get_floating_frames(); if (count($floating_frames) == 0) { return $floating_frames; } // Find nearest floating element $p = $this->_block_frame; while ($p->get_style()->float === "none") { $parent = $p->get_parent(); if (!$parent) { break; } $p = $parent; } if ($p == $root) { return $floating_frames; } $parent = $p; $childs = []; foreach ($floating_frames as $_floating) { $p = $_floating->get_parent(); while (($p = $p->get_parent()) && $p !== $parent); if ($p) { $childs[] = $p; } } return $childs; } public function get_float_offsets() { static $anti_infinite_loop = 10000; // FIXME smelly hack $reflower = $this->_block_frame->get_reflower(); if (!$reflower) { return; } $cb_w = null; $block = $this->_block_frame; $root = $block->get_root(); if (!$root) { return; } $style = $this->_block_frame->get_style(); $floating_frames = $this->get_floats_inside($root); $inside_left_floating_width = 0; $inside_right_floating_width = 0; $outside_left_floating_width = 0; $outside_right_floating_width = 0; foreach ($floating_frames as $child_key => $floating_frame) { $floating_frame_parent = $floating_frame->get_parent(); $id = $floating_frame->get_id(); if (isset($this->floating_blocks[$id])) { continue; } $float = $floating_frame->get_style()->float; $floating_width = $floating_frame->get_margin_width(); if (!$cb_w) { $cb_w = $floating_frame->get_containing_block("w"); } $line_w = $this->get_width(); if (!$floating_frame->_float_next_line && ($cb_w <= $line_w + $floating_width) && ($cb_w > $line_w)) { $floating_frame->_float_next_line = true; continue; } // If the child is still shifted by the floating element if ($anti_infinite_loop-- > 0 && $floating_frame->get_position("y") + $floating_frame->get_margin_height() >= $this->y && $block->get_position("x") + $block->get_margin_width() >= $floating_frame->get_position("x") ) { if ($float === "left") { if ($floating_frame_parent === $this->_block_frame) { $inside_left_floating_width += $floating_width; } else { $outside_left_floating_width += $floating_width; } } elseif ($float === "right") { if ($floating_frame_parent === $this->_block_frame) { $inside_right_floating_width += $floating_width; } else { $outside_right_floating_width += $floating_width; } } $this->floating_blocks[$id] = true; } // else, the floating element won't shift anymore else { $root->remove_floating_frame($child_key); } } $this->left += $inside_left_floating_width; if ($outside_left_floating_width > 0 && $outside_left_floating_width > ((float)$style->length_in_pt($style->margin_left) + (float)$style->length_in_pt($style->padding_left))) { $this->left += $outside_left_floating_width - (float)$style->length_in_pt($style->margin_left) - (float)$style->length_in_pt($style->padding_left); } $this->right += $inside_right_floating_width; if ($outside_right_floating_width > 0 && $outside_right_floating_width > ((float)$style->length_in_pt($style->margin_left) + (float)$style->length_in_pt($style->padding_right))) { $this->right += $outside_right_floating_width - (float)$style->length_in_pt($style->margin_right) - (float)$style->length_in_pt($style->padding_right); } } /** * @return float */ public function get_width() { return $this->left + $this->w + $this->right; } /** * @return Block */ public function get_block_frame() { return $this->_block_frame; } /** * @return AbstractFrameDecorator[] */ function &get_frames() { return $this->_frames; } /** * @param AbstractFrameDecorator $frame */ public function add_frame(Frame $frame): void { $this->_frames[] = $frame; if ($frame->get_positioner() instanceof InlinePositioner) { $this->inline = true; } } /** * Remove the frame at the given index and all following frames from the * line. * * @param int $index */ public function remove_frames(int $index): void { $lastIndex = count($this->_frames) - 1; if ($index < 0 || $index > $lastIndex) { return; } for ($i = $lastIndex; $i >= $index; $i--) { $f = $this->_frames[$i]; unset($this->_frames[$i]); $this->w -= $f->get_margin_width(); } // Reset array indices $this->_frames = array_values($this->_frames); // Recalculate the height of the line $h = 0.0; $this->inline = false; foreach ($this->_frames as $f) { $h = max($h, $f->get_margin_height()); if ($f->get_positioner() instanceof InlinePositioner) { $this->inline = true; } } $this->h = $h; } /** * Get the `outside` positioned list markers to be vertically aligned with * the line box. * * @return ListBullet[] */ public function get_list_markers(): array { return $this->list_markers; } /** * Add a list marker to the line box. * * The list marker is only added for the purpose of vertical alignment, it * is not actually added to the list of frames of the line box. */ public function add_list_marker(ListBullet $marker): void { $this->list_markers[] = $marker; } /** * An iterator of all list markers and inline positioned frames of the line * box. * * @return \Iterator */ public function frames_to_align(): \Iterator { yield from $this->list_markers; foreach ($this->_frames as $frame) { if ($frame->get_positioner() instanceof InlinePositioner) { yield $frame; } } } /** * Trim trailing whitespace from the line. */ public function trim_trailing_ws(): void { $lastIndex = count($this->_frames) - 1; if ($lastIndex < 0) { return; } $lastFrame = $this->_frames[$lastIndex]; $reflower = $lastFrame->get_reflower(); if ($reflower instanceof TextFrameReflower && !$lastFrame->is_pre()) { $reflower->trim_trailing_ws(); $this->recalculate_width(); } } /** * Recalculate LineBox width based on the contained frames total width. * * @return float */ public function recalculate_width(): float { $width = 0.0; foreach ($this->_frames as $frame) { $width += $frame->get_margin_width(); } return $this->w = $width; } /** * @return string */ public function __toString(): string { $props = ["wc", "y", "w", "h", "left", "right", "br"]; $s = ""; foreach ($props as $prop) { $s .= "$prop: " . $this->$prop . "\n"; } $s .= count($this->_frames) . " frames\n"; return $s; } } /* class LineBoxList implements Iterator { private $_p = 0; private $_lines = array(); } */ PKZ呏||src/Options.phpnuW+A ["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]; } } PKZ_bsrc/Frame/FrameListIterator.phpnuW+Aparent = $frame; $this->rewind(); } public function rewind(): void { $this->cur = $this->parent->get_first_child(); $this->prev = null; $this->num = 0; } /** * @return bool */ public function valid(): bool { return $this->cur !== null; } /** * @return int */ public function key(): int { return $this->num; } /** * @return Frame|null */ public function current(): ?Frame { return $this->cur; } public function next(): void { if ($this->cur === null) { return; } if ($this->cur->get_parent() === $this->parent) { $this->prev = $this->cur; $this->cur = $this->cur->get_next_sibling(); $this->num++; } else { // Continue from the previous child if the current frame has been // moved to another parent $this->cur = $this->prev !== null ? $this->prev->get_next_sibling() : $this->parent->get_first_child(); } } } PKZW n,#,#src/Frame/FrameTree.phpnuW+A_dom = $dom; $this->_root = null; $this->_registry = []; } /** * Returns the DOMDocument object representing the current html document * * @return DOMDocument */ public function get_dom() { return $this->_dom; } /** * Returns the root frame of the tree * * @return Frame */ public function get_root() { return $this->_root; } /** * Returns a specific frame given its id * * @param string $id * * @return Frame|null */ public function get_frame($id) { return isset($this->_registry[$id]) ? $this->_registry[$id] : null; } /** * Returns a post-order iterator for all frames in the tree * * @deprecated Iterate the tree directly instead * @return FrameTreeIterator */ public function get_frames(): FrameTreeIterator { return new FrameTreeIterator($this->_root); } /** * Returns a post-order iterator for all frames in the tree * * @return FrameTreeIterator */ public function getIterator(): FrameTreeIterator { return new FrameTreeIterator($this->_root); } /** * Builds the tree */ public function build_tree() { $html = $this->_dom->getElementsByTagName("html")->item(0); if (is_null($html)) { $html = $this->_dom->firstChild; } if (is_null($html)) { throw new Exception("Requested HTML document contains no data."); } $this->fix_tables(); $this->_root = $this->_build_tree_r($html); } /** * Adds missing TBODYs around TR */ protected function fix_tables() { $xp = new DOMXPath($this->_dom); // Move table caption before the table // FIXME find a better way to deal with it... $captions = $xp->query('//table/caption'); foreach ($captions as $caption) { $table = $caption->parentNode; $table->parentNode->insertBefore($caption, $table); } $firstRows = $xp->query('//table/tr[1]'); /** @var DOMElement $tableChild */ foreach ($firstRows as $tableChild) { $tbody = $this->_dom->createElement('tbody'); $tableNode = $tableChild->parentNode; do { if ($tableChild->nodeName === 'tr') { $tmpNode = $tableChild; $tableChild = $tableChild->nextSibling; $tableNode->removeChild($tmpNode); $tbody->appendChild($tmpNode); } else { if ($tbody->hasChildNodes() === true) { $tableNode->insertBefore($tbody, $tableChild); $tbody = $this->_dom->createElement('tbody'); } $tableChild = $tableChild->nextSibling; } } while ($tableChild); if ($tbody->hasChildNodes() === true) { $tableNode->appendChild($tbody); } } } // FIXME: temporary hack, preferably we will improve rendering of sequential #text nodes /** * Remove a child from a node * * Remove a child from a node. If the removed node results in two * adjacent #text nodes then combine them. * * @param DOMNode $node the current DOMNode being considered * @param array $children an array of nodes that are the children of $node * @param int $index index from the $children array of the node to remove */ protected function _remove_node(DOMNode $node, array &$children, $index) { $child = $children[$index]; $previousChild = $child->previousSibling; $nextChild = $child->nextSibling; $node->removeChild($child); if (isset($previousChild, $nextChild)) { if ($previousChild->nodeName === "#text" && $nextChild->nodeName === "#text") { $previousChild->nodeValue .= $nextChild->nodeValue; $this->_remove_node($node, $children, $index+1); } } array_splice($children, $index, 1); } /** * Recursively adds {@link Frame} objects to the tree * * Recursively build a tree of Frame objects based on a dom tree. * No layout information is calculated at this time, although the * tree may be adjusted (i.e. nodes and frames for generated content * and images may be created). * * @param DOMNode $node the current DOMNode being considered * * @return Frame */ protected function _build_tree_r(DOMNode $node) { $frame = new Frame($node); $id = $frame->get_id(); $this->_registry[$id] = $frame; if (!$node->hasChildNodes()) { return $frame; } // Store the children in an array so that the tree can be modified $children = []; $length = $node->childNodes->length; for ($i = 0; $i < $length; $i++) { $children[] = $node->childNodes->item($i); } $index = 0; // INFO: We don't advance $index if a node is removed to avoid skipping nodes while ($index < count($children)) { $child = $children[$index]; $nodeName = strtolower($child->nodeName); // Skip non-displaying nodes if (in_array($nodeName, self::$HIDDEN_TAGS)) { if ($nodeName !== "head" && $nodeName !== "style") { $this->_remove_node($node, $children, $index); } else { $index++; } continue; } // Skip empty text nodes if ($nodeName === "#text" && $child->nodeValue === "") { $this->_remove_node($node, $children, $index); continue; } // Skip empty image nodes if ($nodeName === "img" && $child->getAttribute("src") === "") { $this->_remove_node($node, $children, $index); continue; } if (is_object($child)) { $frame->append_child($this->_build_tree_r($child), false); } $index++; } return $frame; } /** * @param DOMElement $node * @param DOMElement $new_node * @param string $pos * * @return mixed */ public function insert_node(DOMElement $node, DOMElement $new_node, $pos) { if ($pos === "after" || !$node->firstChild) { $node->appendChild($new_node); } else { $node->insertBefore($new_node, $node->firstChild); } $this->_build_tree_r($new_node); $frame_id = $new_node->getAttribute("frame_id"); $frame = $this->get_frame($frame_id); $parent_id = $node->getAttribute("frame_id"); $parent = $this->get_frame($parent_id); if ($parent) { if ($pos === "before") { $parent->prepend_child($frame, false); } else { $parent->append_child($frame, false); } } return $frame_id; } } PKZړ- - src/Frame/Factory.phpnuW+Aset_reflower(new PageFrameReflower($frame)); $root->set_decorator($frame); return $frame; } /** * Decorate a Frame * * @param Frame $frame The frame to decorate * @param Dompdf $dompdf The dompdf instance * @param Frame|null $root The root of the frame * * @throws Exception * @return AbstractFrameDecorator|null * FIXME: this is admittedly a little smelly... */ public static function decorate_frame(Frame $frame, Dompdf $dompdf, ?Frame $root = null): ?AbstractFrameDecorator { $style = $frame->get_style(); $display = $style->display; switch ($display) { case "block": $positioner = "Block"; $decorator = "Block"; $reflower = "Block"; break; case "inline-block": $positioner = "Inline"; $decorator = "Block"; $reflower = "Block"; break; case "inline": $positioner = "Inline"; if ($frame->is_text_node()) { $decorator = "Text"; $reflower = "Text"; } else { $decorator = "Inline"; $reflower = "Inline"; } break; case "table": $positioner = "Block"; $decorator = "Table"; $reflower = "Table"; break; case "inline-table": $positioner = "Inline"; $decorator = "Table"; $reflower = "Table"; break; case "table-row-group": case "table-header-group": case "table-footer-group": $positioner = "NullPositioner"; $decorator = "TableRowGroup"; $reflower = "TableRowGroup"; break; case "table-row": $positioner = "NullPositioner"; $decorator = "TableRow"; $reflower = "TableRow"; break; case "table-cell": $positioner = "TableCell"; $decorator = "TableCell"; $reflower = "TableCell"; break; case "list-item": $positioner = "Block"; $decorator = "Block"; $reflower = "Block"; break; case "-dompdf-list-bullet": if ($style->list_style_position === "inside") { $positioner = "Inline"; } else { $positioner = "ListBullet"; } if ($style->list_style_image !== "none") { $decorator = "ListBulletImage"; } else { $decorator = "ListBullet"; } $reflower = "ListBullet"; break; case "-dompdf-image": $positioner = "Inline"; $decorator = "Image"; $reflower = "Image"; break; case "-dompdf-br": $positioner = "Inline"; $decorator = "Inline"; $reflower = "Inline"; break; default: case "none": if ($style->_dompdf_keep !== "yes") { // Remove the node and the frame $frame->get_parent()->remove_child($frame); return null; } $positioner = "NullPositioner"; $decorator = "NullFrameDecorator"; $reflower = "NullFrameReflower"; break; } // Handle CSS position $position = $style->position; if ($position === "absolute") { $positioner = "Absolute"; } elseif ($position === "fixed") { $positioner = "Fixed"; } $node = $frame->get_node(); // Handle nodeName if ($node->nodeName === "img") { $style->set_prop("display", "-dompdf-image"); $decorator = "Image"; $reflower = "Image"; } $decorator = "Dompdf\\FrameDecorator\\$decorator"; $reflower = "Dompdf\\FrameReflower\\$reflower"; /** @var AbstractFrameDecorator $deco */ $deco = new $decorator($frame, $dompdf); $deco->set_positioner(self::getPositionerInstance($positioner)); $deco->set_reflower(new $reflower($deco, $dompdf->getFontMetrics())); if ($root) { $deco->set_root($root); } if ($display === "list-item") { // Insert a list-bullet frame $xml = $dompdf->getDom(); $bullet_node = $xml->createElement("bullet"); // arbitrary choice $b_f = new Frame($bullet_node); $node = $frame->get_node(); $parent_node = $node->parentNode; if ($parent_node && $parent_node instanceof \DOMElement) { if (!$parent_node->hasAttribute("dompdf-children-count")) { $xpath = new DOMXPath($xml); $count = $xpath->query("li", $parent_node)->length; $parent_node->setAttribute("dompdf-children-count", $count); } if (is_numeric($node->getAttribute("value"))) { $index = intval($node->getAttribute("value")); } else { if (!$parent_node->hasAttribute("dompdf-counter")) { $index = ($parent_node->hasAttribute("start") ? $parent_node->getAttribute("start") : 1); } else { $index = (int)$parent_node->getAttribute("dompdf-counter") + 1; } } $parent_node->setAttribute("dompdf-counter", $index); $bullet_node->setAttribute("dompdf-counter", $index); } $new_style = $dompdf->getCss()->create_style(); $new_style->set_prop("display", "-dompdf-list-bullet"); $new_style->inherit($style); $b_f->set_style($new_style); $deco->prepend_child(Factory::decorate_frame($b_f, $dompdf, $root)); } return $deco; } /** * Creates Positioners * * @param string $type Type of positioner to use * * @return AbstractPositioner */ protected static function getPositionerInstance(string $type): AbstractPositioner { if (!isset(self::$_positioners[$type])) { $class = '\\Dompdf\\Positioner\\'.$type; self::$_positioners[$type] = new $class(); } return self::$_positioners[$type]; } } PKZsrc/Frame/FrameTreeIterator.phpnuW+A_stack[] = $this->_root = $root; $this->_num = 0; } public function rewind(): void { $this->_stack = [$this->_root]; $this->_num = 0; } /** * @return bool */ public function valid(): bool { return count($this->_stack) > 0; } /** * @return int */ public function key(): int { return $this->_num; } /** * @return Frame */ public function current(): Frame { return end($this->_stack); } public function next(): void { $b = array_pop($this->_stack); $this->_num++; // Push all children onto the stack in reverse order if ($c = $b->get_last_child()) { $this->_stack[] = $c; while ($c = $c->get_prev_sibling()) { $this->_stack[] = $c; } } } } PKZסJsrc/CanvasFactory.phpnuW+AgetOptions()->getPdfBackend()); if (isset($class) && class_exists($class, false)) { $class .= "_Adapter"; } else { if (($backend === "auto" || $backend === "pdflib") && class_exists("PDFLib", false) ) { $class = "Dompdf\\Adapter\\PDFLib"; } else { if ($backend === "gd" && extension_loaded('gd')) { $class = "Dompdf\\Adapter\\GD"; } else { $class = "Dompdf\\Adapter\\CPDF"; } } } return new $class($paper, $orientation, $dompdf); } } PKZ"pM FFsrc/FontMetrics.phpnuW+AsetCanvas($canvas); $this->setOptions($options); $this->loadFontFamilies(); } /** * @deprecated */ public function save_font_families() { $this->saveFontFamilies(); } /** * Saves the stored font family cache * * The name and location of the cache file are determined by {@link * FontMetrics::USER_FONTS_FILE}. This file should be writable by the * webserver process. * * @see FontMetrics::loadFontFamilies() */ public function saveFontFamilies() { file_put_contents($this->getUserFontsFilePath(), json_encode($this->userFonts, JSON_PRETTY_PRINT)); } /** * @deprecated */ public function load_font_families() { $this->loadFontFamilies(); } /** * Loads the stored font family cache * * @see FontMetrics::saveFontFamilies() */ public function loadFontFamilies() { $file = $this->options->getRootDir() . "/lib/fonts/installed-fonts.dist.json"; $this->bundledFonts = json_decode(file_get_contents($file), true); if (is_readable($this->getUserFontsFilePath())) { $this->userFonts = json_decode(file_get_contents($this->getUserFontsFilePath()), true); } else { $this->loadFontFamiliesLegacy(); } } private function loadFontFamiliesLegacy() { $legacyCacheFile = $this->options->getFontDir() . '/dompdf_font_family_cache.php'; if (is_readable($legacyCacheFile)) { $fontDir = $this->options->getFontDir(); $rootDir = $this->options->getRootDir(); if (!defined("DOMPDF_DIR")) { define("DOMPDF_DIR", $rootDir); } if (!defined("DOMPDF_FONT_DIR")) { define("DOMPDF_FONT_DIR", $fontDir); } $cacheDataClosure = require $legacyCacheFile; $cacheData = is_array($cacheDataClosure) ? $cacheDataClosure : $cacheDataClosure($fontDir, $rootDir); if (is_array($cacheData)) { foreach ($cacheData as $family => $variants) { if (!isset($this->bundledFonts[$family]) && is_array($variants)) { foreach ($variants as $variant => $variantPath) { $variantName = basename($variantPath); $variantDir = dirname($variantPath); if ($variantDir == $fontDir) { $this->userFonts[$family][$variant] = $variantName; } else { $this->userFonts[$family][$variant] = $variantPath; } } } } $this->saveFontFamilies(); } } } /** * @param array $style * @param string $remote_file * @param resource $context * @return bool * @deprecated */ public function register_font($style, $remote_file, $context = null) { return $this->registerFont($style, $remote_file); } /** * @param array $style * @param string $remoteFile * @param resource $context * @return bool */ public function registerFont($style, $remoteFile, $context = null) { $fontname = mb_strtolower($style["family"]); $families = $this->getFontFamilies(); $entry = []; if (isset($families[$fontname])) { $entry = $families[$fontname]; } $styleString = $this->getType("{$style['weight']} {$style['style']}"); $remoteHash = md5($remoteFile); $prefix = $fontname . "_" . $styleString; $prefix = trim($prefix, "-"); if (function_exists('iconv')) { $prefix = @iconv('utf-8', 'us-ascii//TRANSLIT', $prefix); } $prefix_encoding = mb_detect_encoding($prefix, mb_detect_order(), true); $substchar = mb_substitute_character(); mb_substitute_character(0x005F); $prefix = mb_convert_encoding($prefix, "ISO-8859-1", $prefix_encoding); mb_substitute_character($substchar); $prefix = preg_replace("[\W]", "_", $prefix); $prefix = preg_replace("/[^-_\w]+/", "", $prefix); $localFile = $prefix . "_" . $remoteHash; $localFilePath = $this->getOptions()->getFontDir() . "/" . $localFile; if (isset($entry[$styleString]) && $localFilePath == $entry[$styleString]) { return true; } $entry[$styleString] = $localFile; // Download the remote file [$protocol] = Helpers::explode_url($remoteFile); $allowed_protocols = $this->options->getAllowedProtocols(); if (!array_key_exists($protocol, $allowed_protocols)) { Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__); return false; } foreach ($allowed_protocols[$protocol]["rules"] as $rule) { [$result, $message] = $rule($remoteFile); if ($result !== true) { Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__); return false; } } list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context); if ($remoteFileContent === null) { return false; } $localTempFile = @tempnam($this->options->get("tempDir"), "dompdf-font-"); file_put_contents($localTempFile, $remoteFileContent); $font = Font::load($localTempFile); if (!$font) { unlink($localTempFile); return false; } $font->parse(); $font->saveAdobeFontMetrics("$localFilePath.ufm"); $font->close(); unlink($localTempFile); if ( !file_exists("$localFilePath.ufm") ) { return false; } $fontExtension = ".ttf"; switch ($font->getFontType()) { case "TrueType": default: $fontExtension = ".ttf"; break; } // Save the changes file_put_contents($localFilePath.$fontExtension, $remoteFileContent); if ( !file_exists($localFilePath.$fontExtension) ) { unlink("$localFilePath.ufm"); return false; } $this->setFontFamily($fontname, $entry); return true; } /** * @param $text * @param $font * @param $size * @param float $word_spacing * @param float $char_spacing * @return float * @deprecated */ public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0) { //return self::$_pdf->get_text_width($text, $font, $size, $word_spacing, $char_spacing); return $this->getTextWidth($text, $font, $size, $word_spacing, $char_spacing); } /** * Calculates text size, in points * * @param string $text The text to be sized * @param string $font The font file to use * @param float $size The font size, in points * @param float $wordSpacing Word spacing, if any * @param float $charSpacing Char spacing, if any * * @return float */ public function getTextWidth(string $text, $font, float $size, float $wordSpacing = 0.0, float $charSpacing = 0.0): float { // @todo Make sure this cache is efficient before enabling it static $cache = []; if ($text === "") { return 0; } // Don't cache long strings $useCache = !isset($text[50]); // Faster than strlen // Text-size calculations depend on the canvas used. Make sure to not // return wrong values when switching canvas backends $canvasClass = get_class($this->canvas); $key = "$canvasClass/$font/$size/$wordSpacing/$charSpacing"; if ($useCache && isset($cache[$key][$text])) { return $cache[$key][$text]; } $width = $this->canvas->get_text_width($text, $font, $size, $wordSpacing, $charSpacing); if ($useCache) { $cache[$key][$text] = $width; } return $width; } /** * @param $font * @param $size * @return float * @deprecated */ public function get_font_height($font, $size) { return $this->getFontHeight($font, $size); } /** * Calculates font height, in points * * @param string $font The font file to use * @param float $size The font size, in points * * @return float */ public function getFontHeight($font, float $size): float { return $this->canvas->get_font_height($font, $size); } /** * Calculates font baseline, in points * * @param string $font The font file to use * @param float $size The font size, in points * * @return float */ public function getFontBaseline($font, float $size): float { return $this->canvas->get_font_baseline($font, $size); } /** * @param $family_raw * @param string $subtype_raw * @return string * @deprecated */ public function get_font($family_raw, $subtype_raw = "normal") { return $this->getFont($family_raw, $subtype_raw); } /** * Resolves a font family & subtype into an actual font file * Subtype can be one of 'normal', 'bold', 'italic' or 'bold_italic'. If * the particular font family has no suitable font file, the default font * ({@link Options::defaultFont}) is used. The font file returned * is the absolute pathname to the font file on the system. * * @param string|null $familyRaw * @param string $subtypeRaw * * @return string|null */ public function getFont($familyRaw, $subtypeRaw = "normal") { static $cache = []; if (isset($cache[$familyRaw][$subtypeRaw])) { return $cache[$familyRaw][$subtypeRaw]; } /* Allow calling for various fonts in search path. Therefore not immediately * return replacement on non match. * Only when called with NULL try replacement. * When this is also missing there is really trouble. * If only the subtype fails, nevertheless return failure. * Only on checking the fallback font, check various subtypes on same font. */ $subtype = strtolower($subtypeRaw); $families = $this->getFontFamilies(); if ($familyRaw) { $family = str_replace(["'", '"'], "", strtolower($familyRaw)); if (isset($families[$family][$subtype])) { return $cache[$familyRaw][$subtypeRaw] = $families[$family][$subtype]; } return null; } $fallback_families = [strtolower($this->options->getDefaultFont()), "serif"]; foreach ($fallback_families as $family) { if (isset($families[$family][$subtype])) { return $cache[$familyRaw][$subtypeRaw] = $families[$family][$subtype]; } if (!isset($families[$family])) { continue; } $family = $families[$family]; foreach ($family as $sub => $font) { if (strpos($subtype, $sub) !== false) { return $cache[$familyRaw][$subtypeRaw] = $font; } } if ($subtype !== "normal") { foreach ($family as $sub => $font) { if ($sub !== "normal") { return $cache[$familyRaw][$subtypeRaw] = $font; } } } $subtype = "normal"; if (isset($family[$subtype])) { return $cache[$familyRaw][$subtypeRaw] = $family[$subtype]; } } return null; } /** * @param $family * @return null|string * @deprecated */ public function get_family($family) { return $this->getFamily($family); } /** * @param string $family * @return null|string */ public function getFamily($family) { $family = str_replace(["'", '"'], "", mb_strtolower($family)); $families = $this->getFontFamilies(); if (isset($families[$family])) { return $families[$family]; } return null; } /** * @param $type * @return string * @deprecated */ public function get_type($type) { return $this->getType($type); } /** * @param string $type * @return string */ public function getType($type) { if (preg_match('/bold/i', $type)) { $weight = 700; } elseif (preg_match('/([1-9]00)/', $type, $match)) { $weight = (int)$match[0]; } else { $weight = 400; } $weight = $weight === 400 ? 'normal' : $weight; $weight = $weight === 700 ? 'bold' : $weight; $style = preg_match('/italic|oblique/i', $type) ? 'italic' : null; if ($weight === 'normal' && $style !== null) { return $style; } return $style === null ? $weight : $weight.'_'.$style; } /** * @return array * @deprecated */ public function get_font_families() { return $this->getFontFamilies(); } /** * Returns the current font lookup table * * @return array */ public function getFontFamilies() { if (!isset($this->fontFamilies)) { $this->setFontFamilies(); } return $this->fontFamilies; } /** * Convert loaded fonts to font lookup table * * @return array */ public function setFontFamilies() { $fontFamilies = []; if (isset($this->bundledFonts) && is_array($this->bundledFonts)) { foreach ($this->bundledFonts as $family => $variants) { if (!isset($fontFamilies[$family])) { $fontFamilies[$family] = array_map(function ($variant) { return $this->getOptions()->getRootDir() . '/lib/fonts/' . $variant; }, $variants); } } } if (isset($this->userFonts) && is_array($this->userFonts)) { foreach ($this->userFonts as $family => $variants) { $fontFamilies[$family] = array_map(function ($variant) { $variantName = basename($variant); if ($variantName === $variant) { return $this->getOptions()->getFontDir() . '/' . $variant; } return $variant; }, $variants); } } $this->fontFamilies = $fontFamilies; } /** * @param string $fontname * @param mixed $entry * @deprecated */ public function set_font_family($fontname, $entry) { $this->setFontFamily($fontname, $entry); } /** * @param string $fontname * @param mixed $entry */ public function setFontFamily($fontname, $entry) { $this->userFonts[mb_strtolower($fontname)] = $entry; $this->saveFontFamilies(); unset($this->fontFamilies); } /** * @return string */ public function getUserFontsFilePath() { return $this->options->getFontDir() . '/' . self::USER_FONTS_FILE; } /** * @param Options $options * @return $this */ public function setOptions(Options $options) { $this->options = $options; unset($this->fontFamilies); return $this; } /** * @return Options */ public function getOptions() { return $this->options; } /** * @param Canvas $canvas * @return $this */ public function setCanvas(Canvas $canvas) { $this->canvas = $canvas; return $this; } /** * @return Canvas */ public function getCanvas() { return $this->canvas; } } PKZ}QB''src/Exception.phpnuW+A_dompdf = $dompdf; } /** * @param $script */ public function insert($script) { $this->_dompdf->getCanvas()->javascript($script); } /** * @param Frame $frame */ public function render(Frame $frame) { if (!$this->_dompdf->getOptions()->getIsJavascriptEnabled()) { return; } $this->insert($frame->get_node()->nodeValue); } } PKZ"P+  src/PhpEvaluator.phpnuW+A_canvas = $canvas; } /** * @param $code * @param array $vars */ public function evaluate($code, $vars = []) { if (!$this->_canvas->get_dompdf()->getOptions()->getIsPhpEnabled()) { return; } // Set up some variables for the inline code $pdf = $this->_canvas; $fontMetrics = $pdf->get_dompdf()->getFontMetrics(); $PAGE_NUM = $pdf->get_page_number(); $PAGE_COUNT = $pdf->get_page_count(); // Override those variables if passed in foreach ($vars as $k => $v) { $$k = $v; } eval($code); } /** * @param Frame $frame */ public function render(Frame $frame) { $this->evaluate($frame->get_node()->nodeValue); } } PKZ{wzsrc/Helpers.phpnuW+A tags if the current sapi is not 'cli'. * Returns the output string instead of displaying it if $return is true. * * @param mixed $mixed variable or expression to display * @param bool $return * * @return string|null */ public static function pre_r($mixed, $return = false) { if ($return) { return "
" . print_r($mixed, true) . "
"; } if (php_sapi_name() !== "cli") { echo "
";
        }

        print_r($mixed);

        if (php_sapi_name() !== "cli") {
            echo "
"; } else { echo "\n"; } flush(); return null; } /** * builds a full url given a protocol, hostname, base path and url * * @param string $protocol * @param string $host * @param string $base_path * @param string $url * @return string * * Initially the trailing slash of $base_path was optional, and conditionally appended. * However on dynamically created sites, where the page is given as url parameter, * the base path might not end with an url. * Therefore do not append a slash, and **require** the $base_url to ending in a slash * when needed. * Vice versa, on using the local file system path of a file, make sure that the slash * is appended (o.k. also for Windows) */ public static function build_url($protocol, $host, $base_path, $url) { $protocol = mb_strtolower($protocol); if (empty($protocol)) { $protocol = "file://"; } if ($url === "") { return null; } $url_lc = mb_strtolower($url); // Is the url already fully qualified, a Data URI, or a reference to a named anchor? // File-protocol URLs may require additional processing (e.g. for URLs with a relative path) if ( ( mb_strpos($url_lc, "://") !== false && !in_array(substr($url_lc, 0, 7), ["file://", "phar://"], true) ) || mb_substr($url_lc, 0, 1) === "#" || mb_strpos($url_lc, "data:") === 0 || mb_strpos($url_lc, "mailto:") === 0 || mb_strpos($url_lc, "tel:") === 0 ) { return $url; } $res = ""; if (strpos($url_lc, "file://") === 0) { $url = substr($url, 7); $protocol = "file://"; } elseif (strpos($url_lc, "phar://") === 0) { $res = substr($url, strpos($url_lc, ".phar")+5); $url = substr($url, 7, strpos($url_lc, ".phar")-2); $protocol = "phar://"; } $ret = ""; $is_local_path = in_array($protocol, ["file://", "phar://"], true); if ($is_local_path) { //On Windows local file, an abs path can begin also with a '\' or a drive letter and colon //drive: followed by a relative path would be a drive specific default folder. //not known in php app code, treat as abs path //($url[1] !== ':' || ($url[2]!=='\\' && $url[2]!=='/')) if ($url[0] !== '/' && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' || (mb_strlen($url) > 1 && $url[0] !== '\\' && $url[1] !== ':'))) { // For rel path and local access we ignore the host, and run the path through realpath() $ret .= realpath($base_path) . '/'; } $ret .= $url; $ret = preg_replace('/\?(.*)$/', "", $ret); $filepath = realpath($ret); if ($filepath === false) { return null; } $ret = "$protocol$filepath$res"; return $ret; } $ret = $protocol; // Protocol relative urls (e.g. "//example.org/style.css") if (strpos($url, '//') === 0) { $ret .= substr($url, 2); //remote urls with backslash in html/css are not really correct, but lets be genereous } elseif ($url[0] === '/' || $url[0] === '\\') { // Absolute path $ret .= $host . $url; } else { // Relative path //$base_path = $base_path !== "" ? rtrim($base_path, "/\\") . "/" : ""; $ret .= $host . $base_path . $url; } // URL should now be complete, final cleanup $parsed_url = parse_url($ret); // reproduced from https://www.php.net/manual/en/function.parse-url.php#106731 $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; $pass = ($user || $pass) ? "$pass@" : ''; $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; // partially reproduced from https://stackoverflow.com/a/1243431/264628 /* replace '//' or '/./' or '/foo/../' with '/' */ $re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#'); for ($n=1; $n>0; $path=preg_replace($re, '/', $path, -1, $n)) {} $ret = "$scheme$user$pass$host$port$path$query$fragment"; return $ret; } /** * Builds a HTTP Content-Disposition header string using `$dispositionType` * and `$filename`. * * If the filename contains any characters not in the ISO-8859-1 character * set, a fallback filename will be included for clients not supporting the * `filename*` parameter. * * @param string $dispositionType * @param string $filename * @return string */ public static function buildContentDispositionHeader($dispositionType, $filename) { $encoding = mb_detect_encoding($filename); $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding); $fallbackfilename = str_replace("\"", "", $fallbackfilename); $encodedfilename = rawurlencode($filename); $contentDisposition = "Content-Disposition: $dispositionType; filename=\"$fallbackfilename\""; if ($fallbackfilename !== $filename) { $contentDisposition .= "; filename*=UTF-8''$encodedfilename"; } return $contentDisposition; } /** * Converts decimal numbers to roman numerals. * * As numbers larger than 3999 (and smaller than 1) cannot be represented in * the standard form of roman numerals, those are left in decimal form. * * See https://en.wikipedia.org/wiki/Roman_numerals#Standard_form * * @param int|string $num * * @throws Exception * @return string */ public static function dec2roman($num): string { static $ones = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"]; static $tens = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"]; static $hund = ["", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"]; static $thou = ["", "m", "mm", "mmm"]; if (!is_numeric($num)) { throw new Exception("dec2roman() requires a numeric argument."); } if ($num >= 4000 || $num <= 0) { return (string) $num; } $num = strrev((string)$num); $ret = ""; switch (mb_strlen($num)) { /** @noinspection PhpMissingBreakStatementInspection */ case 4: $ret .= $thou[$num[3]]; /** @noinspection PhpMissingBreakStatementInspection */ case 3: $ret .= $hund[$num[2]]; /** @noinspection PhpMissingBreakStatementInspection */ case 2: $ret .= $tens[$num[1]]; /** @noinspection PhpMissingBreakStatementInspection */ case 1: $ret .= $ones[$num[0]]; default: break; } return $ret; } /** * Restrict a length to the given range. * * If min > max, the result is min. * * @param float $length * @param float $min * @param float $max * * @return float */ public static function clamp(float $length, float $min, float $max): float { return max($min, min($length, $max)); } /** * Determines whether $value is a percentage or not * * @param string|float|int $value * * @return bool */ public static function is_percent($value): bool { return is_string($value) && false !== mb_strpos($value, "%"); } /** * Parses a data URI scheme * http://en.wikipedia.org/wiki/Data_URI_scheme * * @param string $data_uri The data URI to parse * * @return array|bool The result with charset, mime type and decoded data */ public static function parse_data_uri($data_uri) { if (!preg_match('/^data:(?P[a-z0-9\/+-.]+)(;charset=(?P[a-z0-9-])+)?(?P;base64)?\,(?P.*)?/is', $data_uri, $match)) { return false; } $match['data'] = rawurldecode($match['data']); $result = [ 'charset' => $match['charset'] ? $match['charset'] : 'US-ASCII', 'mime' => $match['mime'] ? $match['mime'] : 'text/plain', 'data' => $match['base64'] ? base64_decode($match['data']) : $match['data'], ]; return $result; } /** * Encodes a Uniform Resource Identifier (URI) by replacing non-alphanumeric * characters with a percent (%) sign followed by two hex digits, excepting * characters in the URI reserved character set. * * Assumes that the URI is a complete URI, so does not encode reserved * characters that have special meaning in the URI. * * Simulates the encodeURI function available in JavaScript * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI * * Source: http://stackoverflow.com/q/4929584/264628 * * @param string $uri The URI to encode * @return string The original URL with special characters encoded */ public static function encodeURI($uri) { $unescaped = [ '%2D'=>'-','%5F'=>'_','%2E'=>'.','%21'=>'!', '%7E'=>'~', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')' ]; $reserved = [ '%3B'=>';','%2C'=>',','%2F'=>'/','%3F'=>'?','%3A'=>':', '%40'=>'@','%26'=>'&','%3D'=>'=','%2B'=>'+','%24'=>'$' ]; $score = [ '%23'=>'#' ]; return strtr(rawurlencode(rawurldecode($uri)), array_merge($reserved, $unescaped, $score)); } /** * Decoder for RLE8 compression in windows bitmaps * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp * * @param string $str Data to decode * @param int $width Image width * * @return string */ public static function rle8_decode($str, $width) { $lineWidth = $width + (3 - ($width - 1) % 4); $out = ''; $cnt = strlen($str); for ($i = 0; $i < $cnt; $i++) { $o = ord($str[$i]); switch ($o) { case 0: # ESCAPE $i++; switch (ord($str[$i])) { case 0: # NEW LINE $padCnt = $lineWidth - strlen($out) % $lineWidth; if ($padCnt < $lineWidth) { $out .= str_repeat(chr(0), $padCnt); # pad line } break; case 1: # END OF FILE $padCnt = $lineWidth - strlen($out) % $lineWidth; if ($padCnt < $lineWidth) { $out .= str_repeat(chr(0), $padCnt); # pad line } break 3; case 2: # DELTA $i += 2; break; default: # ABSOLUTE MODE $num = ord($str[$i]); for ($j = 0; $j < $num; $j++) { $out .= $str[++$i]; } if ($num % 2) { $i++; } } break; default: $out .= str_repeat($str[++$i], $o); } } return $out; } /** * Decoder for RLE4 compression in windows bitmaps * see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp * * @param string $str Data to decode * @param int $width Image width * * @return string */ public static function rle4_decode($str, $width) { $w = floor($width / 2) + ($width % 2); $lineWidth = $w + (3 - (($width - 1) / 2) % 4); $pixels = []; $cnt = strlen($str); $c = 0; for ($i = 0; $i < $cnt; $i++) { $o = ord($str[$i]); switch ($o) { case 0: # ESCAPE $i++; switch (ord($str[$i])) { case 0: # NEW LINE while (count($pixels) % $lineWidth != 0) { $pixels[] = 0; } break; case 1: # END OF FILE while (count($pixels) % $lineWidth != 0) { $pixels[] = 0; } break 3; case 2: # DELTA $i += 2; break; default: # ABSOLUTE MODE $num = ord($str[$i]); for ($j = 0; $j < $num; $j++) { if ($j % 2 == 0) { $c = ord($str[++$i]); $pixels[] = ($c & 240) >> 4; } else { $pixels[] = $c & 15; } } if ($num % 2 == 0) { $i++; } } break; default: $c = ord($str[++$i]); for ($j = 0; $j < $o; $j++) { $pixels[] = ($j % 2 == 0 ? ($c & 240) >> 4 : $c & 15); } } } $out = ''; if (count($pixels) % 2) { $pixels[] = 0; } $cnt = count($pixels) / 2; for ($i = 0; $i < $cnt; $i++) { $out .= chr(16 * $pixels[2 * $i] + $pixels[2 * $i + 1]); } return $out; } /** * parse a full url or pathname and return an array(protocol, host, path, * file + query + fragment) * * @param string $url * @return array */ public static function explode_url($url) { $protocol = ""; $host = ""; $path = ""; $file = ""; $res = ""; $arr = parse_url($url); if ( isset($arr["scheme"]) ) { $arr["scheme"] = mb_strtolower($arr["scheme"]); } if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && $arr["scheme"] !== "phar" && strlen($arr["scheme"]) > 1) { $protocol = $arr["scheme"] . "://"; if (isset($arr["user"])) { $host .= $arr["user"]; if (isset($arr["pass"])) { $host .= ":" . $arr["pass"]; } $host .= "@"; } if (isset($arr["host"])) { $host .= $arr["host"]; } if (isset($arr["port"])) { $host .= ":" . $arr["port"]; } if (isset($arr["path"]) && $arr["path"] !== "") { // Do we have a trailing slash? if ($arr["path"][mb_strlen($arr["path"]) - 1] === "/") { $path = $arr["path"]; $file = ""; } else { $path = rtrim(dirname($arr["path"]), '/\\') . "/"; $file = basename($arr["path"]); } } if (isset($arr["query"])) { $file .= "?" . $arr["query"]; } if (isset($arr["fragment"])) { $file .= "#" . $arr["fragment"]; } } else { $protocol = ""; $host = ""; // localhost, really $i = mb_stripos($url, "://"); if ($i !== false) { $protocol = mb_strtolower(mb_substr($url, 0, $i + 3)); $url = mb_substr($url, $i + 3); } else { $protocol = "file://"; } if ($protocol === "phar://") { $res = substr($url, stripos($url, ".phar")+5); $url = substr($url, 7, stripos($url, ".phar")-2); } $file = basename($url); $path = dirname($url) . "/"; } $ret = [$protocol, $host, $path, $file, "protocol" => $protocol, "host" => $host, "path" => $path, "file" => $file, "resource" => $res]; return $ret; } /** * Print debug messages * * @param string $type The type of debug messages to print * @param string $msg The message to show */ public static function dompdf_debug($type, $msg) { global $_DOMPDF_DEBUG_TYPES, $_dompdf_show_warnings, $_dompdf_debug; if (isset($_DOMPDF_DEBUG_TYPES[$type]) && ($_dompdf_show_warnings || $_dompdf_debug)) { $arr = debug_backtrace(); echo basename($arr[0]["file"]) . " (" . $arr[0]["line"] . "): " . $arr[1]["function"] . ": "; Helpers::pre_r($msg); } } /** * Stores warnings in an array for display later * This function allows warnings generated by the DomDocument parser * and CSS loader ({@link Stylesheet}) to be captured and displayed * later. Without this function, errors are displayed immediately and * PDF streaming is impossible. * @see http://www.php.net/manual/en/function.set-error_handler.php * * @param int $errno * @param string $errstr * @param string $errfile * @param string $errline * * @throws Exception */ public static function record_warnings($errno, $errstr, $errfile, $errline) { // Not a warning or notice if (!($errno & (E_WARNING | E_NOTICE | E_USER_NOTICE | E_USER_WARNING | E_STRICT | E_DEPRECATED | E_USER_DEPRECATED))) { throw new Exception($errstr . " $errno"); } global $_dompdf_warnings; global $_dompdf_show_warnings; if ($_dompdf_show_warnings) { echo $errstr . "\n"; } $_dompdf_warnings[] = $errstr; } /** * @param $c * @return bool|string */ public static function unichr($c) { if ($c <= 0x7F) { return chr($c); } elseif ($c <= 0x7FF) { return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F); } elseif ($c <= 0xFFFF) { return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F); } elseif ($c <= 0x10FFFF) { return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F); } return false; } /** * Converts a CMYK color to RGB * * @param float|float[] $c * @param float $m * @param float $y * @param float $k * * @return float[] */ public static function cmyk_to_rgb($c, $m = null, $y = null, $k = null) { if (is_array($c)) { [$c, $m, $y, $k] = $c; } $c *= 255; $m *= 255; $y *= 255; $k *= 255; $r = (1 - round(2.55 * ($c + $k))); $g = (1 - round(2.55 * ($m + $k))); $b = (1 - round(2.55 * ($y + $k))); if ($r < 0) { $r = 0; } if ($g < 0) { $g = 0; } if ($b < 0) { $b = 0; } return [ $r, $g, $b, "r" => $r, "g" => $g, "b" => $b ]; } /** * getimagesize doesn't give a good size for 32bit BMP image v5 * * @param string $filename * @param resource $context * @return array An array of three elements: width and height as * `float|int`, and image type as `string|null`. */ public static function dompdf_getimagesize($filename, $context = null) { static $cache = []; if (isset($cache[$filename])) { return $cache[$filename]; } [$width, $height, $type] = getimagesize($filename); // Custom types $types = [ IMAGETYPE_JPEG => "jpeg", IMAGETYPE_GIF => "gif", IMAGETYPE_BMP => "bmp", IMAGETYPE_PNG => "png", IMAGETYPE_WEBP => "webp", ]; $type = $types[$type] ?? null; if ($width == null || $height == null) { [$data] = Helpers::getFileContent($filename, $context); if ($data !== null) { if (substr($data, 0, 2) === "BM") { $meta = unpack("vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight", $data); $width = (int) $meta["width"]; $height = (int) $meta["height"]; $type = "bmp"; } elseif (strpos($data, "loadFile($filename); [$width, $height] = $doc->getDimensions(); $width = (float) $width; $height = (float) $height; $type = "svg"; } } } return $cache[$filename] = [$width ?? 0, $height ?? 0, $type]; } /** * Credit goes to mgutt * http://www.programmierer-forum.de/function-imagecreatefrombmp-welche-variante-laeuft-t143137.htm * Modified by Fabien Menager to support RGB555 BMP format */ public static function imagecreatefrombmp($filename, $context = null) { if (!function_exists("imagecreatetruecolor")) { trigger_error("The PHP GD extension is required, but is not installed.", E_ERROR); return false; } // version 1.00 if (!($fh = fopen($filename, 'rb'))) { trigger_error('imagecreatefrombmp: Can not open ' . $filename, E_USER_WARNING); return false; } $bytes_read = 0; // read file header $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); // check for bitmap if ($meta['type'] != 19778) { trigger_error('imagecreatefrombmp: ' . $filename . ' is not a bitmap!', E_USER_WARNING); return false; } // read image header $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); $bytes_read += 40; // read additional bitfield header if ($meta['compression'] == 3) { $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); $bytes_read += 12; } // set bytes and padding $meta['bytes'] = $meta['bits'] / 8; $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); if ($meta['decal'] == 4) { $meta['decal'] = 0; } // obtain imagesize if ($meta['imagesize'] < 1) { $meta['imagesize'] = $meta['filesize'] - $meta['offset']; // in rare cases filesize is equal to offset so we need to read physical size if ($meta['imagesize'] < 1) { $meta['imagesize'] = @filesize($filename) - $meta['offset']; if ($meta['imagesize'] < 1) { trigger_error('imagecreatefrombmp: Can not obtain filesize of ' . $filename . '!', E_USER_WARNING); return false; } } } // calculate colors $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; // read color palette $palette = []; if ($meta['bits'] < 16) { $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); // in rare cases the color value is signed if ($palette[1] < 0) { foreach ($palette as $i => $color) { $palette[$i] = $color + 16777216; } } } // ignore extra bitmap headers if ($meta['headersize'] > $bytes_read) { fread($fh, $meta['headersize'] - $bytes_read); } // create gd image $im = imagecreatetruecolor($meta['width'], $meta['height']); $data = fread($fh, $meta['imagesize']); // uncompress data switch ($meta['compression']) { case 1: $data = Helpers::rle8_decode($data, $meta['width']); break; case 2: $data = Helpers::rle4_decode($data, $meta['width']); break; } $p = 0; $vide = chr(0); $y = $meta['height'] - 1; $error = 'imagecreatefrombmp: ' . $filename . ' has not enough data!'; // loop through the image data beginning with the lower left corner while ($y >= 0) { $x = 0; while ($x < $meta['width']) { switch ($meta['bits']) { case 32: case 24: if (!($part = substr($data, $p, 3 /*$meta['bytes']*/))) { trigger_error($error, E_USER_WARNING); return $im; } $color = unpack('V', $part . $vide); break; case 16: if (!($part = substr($data, $p, 2 /*$meta['bytes']*/))) { trigger_error($error, E_USER_WARNING); return $im; } $color = unpack('v', $part); if (empty($meta['rMask']) || $meta['rMask'] != 0xf800) { $color[1] = (($color[1] & 0x7c00) >> 7) * 65536 + (($color[1] & 0x03e0) >> 2) * 256 + (($color[1] & 0x001f) << 3); // 555 } else { $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); // 565 } break; case 8: $color = unpack('n', $vide . substr($data, $p, 1)); $color[1] = $palette[$color[1] + 1]; break; case 4: $color = unpack('n', $vide . substr($data, floor($p), 1)); $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; $color[1] = $palette[$color[1] + 1]; break; case 1: $color = unpack('n', $vide . substr($data, floor($p), 1)); switch (($p * 8) % 8) { case 0: $color[1] = $color[1] >> 7; break; case 1: $color[1] = ($color[1] & 0x40) >> 6; break; case 2: $color[1] = ($color[1] & 0x20) >> 5; break; case 3: $color[1] = ($color[1] & 0x10) >> 4; break; case 4: $color[1] = ($color[1] & 0x8) >> 3; break; case 5: $color[1] = ($color[1] & 0x4) >> 2; break; case 6: $color[1] = ($color[1] & 0x2) >> 1; break; case 7: $color[1] = ($color[1] & 0x1); break; } $color[1] = $palette[$color[1] + 1]; break; default: trigger_error('imagecreatefrombmp: ' . $filename . ' has ' . $meta['bits'] . ' bits and this is not supported!', E_USER_WARNING); return false; } imagesetpixel($im, $x, $y, $color[1]); $x++; $p += $meta['bytes']; } $y--; $p += $meta['decal']; } fclose($fh); return $im; } /** * Gets the content of the file at the specified path using one of * the following methods, in preferential order: * - file_get_contents: if allow_url_fopen is true or the file is local * - curl: if allow_url_fopen is false and curl is available * * @param string $uri * @param resource $context * @param int $offset * @param int $maxlen * @return string[] */ public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null) { $content = null; $headers = null; [$protocol] = Helpers::explode_url($uri); $is_local_path = in_array(strtolower($protocol), ["", "file://", "phar://"], true); $can_use_curl = in_array(strtolower($protocol), ["http://", "https://"], true); set_error_handler([self::class, 'record_warnings']); try { if ($is_local_path || ini_get('allow_url_fopen') || !$can_use_curl) { if ($is_local_path === false) { $uri = Helpers::encodeURI($uri); } if (isset($maxlen)) { $result = file_get_contents($uri, false, $context, $offset, $maxlen); } else { $result = file_get_contents($uri, false, $context, $offset); } if ($result !== false) { $content = $result; } if (isset($http_response_header)) { $headers = $http_response_header; } } elseif ($can_use_curl && function_exists('curl_exec')) { $curl = curl_init($uri); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HEADER, true); if ($offset > 0) { curl_setopt($curl, CURLOPT_RESUME_FROM, $offset); } if ($maxlen > 0) { curl_setopt($curl, CURLOPT_BUFFERSIZE, 128); curl_setopt($curl, CURLOPT_NOPROGRESS, false); curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, function ($res, $download_size_total, $download_size, $upload_size_total, $upload_size) use ($maxlen) { return ($download_size > $maxlen) ? 1 : 0; }); } $context_options = []; if (!is_null($context)) { $context_options = stream_context_get_options($context); } foreach ($context_options as $stream => $options) { foreach ($options as $option => $value) { $key = strtolower($stream) . ":" . strtolower($option); switch ($key) { case "curl:curl_verify_ssl_host": curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, !$value ? 0 : 2); break; case "curl:max_redirects": curl_setopt($curl, CURLOPT_MAXREDIRS, $value); break; case "http:follow_location": curl_setopt($curl, CURLOPT_FOLLOWLOCATION, $value); break; case "http:header": if (is_string($value)) { curl_setopt($curl, CURLOPT_HTTPHEADER, [$value]); } else { curl_setopt($curl, CURLOPT_HTTPHEADER, $value); } break; case "http:timeout": curl_setopt($curl, CURLOPT_TIMEOUT, $value); break; case "http:user_agent": curl_setopt($curl, CURLOPT_USERAGENT, $value); break; case "curl:curl_verify_ssl_peer": case "ssl:verify_peer": curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $value); break; } } } $data = curl_exec($curl); if ($data !== false && !curl_errno($curl)) { switch ($http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE)) { case 200: $raw_headers = substr($data, 0, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); $headers = preg_split("/[\n\r]+/", trim($raw_headers)); $content = substr($data, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); break; } } curl_close($curl); } } finally { restore_error_handler(); } return [$content, $headers]; } /** * @param string $str * @return string */ public static function mb_ucwords(string $str): string { $max_len = mb_strlen($str); if ($max_len === 1) { return mb_strtoupper($str); } $str = mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 1); foreach ([' ', '.', ',', '!', '?', '-', '+'] as $s) { $pos = 0; while (($pos = mb_strpos($str, $s, $pos)) !== false) { $pos++; // Nothing to do if the separator is the last char of the string if ($pos !== false && $pos < $max_len) { // If the char we want to upper is the last char there is nothing to append behind if ($pos + 1 < $max_len) { $str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1)) . mb_substr($str, $pos + 1); } else { $str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1)); } } } } return $str; } /** * Check whether two lengths should be considered equal, accounting for * inaccuracies in float computation. * * The implementation relies on the fact that we are neither dealing with * very large, nor with very small numbers in layout. Adapted from * https://floating-point-gui.de/errors/comparison/. * * @param float $a * @param float $b * * @return bool */ public static function lengthEqual(float $a, float $b): bool { // The epsilon results in a precision of at least: // * 7 decimal digits at around 1 // * 4 decimal digits at around 1000 (around the size of common paper formats) // * 2 decimal digits at around 100,000 (100,000pt ~ 35.28m) static $epsilon = 1e-8; static $almostZero = 1e-12; $diff = abs($a - $b); if ($a === $b || $diff < $almostZero) { return true; } return $diff < $epsilon * max(abs($a), abs($b)); } /** * Check `$a < $b`, accounting for inaccuracies in float computation. */ public static function lengthLess(float $a, float $b): bool { return $a < $b && !self::lengthEqual($a, $b); } /** * Check `$a <= $b`, accounting for inaccuracies in float computation. */ public static function lengthLessOrEqual(float $a, float $b): bool { return $a <= $b || self::lengthEqual($a, $b); } /** * Check `$a > $b`, accounting for inaccuracies in float computation. */ public static function lengthGreater(float $a, float $b): bool { return $a > $b && !self::lengthEqual($a, $b); } /** * Check `$a >= $b`, accounting for inaccuracies in float computation. */ public static function lengthGreaterOrEqual(float $a, float $b): bool { return $a >= $b || self::lengthEqual($a, $b); } } PKZ \ʠ!src/Renderer/AbstractRenderer.phpnuW+A_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]; } } PKZIpPPsrc/Renderer/TableRowGroup.phpnuW+Aget_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"); } } PKZj*|src/Renderer/ListBullet.phpnuW+Aget_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); } } } PKZx@@src/Renderer/Text.phpnuW+A_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]); } } } PKZQgsrc/Renderer/Inline.phpnuW+Aget_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]; } } PKZm4t t src/Renderer/Block.phpnuW+Aget_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"); } } } } PKZC D D src/Renderer/Image.phpnuW+Aget_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"); } } PKZXosrc/Renderer/TableCell.phpnuW+Aget_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"); } } } } PKZU>cnnsrc/Cellmap.phpnuW+A 8, "solid" => 7, "dashed" => 6, "dotted" => 5, "ridge" => 4, "outset" => 3, "groove" => 2, "inset" => 1, "none" => 0 ]; /** * The table object this cellmap is attached to. * * @var TableFrameDecorator */ protected $_table; /** * The total number of rows in the table * * @var int */ protected $_num_rows; /** * The total number of columns in the table * * @var int */ protected $_num_cols; /** * 2D array mapping to frames * * @var Frame[][] */ protected $_cells; /** * 1D array of column dimensions * * @var array */ protected $_columns; /** * 1D array of row dimensions * * @var array */ protected $_rows; /** * 2D array of border specs * * @var array */ protected $_borders; /** * 1D Array mapping frames to (multiple) pairs, keyed on frame_id. * * @var array[] */ protected $_frames; /** * Current column when adding cells, 0-based * * @var int */ private $__col; /** * Current row when adding cells, 0-based * * @var int */ private $__row; /** * Tells whether the columns' width can be modified * * @var bool */ private $_columns_locked = false; /** * Tells whether the table has table-layout:fixed * * @var bool */ private $_fixed_layout = false; /** * @param TableFrameDecorator $table */ public function __construct(TableFrameDecorator $table) { $this->_table = $table; $this->reset(); } public function reset(): void { $this->_num_rows = 0; $this->_num_cols = 0; $this->_cells = []; $this->_frames = []; if (!$this->_columns_locked) { $this->_columns = []; } $this->_rows = []; $this->_borders = []; $this->__col = $this->__row = 0; } public function lock_columns(): void { $this->_columns_locked = true; } /** * @return bool */ public function is_columns_locked() { return $this->_columns_locked; } /** * @param bool $fixed */ public function set_layout_fixed(bool $fixed) { $this->_fixed_layout = $fixed; } /** * @return bool */ public function is_layout_fixed() { return $this->_fixed_layout; } /** * @return int */ public function get_num_rows() { return $this->_num_rows; } /** * @return int */ public function get_num_cols() { return $this->_num_cols; } /** * @return array */ public function &get_columns() { return $this->_columns; } /** * @param $columns */ public function set_columns($columns) { $this->_columns = $columns; } /** * @param int $i * * @return mixed */ public function &get_column($i) { if (!isset($this->_columns[$i])) { $this->_columns[$i] = [ "x" => 0, "min-width" => 0, "max-width" => 0, "used-width" => null, "absolute" => 0, "percent" => 0, "auto" => true, ]; } return $this->_columns[$i]; } /** * @return array */ public function &get_rows() { return $this->_rows; } /** * @param int $j * * @return mixed */ public function &get_row($j) { if (!isset($this->_rows[$j])) { $this->_rows[$j] = [ "y" => 0, "first-column" => 0, "height" => null, ]; } return $this->_rows[$j]; } /** * @param int $i * @param int $j * @param mixed $h_v * @param null|mixed $prop * * @return mixed */ public function get_border($i, $j, $h_v, $prop = null) { if (!isset($this->_borders[$i][$j][$h_v])) { $this->_borders[$i][$j][$h_v] = [ "width" => 0, "style" => "solid", "color" => "black", ]; } if (isset($prop)) { return $this->_borders[$i][$j][$h_v][$prop]; } return $this->_borders[$i][$j][$h_v]; } /** * @param int $i * @param int $j * * @return array */ public function get_border_properties($i, $j) { return [ "top" => $this->get_border($i, $j, "horizontal"), "right" => $this->get_border($i, $j + 1, "vertical"), "bottom" => $this->get_border($i + 1, $j, "horizontal"), "left" => $this->get_border($i, $j, "vertical"), ]; } /** * @param Frame $frame * * @return array|null */ public function get_spanned_cells(Frame $frame) { $key = $frame->get_id(); if (isset($this->_frames[$key])) { return $this->_frames[$key]; } return null; } /** * @param Frame $frame * * @return bool */ public function frame_exists_in_cellmap(Frame $frame) { $key = $frame->get_id(); return isset($this->_frames[$key]); } /** * @param Frame $frame * * @return array * @throws Exception */ public function get_frame_position(Frame $frame) { global $_dompdf_warnings; $key = $frame->get_id(); if (!isset($this->_frames[$key])) { throw new Exception("Frame not found in cellmap"); } // Positions are stored relative to the table position [$table_x, $table_y] = $this->_table->get_position(); $col = $this->_frames[$key]["columns"][0]; $row = $this->_frames[$key]["rows"][0]; if (!isset($this->_columns[$col])) { $_dompdf_warnings[] = "Frame not found in columns array. Check your table layout for missing or extra TDs."; $x = $table_x; } else { $x = $table_x + $this->_columns[$col]["x"]; } if (!isset($this->_rows[$row])) { $_dompdf_warnings[] = "Frame not found in row array. Check your table layout for missing or extra TDs."; $y = $table_y; } else { $y = $table_y + $this->_rows[$row]["y"]; } return [$x, $y, "x" => $x, "y" => $y]; } /** * @param Frame $frame * * @return int * @throws Exception */ public function get_frame_width(Frame $frame) { $key = $frame->get_id(); if (!isset($this->_frames[$key])) { throw new Exception("Frame not found in cellmap"); } $cols = $this->_frames[$key]["columns"]; $w = 0; foreach ($cols as $i) { $w += $this->_columns[$i]["used-width"]; } return $w; } /** * @param Frame $frame * * @return int * @throws Exception * @throws Exception */ public function get_frame_height(Frame $frame) { $key = $frame->get_id(); if (!isset($this->_frames[$key])) { throw new Exception("Frame not found in cellmap"); } $rows = $this->_frames[$key]["rows"]; $h = 0; foreach ($rows as $i) { if (!isset($this->_rows[$i])) { throw new Exception("The row #$i could not be found, please file an issue in the tracker with the HTML code"); } $h += $this->_rows[$i]["height"]; } return $h; } /** * @param int $j * @param mixed $width */ public function set_column_width($j, $width) { if ($this->_columns_locked) { return; } $col =& $this->get_column($j); $col["used-width"] = $width; $next_col =& $this->get_column($j + 1); $next_col["x"] = $col["x"] + $width; } /** * @param int $i * @param long $height */ public function set_row_height($i, $height) { $row =& $this->get_row($i); if ($height > $row["height"]) { $row["height"] = $height; } $next_row =& $this->get_row($i + 1); $next_row["y"] = $row["y"] + $row["height"]; } /** * https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution * * @param int $i * @param int $j * @param string $h_v `horizontal` or `vertical` * @param array $border_spec */ protected function resolve_border(int $i, int $j, string $h_v, array $border_spec): void { if (!isset($this->_borders[$i][$j][$h_v])) { $this->_borders[$i][$j][$h_v] = $border_spec; return; } $border = $this->_borders[$i][$j][$h_v]; $n_width = $border_spec["width"]; $n_style = $border_spec["style"]; $o_width = $border["width"]; $o_style = $border["style"]; if ($o_style === "hidden") { return; } // A style of `none` has lowest priority independent of its specified // width here, as its resolved width is always 0 if ($n_style === "hidden" || $n_width > $o_width || ($o_width == $n_width && isset(self::BORDER_STYLE_SCORE[$n_style]) && isset(self::BORDER_STYLE_SCORE[$o_style]) && self::BORDER_STYLE_SCORE[$n_style] > self::BORDER_STYLE_SCORE[$o_style]) ) { $this->_borders[$i][$j][$h_v] = $border_spec; } } /** * Get the resolved border properties for the given frame. * * @param AbstractFrameDecorator $frame * * @return array[] */ protected function get_resolved_border(AbstractFrameDecorator $frame): array { $key = $frame->get_id(); $columns = $this->_frames[$key]["columns"]; $rows = $this->_frames[$key]["rows"]; $first_col = $columns[0]; $last_col = $columns[count($columns) - 1]; $first_row = $rows[0]; $last_row = $rows[count($rows) - 1]; $max_top = null; $max_bottom = null; $max_left = null; $max_right = null; foreach ($columns as $col) { $top = $this->_borders[$first_row][$col]["horizontal"]; $bottom = $this->_borders[$last_row + 1][$col]["horizontal"]; if ($max_top === null || $top["width"] > $max_top["width"]) { $max_top = $top; } if ($max_bottom === null || $bottom["width"] > $max_bottom["width"]) { $max_bottom = $bottom; } } foreach ($rows as $row) { $left = $this->_borders[$row][$first_col]["vertical"]; $right = $this->_borders[$row][$last_col + 1]["vertical"]; if ($max_left === null || $left["width"] > $max_left["width"]) { $max_left = $left; } if ($max_right === null || $right["width"] > $max_right["width"]) { $max_right = $right; } } return [$max_top, $max_right, $max_bottom, $max_left]; } /** * @param AbstractFrameDecorator $frame */ public function add_frame(Frame $frame): void { $style = $frame->get_style(); $display = $style->display; $collapse = $this->_table->get_style()->border_collapse === "collapse"; // Recursively add the frames within the table, its row groups and rows if ($frame === $this->_table || $display === "table-row" || in_array($display, TableFrameDecorator::ROW_GROUPS, true) ) { $start_row = $this->__row; foreach ($frame->get_children() as $child) { $this->add_frame($child); } if ($display === "table-row") { $this->add_row(); } $num_rows = $this->__row - $start_row - 1; $key = $frame->get_id(); // Row groups always span across the entire table $this->_frames[$key]["columns"] = range(0, max(0, $this->_num_cols - 1)); $this->_frames[$key]["rows"] = range($start_row, max(0, $this->__row - 1)); $this->_frames[$key]["frame"] = $frame; if ($collapse) { $bp = $style->get_border_properties(); // Resolve vertical borders for ($i = 0; $i < $num_rows + 1; $i++) { $this->resolve_border($start_row + $i, 0, "vertical", $bp["left"]); $this->resolve_border($start_row + $i, $this->_num_cols, "vertical", $bp["right"]); } // Resolve horizontal borders for ($j = 0; $j < $this->_num_cols; $j++) { $this->resolve_border($start_row, $j, "horizontal", $bp["top"]); $this->resolve_border($this->__row, $j, "horizontal", $bp["bottom"]); } if ($frame === $this->_table) { // Clear borders because the cells are now using them. The // border width still needs to be set to half the resolved // width so that the table is positioned properly [$top, $right, $bottom, $left] = $this->get_resolved_border($frame); $style->set_used("border_top_width", $top["width"] / 2); $style->set_used("border_right_width", $right["width"] / 2); $style->set_used("border_bottom_width", $bottom["width"] / 2); $style->set_used("border_left_width", $left["width"] / 2); $style->set_used("border_style", "none"); } else { // Clear borders for rows and row groups $style->set_used("border_width", 0); $style->set_used("border_style", "none"); } } if ($frame === $this->_table) { // Apply resolved borders to table cells and calculate column // widths after all frames have been added $this->calculate_column_widths(); } return; } // Add the frame to the cellmap $key = $frame->get_id(); $node = $frame->get_node(); $bp = $style->get_border_properties(); // Determine where this cell is going $colspan = max((int) $node->getAttribute("colspan"), 1); $rowspan = max((int) $node->getAttribute("rowspan"), 1); // Find the next available column (fix by Ciro Mondueri) $ac = $this->__col; while (isset($this->_cells[$this->__row][$ac])) { $ac++; } $this->__col = $ac; // Rows: for ($i = 0; $i < $rowspan; $i++) { $row = $this->__row + $i; $this->_frames[$key]["rows"][] = $row; for ($j = 0; $j < $colspan; $j++) { $this->_cells[$row][$this->__col + $j] = $frame; } if ($collapse) { // Resolve vertical borders $this->resolve_border($row, $this->__col, "vertical", $bp["left"]); $this->resolve_border($row, $this->__col + $colspan, "vertical", $bp["right"]); } } // Columns: for ($j = 0; $j < $colspan; $j++) { $col = $this->__col + $j; $this->_frames[$key]["columns"][] = $col; if ($collapse) { // Resolve horizontal borders $this->resolve_border($this->__row, $col, "horizontal", $bp["top"]); $this->resolve_border($this->__row + $rowspan, $col, "horizontal", $bp["bottom"]); } } $this->_frames[$key]["frame"] = $frame; $this->__col += $colspan; if ($this->__col > $this->_num_cols) { $this->_num_cols = $this->__col; } } /** * Apply resolved borders to table cells and calculate column widths. */ protected function calculate_column_widths(): void { $table = $this->_table; $table_style = $table->get_style(); $collapse = $table_style->border_collapse === "collapse"; if ($collapse) { $v_spacing = 0; $h_spacing = 0; } else { // The additional 1/2 width gets added to the table proper [$h, $v] = $table_style->border_spacing; $v_spacing = $v / 2; $h_spacing = $h / 2; } foreach ($this->_frames as $frame_info) { /** @var TableCellFrameDecorator */ $frame = $frame_info["frame"]; $style = $frame->get_style(); $display = $style->display; if ($display !== "table-cell") { continue; } if ($collapse) { // Set the resolved border at half width [$top, $right, $bottom, $left] = $this->get_resolved_border($frame); $style->set_used("border_top_width", $top["width"] / 2); $style->set_used("border_top_style", $top["style"]); $style->set_used("border_top_color", $top["color"]); $style->set_used("border_right_width", $right["width"] / 2); $style->set_used("border_right_style", $right["style"]); $style->set_used("border_right_color", $right["color"]); $style->set_used("border_bottom_width", $bottom["width"] / 2); $style->set_used("border_bottom_style", $bottom["style"]); $style->set_used("border_bottom_color", $bottom["color"]); $style->set_used("border_left_width", $left["width"] / 2); $style->set_used("border_left_style", $left["style"]); $style->set_used("border_left_color", $left["color"]); $style->set_used("margin", 0); } else { // Border spacing is effectively a margin between cells $style->set_used("margin_top", $v_spacing); $style->set_used("margin_bottom", $v_spacing); $style->set_used("margin_left", $h_spacing); $style->set_used("margin_right", $h_spacing); } if ($this->_columns_locked) { continue; } $node = $frame->get_node(); $colspan = max((int) $node->getAttribute("colspan"), 1); $first_col = $frame_info["columns"][0]; // Resolve the frame's width if ($this->_fixed_layout) { list($frame_min, $frame_max) = [0, 10e-10]; } else { list($frame_min, $frame_max) = $frame->get_min_max_width(); } $width = $style->width; $val = null; if (Helpers::is_percent($width) && $colspan === 1) { $var = "percent"; $val = (float)rtrim($width, "% "); } elseif ($width !== "auto" && $colspan === 1) { $var = "absolute"; $val = $frame_min; } $min = 0; $max = 0; for ($cs = 0; $cs < $colspan; $cs++) { // Resolve the frame's width(s) with other cells $col =& $this->get_column($first_col + $cs); // Note: $var is either 'percent' or 'absolute'. We compare the // requested percentage or absolute values with the existing widths // and adjust accordingly. if (isset($var) && $val > $col[$var]) { $col[$var] = $val; $col["auto"] = false; } $min += $col["min-width"]; $max += $col["max-width"]; } if ($frame_min > $min && $colspan === 1) { // The frame needs more space. Expand each sub-column // FIXME try to avoid putting this dummy value when table-layout:fixed $inc = ($this->is_layout_fixed() ? 10e-10 : ($frame_min - $min)); for ($c = 0; $c < $colspan; $c++) { $col =& $this->get_column($first_col + $c); $col["min-width"] += $inc; } } if ($frame_max > $max) { // FIXME try to avoid putting this dummy value when table-layout:fixed $inc = ($this->is_layout_fixed() ? 10e-10 : ($frame_max - $max) / $colspan); for ($c = 0; $c < $colspan; $c++) { $col =& $this->get_column($first_col + $c); $col["max-width"] += $inc; } } } // Adjust absolute columns so that the absolute (and max) width is the // largest minimum width of all cells. This accounts for cells without // absolute width within an absolute column foreach ($this->_columns as &$col) { if ($col["absolute"] > 0) { $col["absolute"] = $col["min-width"]; $col["max-width"] = $col["min-width"]; } } } protected function add_row(): void { $this->__row++; $this->_num_rows++; // Find the next available column $i = 0; while (isset($this->_cells[$this->__row][$i])) { $i++; } $this->__col = $i; } /** * Remove a row from the cellmap. * * @param Frame */ public function remove_row(Frame $row) { $key = $row->get_id(); if (!isset($this->_frames[$key])) { return; // Presumably this row has already been removed } $this->__row = $this->_num_rows--; $rows = $this->_frames[$key]["rows"]; $columns = $this->_frames[$key]["columns"]; // Remove all frames from this row foreach ($rows as $r) { foreach ($columns as $c) { if (isset($this->_cells[$r][$c])) { $id = $this->_cells[$r][$c]->get_id(); $this->_cells[$r][$c] = null; unset($this->_cells[$r][$c]); // has multiple rows? if (isset($this->_frames[$id]) && count($this->_frames[$id]["rows"]) > 1) { // remove just the desired row, but leave the frame if (($row_key = array_search($r, $this->_frames[$id]["rows"])) !== false) { unset($this->_frames[$id]["rows"][$row_key]); } continue; } $this->_frames[$id] = null; unset($this->_frames[$id]); } } $this->_rows[$r] = null; unset($this->_rows[$r]); } $this->_frames[$key] = null; unset($this->_frames[$key]); } /** * Remove a row group from the cellmap. * * @param Frame $group The group to remove */ public function remove_row_group(Frame $group) { $key = $group->get_id(); if (!isset($this->_frames[$key])) { return; // Presumably this row has already been removed } $iter = $group->get_first_child(); while ($iter) { $this->remove_row($iter); $iter = $iter->get_next_sibling(); } $this->_frames[$key] = null; unset($this->_frames[$key]); } /** * Update a row group after rows have been removed * * @param Frame $group The group to update * @param Frame $last_row The last row in the row group */ public function update_row_group(Frame $group, Frame $last_row) { $g_key = $group->get_id(); $first_index = $this->_frames[$g_key]["rows"][0]; $last_index = $first_index; $row = $last_row; while ($row = $row->get_prev_sibling()) { $last_index++; } $this->_frames[$g_key]["rows"] = range($first_index, $last_index); } public function assign_x_positions(): void { // Pre-condition: widths must be resolved and assigned to columns and // column[0]["x"] must be set. if ($this->_columns_locked) { return; } $x = $this->_columns[0]["x"]; foreach (array_keys($this->_columns) as $j) { $this->_columns[$j]["x"] = $x; $x += $this->_columns[$j]["used-width"]; } } public function assign_frame_heights(): void { // Pre-condition: widths and heights of each column & row must be // calcluated foreach ($this->_frames as $arr) { $frame = $arr["frame"]; $h = 0.0; foreach ($arr["rows"] as $row) { if (!isset($this->_rows[$row])) { // The row has been removed because of a page split, so skip it. continue; } $h += $this->_rows[$row]["height"]; } if ($frame instanceof TableCellFrameDecorator) { $frame->set_cell_height($h); } else { $frame->get_style()->set_used("height", $h); } } } /** * Re-adjust frame height if the table height is larger than its content */ public function set_frame_heights(float $table_height, float $content_height): void { // Distribute the increased height proportionally amongst each row foreach ($this->_frames as $arr) { $frame = $arr["frame"]; $h = 0.0; foreach ($arr["rows"] as $row) { if (!isset($this->_rows[$row])) { continue; } $h += $this->_rows[$row]["height"]; } if ($content_height > 0) { $new_height = ($h / $content_height) * $table_height; } else { $new_height = 0.0; } if ($frame instanceof TableCellFrameDecorator) { $frame->set_cell_height($new_height); } else { $frame->get_style()->set_used("height", $new_height); } } } /** * Used for debugging: * * @return string */ public function __toString(): string { $str = ""; $str .= "Columns:
"; $str .= Helpers::pre_r($this->_columns, true); $str .= "Rows:
"; $str .= Helpers::pre_r($this->_rows, true); $str .= "Frames:
"; $arr = []; foreach ($this->_frames as $key => $val) { $arr[$key] = ["columns" => $val["columns"], "rows" => $val["rows"]]; } $str .= Helpers::pre_r($arr, true); if (php_sapi_name() == "cli") { $str = strip_tags(str_replace(["
", "", ""], ["\n", chr(27) . "[01;33m", chr(27) . "[0m"], $str)); } return $str; } } PKZY9))src/Css/Color.phpnuW+A "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; } } PKZ@Osrc/Css/Stylesheet.phpnuW+A 0x00000000, // user agent declarations self::ORIG_USER => 0x10000000, // user normal declarations self::ORIG_AUTHOR => 0x30000000, // author normal declarations ]; /** * Non-CSS presentational hints (i.e. HTML 4 attributes) are handled as if added * to the beginning of an author stylesheet, i.e. anything in author stylesheets * should override them. */ const SPEC_NON_CSS = 0x20000000; /** * Current dompdf instance * * @var Dompdf */ private $_dompdf; /** * Array of currently defined styles * * @var Style[][] */ private $_styles; /** * Base protocol of the document being parsed * Used to handle relative urls. * * @var string */ private $_protocol = ""; /** * Base hostname of the document being parsed * Used to handle relative urls. * * @var string */ private $_base_host = ""; /** * Base path of the document being parsed * Used to handle relative urls. * * @var string */ private $_base_path = ""; /** * The styles defined by @page rules * * @var array $child = $child->nextSibling; } } else { $css = $tag->nodeValue; } // Set the base path of the Stylesheet to that of the file being processed $this->css->set_protocol($this->protocol); $this->css->set_host($this->baseHost); $this->css->set_base_path($this->basePath); $this->css->load_css($css, Stylesheet::ORIG_AUTHOR); break; } // Set the base path of the Stylesheet to that of the file being processed $this->css->set_protocol($this->protocol); $this->css->set_host($this->baseHost); $this->css->set_base_path($this->basePath); } } /** * @param string $cacheId * @deprecated */ public function enable_caching($cacheId) { $this->enableCaching($cacheId); } /** * Enable experimental caching capability * * @param string $cacheId */ public function enableCaching($cacheId) { $this->cacheId = $cacheId; } /** * @param string $value * @return bool * @deprecated */ public function parse_default_view($value) { return $this->parseDefaultView($value); } /** * @param string $value * @return bool */ public function parseDefaultView($value) { $valid = ["XYZ", "Fit", "FitH", "FitV", "FitR", "FitB", "FitBH", "FitBV"]; $options = preg_split("/\s*,\s*/", trim($value)); $defaultView = array_shift($options); if (!in_array($defaultView, $valid)) { return false; } $this->setDefaultView($defaultView, $options); return true; } /** * Renders the HTML to PDF */ public function render() { $this->setPhpConfig(); $logOutputFile = $this->options->getLogOutputFile(); if ($logOutputFile) { if (!file_exists($logOutputFile) && is_writable(dirname($logOutputFile))) { touch($logOutputFile); } $startTime = microtime(true); if (is_writable($logOutputFile)) { ob_start(); } } $this->processHtml(); $this->css->apply_styles($this->tree); // @page style rules : size, margins $pageStyles = $this->css->get_page_styles(); $basePageStyle = $pageStyles["base"]; unset($pageStyles["base"]); foreach ($pageStyles as $pageStyle) { $pageStyle->inherit($basePageStyle); } // Set paper size if defined via CSS if (is_array($basePageStyle->size)) { [$width, $height] = $basePageStyle->size; $this->setPaper([0, 0, $width, $height]); } // Create a new canvas instance if the current one does not match the // desired paper size $canvasWidth = $this->canvas->get_width(); $canvasHeight = $this->canvas->get_height(); $size = $this->getPaperSize(); if ($canvasWidth !== $size[2] || $canvasHeight !== $size[3]) { $this->canvas = CanvasFactory::get_instance($this, $this->paperSize, $this->paperOrientation); $this->fontMetrics->setCanvas($this->canvas); } $canvas = $this->canvas; $root_frame = $this->tree->get_root(); $root = Factory::decorate_root($root_frame, $this); foreach ($this->tree as $frame) { if ($frame === $root_frame) { continue; } Factory::decorate_frame($frame, $this, $root); } // Add meta information $title = $this->dom->getElementsByTagName("title"); if ($title->length) { $canvas->add_info("Title", trim($title->item(0)->nodeValue)); } $metas = $this->dom->getElementsByTagName("meta"); $labels = [ "author" => "Author", "keywords" => "Keywords", "description" => "Subject", ]; /** @var \DOMElement $meta */ foreach ($metas as $meta) { $name = mb_strtolower($meta->getAttribute("name")); $value = trim($meta->getAttribute("content")); if (isset($labels[$name])) { $canvas->add_info($labels[$name], $value); continue; } if ($name === "dompdf.view" && $this->parseDefaultView($value)) { $canvas->set_default_view($this->defaultView, $this->defaultViewOptions); } } $root->set_containing_block(0, 0, $canvas->get_width(), $canvas->get_height()); $root->set_renderer(new Renderer($this)); // This is where the magic happens: $root->reflow(); if (isset($this->callbacks["end_document"])) { $fs = $this->callbacks["end_document"]; foreach ($fs as $f) { $canvas->page_script($f); } } // Clean up cached images if (!$this->options->getDebugKeepTemp()) { Cache::clear($this->options->getDebugPng()); } global $_dompdf_warnings, $_dompdf_show_warnings; if ($_dompdf_show_warnings && isset($_dompdf_warnings)) { echo 'Dompdf Warnings
';
            foreach ($_dompdf_warnings as $msg) {
                echo $msg . "\n";
            }

            if ($canvas instanceof CPDF) {
                echo $canvas->get_cpdf()->messages;
            }
            echo '
'; flush(); } if ($logOutputFile && is_writable($logOutputFile)) { $this->writeLog($logOutputFile, $startTime); ob_end_clean(); } $this->restorePhpConfig(); } /** * Writes the output buffer in the log file * * @param string $logOutputFile * @param float $startTime */ private function writeLog(string $logOutputFile, float $startTime): void { $frames = Frame::$ID_COUNTER; $memory = memory_get_peak_usage(true) / 1024; $time = (microtime(true) - $startTime) * 1000; $out = sprintf( "%6d" . "%10.2f KB" . "%10.2f ms" . " " . ($this->quirksmode ? " ON" : "OFF") . "
", $frames, $memory, $time); $out .= ob_get_contents(); ob_clean(); file_put_contents($logOutputFile, $out); } /** * Add meta information to the PDF after rendering. * * @deprecated */ public function add_info($label, $value) { $this->addInfo($label, $value); } /** * Add meta information to the PDF after rendering. * * @param string $label Label of the value (Creator, Producer, etc.) * @param string $value The text to set */ public function addInfo(string $label, string $value): void { $this->canvas->add_info($label, $value); } /** * Streams the PDF to the client. * * The file will open a download dialog by default. The options * parameter controls the output. Accepted options (array keys) are: * * 'compress' = > 1 (=default) or 0: * Apply content stream compression * * 'Attachment' => 1 (=default) or 0: * Set the 'Content-Disposition:' HTTP header to 'attachment' * (thereby causing the browser to open a download dialog) * * @param string $filename the name of the streamed file * @param array $options header options (see above) */ public function stream($filename = "document.pdf", $options = []) { $this->setPhpConfig(); $this->canvas->stream($filename, $options); $this->restorePhpConfig(); } /** * Returns the PDF as a string. * * The options parameter controls the output. Accepted options are: * * 'compress' = > 1 or 0 - apply content stream compression, this is * on (1) by default * * @param array $options options (see above) * * @return string|null */ public function output($options = []) { $this->setPhpConfig(); $output = $this->canvas->output($options); $this->restorePhpConfig(); return $output; } /** * @return string * @deprecated */ public function output_html() { return $this->outputHtml(); } /** * Returns the underlying HTML document as a string * * @return string */ public function outputHtml() { return $this->dom->saveHTML(); } /** * Get the dompdf option value * * @param string $key * @return mixed * @deprecated */ public function get_option($key) { return $this->options->get($key); } /** * @param string $key * @param mixed $value * @return $this * @deprecated */ public function set_option($key, $value) { $this->options->set($key, $value); return $this; } /** * @param array $options * @return $this * @deprecated */ public function set_options(array $options) { $this->options->set($options); return $this; } /** * @param string $size * @param string $orientation * @deprecated */ public function set_paper($size, $orientation = "portrait") { $this->setPaper($size, $orientation); } /** * Sets the paper size & orientation * * @param string|float[] $size 'letter', 'legal', 'A4', etc. {@link Dompdf\Adapter\CPDF::$PAPER_SIZES} * @param string $orientation 'portrait' or 'landscape' * @return $this */ public function setPaper($size, string $orientation = "portrait"): self { $this->paperSize = $size; $this->paperOrientation = $orientation; return $this; } /** * Gets the paper size * * @return float[] A four-element float array */ public function getPaperSize(): array { $paper = $this->paperSize; $orientation = $this->paperOrientation; if (is_array($paper)) { $size = array_map("floatval", $paper); } else { $paper = strtolower($paper); $size = CPDF::$PAPER_SIZES[$paper] ?? CPDF::$PAPER_SIZES["letter"]; } if (strtolower($orientation) === "landscape") { [$size[2], $size[3]] = [$size[3], $size[2]]; } return $size; } /** * Gets the paper orientation * * @return string Either "portrait" or "landscape" */ public function getPaperOrientation(): string { return $this->paperOrientation; } /** * @param FrameTree $tree * @return $this */ public function setTree(FrameTree $tree) { $this->tree = $tree; return $this; } /** * @return FrameTree * @deprecated */ public function get_tree() { return $this->getTree(); } /** * Returns the underlying {@link FrameTree} object * * @return FrameTree */ public function getTree() { return $this->tree; } /** * @param string $protocol * @return $this * @deprecated */ public function set_protocol($protocol) { return $this->setProtocol($protocol); } /** * Sets the protocol to use * FIXME validate these * * @param string $protocol * @return $this */ public function setProtocol(string $protocol) { $this->protocol = $protocol; return $this; } /** * @return string * @deprecated */ public function get_protocol() { return $this->getProtocol(); } /** * Returns the protocol in use * * @return string */ public function getProtocol() { return $this->protocol; } /** * @param string $host * @deprecated */ public function set_host($host) { $this->setBaseHost($host); } /** * Sets the base hostname * * @param string $baseHost * @return $this */ public function setBaseHost(string $baseHost) { $this->baseHost = $baseHost; return $this; } /** * @return string * @deprecated */ public function get_host() { return $this->getBaseHost(); } /** * Returns the base hostname * * @return string */ public function getBaseHost() { return $this->baseHost; } /** * Sets the base path * * @param string $path * @deprecated */ public function set_base_path($path) { $this->setBasePath($path); } /** * Sets the base path * * @param string $basePath * @return $this */ public function setBasePath(string $basePath) { $this->basePath = $basePath; return $this; } /** * @return string * @deprecated */ public function get_base_path() { return $this->getBasePath(); } /** * Returns the base path * * @return string */ public function getBasePath() { return $this->basePath; } /** * @param string $default_view The default document view * @param array $options The view's options * @return $this * @deprecated */ public function set_default_view($default_view, $options) { return $this->setDefaultView($default_view, $options); } /** * Sets the default view * * @param string $defaultView The default document view * @param array $options The view's options * @return $this */ public function setDefaultView($defaultView, $options) { $this->defaultView = $defaultView; $this->defaultViewOptions = $options; return $this; } /** * @param resource $http_context * @return $this * @deprecated */ public function set_http_context($http_context) { return $this->setHttpContext($http_context); } /** * Sets the HTTP context * * @param resource|array $httpContext * @return $this */ public function setHttpContext($httpContext) { $this->options->setHttpContext($httpContext); return $this; } /** * @return resource * @deprecated */ public function get_http_context() { return $this->getHttpContext(); } /** * Returns the HTTP context * * @return resource */ public function getHttpContext() { return $this->options->getHttpContext(); } /** * Set a custom `Canvas` instance to render the document to. * * Be aware that the instance will be replaced on render if the document * defines a paper size different from the canvas. * * @param Canvas $canvas * @return $this */ public function setCanvas(Canvas $canvas) { $this->canvas = $canvas; return $this; } /** * @return Canvas * @deprecated */ public function get_canvas() { return $this->getCanvas(); } /** * Return the underlying Canvas instance (e.g. Dompdf\Adapter\CPDF, Dompdf\Adapter\GD) * * @return Canvas */ public function getCanvas() { return $this->canvas; } /** * @param Stylesheet $css * @return $this */ public function setCss(Stylesheet $css) { $this->css = $css; return $this; } /** * @return Stylesheet * @deprecated */ public function get_css() { return $this->getCss(); } /** * Returns the stylesheet * * @return Stylesheet */ public function getCss() { return $this->css; } /** * @param DOMDocument $dom * @return $this */ public function setDom(DOMDocument $dom) { $this->dom = $dom; return $this; } /** * @return DOMDocument * @deprecated */ public function get_dom() { return $this->getDom(); } /** * @return DOMDocument */ public function getDom() { return $this->dom; } /** * @param Options $options * @return $this */ public function setOptions(Options $options) { // For backwards compatibility if ($this->options && $this->options->getHttpContext() && !$options->getHttpContext()) { $options->setHttpContext($this->options->getHttpContext()); } $this->options = $options; $fontMetrics = $this->fontMetrics; if (isset($fontMetrics)) { $fontMetrics->setOptions($options); } return $this; } /** * @return Options */ public function getOptions() { return $this->options; } /** * @return array * @deprecated */ public function get_callbacks() { return $this->getCallbacks(); } /** * Returns the callbacks array * * @return array */ public function getCallbacks() { return $this->callbacks; } /** * @param array $callbacks the set of callbacks to set * @return $this * @deprecated */ public function set_callbacks($callbacks) { return $this->setCallbacks($callbacks); } /** * Define callbacks that allow modifying the document during render. * * The callbacks array should contain arrays with `event` set to a callback * event name and `f` set to a function or any other callable. * * The available callback events are: * * `begin_page_reflow`: called before page reflow * * `begin_frame`: called before a frame is rendered * * `end_frame`: called after frame rendering is complete * * `begin_page_render`: called before a page is rendered * * `end_page_render`: called after page rendering is complete * * `end_document`: called for every page after rendering is complete * * The function `f` receives three arguments `Frame $frame`, `Canvas $canvas`, * and `FontMetrics $fontMetrics` for all events but `end_document`. For * `end_document`, the function receives four arguments `int $pageNumber`, * `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics` instead. * * @param array $callbacks The set of callbacks to set. * @return $this */ public function setCallbacks(array $callbacks): self { $this->callbacks = []; foreach ($callbacks as $c) { if (is_array($c) && isset($c["event"]) && isset($c["f"])) { $event = $c["event"]; $f = $c["f"]; if (is_string($event) && is_callable($f)) { $this->callbacks[$event][] = $f; } } } return $this; } /** * @return boolean * @deprecated */ public function get_quirksmode() { return $this->getQuirksmode(); } /** * Get the quirks mode * * @return boolean true if quirks mode is active */ public function getQuirksmode() { return $this->quirksmode; } /** * @param FontMetrics $fontMetrics * @return $this */ public function setFontMetrics(FontMetrics $fontMetrics) { $this->fontMetrics = $fontMetrics; return $this; } /** * @return FontMetrics */ public function getFontMetrics() { return $this->fontMetrics; } /** * PHP5 overloaded getter * Along with {@link Dompdf::__set()} __get() provides access to all * properties directly. Typically __get() is not called directly outside * of this class. * * @param string $prop * * @throws Exception * @return mixed */ function __get($prop) { switch ($prop) { case 'version': return $this->version; default: throw new Exception('Invalid property: ' . $prop); } } } PKZ5wj;;'dompdf/src/Exception/ImageException.phpnuW+AgetAllowedProtocols(); if (!array_key_exists($protocol, $allowed_protocols)) { throw new ImageException("Permission denied on $url. The communication protocol is not supported.", E_WARNING); } foreach ($allowed_protocols[$protocol]["rules"] as $rule) { [$result, $message] = $rule($full_url); if (!$result) { throw new ImageException("Error loading $url: $message", E_WARNING); } } } if ($protocol === "file://") { $resolved_url = $full_url; } elseif (isset(self::$_cache[$full_url])) { $resolved_url = self::$_cache[$full_url]; } else { $tmp_dir = $options->getTempDir(); if (($resolved_url = @tempnam($tmp_dir, "ca_dompdf_img_")) === false) { throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING); } $tempfile = $resolved_url; $image = null; if ($is_data_uri) { if (($parsed_data_uri = Helpers::parse_data_uri($url)) !== false) { $image = $parsed_data_uri["data"]; } } else { list($image, $http_response_header) = Helpers::getFileContent($full_url, $options->getHttpContext()); } // Image not found or invalid if ($image === null) { $msg = ($is_data_uri ? "Data-URI could not be parsed" : "Image not found"); throw new ImageException($msg, E_WARNING); } if (@file_put_contents($resolved_url, $image) === false) { throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING); } self::$_cache[$full_url] = $resolved_url; } // Check if the local file is readable if (!is_readable($resolved_url) || !filesize($resolved_url)) { throw new ImageException("Image not readable or empty", E_WARNING); } list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext()); if (($width && $height && in_array($type, ["gif", "png", "jpeg", "bmp", "svg","webp"], true)) === false) { throw new ImageException("Image type unknown", E_WARNING); } if ($type === "svg") { $parser = xml_parser_create("utf-8"); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); xml_set_element_handler( $parser, function ($parser, $name, $attributes) use ($options, $parsed_url, $full_url) { if (strtolower($name) === "image") { if (!\array_key_exists($full_url, self::$svgRefs)) { self::$svgRefs[$full_url] = []; } $attributes = array_change_key_case($attributes, CASE_LOWER); $urls = []; $urls[] = $attributes["xlink:href"] ?? ""; $urls[] = $attributes["href"] ?? ""; foreach ($urls as $url) { if (empty($url)) { continue; } $inner_full_url = Helpers::build_url($parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $url); if (empty($inner_full_url)) { continue; } self::detectCircularRef($full_url, $inner_full_url); self::$svgRefs[$full_url][] = $inner_full_url; [$resolved_url, $type, $message] = self::resolve_url($url, $parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $options); if (!empty($message)) { throw new ImageException("This SVG document references a restricted resource. $message", E_WARNING); } } } }, false ); if (($fp = fopen($resolved_url, "r")) !== false) { while ($line = fread($fp, 8192)) { xml_parse($parser, $line, false); } fclose($fp); xml_parse($parser, "", true); } xml_parser_free($parser); } } catch (ImageException $e) { if ($tempfile) { unlink($tempfile); } $resolved_url = self::$broken_image; list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext()); $message = self::$error_message; Helpers::record_warnings($e->getCode(), $e->getMessage() . " \n $url", $e->getFile(), $e->getLine()); self::$_cache[$full_url] = $resolved_url; } return [$resolved_url, $type, $message]; } static function detectCircularRef(string $src, string $target) { if (!\array_key_exists($target, self::$svgRefs)) { return; } foreach (self::$svgRefs[$target] as $ref) { if ($ref === $src) { throw new ImageException("Circular external SVG image reference detected.", E_WARNING); } self::detectCircularRef($src, $ref); } } /** * Register a temp file for the given original image file. * * @param string $filePath The path of the original image. * @param string $tempPath The path of the temp file to register. * @param string $key An optional key to register the temp file at. */ static function addTempImage(string $filePath, string $tempPath, string $key = "default"): void { if (!isset(self::$tempImages[$filePath])) { self::$tempImages[$filePath] = []; } self::$tempImages[$filePath][$key] = $tempPath; } /** * Get the path of a temp file registered for the given original image file. * * @param string $filePath The path of the original image. * @param string $key The key the temp file is registered at. */ static function getTempImage(string $filePath, string $key = "default"): ?string { return self::$tempImages[$filePath][$key] ?? null; } /** * Unlink all cached images (i.e. temporary images either downloaded * or converted) except for the bundled "broken image" */ static function clear(bool $debugPng = false) { foreach (self::$_cache as $file) { if ($file === self::$broken_image) { continue; } if ($debugPng) { print "[clear unlink $file]"; } if (file_exists($file)) { unlink($file); } } foreach (self::$tempImages as $versions) { foreach ($versions as $file) { if ($file === self::$broken_image) { continue; } if ($debugPng) { print "[unlink temp image $file]"; } if (file_exists($file)) { unlink($file); } } } self::$_cache = []; self::$tempImages = []; self::$svgRefs = []; } static function detect_type($file, $context = null) { list(, , $type) = Helpers::dompdf_getimagesize($file, $context); return $type; } static function is_broken($url) { return $url === self::$broken_image; } } if (file_exists(realpath(__DIR__ . "/../../lib/res/broken_image.svg"))) { Cache::$broken_image = realpath(__DIR__ . "/../../lib/res/broken_image.svg"); } PKZmXX2dompdf/src/FrameReflower/AbstractFrameReflower.phpnuW+A_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; } } PKZ *dompdf/src/FrameReflower/TableRowGroup.phpnuW+A_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)); } } PKZ'dompdf/src/FrameReflower/ListBullet.phpnuW+A_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); } } } PKZLU.dompdf/src/FrameReflower/NullFrameReflower.phpnuW+A */ 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; } } PKZAѰ#dompdf/src/FrameReflower/Inline.phpnuW+A_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); } } } PKZY"dompdf/src/FrameReflower/Block.phpnuW+A_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]; } } PKZʼ$$"dompdf/src/FrameReflower/Image.phpnuW+Adetermine_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); } } } PKZ^#|{{&dompdf/src/FrameReflower/TableCell.phpnuW+A_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]; } } PKZRJVV!dompdf/src/FrameReflower/Page.phpnuW+Aget_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); } } } } PKZ__%dompdf/src/FrameReflower/TableRow.phpnuW+A_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"); } } PKZwCC"dompdf/src/FrameReflower/Table.phpnuW+A_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 ]; } } PKZAB!B!dompdf/src/Renderer.phpnuW+A_canvas->new_page(); } /** * Render frames recursively * * @param Frame $frame the frame to render */ public function render(Frame $frame) { global $_dompdf_debug; $this->_check_callbacks("begin_frame", $frame); if ($_dompdf_debug) { echo $frame; flush(); } $style = $frame->get_style(); if (in_array($style->visibility, ["hidden", "collapse"], true)) { return; } $display = $style->display; $transformList = $style->transform; $hasTransform = $transformList !== []; // Starts the CSS transformation if ($hasTransform) { $this->_canvas->save(); list($x, $y) = $frame->get_padding_box(); $origin = $style->transform_origin; foreach ($transformList as $transform) { list($function, $values) = $transform; if ($function === "matrix") { $function = "transform"; } $values = array_map("floatval", $values); $values[] = $x + (float)$style->length_in_pt($origin[0], (float)$style->length_in_pt($style->width)); $values[] = $y + (float)$style->length_in_pt($origin[1], (float)$style->length_in_pt($style->height)); call_user_func_array([$this->_canvas, $function], $values); } } switch ($display) { case "block": case "list-item": case "inline-block": case "table": case "inline-table": $this->_render_frame("block", $frame); break; case "inline": if ($frame->is_text_node()) { $this->_render_frame("text", $frame); } else { $this->_render_frame("inline", $frame); } break; case "table-cell": $this->_render_frame("table-cell", $frame); break; case "table-row-group": case "table-header-group": case "table-footer-group": $this->_render_frame("table-row-group", $frame); break; case "-dompdf-list-bullet": $this->_render_frame("list-bullet", $frame); break; case "-dompdf-image": $this->_render_frame("image", $frame); break; case "none": $node = $frame->get_node(); if ($node->nodeName === "script") { if ($node->getAttribute("type") === "text/php" || $node->getAttribute("language") === "php" ) { // Evaluate embedded php scripts $this->_render_frame("php", $frame); } elseif ($node->getAttribute("type") === "text/javascript" || $node->getAttribute("language") === "javascript" ) { // Insert JavaScript $this->_render_frame("javascript", $frame); } } // Don't render children, so skip to next iter return; default: break; } // Starts the overflow: hidden box if ($style->overflow === "hidden") { $padding_box = $frame->get_padding_box(); [$x, $y, $w, $h] = $padding_box; $style = $frame->get_style(); if ($style->has_border_radius()) { $border_box = $frame->get_border_box(); [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $padding_box); $this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl); } else { $this->_canvas->clipping_rectangle($x, $y, $w, $h); } } $stack = []; foreach ($frame->get_children() as $child) { // < 0 : negative z-index // = 0 : no z-index, no stacking context // = 1 : stacking context without z-index // > 1 : z-index $child_style = $child->get_style(); $child_z_index = $child_style->z_index; $z_index = 0; if ($child_z_index !== "auto") { $z_index = $child_z_index + 1; } elseif ($child_style->float !== "none" || $child->is_positioned()) { $z_index = 1; } $stack[$z_index][] = $child; } ksort($stack); foreach ($stack as $by_index) { foreach ($by_index as $child) { $this->render($child); } } // Ends the overflow: hidden box if ($style->overflow === "hidden") { $this->_canvas->clipping_end(); } if ($hasTransform) { $this->_canvas->restore(); } // Check for end frame callback $this->_check_callbacks("end_frame", $frame); } /** * Check for callbacks that need to be performed when a given event * gets triggered on a frame * * @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)) { $this->_callbacks = $this->_dompdf->getCallbacks(); } if (isset($this->_callbacks[$event])) { $fs = $this->_callbacks[$event]; $canvas = $this->_canvas; $fontMetrics = $this->_dompdf->getFontMetrics(); foreach ($fs as $f) { $f($frame, $canvas, $fontMetrics); } } } /** * Render a single frame * * Creates Renderer objects on demand * * @param string $type type of renderer to use * @param Frame $frame the frame to render */ protected function _render_frame($type, $frame) { if (!isset($this->_renderers[$type])) { switch ($type) { case "block": $this->_renderers[$type] = new Block($this->_dompdf); break; case "inline": $this->_renderers[$type] = new Renderer\Inline($this->_dompdf); break; case "text": $this->_renderers[$type] = new Text($this->_dompdf); break; case "image": $this->_renderers[$type] = new Image($this->_dompdf); break; case "table-cell": $this->_renderers[$type] = new TableCell($this->_dompdf); break; case "table-row-group": $this->_renderers[$type] = new TableRowGroup($this->_dompdf); break; case "list-bullet": $this->_renderers[$type] = new ListBullet($this->_dompdf); break; case "php": $this->_renderers[$type] = new PhpEvaluator($this->_canvas); break; case "javascript": $this->_renderers[$type] = new JavascriptEmbedder($this->_dompdf); break; } } $this->_renderers[$type]->render($frame); } } PKZ(dompdf/src/Positioner/NullPositioner.phpnuW+Aget_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); } } PKZT dompdf/src/Positioner/Inline.phpnuW+Afind_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); } } PKZ)]]dompdf/src/Positioner/Block.phpnuW+Aget_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); } } PKZ|<<"dompdf/src/Positioner/Absolute.phpnuW+Aget_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); } } } PKZGxss,dompdf/src/Positioner/AbstractPositioner.phpnuW+Aget_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); } } } PKZ'7,,dompdf/src/Positioner/Fixed.phpnuW+Aget_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); } } } } PKZ3#dompdf/src/Positioner/TableCell.phpnuW+Aget_cellmap(); $frame->set_position($cellmap->get_frame_position($frame)); } } PKZ뀦"dompdf/src/Positioner/TableRow.phpnuW+Aget_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); } } PKZ=N''dompdf/src/LineBox.phpnuW+A_block_frame = $frame; $this->_frames = []; $this->y = $y; $this->get_float_offsets(); } /** * Returns the floating elements inside the first floating parent * * @param Page $root * * @return Frame[] */ public function get_floats_inside(Page $root) { $floating_frames = $root->get_floating_frames(); if (count($floating_frames) == 0) { return $floating_frames; } // Find nearest floating element $p = $this->_block_frame; while ($p->get_style()->float === "none") { $parent = $p->get_parent(); if (!$parent) { break; } $p = $parent; } if ($p == $root) { return $floating_frames; } $parent = $p; $childs = []; foreach ($floating_frames as $_floating) { $p = $_floating->get_parent(); while (($p = $p->get_parent()) && $p !== $parent); if ($p) { $childs[] = $p; } } return $childs; } public function get_float_offsets() { static $anti_infinite_loop = 10000; // FIXME smelly hack $reflower = $this->_block_frame->get_reflower(); if (!$reflower) { return; } $cb_w = null; $block = $this->_block_frame; $root = $block->get_root(); if (!$root) { return; } $style = $this->_block_frame->get_style(); $floating_frames = $this->get_floats_inside($root); $inside_left_floating_width = 0; $inside_right_floating_width = 0; $outside_left_floating_width = 0; $outside_right_floating_width = 0; foreach ($floating_frames as $child_key => $floating_frame) { $floating_frame_parent = $floating_frame->get_parent(); $id = $floating_frame->get_id(); if (isset($this->floating_blocks[$id])) { continue; } $float = $floating_frame->get_style()->float; $floating_width = $floating_frame->get_margin_width(); if (!$cb_w) { $cb_w = $floating_frame->get_containing_block("w"); } $line_w = $this->get_width(); if (!$floating_frame->_float_next_line && ($cb_w <= $line_w + $floating_width) && ($cb_w > $line_w)) { $floating_frame->_float_next_line = true; continue; } // If the child is still shifted by the floating element if ($anti_infinite_loop-- > 0 && $floating_frame->get_position("y") + $floating_frame->get_margin_height() >= $this->y && $block->get_position("x") + $block->get_margin_width() >= $floating_frame->get_position("x") ) { if ($float === "left") { if ($floating_frame_parent === $this->_block_frame) { $inside_left_floating_width += $floating_width; } else { $outside_left_floating_width += $floating_width; } } elseif ($float === "right") { if ($floating_frame_parent === $this->_block_frame) { $inside_right_floating_width += $floating_width; } else { $outside_right_floating_width += $floating_width; } } $this->floating_blocks[$id] = true; } // else, the floating element won't shift anymore else { $root->remove_floating_frame($child_key); } } $this->left += $inside_left_floating_width; if ($outside_left_floating_width > 0 && $outside_left_floating_width > ((float)$style->length_in_pt($style->margin_left) + (float)$style->length_in_pt($style->padding_left))) { $this->left += $outside_left_floating_width - (float)$style->length_in_pt($style->margin_left) - (float)$style->length_in_pt($style->padding_left); } $this->right += $inside_right_floating_width; if ($outside_right_floating_width > 0 && $outside_right_floating_width > ((float)$style->length_in_pt($style->margin_left) + (float)$style->length_in_pt($style->padding_right))) { $this->right += $outside_right_floating_width - (float)$style->length_in_pt($style->margin_right) - (float)$style->length_in_pt($style->padding_right); } } /** * @return float */ public function get_width() { return $this->left + $this->w + $this->right; } /** * @return Block */ public function get_block_frame() { return $this->_block_frame; } /** * @return AbstractFrameDecorator[] */ function &get_frames() { return $this->_frames; } /** * @param AbstractFrameDecorator $frame */ public function add_frame(Frame $frame): void { $this->_frames[] = $frame; if ($frame->get_positioner() instanceof InlinePositioner) { $this->inline = true; } } /** * Remove the frame at the given index and all following frames from the * line. * * @param int $index */ public function remove_frames(int $index): void { $lastIndex = count($this->_frames) - 1; if ($index < 0 || $index > $lastIndex) { return; } for ($i = $lastIndex; $i >= $index; $i--) { $f = $this->_frames[$i]; unset($this->_frames[$i]); $this->w -= $f->get_margin_width(); } // Reset array indices $this->_frames = array_values($this->_frames); // Recalculate the height of the line $h = 0.0; $this->inline = false; foreach ($this->_frames as $f) { $h = max($h, $f->get_margin_height()); if ($f->get_positioner() instanceof InlinePositioner) { $this->inline = true; } } $this->h = $h; } /** * Get the `outside` positioned list markers to be vertically aligned with * the line box. * * @return ListBullet[] */ public function get_list_markers(): array { return $this->list_markers; } /** * Add a list marker to the line box. * * The list marker is only added for the purpose of vertical alignment, it * is not actually added to the list of frames of the line box. */ public function add_list_marker(ListBullet $marker): void { $this->list_markers[] = $marker; } /** * An iterator of all list markers and inline positioned frames of the line * box. * * @return \Iterator */ public function frames_to_align(): \Iterator { yield from $this->list_markers; foreach ($this->_frames as $frame) { if ($frame->get_positioner() instanceof InlinePositioner) { yield $frame; } } } /** * Trim trailing whitespace from the line. */ public function trim_trailing_ws(): void { $lastIndex = count($this->_frames) - 1; if ($lastIndex < 0) { return; } $lastFrame = $this->_frames[$lastIndex]; $reflower = $lastFrame->get_reflower(); if ($reflower instanceof TextFrameReflower && !$lastFrame->is_pre()) { $reflower->trim_trailing_ws(); $this->recalculate_width(); } } /** * Recalculate LineBox width based on the contained frames total width. * * @return float */ public function recalculate_width(): float { $width = 0.0; foreach ($this->_frames as $frame) { $width += $frame->get_margin_width(); } return $this->w = $width; } /** * @return string */ public function __toString(): string { $props = ["wc", "y", "w", "h", "left", "right", "br"]; $s = ""; foreach ($props as $prop) { $s .= "$prop: " . $this->$prop . "\n"; } $s .= count($this->_frames) . " frames\n"; return $s; } } /* class LineBoxList implements Iterator { private $_p = 0; private $_lines = array(); } */ PKZ呏||dompdf/src/Options.phpnuW+A ["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]; } } PKZ_b&dompdf/src/Frame/FrameListIterator.phpnuW+Aparent = $frame; $this->rewind(); } public function rewind(): void { $this->cur = $this->parent->get_first_child(); $this->prev = null; $this->num = 0; } /** * @return bool */ public function valid(): bool { return $this->cur !== null; } /** * @return int */ public function key(): int { return $this->num; } /** * @return Frame|null */ public function current(): ?Frame { return $this->cur; } public function next(): void { if ($this->cur === null) { return; } if ($this->cur->get_parent() === $this->parent) { $this->prev = $this->cur; $this->cur = $this->cur->get_next_sibling(); $this->num++; } else { // Continue from the previous child if the current frame has been // moved to another parent $this->cur = $this->prev !== null ? $this->prev->get_next_sibling() : $this->parent->get_first_child(); } } } PKZW n,#,#dompdf/src/Frame/FrameTree.phpnuW+A_dom = $dom; $this->_root = null; $this->_registry = []; } /** * Returns the DOMDocument object representing the current html document * * @return DOMDocument */ public function get_dom() { return $this->_dom; } /** * Returns the root frame of the tree * * @return Frame */ public function get_root() { return $this->_root; } /** * Returns a specific frame given its id * * @param string $id * * @return Frame|null */ public function get_frame($id) { return isset($this->_registry[$id]) ? $this->_registry[$id] : null; } /** * Returns a post-order iterator for all frames in the tree * * @deprecated Iterate the tree directly instead * @return FrameTreeIterator */ public function get_frames(): FrameTreeIterator { return new FrameTreeIterator($this->_root); } /** * Returns a post-order iterator for all frames in the tree * * @return FrameTreeIterator */ public function getIterator(): FrameTreeIterator { return new FrameTreeIterator($this->_root); } /** * Builds the tree */ public function build_tree() { $html = $this->_dom->getElementsByTagName("html")->item(0); if (is_null($html)) { $html = $this->_dom->firstChild; } if (is_null($html)) { throw new Exception("Requested HTML document contains no data."); } $this->fix_tables(); $this->_root = $this->_build_tree_r($html); } /** * Adds missing TBODYs around TR */ protected function fix_tables() { $xp = new DOMXPath($this->_dom); // Move table caption before the table // FIXME find a better way to deal with it... $captions = $xp->query('//table/caption'); foreach ($captions as $caption) { $table = $caption->parentNode; $table->parentNode->insertBefore($caption, $table); } $firstRows = $xp->query('//table/tr[1]'); /** @var DOMElement $tableChild */ foreach ($firstRows as $tableChild) { $tbody = $this->_dom->createElement('tbody'); $tableNode = $tableChild->parentNode; do { if ($tableChild->nodeName === 'tr') { $tmpNode = $tableChild; $tableChild = $tableChild->nextSibling; $tableNode->removeChild($tmpNode); $tbody->appendChild($tmpNode); } else { if ($tbody->hasChildNodes() === true) { $tableNode->insertBefore($tbody, $tableChild); $tbody = $this->_dom->createElement('tbody'); } $tableChild = $tableChild->nextSibling; } } while ($tableChild); if ($tbody->hasChildNodes() === true) { $tableNode->appendChild($tbody); } } } // FIXME: temporary hack, preferably we will improve rendering of sequential #text nodes /** * Remove a child from a node * * Remove a child from a node. If the removed node results in two * adjacent #text nodes then combine them. * * @param DOMNode $node the current DOMNode being considered * @param array $children an array of nodes that are the children of $node * @param int $index index from the $children array of the node to remove */ protected function _remove_node(DOMNode $node, array &$children, $index) { $child = $children[$index]; $previousChild = $child->previousSibling; $nextChild = $child->nextSibling; $node->removeChild($child); if (isset($previousChild, $nextChild)) { if ($previousChild->nodeName === "#text" && $nextChild->nodeName === "#text") { $previousChild->nodeValue .= $nextChild->nodeValue; $this->_remove_node($node, $children, $index+1); } } array_splice($children, $index, 1); } /** * Recursively adds {@link Frame} objects to the tree * * Recursively build a tree of Frame objects based on a dom tree. * No layout information is calculated at this time, although the * tree may be adjusted (i.e. nodes and frames for generated content * and images may be created). * * @param DOMNode $node the current DOMNode being considered * * @return Frame */ protected function _build_tree_r(DOMNode $node) { $frame = new Frame($node); $id = $frame->get_id(); $this->_registry[$id] = $frame; if (!$node->hasChildNodes()) { return $frame; } // Store the children in an array so that the tree can be modified $children = []; $length = $node->childNodes->length; for ($i = 0; $i < $length; $i++) { $children[] = $node->childNodes->item($i); } $index = 0; // INFO: We don't advance $index if a node is removed to avoid skipping nodes while ($index < count($children)) { $child = $children[$index]; $nodeName = strtolower($child->nodeName); // Skip non-displaying nodes if (in_array($nodeName, self::$HIDDEN_TAGS)) { if ($nodeName !== "head" && $nodeName !== "style") { $this->_remove_node($node, $children, $index); } else { $index++; } continue; } // Skip empty text nodes if ($nodeName === "#text" && $child->nodeValue === "") { $this->_remove_node($node, $children, $index); continue; } // Skip empty image nodes if ($nodeName === "img" && $child->getAttribute("src") === "") { $this->_remove_node($node, $children, $index); continue; } if (is_object($child)) { $frame->append_child($this->_build_tree_r($child), false); } $index++; } return $frame; } /** * @param DOMElement $node * @param DOMElement $new_node * @param string $pos * * @return mixed */ public function insert_node(DOMElement $node, DOMElement $new_node, $pos) { if ($pos === "after" || !$node->firstChild) { $node->appendChild($new_node); } else { $node->insertBefore($new_node, $node->firstChild); } $this->_build_tree_r($new_node); $frame_id = $new_node->getAttribute("frame_id"); $frame = $this->get_frame($frame_id); $parent_id = $node->getAttribute("frame_id"); $parent = $this->get_frame($parent_id); if ($parent) { if ($pos === "before") { $parent->prepend_child($frame, false); } else { $parent->append_child($frame, false); } } return $frame_id; } } PKZړ- - dompdf/src/Frame/Factory.phpnuW+Aset_reflower(new PageFrameReflower($frame)); $root->set_decorator($frame); return $frame; } /** * Decorate a Frame * * @param Frame $frame The frame to decorate * @param Dompdf $dompdf The dompdf instance * @param Frame|null $root The root of the frame * * @throws Exception * @return AbstractFrameDecorator|null * FIXME: this is admittedly a little smelly... */ public static function decorate_frame(Frame $frame, Dompdf $dompdf, ?Frame $root = null): ?AbstractFrameDecorator { $style = $frame->get_style(); $display = $style->display; switch ($display) { case "block": $positioner = "Block"; $decorator = "Block"; $reflower = "Block"; break; case "inline-block": $positioner = "Inline"; $decorator = "Block"; $reflower = "Block"; break; case "inline": $positioner = "Inline"; if ($frame->is_text_node()) { $decorator = "Text"; $reflower = "Text"; } else { $decorator = "Inline"; $reflower = "Inline"; } break; case "table": $positioner = "Block"; $decorator = "Table"; $reflower = "Table"; break; case "inline-table": $positioner = "Inline"; $decorator = "Table"; $reflower = "Table"; break; case "table-row-group": case "table-header-group": case "table-footer-group": $positioner = "NullPositioner"; $decorator = "TableRowGroup"; $reflower = "TableRowGroup"; break; case "table-row": $positioner = "NullPositioner"; $decorator = "TableRow"; $reflower = "TableRow"; break; case "table-cell": $positioner = "TableCell"; $decorator = "TableCell"; $reflower = "TableCell"; break; case "list-item": $positioner = "Block"; $decorator = "Block"; $reflower = "Block"; break; case "-dompdf-list-bullet": if ($style->list_style_position === "inside") { $positioner = "Inline"; } else { $positioner = "ListBullet"; } if ($style->list_style_image !== "none") { $decorator = "ListBulletImage"; } else { $decorator = "ListBullet"; } $reflower = "ListBullet"; break; case "-dompdf-image": $positioner = "Inline"; $decorator = "Image"; $reflower = "Image"; break; case "-dompdf-br": $positioner = "Inline"; $decorator = "Inline"; $reflower = "Inline"; break; default: case "none": if ($style->_dompdf_keep !== "yes") { // Remove the node and the frame $frame->get_parent()->remove_child($frame); return null; } $positioner = "NullPositioner"; $decorator = "NullFrameDecorator"; $reflower = "NullFrameReflower"; break; } // Handle CSS position $position = $style->position; if ($position === "absolute") { $positioner = "Absolute"; } elseif ($position === "fixed") { $positioner = "Fixed"; } $node = $frame->get_node(); // Handle nodeName if ($node->nodeName === "img") { $style->set_prop("display", "-dompdf-image"); $decorator = "Image"; $reflower = "Image"; } $decorator = "Dompdf\\FrameDecorator\\$decorator"; $reflower = "Dompdf\\FrameReflower\\$reflower"; /** @var AbstractFrameDecorator $deco */ $deco = new $decorator($frame, $dompdf); $deco->set_positioner(self::getPositionerInstance($positioner)); $deco->set_reflower(new $reflower($deco, $dompdf->getFontMetrics())); if ($root) { $deco->set_root($root); } if ($display === "list-item") { // Insert a list-bullet frame $xml = $dompdf->getDom(); $bullet_node = $xml->createElement("bullet"); // arbitrary choice $b_f = new Frame($bullet_node); $node = $frame->get_node(); $parent_node = $node->parentNode; if ($parent_node && $parent_node instanceof \DOMElement) { if (!$parent_node->hasAttribute("dompdf-children-count")) { $xpath = new DOMXPath($xml); $count = $xpath->query("li", $parent_node)->length; $parent_node->setAttribute("dompdf-children-count", $count); } if (is_numeric($node->getAttribute("value"))) { $index = intval($node->getAttribute("value")); } else { if (!$parent_node->hasAttribute("dompdf-counter")) { $index = ($parent_node->hasAttribute("start") ? $parent_node->getAttribute("start") : 1); } else { $index = (int)$parent_node->getAttribute("dompdf-counter") + 1; } } $parent_node->setAttribute("dompdf-counter", $index); $bullet_node->setAttribute("dompdf-counter", $index); } $new_style = $dompdf->getCss()->create_style(); $new_style->set_prop("display", "-dompdf-list-bullet"); $new_style->inherit($style); $b_f->set_style($new_style); $deco->prepend_child(Factory::decorate_frame($b_f, $dompdf, $root)); } return $deco; } /** * Creates Positioners * * @param string $type Type of positioner to use * * @return AbstractPositioner */ protected static function getPositionerInstance(string $type): AbstractPositioner { if (!isset(self::$_positioners[$type])) { $class = '\\Dompdf\\Positioner\\'.$type; self::$_positioners[$type] = new $class(); } return self::$_positioners[$type]; } } PKZ&dompdf/src/Frame/FrameTreeIterator.phpnuW+A_stack[] = $this->_root = $root; $this->_num = 0; } public function rewind(): void { $this->_stack = [$this->_root]; $this->_num = 0; } /** * @return bool */ public function valid(): bool { return count($this->_stack) > 0; } /** * @return int */ public function key(): int { return $this->_num; } /** * @return Frame */ public function current(): Frame { return end($this->_stack); } public function next(): void { $b = array_pop($this->_stack); $this->_num++; // Push all children onto the stack in reverse order if ($c = $b->get_last_child()) { $this->_stack[] = $c; while ($c = $c->get_prev_sibling()) { $this->_stack[] = $c; } } } } PKZסJdompdf/src/CanvasFactory.phpnuW+AgetOptions()->getPdfBackend()); if (isset($class) && class_exists($class, false)) { $class .= "_Adapter"; } else { if (($backend === "auto" || $backend === "pdflib") && class_exists("PDFLib", false) ) { $class = "Dompdf\\Adapter\\PDFLib"; } else { if ($backend === "gd" && extension_loaded('gd')) { $class = "Dompdf\\Adapter\\GD"; } else { $class = "Dompdf\\Adapter\\CPDF"; } } } return new $class($paper, $orientation, $dompdf); } } PKZ"pM FFdompdf/src/FontMetrics.phpnuW+AsetCanvas($canvas); $this->setOptions($options); $this->loadFontFamilies(); } /** * @deprecated */ public function save_font_families() { $this->saveFontFamilies(); } /** * Saves the stored font family cache * * The name and location of the cache file are determined by {@link * FontMetrics::USER_FONTS_FILE}. This file should be writable by the * webserver process. * * @see FontMetrics::loadFontFamilies() */ public function saveFontFamilies() { file_put_contents($this->getUserFontsFilePath(), json_encode($this->userFonts, JSON_PRETTY_PRINT)); } /** * @deprecated */ public function load_font_families() { $this->loadFontFamilies(); } /** * Loads the stored font family cache * * @see FontMetrics::saveFontFamilies() */ public function loadFontFamilies() { $file = $this->options->getRootDir() . "/lib/fonts/installed-fonts.dist.json"; $this->bundledFonts = json_decode(file_get_contents($file), true); if (is_readable($this->getUserFontsFilePath())) { $this->userFonts = json_decode(file_get_contents($this->getUserFontsFilePath()), true); } else { $this->loadFontFamiliesLegacy(); } } private function loadFontFamiliesLegacy() { $legacyCacheFile = $this->options->getFontDir() . '/dompdf_font_family_cache.php'; if (is_readable($legacyCacheFile)) { $fontDir = $this->options->getFontDir(); $rootDir = $this->options->getRootDir(); if (!defined("DOMPDF_DIR")) { define("DOMPDF_DIR", $rootDir); } if (!defined("DOMPDF_FONT_DIR")) { define("DOMPDF_FONT_DIR", $fontDir); } $cacheDataClosure = require $legacyCacheFile; $cacheData = is_array($cacheDataClosure) ? $cacheDataClosure : $cacheDataClosure($fontDir, $rootDir); if (is_array($cacheData)) { foreach ($cacheData as $family => $variants) { if (!isset($this->bundledFonts[$family]) && is_array($variants)) { foreach ($variants as $variant => $variantPath) { $variantName = basename($variantPath); $variantDir = dirname($variantPath); if ($variantDir == $fontDir) { $this->userFonts[$family][$variant] = $variantName; } else { $this->userFonts[$family][$variant] = $variantPath; } } } } $this->saveFontFamilies(); } } } /** * @param array $style * @param string $remote_file * @param resource $context * @return bool * @deprecated */ public function register_font($style, $remote_file, $context = null) { return $this->registerFont($style, $remote_file); } /** * @param array $style * @param string $remoteFile * @param resource $context * @return bool */ public function registerFont($style, $remoteFile, $context = null) { $fontname = mb_strtolower($style["family"]); $families = $this->getFontFamilies(); $entry = []; if (isset($families[$fontname])) { $entry = $families[$fontname]; } $styleString = $this->getType("{$style['weight']} {$style['style']}"); $remoteHash = md5($remoteFile); $prefix = $fontname . "_" . $styleString; $prefix = trim($prefix, "-"); if (function_exists('iconv')) { $prefix = @iconv('utf-8', 'us-ascii//TRANSLIT', $prefix); } $prefix_encoding = mb_detect_encoding($prefix, mb_detect_order(), true); $substchar = mb_substitute_character(); mb_substitute_character(0x005F); $prefix = mb_convert_encoding($prefix, "ISO-8859-1", $prefix_encoding); mb_substitute_character($substchar); $prefix = preg_replace("[\W]", "_", $prefix); $prefix = preg_replace("/[^-_\w]+/", "", $prefix); $localFile = $prefix . "_" . $remoteHash; $localFilePath = $this->getOptions()->getFontDir() . "/" . $localFile; if (isset($entry[$styleString]) && $localFilePath == $entry[$styleString]) { return true; } $entry[$styleString] = $localFile; // Download the remote file [$protocol] = Helpers::explode_url($remoteFile); $allowed_protocols = $this->options->getAllowedProtocols(); if (!array_key_exists($protocol, $allowed_protocols)) { Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__); return false; } foreach ($allowed_protocols[$protocol]["rules"] as $rule) { [$result, $message] = $rule($remoteFile); if ($result !== true) { Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__); return false; } } list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context); if ($remoteFileContent === null) { return false; } $localTempFile = @tempnam($this->options->get("tempDir"), "dompdf-font-"); file_put_contents($localTempFile, $remoteFileContent); $font = Font::load($localTempFile); if (!$font) { unlink($localTempFile); return false; } $font->parse(); $font->saveAdobeFontMetrics("$localFilePath.ufm"); $font->close(); unlink($localTempFile); if ( !file_exists("$localFilePath.ufm") ) { return false; } $fontExtension = ".ttf"; switch ($font->getFontType()) { case "TrueType": default: $fontExtension = ".ttf"; break; } // Save the changes file_put_contents($localFilePath.$fontExtension, $remoteFileContent); if ( !file_exists($localFilePath.$fontExtension) ) { unlink("$localFilePath.ufm"); return false; } $this->setFontFamily($fontname, $entry); return true; } /** * @param $text * @param $font * @param $size * @param float $word_spacing * @param float $char_spacing * @return float * @deprecated */ public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0) { //return self::$_pdf->get_text_width($text, $font, $size, $word_spacing, $char_spacing); return $this->getTextWidth($text, $font, $size, $word_spacing, $char_spacing); } /** * Calculates text size, in points * * @param string $text The text to be sized * @param string $font The font file to use * @param float $size The font size, in points * @param float $wordSpacing Word spacing, if any * @param float $charSpacing Char spacing, if any * * @return float */ public function getTextWidth(string $text, $font, float $size, float $wordSpacing = 0.0, float $charSpacing = 0.0): float { // @todo Make sure this cache is efficient before enabling it static $cache = []; if ($text === "") { return 0; } // Don't cache long strings $useCache = !isset($text[50]); // Faster than strlen // Text-size calculations depend on the canvas used. Make sure to not // return wrong values when switching canvas backends $canvasClass = get_class($this->canvas); $key = "$canvasClass/$font/$size/$wordSpacing/$charSpacing"; if ($useCache && isset($cache[$key][$text])) { return $cache[$key][$text]; } $width = $this->canvas->get_text_width($text, $font, $size, $wordSpacing, $charSpacing); if ($useCache) { $cache[$key][$text] = $width; } return $width; } /** * @param $font * @param $size * @return float * @deprecated */ public function get_font_height($font, $size) { return $this->getFontHeight($font, $size); } /** * Calculates font height, in points * * @param string $font The font file to use * @param float $size The font size, in points * * @return float */ public function getFontHeight($font, float $size): float { return $this->canvas->get_font_height($font, $size); } /** * Calculates font baseline, in points * * @param string $font The font file to use * @param float $size The font size, in points * * @return float */ public function getFontBaseline($font, float $size): float { return $this->canvas->get_font_baseline($font, $size); } /** * @param $family_raw * @param string $subtype_raw * @return string * @deprecated */ public function get_font($family_raw, $subtype_raw = "normal") { return $this->getFont($family_raw, $subtype_raw); } /** * Resolves a font family & subtype into an actual font file * Subtype can be one of 'normal', 'bold', 'italic' or 'bold_italic'. If * the particular font family has no suitable font file, the default font * ({@link Options::defaultFont}) is used. The font file returned * is the absolute pathname to the font file on the system. * * @param string|null $familyRaw * @param string $subtypeRaw * * @return string|null */ public function getFont($familyRaw, $subtypeRaw = "normal") { static $cache = []; if (isset($cache[$familyRaw][$subtypeRaw])) { return $cache[$familyRaw][$subtypeRaw]; } /* Allow calling for various fonts in search path. Therefore not immediately * return replacement on non match. * Only when called with NULL try replacement. * When this is also missing there is really trouble. * If only the subtype fails, nevertheless return failure. * Only on checking the fallback font, check various subtypes on same font. */ $subtype = strtolower($subtypeRaw); $families = $this->getFontFamilies(); if ($familyRaw) { $family = str_replace(["'", '"'], "", strtolower($familyRaw)); if (isset($families[$family][$subtype])) { return $cache[$familyRaw][$subtypeRaw] = $families[$family][$subtype]; } return null; } $fallback_families = [strtolower($this->options->getDefaultFont()), "serif"]; foreach ($fallback_families as $family) { if (isset($families[$family][$subtype])) { return $cache[$familyRaw][$subtypeRaw] = $families[$family][$subtype]; } if (!isset($families[$family])) { continue; } $family = $families[$family]; foreach ($family as $sub => $font) { if (strpos($subtype, $sub) !== false) { return $cache[$familyRaw][$subtypeRaw] = $font; } } if ($subtype !== "normal") { foreach ($family as $sub => $font) { if ($sub !== "normal") { return $cache[$familyRaw][$subtypeRaw] = $font; } } } $subtype = "normal"; if (isset($family[$subtype])) { return $cache[$familyRaw][$subtypeRaw] = $family[$subtype]; } } return null; } /** * @param $family * @return null|string * @deprecated */ public function get_family($family) { return $this->getFamily($family); } /** * @param string $family * @return null|string */ public function getFamily($family) { $family = str_replace(["'", '"'], "", mb_strtolower($family)); $families = $this->getFontFamilies(); if (isset($families[$family])) { return $families[$family]; } return null; } /** * @param $type * @return string * @deprecated */ public function get_type($type) { return $this->getType($type); } /** * @param string $type * @return string */ public function getType($type) { if (preg_match('/bold/i', $type)) { $weight = 700; } elseif (preg_match('/([1-9]00)/', $type, $match)) { $weight = (int)$match[0]; } else { $weight = 400; } $weight = $weight === 400 ? 'normal' : $weight; $weight = $weight === 700 ? 'bold' : $weight; $style = preg_match('/italic|oblique/i', $type) ? 'italic' : null; if ($weight === 'normal' && $style !== null) { return $style; } return $style === null ? $weight : $weight.'_'.$style; } /** * @return array * @deprecated */ public function get_font_families() { return $this->getFontFamilies(); } /** * Returns the current font lookup table * * @return array */ public function getFontFamilies() { if (!isset($this->fontFamilies)) { $this->setFontFamilies(); } return $this->fontFamilies; } /** * Convert loaded fonts to font lookup table * * @return array */ public function setFontFamilies() { $fontFamilies = []; if (isset($this->bundledFonts) && is_array($this->bundledFonts)) { foreach ($this->bundledFonts as $family => $variants) { if (!isset($fontFamilies[$family])) { $fontFamilies[$family] = array_map(function ($variant) { return $this->getOptions()->getRootDir() . '/lib/fonts/' . $variant; }, $variants); } } } if (isset($this->userFonts) && is_array($this->userFonts)) { foreach ($this->userFonts as $family => $variants) { $fontFamilies[$family] = array_map(function ($variant) { $variantName = basename($variant); if ($variantName === $variant) { return $this->getOptions()->getFontDir() . '/' . $variant; } return $variant; }, $variants); } } $this->fontFamilies = $fontFamilies; } /** * @param string $fontname * @param mixed $entry * @deprecated */ public function set_font_family($fontname, $entry) { $this->setFontFamily($fontname, $entry); } /** * @param string $fontname * @param mixed $entry */ public function setFontFamily($fontname, $entry) { $this->userFonts[mb_strtolower($fontname)] = $entry; $this->saveFontFamilies(); unset($this->fontFamilies); } /** * @return string */ public function getUserFontsFilePath() { return $this->options->getFontDir() . '/' . self::USER_FONTS_FILE; } /** * @param Options $options * @return $this */ public function setOptions(Options $options) { $this->options = $options; unset($this->fontFamilies); return $this; } /** * @return Options */ public function getOptions() { return $this->options; } /** * @param Canvas $canvas * @return $this */ public function setCanvas(Canvas $canvas) { $this->canvas = $canvas; return $this; } /** * @return Canvas */ public function getCanvas() { return $this->canvas; } } PKZ}QB''dompdf/src/Exception.phpnuW+A_dompdf = $dompdf; } /** * @param $script */ public function insert($script) { $this->_dompdf->getCanvas()->javascript($script); } /** * @param Frame $frame */ public function render(Frame $frame) { if (!$this->_dompdf->getOptions()->getIsJavascriptEnabled()) { return; } $this->insert($frame->get_node()->nodeValue); } } PKZ"P+  dompdf/src/PhpEvaluator.phpnuW+A_canvas = $canvas; } /** * @param $code * @param array $vars */ public function evaluate($code, $vars = []) { if (!$this->_canvas->get_dompdf()->getOptions()->getIsPhpEnabled()) { return; } // Set up some variables for the inline code $pdf = $this->_canvas; $fontMetrics = $pdf->get_dompdf()->getFontMetrics(); $PAGE_NUM = $pdf->get_page_number(); $PAGE_COUNT = $pdf->get_page_count(); // Override those variables if passed in foreach ($vars as $k => $v) { $$k = $v; } eval($code); } /** * @param Frame $frame */ public function render(Frame $frame) { $this->evaluate($frame->get_node()->nodeValue); } } PKZ{wzdompdf/src/Helpers.phpnuW+A tags if the current sapi is not 'cli'. * Returns the output string instead of displaying it if $return is true. * * @param mixed $mixed variable or expression to display * @param bool $return * * @return string|null */ public static function pre_r($mixed, $return = false) { if ($return) { return "
" . print_r($mixed, true) . "
"; } if (php_sapi_name() !== "cli") { echo "
";
        }

        print_r($mixed);

        if (php_sapi_name() !== "cli") {
            echo "
"; } else { echo "\n"; } flush(); return null; } /** * builds a full url given a protocol, hostname, base path and url * * @param string $protocol * @param string $host * @param string $base_path * @param string $url * @return string * * Initially the trailing slash of $base_path was optional, and conditionally appended. * However on dynamically created sites, where the page is given as url parameter, * the base path might not end with an url. * Therefore do not append a slash, and **require** the $base_url to ending in a slash * when needed. * Vice versa, on using the local file system path of a file, make sure that the slash * is appended (o.k. also for Windows) */ public static function build_url($protocol, $host, $base_path, $url) { $protocol = mb_strtolower($protocol); if (empty($protocol)) { $protocol = "file://"; } if ($url === "") { return null; } $url_lc = mb_strtolower($url); // Is the url already fully qualified, a Data URI, or a reference to a named anchor? // File-protocol URLs may require additional processing (e.g. for URLs with a relative path) if ( ( mb_strpos($url_lc, "://") !== false && !in_array(substr($url_lc, 0, 7), ["file://", "phar://"], true) ) || mb_substr($url_lc, 0, 1) === "#" || mb_strpos($url_lc, "data:") === 0 || mb_strpos($url_lc, "mailto:") === 0 || mb_strpos($url_lc, "tel:") === 0 ) { return $url; } $res = ""; if (strpos($url_lc, "file://") === 0) { $url = substr($url, 7); $protocol = "file://"; } elseif (strpos($url_lc, "phar://") === 0) { $res = substr($url, strpos($url_lc, ".phar")+5); $url = substr($url, 7, strpos($url_lc, ".phar")-2); $protocol = "phar://"; } $ret = ""; $is_local_path = in_array($protocol, ["file://", "phar://"], true); if ($is_local_path) { //On Windows local file, an abs path can begin also with a '\' or a drive letter and colon //drive: followed by a relative path would be a drive specific default folder. //not known in php app code, treat as abs path //($url[1] !== ':' || ($url[2]!=='\\' && $url[2]!=='/')) if ($url[0] !== '/' && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' || (mb_strlen($url) > 1 && $url[0] !== '\\' && $url[1] !== ':'))) { // For rel path and local access we ignore the host, and run the path through realpath() $ret .= realpath($base_path) . '/'; } $ret .= $url; $ret = preg_replace('/\?(.*)$/', "", $ret); $filepath = realpath($ret); if ($filepath === false) { return null; } $ret = "$protocol$filepath$res"; return $ret; } $ret = $protocol; // Protocol relative urls (e.g. "//example.org/style.css") if (strpos($url, '//') === 0) { $ret .= substr($url, 2); //remote urls with backslash in html/css are not really correct, but lets be genereous } elseif ($url[0] === '/' || $url[0] === '\\') { // Absolute path $ret .= $host . $url; } else { // Relative path //$base_path = $base_path !== "" ? rtrim($base_path, "/\\") . "/" : ""; $ret .= $host . $base_path . $url; } // URL should now be complete, final cleanup $parsed_url = parse_url($ret); // reproduced from https://www.php.net/manual/en/function.parse-url.php#106731 $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; $pass = ($user || $pass) ? "$pass@" : ''; $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; // partially reproduced from https://stackoverflow.com/a/1243431/264628 /* replace '//' or '/./' or '/foo/../' with '/' */ $re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#'); for ($n=1; $n>0; $path=preg_replace($re, '/', $path, -1, $n)) {} $ret = "$scheme$user$pass$host$port$path$query$fragment"; return $ret; } /** * Builds a HTTP Content-Disposition header string using `$dispositionType` * and `$filename`. * * If the filename contains any characters not in the ISO-8859-1 character * set, a fallback filename will be included for clients not supporting the * `filename*` parameter. * * @param string $dispositionType * @param string $filename * @return string */ public static function buildContentDispositionHeader($dispositionType, $filename) { $encoding = mb_detect_encoding($filename); $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding); $fallbackfilename = str_replace("\"", "", $fallbackfilename); $encodedfilename = rawurlencode($filename); $contentDisposition = "Content-Disposition: $dispositionType; filename=\"$fallbackfilename\""; if ($fallbackfilename !== $filename) { $contentDisposition .= "; filename*=UTF-8''$encodedfilename"; } return $contentDisposition; } /** * Converts decimal numbers to roman numerals. * * As numbers larger than 3999 (and smaller than 1) cannot be represented in * the standard form of roman numerals, those are left in decimal form. * * See https://en.wikipedia.org/wiki/Roman_numerals#Standard_form * * @param int|string $num * * @throws Exception * @return string */ public static function dec2roman($num): string { static $ones = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"]; static $tens = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"]; static $hund = ["", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"]; static $thou = ["", "m", "mm", "mmm"]; if (!is_numeric($num)) { throw new Exception("dec2roman() requires a numeric argument."); } if ($num >= 4000 || $num <= 0) { return (string) $num; } $num = strrev((string)$num); $ret = ""; switch (mb_strlen($num)) { /** @noinspection PhpMissingBreakStatementInspection */ case 4: $ret .= $thou[$num[3]]; /** @noinspection PhpMissingBreakStatementInspection */ case 3: $ret .= $hund[$num[2]]; /** @noinspection PhpMissingBreakStatementInspection */ case 2: $ret .= $tens[$num[1]]; /** @noinspection PhpMissingBreakStatementInspection */ case 1: $ret .= $ones[$num[0]]; default: break; } return $ret; } /** * Restrict a length to the given range. * * If min > max, the result is min. * * @param float $length * @param float $min * @param float $max * * @return float */ public static function clamp(float $length, float $min, float $max): float { return max($min, min($length, $max)); } /** * Determines whether $value is a percentage or not * * @param string|float|int $value * * @return bool */ public static function is_percent($value): bool { return is_string($value) && false !== mb_strpos($value, "%"); } /** * Parses a data URI scheme * http://en.wikipedia.org/wiki/Data_URI_scheme * * @param string $data_uri The data URI to parse * * @return array|bool The result with charset, mime type and decoded data */ public static function parse_data_uri($data_uri) { if (!preg_match('/^data:(?P[a-z0-9\/+-.]+)(;charset=(?P[a-z0-9-])+)?(?P;base64)?\,(?P.*)?/is', $data_uri, $match)) { return false; } $match['data'] = rawurldecode($match['data']); $result = [ 'charset' => $match['charset'] ? $match['charset'] : 'US-ASCII', 'mime' => $match['mime'] ? $match['mime'] : 'text/plain', 'data' => $match['base64'] ? base64_decode($match['data']) : $match['data'], ]; return $result; } /** * Encodes a Uniform Resource Identifier (URI) by replacing non-alphanumeric * characters with a percent (%) sign followed by two hex digits, excepting * characters in the URI reserved character set. * * Assumes that the URI is a complete URI, so does not encode reserved * characters that have special meaning in the URI. * * Simulates the encodeURI function available in JavaScript * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI * * Source: http://stackoverflow.com/q/4929584/264628 * * @param string $uri The URI to encode * @return string The original URL with special characters encoded */ public static function encodeURI($uri) { $unescaped = [ '%2D'=>'-','%5F'=>'_','%2E'=>'.','%21'=>'!', '%7E'=>'~', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')' ]; $reserved = [ '%3B'=>';','%2C'=>',','%2F'=>'/','%3F'=>'?','%3A'=>':', '%40'=>'@','%26'=>'&','%3D'=>'=','%2B'=>'+','%24'=>'$' ]; $score = [ '%23'=>'#' ]; return strtr(rawurlencode(rawurldecode($uri)), array_merge($reserved, $unescaped, $score)); } /** * Decoder for RLE8 compression in windows bitmaps * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp * * @param string $str Data to decode * @param int $width Image width * * @return string */ public static function rle8_decode($str, $width) { $lineWidth = $width + (3 - ($width - 1) % 4); $out = ''; $cnt = strlen($str); for ($i = 0; $i < $cnt; $i++) { $o = ord($str[$i]); switch ($o) { case 0: # ESCAPE $i++; switch (ord($str[$i])) { case 0: # NEW LINE $padCnt = $lineWidth - strlen($out) % $lineWidth; if ($padCnt < $lineWidth) { $out .= str_repeat(chr(0), $padCnt); # pad line } break; case 1: # END OF FILE $padCnt = $lineWidth - strlen($out) % $lineWidth; if ($padCnt < $lineWidth) { $out .= str_repeat(chr(0), $padCnt); # pad line } break 3; case 2: # DELTA $i += 2; break; default: # ABSOLUTE MODE $num = ord($str[$i]); for ($j = 0; $j < $num; $j++) { $out .= $str[++$i]; } if ($num % 2) { $i++; } } break; default: $out .= str_repeat($str[++$i], $o); } } return $out; } /** * Decoder for RLE4 compression in windows bitmaps * see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp * * @param string $str Data to decode * @param int $width Image width * * @return string */ public static function rle4_decode($str, $width) { $w = floor($width / 2) + ($width % 2); $lineWidth = $w + (3 - (($width - 1) / 2) % 4); $pixels = []; $cnt = strlen($str); $c = 0; for ($i = 0; $i < $cnt; $i++) { $o = ord($str[$i]); switch ($o) { case 0: # ESCAPE $i++; switch (ord($str[$i])) { case 0: # NEW LINE while (count($pixels) % $lineWidth != 0) { $pixels[] = 0; } break; case 1: # END OF FILE while (count($pixels) % $lineWidth != 0) { $pixels[] = 0; } break 3; case 2: # DELTA $i += 2; break; default: # ABSOLUTE MODE $num = ord($str[$i]); for ($j = 0; $j < $num; $j++) { if ($j % 2 == 0) { $c = ord($str[++$i]); $pixels[] = ($c & 240) >> 4; } else { $pixels[] = $c & 15; } } if ($num % 2 == 0) { $i++; } } break; default: $c = ord($str[++$i]); for ($j = 0; $j < $o; $j++) { $pixels[] = ($j % 2 == 0 ? ($c & 240) >> 4 : $c & 15); } } } $out = ''; if (count($pixels) % 2) { $pixels[] = 0; } $cnt = count($pixels) / 2; for ($i = 0; $i < $cnt; $i++) { $out .= chr(16 * $pixels[2 * $i] + $pixels[2 * $i + 1]); } return $out; } /** * parse a full url or pathname and return an array(protocol, host, path, * file + query + fragment) * * @param string $url * @return array */ public static function explode_url($url) { $protocol = ""; $host = ""; $path = ""; $file = ""; $res = ""; $arr = parse_url($url); if ( isset($arr["scheme"]) ) { $arr["scheme"] = mb_strtolower($arr["scheme"]); } if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && $arr["scheme"] !== "phar" && strlen($arr["scheme"]) > 1) { $protocol = $arr["scheme"] . "://"; if (isset($arr["user"])) { $host .= $arr["user"]; if (isset($arr["pass"])) { $host .= ":" . $arr["pass"]; } $host .= "@"; } if (isset($arr["host"])) { $host .= $arr["host"]; } if (isset($arr["port"])) { $host .= ":" . $arr["port"]; } if (isset($arr["path"]) && $arr["path"] !== "") { // Do we have a trailing slash? if ($arr["path"][mb_strlen($arr["path"]) - 1] === "/") { $path = $arr["path"]; $file = ""; } else { $path = rtrim(dirname($arr["path"]), '/\\') . "/"; $file = basename($arr["path"]); } } if (isset($arr["query"])) { $file .= "?" . $arr["query"]; } if (isset($arr["fragment"])) { $file .= "#" . $arr["fragment"]; } } else { $protocol = ""; $host = ""; // localhost, really $i = mb_stripos($url, "://"); if ($i !== false) { $protocol = mb_strtolower(mb_substr($url, 0, $i + 3)); $url = mb_substr($url, $i + 3); } else { $protocol = "file://"; } if ($protocol === "phar://") { $res = substr($url, stripos($url, ".phar")+5); $url = substr($url, 7, stripos($url, ".phar")-2); } $file = basename($url); $path = dirname($url) . "/"; } $ret = [$protocol, $host, $path, $file, "protocol" => $protocol, "host" => $host, "path" => $path, "file" => $file, "resource" => $res]; return $ret; } /** * Print debug messages * * @param string $type The type of debug messages to print * @param string $msg The message to show */ public static function dompdf_debug($type, $msg) { global $_DOMPDF_DEBUG_TYPES, $_dompdf_show_warnings, $_dompdf_debug; if (isset($_DOMPDF_DEBUG_TYPES[$type]) && ($_dompdf_show_warnings || $_dompdf_debug)) { $arr = debug_backtrace(); echo basename($arr[0]["file"]) . " (" . $arr[0]["line"] . "): " . $arr[1]["function"] . ": "; Helpers::pre_r($msg); } } /** * Stores warnings in an array for display later * This function allows warnings generated by the DomDocument parser * and CSS loader ({@link Stylesheet}) to be captured and displayed * later. Without this function, errors are displayed immediately and * PDF streaming is impossible. * @see http://www.php.net/manual/en/function.set-error_handler.php * * @param int $errno * @param string $errstr * @param string $errfile * @param string $errline * * @throws Exception */ public static function record_warnings($errno, $errstr, $errfile, $errline) { // Not a warning or notice if (!($errno & (E_WARNING | E_NOTICE | E_USER_NOTICE | E_USER_WARNING | E_STRICT | E_DEPRECATED | E_USER_DEPRECATED))) { throw new Exception($errstr . " $errno"); } global $_dompdf_warnings; global $_dompdf_show_warnings; if ($_dompdf_show_warnings) { echo $errstr . "\n"; } $_dompdf_warnings[] = $errstr; } /** * @param $c * @return bool|string */ public static function unichr($c) { if ($c <= 0x7F) { return chr($c); } elseif ($c <= 0x7FF) { return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F); } elseif ($c <= 0xFFFF) { return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F); } elseif ($c <= 0x10FFFF) { return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F); } return false; } /** * Converts a CMYK color to RGB * * @param float|float[] $c * @param float $m * @param float $y * @param float $k * * @return float[] */ public static function cmyk_to_rgb($c, $m = null, $y = null, $k = null) { if (is_array($c)) { [$c, $m, $y, $k] = $c; } $c *= 255; $m *= 255; $y *= 255; $k *= 255; $r = (1 - round(2.55 * ($c + $k))); $g = (1 - round(2.55 * ($m + $k))); $b = (1 - round(2.55 * ($y + $k))); if ($r < 0) { $r = 0; } if ($g < 0) { $g = 0; } if ($b < 0) { $b = 0; } return [ $r, $g, $b, "r" => $r, "g" => $g, "b" => $b ]; } /** * getimagesize doesn't give a good size for 32bit BMP image v5 * * @param string $filename * @param resource $context * @return array An array of three elements: width and height as * `float|int`, and image type as `string|null`. */ public static function dompdf_getimagesize($filename, $context = null) { static $cache = []; if (isset($cache[$filename])) { return $cache[$filename]; } [$width, $height, $type] = getimagesize($filename); // Custom types $types = [ IMAGETYPE_JPEG => "jpeg", IMAGETYPE_GIF => "gif", IMAGETYPE_BMP => "bmp", IMAGETYPE_PNG => "png", IMAGETYPE_WEBP => "webp", ]; $type = $types[$type] ?? null; if ($width == null || $height == null) { [$data] = Helpers::getFileContent($filename, $context); if ($data !== null) { if (substr($data, 0, 2) === "BM") { $meta = unpack("vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight", $data); $width = (int) $meta["width"]; $height = (int) $meta["height"]; $type = "bmp"; } elseif (strpos($data, "loadFile($filename); [$width, $height] = $doc->getDimensions(); $width = (float) $width; $height = (float) $height; $type = "svg"; } } } return $cache[$filename] = [$width ?? 0, $height ?? 0, $type]; } /** * Credit goes to mgutt * http://www.programmierer-forum.de/function-imagecreatefrombmp-welche-variante-laeuft-t143137.htm * Modified by Fabien Menager to support RGB555 BMP format */ public static function imagecreatefrombmp($filename, $context = null) { if (!function_exists("imagecreatetruecolor")) { trigger_error("The PHP GD extension is required, but is not installed.", E_ERROR); return false; } // version 1.00 if (!($fh = fopen($filename, 'rb'))) { trigger_error('imagecreatefrombmp: Can not open ' . $filename, E_USER_WARNING); return false; } $bytes_read = 0; // read file header $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); // check for bitmap if ($meta['type'] != 19778) { trigger_error('imagecreatefrombmp: ' . $filename . ' is not a bitmap!', E_USER_WARNING); return false; } // read image header $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); $bytes_read += 40; // read additional bitfield header if ($meta['compression'] == 3) { $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); $bytes_read += 12; } // set bytes and padding $meta['bytes'] = $meta['bits'] / 8; $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); if ($meta['decal'] == 4) { $meta['decal'] = 0; } // obtain imagesize if ($meta['imagesize'] < 1) { $meta['imagesize'] = $meta['filesize'] - $meta['offset']; // in rare cases filesize is equal to offset so we need to read physical size if ($meta['imagesize'] < 1) { $meta['imagesize'] = @filesize($filename) - $meta['offset']; if ($meta['imagesize'] < 1) { trigger_error('imagecreatefrombmp: Can not obtain filesize of ' . $filename . '!', E_USER_WARNING); return false; } } } // calculate colors $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; // read color palette $palette = []; if ($meta['bits'] < 16) { $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); // in rare cases the color value is signed if ($palette[1] < 0) { foreach ($palette as $i => $color) { $palette[$i] = $color + 16777216; } } } // ignore extra bitmap headers if ($meta['headersize'] > $bytes_read) { fread($fh, $meta['headersize'] - $bytes_read); } // create gd image $im = imagecreatetruecolor($meta['width'], $meta['height']); $data = fread($fh, $meta['imagesize']); // uncompress data switch ($meta['compression']) { case 1: $data = Helpers::rle8_decode($data, $meta['width']); break; case 2: $data = Helpers::rle4_decode($data, $meta['width']); break; } $p = 0; $vide = chr(0); $y = $meta['height'] - 1; $error = 'imagecreatefrombmp: ' . $filename . ' has not enough data!'; // loop through the image data beginning with the lower left corner while ($y >= 0) { $x = 0; while ($x < $meta['width']) { switch ($meta['bits']) { case 32: case 24: if (!($part = substr($data, $p, 3 /*$meta['bytes']*/))) { trigger_error($error, E_USER_WARNING); return $im; } $color = unpack('V', $part . $vide); break; case 16: if (!($part = substr($data, $p, 2 /*$meta['bytes']*/))) { trigger_error($error, E_USER_WARNING); return $im; } $color = unpack('v', $part); if (empty($meta['rMask']) || $meta['rMask'] != 0xf800) { $color[1] = (($color[1] & 0x7c00) >> 7) * 65536 + (($color[1] & 0x03e0) >> 2) * 256 + (($color[1] & 0x001f) << 3); // 555 } else { $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); // 565 } break; case 8: $color = unpack('n', $vide . substr($data, $p, 1)); $color[1] = $palette[$color[1] + 1]; break; case 4: $color = unpack('n', $vide . substr($data, floor($p), 1)); $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; $color[1] = $palette[$color[1] + 1]; break; case 1: $color = unpack('n', $vide . substr($data, floor($p), 1)); switch (($p * 8) % 8) { case 0: $color[1] = $color[1] >> 7; break; case 1: $color[1] = ($color[1] & 0x40) >> 6; break; case 2: $color[1] = ($color[1] & 0x20) >> 5; break; case 3: $color[1] = ($color[1] & 0x10) >> 4; break; case 4: $color[1] = ($color[1] & 0x8) >> 3; break; case 5: $color[1] = ($color[1] & 0x4) >> 2; break; case 6: $color[1] = ($color[1] & 0x2) >> 1; break; case 7: $color[1] = ($color[1] & 0x1); break; } $color[1] = $palette[$color[1] + 1]; break; default: trigger_error('imagecreatefrombmp: ' . $filename . ' has ' . $meta['bits'] . ' bits and this is not supported!', E_USER_WARNING); return false; } imagesetpixel($im, $x, $y, $color[1]); $x++; $p += $meta['bytes']; } $y--; $p += $meta['decal']; } fclose($fh); return $im; } /** * Gets the content of the file at the specified path using one of * the following methods, in preferential order: * - file_get_contents: if allow_url_fopen is true or the file is local * - curl: if allow_url_fopen is false and curl is available * * @param string $uri * @param resource $context * @param int $offset * @param int $maxlen * @return string[] */ public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null) { $content = null; $headers = null; [$protocol] = Helpers::explode_url($uri); $is_local_path = in_array(strtolower($protocol), ["", "file://", "phar://"], true); $can_use_curl = in_array(strtolower($protocol), ["http://", "https://"], true); set_error_handler([self::class, 'record_warnings']); try { if ($is_local_path || ini_get('allow_url_fopen') || !$can_use_curl) { if ($is_local_path === false) { $uri = Helpers::encodeURI($uri); } if (isset($maxlen)) { $result = file_get_contents($uri, false, $context, $offset, $maxlen); } else { $result = file_get_contents($uri, false, $context, $offset); } if ($result !== false) { $content = $result; } if (isset($http_response_header)) { $headers = $http_response_header; } } elseif ($can_use_curl && function_exists('curl_exec')) { $curl = curl_init($uri); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HEADER, true); if ($offset > 0) { curl_setopt($curl, CURLOPT_RESUME_FROM, $offset); } if ($maxlen > 0) { curl_setopt($curl, CURLOPT_BUFFERSIZE, 128); curl_setopt($curl, CURLOPT_NOPROGRESS, false); curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, function ($res, $download_size_total, $download_size, $upload_size_total, $upload_size) use ($maxlen) { return ($download_size > $maxlen) ? 1 : 0; }); } $context_options = []; if (!is_null($context)) { $context_options = stream_context_get_options($context); } foreach ($context_options as $stream => $options) { foreach ($options as $option => $value) { $key = strtolower($stream) . ":" . strtolower($option); switch ($key) { case "curl:curl_verify_ssl_host": curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, !$value ? 0 : 2); break; case "curl:max_redirects": curl_setopt($curl, CURLOPT_MAXREDIRS, $value); break; case "http:follow_location": curl_setopt($curl, CURLOPT_FOLLOWLOCATION, $value); break; case "http:header": if (is_string($value)) { curl_setopt($curl, CURLOPT_HTTPHEADER, [$value]); } else { curl_setopt($curl, CURLOPT_HTTPHEADER, $value); } break; case "http:timeout": curl_setopt($curl, CURLOPT_TIMEOUT, $value); break; case "http:user_agent": curl_setopt($curl, CURLOPT_USERAGENT, $value); break; case "curl:curl_verify_ssl_peer": case "ssl:verify_peer": curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $value); break; } } } $data = curl_exec($curl); if ($data !== false && !curl_errno($curl)) { switch ($http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE)) { case 200: $raw_headers = substr($data, 0, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); $headers = preg_split("/[\n\r]+/", trim($raw_headers)); $content = substr($data, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); break; } } curl_close($curl); } } finally { restore_error_handler(); } return [$content, $headers]; } /** * @param string $str * @return string */ public static function mb_ucwords(string $str): string { $max_len = mb_strlen($str); if ($max_len === 1) { return mb_strtoupper($str); } $str = mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 1); foreach ([' ', '.', ',', '!', '?', '-', '+'] as $s) { $pos = 0; while (($pos = mb_strpos($str, $s, $pos)) !== false) { $pos++; // Nothing to do if the separator is the last char of the string if ($pos !== false && $pos < $max_len) { // If the char we want to upper is the last char there is nothing to append behind if ($pos + 1 < $max_len) { $str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1)) . mb_substr($str, $pos + 1); } else { $str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1)); } } } } return $str; } /** * Check whether two lengths should be considered equal, accounting for * inaccuracies in float computation. * * The implementation relies on the fact that we are neither dealing with * very large, nor with very small numbers in layout. Adapted from * https://floating-point-gui.de/errors/comparison/. * * @param float $a * @param float $b * * @return bool */ public static function lengthEqual(float $a, float $b): bool { // The epsilon results in a precision of at least: // * 7 decimal digits at around 1 // * 4 decimal digits at around 1000 (around the size of common paper formats) // * 2 decimal digits at around 100,000 (100,000pt ~ 35.28m) static $epsilon = 1e-8; static $almostZero = 1e-12; $diff = abs($a - $b); if ($a === $b || $diff < $almostZero) { return true; } return $diff < $epsilon * max(abs($a), abs($b)); } /** * Check `$a < $b`, accounting for inaccuracies in float computation. */ public static function lengthLess(float $a, float $b): bool { return $a < $b && !self::lengthEqual($a, $b); } /** * Check `$a <= $b`, accounting for inaccuracies in float computation. */ public static function lengthLessOrEqual(float $a, float $b): bool { return $a <= $b || self::lengthEqual($a, $b); } /** * Check `$a > $b`, accounting for inaccuracies in float computation. */ public static function lengthGreater(float $a, float $b): bool { return $a > $b && !self::lengthEqual($a, $b); } /** * Check `$a >= $b`, accounting for inaccuracies in float computation. */ public static function lengthGreaterOrEqual(float $a, float $b): bool { return $a >= $b || self::lengthEqual($a, $b); } } PKZ \ʠ(dompdf/src/Renderer/AbstractRenderer.phpnuW+A_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]; } } PKZIpPP%dompdf/src/Renderer/TableRowGroup.phpnuW+Aget_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"); } } PKZj*|"dompdf/src/Renderer/ListBullet.phpnuW+Aget_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); } } } PKZx@@dompdf/src/Renderer/Text.phpnuW+A_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]); } } } PKZQgdompdf/src/Renderer/Inline.phpnuW+Aget_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]; } } PKZm4t t dompdf/src/Renderer/Block.phpnuW+Aget_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"); } } } } PKZC D D dompdf/src/Renderer/Image.phpnuW+Aget_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"); } } PKZXo!dompdf/src/Renderer/TableCell.phpnuW+Aget_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"); } } } } PKZU>cnndompdf/src/Cellmap.phpnuW+A 8, "solid" => 7, "dashed" => 6, "dotted" => 5, "ridge" => 4, "outset" => 3, "groove" => 2, "inset" => 1, "none" => 0 ]; /** * The table object this cellmap is attached to. * * @var TableFrameDecorator */ protected $_table; /** * The total number of rows in the table * * @var int */ protected $_num_rows; /** * The total number of columns in the table * * @var int */ protected $_num_cols; /** * 2D array mapping to frames * * @var Frame[][] */ protected $_cells; /** * 1D array of column dimensions * * @var array */ protected $_columns; /** * 1D array of row dimensions * * @var array */ protected $_rows; /** * 2D array of border specs * * @var array */ protected $_borders; /** * 1D Array mapping frames to (multiple) pairs, keyed on frame_id. * * @var array[] */ protected $_frames; /** * Current column when adding cells, 0-based * * @var int */ private $__col; /** * Current row when adding cells, 0-based * * @var int */ private $__row; /** * Tells whether the columns' width can be modified * * @var bool */ private $_columns_locked = false; /** * Tells whether the table has table-layout:fixed * * @var bool */ private $_fixed_layout = false; /** * @param TableFrameDecorator $table */ public function __construct(TableFrameDecorator $table) { $this->_table = $table; $this->reset(); } public function reset(): void { $this->_num_rows = 0; $this->_num_cols = 0; $this->_cells = []; $this->_frames = []; if (!$this->_columns_locked) { $this->_columns = []; } $this->_rows = []; $this->_borders = []; $this->__col = $this->__row = 0; } public function lock_columns(): void { $this->_columns_locked = true; } /** * @return bool */ public function is_columns_locked() { return $this->_columns_locked; } /** * @param bool $fixed */ public function set_layout_fixed(bool $fixed) { $this->_fixed_layout = $fixed; } /** * @return bool */ public function is_layout_fixed() { return $this->_fixed_layout; } /** * @return int */ public function get_num_rows() { return $this->_num_rows; } /** * @return int */ public function get_num_cols() { return $this->_num_cols; } /** * @return array */ public function &get_columns() { return $this->_columns; } /** * @param $columns */ public function set_columns($columns) { $this->_columns = $columns; } /** * @param int $i * * @return mixed */ public function &get_column($i) { if (!isset($this->_columns[$i])) { $this->_columns[$i] = [ "x" => 0, "min-width" => 0, "max-width" => 0, "used-width" => null, "absolute" => 0, "percent" => 0, "auto" => true, ]; } return $this->_columns[$i]; } /** * @return array */ public function &get_rows() { return $this->_rows; } /** * @param int $j * * @return mixed */ public function &get_row($j) { if (!isset($this->_rows[$j])) { $this->_rows[$j] = [ "y" => 0, "first-column" => 0, "height" => null, ]; } return $this->_rows[$j]; } /** * @param int $i * @param int $j * @param mixed $h_v * @param null|mixed $prop * * @return mixed */ public function get_border($i, $j, $h_v, $prop = null) { if (!isset($this->_borders[$i][$j][$h_v])) { $this->_borders[$i][$j][$h_v] = [ "width" => 0, "style" => "solid", "color" => "black", ]; } if (isset($prop)) { return $this->_borders[$i][$j][$h_v][$prop]; } return $this->_borders[$i][$j][$h_v]; } /** * @param int $i * @param int $j * * @return array */ public function get_border_properties($i, $j) { return [ "top" => $this->get_border($i, $j, "horizontal"), "right" => $this->get_border($i, $j + 1, "vertical"), "bottom" => $this->get_border($i + 1, $j, "horizontal"), "left" => $this->get_border($i, $j, "vertical"), ]; } /** * @param Frame $frame * * @return array|null */ public function get_spanned_cells(Frame $frame) { $key = $frame->get_id(); if (isset($this->_frames[$key])) { return $this->_frames[$key]; } return null; } /** * @param Frame $frame * * @return bool */ public function frame_exists_in_cellmap(Frame $frame) { $key = $frame->get_id(); return isset($this->_frames[$key]); } /** * @param Frame $frame * * @return array * @throws Exception */ public function get_frame_position(Frame $frame) { global $_dompdf_warnings; $key = $frame->get_id(); if (!isset($this->_frames[$key])) { throw new Exception("Frame not found in cellmap"); } // Positions are stored relative to the table position [$table_x, $table_y] = $this->_table->get_position(); $col = $this->_frames[$key]["columns"][0]; $row = $this->_frames[$key]["rows"][0]; if (!isset($this->_columns[$col])) { $_dompdf_warnings[] = "Frame not found in columns array. Check your table layout for missing or extra TDs."; $x = $table_x; } else { $x = $table_x + $this->_columns[$col]["x"]; } if (!isset($this->_rows[$row])) { $_dompdf_warnings[] = "Frame not found in row array. Check your table layout for missing or extra TDs."; $y = $table_y; } else { $y = $table_y + $this->_rows[$row]["y"]; } return [$x, $y, "x" => $x, "y" => $y]; } /** * @param Frame $frame * * @return int * @throws Exception */ public function get_frame_width(Frame $frame) { $key = $frame->get_id(); if (!isset($this->_frames[$key])) { throw new Exception("Frame not found in cellmap"); } $cols = $this->_frames[$key]["columns"]; $w = 0; foreach ($cols as $i) { $w += $this->_columns[$i]["used-width"]; } return $w; } /** * @param Frame $frame * * @return int * @throws Exception * @throws Exception */ public function get_frame_height(Frame $frame) { $key = $frame->get_id(); if (!isset($this->_frames[$key])) { throw new Exception("Frame not found in cellmap"); } $rows = $this->_frames[$key]["rows"]; $h = 0; foreach ($rows as $i) { if (!isset($this->_rows[$i])) { throw new Exception("The row #$i could not be found, please file an issue in the tracker with the HTML code"); } $h += $this->_rows[$i]["height"]; } return $h; } /** * @param int $j * @param mixed $width */ public function set_column_width($j, $width) { if ($this->_columns_locked) { return; } $col =& $this->get_column($j); $col["used-width"] = $width; $next_col =& $this->get_column($j + 1); $next_col["x"] = $col["x"] + $width; } /** * @param int $i * @param long $height */ public function set_row_height($i, $height) { $row =& $this->get_row($i); if ($height > $row["height"]) { $row["height"] = $height; } $next_row =& $this->get_row($i + 1); $next_row["y"] = $row["y"] + $row["height"]; } /** * https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution * * @param int $i * @param int $j * @param string $h_v `horizontal` or `vertical` * @param array $border_spec */ protected function resolve_border(int $i, int $j, string $h_v, array $border_spec): void { if (!isset($this->_borders[$i][$j][$h_v])) { $this->_borders[$i][$j][$h_v] = $border_spec; return; } $border = $this->_borders[$i][$j][$h_v]; $n_width = $border_spec["width"]; $n_style = $border_spec["style"]; $o_width = $border["width"]; $o_style = $border["style"]; if ($o_style === "hidden") { return; } // A style of `none` has lowest priority independent of its specified // width here, as its resolved width is always 0 if ($n_style === "hidden" || $n_width > $o_width || ($o_width == $n_width && isset(self::BORDER_STYLE_SCORE[$n_style]) && isset(self::BORDER_STYLE_SCORE[$o_style]) && self::BORDER_STYLE_SCORE[$n_style] > self::BORDER_STYLE_SCORE[$o_style]) ) { $this->_borders[$i][$j][$h_v] = $border_spec; } } /** * Get the resolved border properties for the given frame. * * @param AbstractFrameDecorator $frame * * @return array[] */ protected function get_resolved_border(AbstractFrameDecorator $frame): array { $key = $frame->get_id(); $columns = $this->_frames[$key]["columns"]; $rows = $this->_frames[$key]["rows"]; $first_col = $columns[0]; $last_col = $columns[count($columns) - 1]; $first_row = $rows[0]; $last_row = $rows[count($rows) - 1]; $max_top = null; $max_bottom = null; $max_left = null; $max_right = null; foreach ($columns as $col) { $top = $this->_borders[$first_row][$col]["horizontal"]; $bottom = $this->_borders[$last_row + 1][$col]["horizontal"]; if ($max_top === null || $top["width"] > $max_top["width"]) { $max_top = $top; } if ($max_bottom === null || $bottom["width"] > $max_bottom["width"]) { $max_bottom = $bottom; } } foreach ($rows as $row) { $left = $this->_borders[$row][$first_col]["vertical"]; $right = $this->_borders[$row][$last_col + 1]["vertical"]; if ($max_left === null || $left["width"] > $max_left["width"]) { $max_left = $left; } if ($max_right === null || $right["width"] > $max_right["width"]) { $max_right = $right; } } return [$max_top, $max_right, $max_bottom, $max_left]; } /** * @param AbstractFrameDecorator $frame */ public function add_frame(Frame $frame): void { $style = $frame->get_style(); $display = $style->display; $collapse = $this->_table->get_style()->border_collapse === "collapse"; // Recursively add the frames within the table, its row groups and rows if ($frame === $this->_table || $display === "table-row" || in_array($display, TableFrameDecorator::ROW_GROUPS, true) ) { $start_row = $this->__row; foreach ($frame->get_children() as $child) { $this->add_frame($child); } if ($display === "table-row") { $this->add_row(); } $num_rows = $this->__row - $start_row - 1; $key = $frame->get_id(); // Row groups always span across the entire table $this->_frames[$key]["columns"] = range(0, max(0, $this->_num_cols - 1)); $this->_frames[$key]["rows"] = range($start_row, max(0, $this->__row - 1)); $this->_frames[$key]["frame"] = $frame; if ($collapse) { $bp = $style->get_border_properties(); // Resolve vertical borders for ($i = 0; $i < $num_rows + 1; $i++) { $this->resolve_border($start_row + $i, 0, "vertical", $bp["left"]); $this->resolve_border($start_row + $i, $this->_num_cols, "vertical", $bp["right"]); } // Resolve horizontal borders for ($j = 0; $j < $this->_num_cols; $j++) { $this->resolve_border($start_row, $j, "horizontal", $bp["top"]); $this->resolve_border($this->__row, $j, "horizontal", $bp["bottom"]); } if ($frame === $this->_table) { // Clear borders because the cells are now using them. The // border width still needs to be set to half the resolved // width so that the table is positioned properly [$top, $right, $bottom, $left] = $this->get_resolved_border($frame); $style->set_used("border_top_width", $top["width"] / 2); $style->set_used("border_right_width", $right["width"] / 2); $style->set_used("border_bottom_width", $bottom["width"] / 2); $style->set_used("border_left_width", $left["width"] / 2); $style->set_used("border_style", "none"); } else { // Clear borders for rows and row groups $style->set_used("border_width", 0); $style->set_used("border_style", "none"); } } if ($frame === $this->_table) { // Apply resolved borders to table cells and calculate column // widths after all frames have been added $this->calculate_column_widths(); } return; } // Add the frame to the cellmap $key = $frame->get_id(); $node = $frame->get_node(); $bp = $style->get_border_properties(); // Determine where this cell is going $colspan = max((int) $node->getAttribute("colspan"), 1); $rowspan = max((int) $node->getAttribute("rowspan"), 1); // Find the next available column (fix by Ciro Mondueri) $ac = $this->__col; while (isset($this->_cells[$this->__row][$ac])) { $ac++; } $this->__col = $ac; // Rows: for ($i = 0; $i < $rowspan; $i++) { $row = $this->__row + $i; $this->_frames[$key]["rows"][] = $row; for ($j = 0; $j < $colspan; $j++) { $this->_cells[$row][$this->__col + $j] = $frame; } if ($collapse) { // Resolve vertical borders $this->resolve_border($row, $this->__col, "vertical", $bp["left"]); $this->resolve_border($row, $this->__col + $colspan, "vertical", $bp["right"]); } } // Columns: for ($j = 0; $j < $colspan; $j++) { $col = $this->__col + $j; $this->_frames[$key]["columns"][] = $col; if ($collapse) { // Resolve horizontal borders $this->resolve_border($this->__row, $col, "horizontal", $bp["top"]); $this->resolve_border($this->__row + $rowspan, $col, "horizontal", $bp["bottom"]); } } $this->_frames[$key]["frame"] = $frame; $this->__col += $colspan; if ($this->__col > $this->_num_cols) { $this->_num_cols = $this->__col; } } /** * Apply resolved borders to table cells and calculate column widths. */ protected function calculate_column_widths(): void { $table = $this->_table; $table_style = $table->get_style(); $collapse = $table_style->border_collapse === "collapse"; if ($collapse) { $v_spacing = 0; $h_spacing = 0; } else { // The additional 1/2 width gets added to the table proper [$h, $v] = $table_style->border_spacing; $v_spacing = $v / 2; $h_spacing = $h / 2; } foreach ($this->_frames as $frame_info) { /** @var TableCellFrameDecorator */ $frame = $frame_info["frame"]; $style = $frame->get_style(); $display = $style->display; if ($display !== "table-cell") { continue; } if ($collapse) { // Set the resolved border at half width [$top, $right, $bottom, $left] = $this->get_resolved_border($frame); $style->set_used("border_top_width", $top["width"] / 2); $style->set_used("border_top_style", $top["style"]); $style->set_used("border_top_color", $top["color"]); $style->set_used("border_right_width", $right["width"] / 2); $style->set_used("border_right_style", $right["style"]); $style->set_used("border_right_color", $right["color"]); $style->set_used("border_bottom_width", $bottom["width"] / 2); $style->set_used("border_bottom_style", $bottom["style"]); $style->set_used("border_bottom_color", $bottom["color"]); $style->set_used("border_left_width", $left["width"] / 2); $style->set_used("border_left_style", $left["style"]); $style->set_used("border_left_color", $left["color"]); $style->set_used("margin", 0); } else { // Border spacing is effectively a margin between cells $style->set_used("margin_top", $v_spacing); $style->set_used("margin_bottom", $v_spacing); $style->set_used("margin_left", $h_spacing); $style->set_used("margin_right", $h_spacing); } if ($this->_columns_locked) { continue; } $node = $frame->get_node(); $colspan = max((int) $node->getAttribute("colspan"), 1); $first_col = $frame_info["columns"][0]; // Resolve the frame's width if ($this->_fixed_layout) { list($frame_min, $frame_max) = [0, 10e-10]; } else { list($frame_min, $frame_max) = $frame->get_min_max_width(); } $width = $style->width; $val = null; if (Helpers::is_percent($width) && $colspan === 1) { $var = "percent"; $val = (float)rtrim($width, "% "); } elseif ($width !== "auto" && $colspan === 1) { $var = "absolute"; $val = $frame_min; } $min = 0; $max = 0; for ($cs = 0; $cs < $colspan; $cs++) { // Resolve the frame's width(s) with other cells $col =& $this->get_column($first_col + $cs); // Note: $var is either 'percent' or 'absolute'. We compare the // requested percentage or absolute values with the existing widths // and adjust accordingly. if (isset($var) && $val > $col[$var]) { $col[$var] = $val; $col["auto"] = false; } $min += $col["min-width"]; $max += $col["max-width"]; } if ($frame_min > $min && $colspan === 1) { // The frame needs more space. Expand each sub-column // FIXME try to avoid putting this dummy value when table-layout:fixed $inc = ($this->is_layout_fixed() ? 10e-10 : ($frame_min - $min)); for ($c = 0; $c < $colspan; $c++) { $col =& $this->get_column($first_col + $c); $col["min-width"] += $inc; } } if ($frame_max > $max) { // FIXME try to avoid putting this dummy value when table-layout:fixed $inc = ($this->is_layout_fixed() ? 10e-10 : ($frame_max - $max) / $colspan); for ($c = 0; $c < $colspan; $c++) { $col =& $this->get_column($first_col + $c); $col["max-width"] += $inc; } } } // Adjust absolute columns so that the absolute (and max) width is the // largest minimum width of all cells. This accounts for cells without // absolute width within an absolute column foreach ($this->_columns as &$col) { if ($col["absolute"] > 0) { $col["absolute"] = $col["min-width"]; $col["max-width"] = $col["min-width"]; } } } protected function add_row(): void { $this->__row++; $this->_num_rows++; // Find the next available column $i = 0; while (isset($this->_cells[$this->__row][$i])) { $i++; } $this->__col = $i; } /** * Remove a row from the cellmap. * * @param Frame */ public function remove_row(Frame $row) { $key = $row->get_id(); if (!isset($this->_frames[$key])) { return; // Presumably this row has already been removed } $this->__row = $this->_num_rows--; $rows = $this->_frames[$key]["rows"]; $columns = $this->_frames[$key]["columns"]; // Remove all frames from this row foreach ($rows as $r) { foreach ($columns as $c) { if (isset($this->_cells[$r][$c])) { $id = $this->_cells[$r][$c]->get_id(); $this->_cells[$r][$c] = null; unset($this->_cells[$r][$c]); // has multiple rows? if (isset($this->_frames[$id]) && count($this->_frames[$id]["rows"]) > 1) { // remove just the desired row, but leave the frame if (($row_key = array_search($r, $this->_frames[$id]["rows"])) !== false) { unset($this->_frames[$id]["rows"][$row_key]); } continue; } $this->_frames[$id] = null; unset($this->_frames[$id]); } } $this->_rows[$r] = null; unset($this->_rows[$r]); } $this->_frames[$key] = null; unset($this->_frames[$key]); } /** * Remove a row group from the cellmap. * * @param Frame $group The group to remove */ public function remove_row_group(Frame $group) { $key = $group->get_id(); if (!isset($this->_frames[$key])) { return; // Presumably this row has already been removed } $iter = $group->get_first_child(); while ($iter) { $this->remove_row($iter); $iter = $iter->get_next_sibling(); } $this->_frames[$key] = null; unset($this->_frames[$key]); } /** * Update a row group after rows have been removed * * @param Frame $group The group to update * @param Frame $last_row The last row in the row group */ public function update_row_group(Frame $group, Frame $last_row) { $g_key = $group->get_id(); $first_index = $this->_frames[$g_key]["rows"][0]; $last_index = $first_index; $row = $last_row; while ($row = $row->get_prev_sibling()) { $last_index++; } $this->_frames[$g_key]["rows"] = range($first_index, $last_index); } public function assign_x_positions(): void { // Pre-condition: widths must be resolved and assigned to columns and // column[0]["x"] must be set. if ($this->_columns_locked) { return; } $x = $this->_columns[0]["x"]; foreach (array_keys($this->_columns) as $j) { $this->_columns[$j]["x"] = $x; $x += $this->_columns[$j]["used-width"]; } } public function assign_frame_heights(): void { // Pre-condition: widths and heights of each column & row must be // calcluated foreach ($this->_frames as $arr) { $frame = $arr["frame"]; $h = 0.0; foreach ($arr["rows"] as $row) { if (!isset($this->_rows[$row])) { // The row has been removed because of a page split, so skip it. continue; } $h += $this->_rows[$row]["height"]; } if ($frame instanceof TableCellFrameDecorator) { $frame->set_cell_height($h); } else { $frame->get_style()->set_used("height", $h); } } } /** * Re-adjust frame height if the table height is larger than its content */ public function set_frame_heights(float $table_height, float $content_height): void { // Distribute the increased height proportionally amongst each row foreach ($this->_frames as $arr) { $frame = $arr["frame"]; $h = 0.0; foreach ($arr["rows"] as $row) { if (!isset($this->_rows[$row])) { continue; } $h += $this->_rows[$row]["height"]; } if ($content_height > 0) { $new_height = ($h / $content_height) * $table_height; } else { $new_height = 0.0; } if ($frame instanceof TableCellFrameDecorator) { $frame->set_cell_height($new_height); } else { $frame->get_style()->set_used("height", $new_height); } } } /** * Used for debugging: * * @return string */ public function __toString(): string { $str = ""; $str .= "Columns:
"; $str .= Helpers::pre_r($this->_columns, true); $str .= "Rows:
"; $str .= Helpers::pre_r($this->_rows, true); $str .= "Frames:
"; $arr = []; foreach ($this->_frames as $key => $val) { $arr[$key] = ["columns" => $val["columns"], "rows" => $val["rows"]]; } $str .= Helpers::pre_r($arr, true); if (php_sapi_name() == "cli") { $str = strip_tags(str_replace(["
", "", ""], ["\n", chr(27) . "[01;33m", chr(27) . "[0m"], $str)); } return $str; } } PKZY9))dompdf/src/Css/Color.phpnuW+A "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; } } PKZ@Odompdf/src/Css/Stylesheet.phpnuW+A 0x00000000, // user agent declarations self::ORIG_USER => 0x10000000, // user normal declarations self::ORIG_AUTHOR => 0x30000000, // author normal declarations ]; /** * Non-CSS presentational hints (i.e. HTML 4 attributes) are handled as if added * to the beginning of an author stylesheet, i.e. anything in author stylesheets * should override them. */ const SPEC_NON_CSS = 0x20000000; /** * Current dompdf instance * * @var Dompdf */ private $_dompdf; /** * Array of currently defined styles * * @var Style[][] */ private $_styles; /** * Base protocol of the document being parsed * Used to handle relative urls. * * @var string */ private $_protocol = ""; /** * Base hostname of the document being parsed * Used to handle relative urls. * * @var string */ private $_base_host = ""; /** * Base path of the document being parsed * Used to handle relative urls. * * @var string */ private $_base_path = ""; /** * The styles defined by @page rules * * @var array