Media Library×
+No media selected.
+commit 6f1cc27ec43e23b6e2f332b64ec4c20ced028131 Author: “VeLiTi” <“info@veliti.nl”> Date: Thu Jan 30 11:43:37 2025 +0100 Initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..8d83a45 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/admin/account.php b/admin/account.php new file mode 100644 index 0000000..760e32d --- /dev/null +++ b/admin/account.php @@ -0,0 +1,131 @@ + '', + 'password' => '', + 'role' => 'Member', + 'first_name' => '', + 'last_name' => '', + 'address_street' => '', + 'address_city' => '', + 'address_state' => '', + 'address_zip' => '', + 'address_country' => '', + 'registered' => date('Y-m-d\TH:i'), + 'address_phone' => '' +]; +if (isset($_GET['id'])) { + // Retrieve the account from the database + $stmt = $pdo->prepare('SELECT * FROM accounts WHERE id = ?'); + $stmt->execute([ $_GET['id'] ]); + $account = $stmt->fetch(PDO::FETCH_ASSOC); + // ID param exists, edit an existing account + $page = 'Edit'; + if (isset($_POST['submit'])) { + // Update the account + $password = !empty($_POST['password']) ? password_hash($_POST['password'], PASSWORD_DEFAULT) : $account['password']; + $stmt = $pdo->prepare('UPDATE accounts SET email = ?, password = ?, first_name = ?, last_name = ?, address_street = ?, address_city = ?, address_state = ?, address_zip = ?, address_country = ?, role = ?, registered = ?, address_phone = ? WHERE id = ?'); + $stmt->execute([ $_POST['email'], $password, $_POST['first_name'], $_POST['last_name'], $_POST['address_street'], $_POST['address_city'], $_POST['address_state'], $_POST['address_zip'], $_POST['address_country'], $_POST['role'], date('Y-m-d H:i:s', strtotime($_POST['registered'])), $_POST['address_phone'],$_GET['id'] ]); + header('Location: index.php?page=accounts&success_msg=2'); + exit; + } + if (isset($_POST['delete'])) { + // Delete the account + $stmt = $pdo->prepare('DELETE FROM accounts WHERE id = ?'); + $stmt->execute([ $_GET['id'] ]); + header('Location: index.php?page=accounts&success_msg=3'); + exit; + } +} else { + // Create a new account + $page = 'Create'; + if (isset($_POST['submit'])) { + $password = password_hash($_POST['password'], PASSWORD_DEFAULT); + $stmt = $pdo->prepare('INSERT INTO accounts (email,password,first_name,last_name,address_street,address_city,address_state,address_zip,address_country,role,registered, address_phone) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'); + $stmt->execute([ $_POST['email'], $password, $_POST['first_name'], $_POST['last_name'], $_POST['address_street'], $_POST['address_city'], $_POST['address_state'], $_POST['address_zip'], $_POST['address_country'], $_POST['role'], date('Y-m-d H:i:s', strtotime($_POST['registered'])), $_POST['address_phone'] ]); + header('Location: index.php?page=accounts&success_msg=1'); + exit; + } +} +?> +=template_admin_header($page . ' Account', 'accounts', 'manage')?> + +
+ +=template_admin_footer()?> \ No newline at end of file diff --git a/admin/accounts.php b/admin/accounts.php new file mode 100644 index 0000000..49ca59f --- /dev/null +++ b/admin/accounts.php @@ -0,0 +1,138 @@ +prepare('SELECT COUNT(*) AS total FROM accounts a ' . $where); +if ($search) $stmt->bindParam('search', $param3, PDO::PARAM_STR); +$stmt->execute(); +$accounts_total = $stmt->fetchColumn(); +// SQL query to get all products from the "products" table +$stmt = $pdo->prepare('SELECT a.*, count(t.id) AS orders FROM accounts a LEFT JOIN transactions t ON t.account_id = a.id ' . $where . ' GROUP BY a.id, a.email, a.password, a.role, a.first_name, a.last_name, a.address_street, a.address_city, a.address_state, a.address_zip, a.address_country, a.registered ORDER BY ' . $order_by . ' ' . $order . ' LIMIT :start_results,:num_results'); +// Bind params +$stmt->bindParam('start_results', $param1, PDO::PARAM_INT); +$stmt->bindParam('num_results', $param2, PDO::PARAM_INT); +if ($search) $stmt->bindParam('search', $param3, PDO::PARAM_STR); +$stmt->execute(); +// Retrieve query results +$accounts = $stmt->fetchAll(PDO::FETCH_ASSOC); +// Handle success messages +if (isset($_GET['success_msg'])) { + if ($_GET['success_msg'] == 1) { + $success_msg = 'Account created successfully!'; + } + if ($_GET['success_msg'] == 2) { + $success_msg = 'Account updated successfully!'; + } + if ($_GET['success_msg'] == 3) { + $success_msg = 'Account deleted successfully!'; + } +} +// Determine the URL +$url = 'index.php?page=accounts&search=' . $search; +?> +=template_admin_header('Accounts', 'accounts', 'view')?> + +View, create, and edit accounts.
+=$success_msg?>
+ +| # | +Name | +Address | +Role | +Orders Placed | +Registered Date | +Actions | +|
| There are no accounts | +|||||||
| =$account['id']?> | +=htmlspecialchars($account['email'], ENT_QUOTES)?> | +=htmlspecialchars($account['first_name'], ENT_QUOTES)?> =htmlspecialchars($account['last_name'], ENT_QUOTES)?> | ++ =htmlspecialchars($account['address_street'], ENT_QUOTES)?>=$account['address_street']?', ':''?> + =htmlspecialchars($account['address_city'], ENT_QUOTES)?>=$account['address_city']?', ':''?> + =htmlspecialchars($account['address_state'], ENT_QUOTES)?>=$account['address_state']?', ':''?> + =htmlspecialchars($account['address_zip'], ENT_QUOTES)?>=$account['address_zip']?', ':''?> + =htmlspecialchars($account['address_country'], ENT_QUOTES)?> + | +=$account['role']?> | +=number_format($account['orders'])?> | +=date('F j, Y', strtotime($account['registered']))?> | +Edit | +
No media selected.
+No media selected.
`; + } + document.querySelectorAll('.media-library-modal .media .details input').forEach(element => element.onkeyup = () => document.querySelector('.media-library-modal .save-media').style.display = 'inline-flex'); + if (document.querySelector('.media-library-modal .save-media')) { + let mediaDetailsForm = document.querySelector('.media-library-modal .media-details-form'); + document.querySelector('.media-library-modal .save-media').onclick = event => { + event.preventDefault(); + fetch(mediaDetailsForm.action, { + method: 'POST', + body: new FormData(mediaDetailsForm) + }).then(response => response.json()).then(newData => { + this.media[i].title = newData[i].title; + this.media[i].caption = newData[i].caption; + this.media[i].full_path = newData[i].full_path; + this.media[i].date_uploaded = newData[i].date_uploaded; + data[i].title = newData[i].title; + data[i].caption = newData[i].caption; + data[i].full_path = newData[i].full_path; + data[i].date_uploaded = newData[i].date_uploaded; + document.querySelector('.media-library-modal .save-media').style.display = 'none'; + }); + }; + document.querySelector('.media-library-modal .delete-media').onclick = event => { + event.preventDefault(); + if (confirm('Are you sure you want to delete this media?')) { + fetch(mediaDetailsForm.action + '&delete=true').then(response => response.json()).then(newData => { + for (let j = 0; j < this.media.length; j++) { + for (let k = 0; k < newData.length; k++) { + if (this.media[j].id == newData[k].id && this.media[j].selected) { + newData[k].selected = true; + } + } + } + this.media = newData; + document.querySelector('.media-library-modal .media .details').innerHTML = `No media selected.
`; + this.populateMedia(); + }); + } + }; + } + }; + data[i].element = a; + document.querySelector('.media-library-modal .media .list').append(a); + } + this.getAllSelectedMedia().forEach(media => { + if (media.selected) this.selectMedia(media.id); + }); + if (document.querySelector('.media-library-modal .media .loader')) { + document.querySelector('.media-library-modal .media .loader').remove(); + } + }, + onOpen: function() { + fetch('index.php?page=api&action=media', { cache: 'no-store' }).then(response => response.json()).then(data => { + this.media = data; + this.populateMedia(); + if (options.onMediaLoad) options.onMediaLoad(); + }); + document.querySelector('.media-library-modal .upload-media').onclick = event => { + event.preventDefault(); + let input = document.createElement('input'); + input.type = 'file'; + input.multiple = 'multiple'; + input.accept = 'image/*'; + input.onchange = event => { + document.querySelector('.media-library-modal .upload-media').innerHTML = ''; + let form = new FormData(); + for (let i = 0; i < event.target.files.length; i++) { + form.append('file_' + i, event.target.files[i]); + } + form.append('total_files', event.target.files.length); + fetch('index.php?page=api&action=media', { + method: 'POST', + body: form + }).then(response => response.json()).then(data => { + for (let j = 0; j < this.media.length; j++) { + for (let k = 0; k < data.length; k++) { + if (this.media[j].id == data[k].id && this.media[j].selected) { + data[k].selected = true; + } + } + } + this.media = data; + this.populateMedia(); + document.querySelector('.media-library-modal .upload-media').innerHTML = 'Upload'; + }); + }; + input.click(); + }; + document.querySelector('.media-library-modal .search-media').onchange = () => { + document.querySelector('.media-library-modal .media .details').innerHTML = `No media selected.
`; + this.populateMedia(this.media.filter(media => { + return media.title.toLowerCase().includes(document.querySelector('.media-library-modal .search-media').value.toLowerCase()) + || media.caption.toLowerCase().includes(document.querySelector('.media-library-modal .search-media').value.toLowerCase()); + })); + this.getAllSelectedMedia().forEach(media => { + if (media.selected) this.selectMedia(media.id); + }); + }; + }, + onClose: function(event) { + if (options.onSave && event && event.button && event.button.classList.contains('save')) options.onSave(this.getAllSelectedMedia()); + if (options.onClose) options.onClose(); + } +}); +const openOptionsModal = options => modal({ + state: 'open', + selectedOptionContainer: null, + selectedOptionType: null, + modalTemplate: function() { + return ` + + `; + }, + optionTemplate: function(type) { + return ` +| Default Value | ' : 'Name | '} + ${type == 'text' || type == 'datetime' ? '' : 'Quantity | '} +Price | +Weight | + ${type == 'text' || type == 'datetime' ? '' : ''} + |
${optionsValuesHtml.replace(/, $/, '')}
+${optionsValuesHtml.replace(/, $/, '')}
+The file path must be relative to the shopping cart root directory.
+ + + +View, create, and edit categories.
+=$success_msg?>
+ +| Name | +Actions | +||||||
| There are no categories | +|||||||
View statistics, today's transactions, and more.
+=number_format(count($orders))?>
+=currency_code?>=number_format($order_stats['earnings'] ?? 0, 2)?>
+=number_format($accounts['total'])?>
+=number_format($products['total'])?>
+List of transactions for today.
+| # | +Customer | +Products | +Total | +Method | +Status | +Date | +Actions | +|
| There are no recent orders | +||||||||
| =$order['id']?> | +=htmlspecialchars($order['first_name'], ENT_QUOTES)?> =htmlspecialchars($order['last_name'], ENT_QUOTES)?> | +=htmlspecialchars($order['payer_email'], ENT_QUOTES)?> | +=$order['total_products']?> | +=currency_code?>=number_format($order['payment_amount'], 2)?> | +=$order['payment_method']?> | +=$order['payment_status']?> | +=date('F j, Y', strtotime($order['created']))?> | +View Edit | +
View, create, and edit discounts.
+=$success_msg?>
+ +| # | +Code | +Active | +Categories | +Products | +Type | +Value | +Start Date | +End Date | +Actions | +
| There are no discounts | +|||||||||
| =$discount['id']?> | +=$discount['discount_code']?> | +=$current_date >= strtotime($discount['start_date']) && $current_date <= strtotime($discount['end_date']) ? 'Yes' : 'No'?> | +=$discount['category_names'] ? str_replace(',', ', ', $discount['category_names']) : 'all'?> | +=$discount['product_names'] ? str_replace(',', ', ', $discount['product_names']) : 'all'?> | +=$discount['discount_type']?> | +=$discount['discount_value']?> | +=date('Y-m-d h:ia', strtotime($discount['start_date']))?> | +=date('Y-m-d h:ia', strtotime($discount['end_date']))?> | +Edit | +
View, manage, and search media files.
+=$success_msg?>
+ +=$order['id']?>
+=$order['txn_id']?>
+=$order['shipping_method'] ? htmlspecialchars($order['shipping_method'], ENT_QUOTES) : '--'?>
+=$order['payment_method']?>
+=$order['payment_status']?>
+=date('F j, Y H:ia', strtotime($order['created']))?>
+=htmlspecialchars($order['discount_code'], ENT_QUOTES)?>
+=htmlspecialchars($order['a_first_name'], ENT_QUOTES)?> =htmlspecialchars($order['a_last_name'], ENT_QUOTES)?>
+=htmlspecialchars($order['a_address_street'], ENT_QUOTES)?>
+ =htmlspecialchars($order['a_address_city'], ENT_QUOTES)?>
+ =htmlspecialchars($order['a_address_state'], ENT_QUOTES)?>
+ =htmlspecialchars($order['a_address_zip'], ENT_QUOTES)?>
+ =htmlspecialchars($order['a_address_country'], ENT_QUOTES)?>
+
=htmlspecialchars($order['a_address_phone'], ENT_QUOTES)?> +
+The order is not associated with an account.
+ +=htmlspecialchars($order['payer_email'], ENT_QUOTES)?>
+=htmlspecialchars($order['first_name'], ENT_QUOTES)?> =htmlspecialchars($order['last_name'], ENT_QUOTES)?>
+=htmlspecialchars($order['address_street'], ENT_QUOTES)?>
+ =htmlspecialchars($order['address_city'], ENT_QUOTES)?>
+ =htmlspecialchars($order['address_state'], ENT_QUOTES)?>
+ =htmlspecialchars($order['address_zip'], ENT_QUOTES)?>
+ =htmlspecialchars($order['address_country'], ENT_QUOTES)?>
+
=htmlspecialchars($order['a_address_phone'], ENT_QUOTES)?> +
+| Product | +Options | +Qty | +Price | +Total | +
| There are no order items | +||||
| =$item['productcode']?> =$item['name'] ? htmlspecialchars($item['name'], ENT_QUOTES) : '(Product ' . $item['item_id'] . ')'?> | +=$item['item_options'] ? htmlspecialchars(str_replace(',', ', ', $item['item_options']), ENT_QUOTES) : '--'?> | +=$item['item_quantity']?> | +=currency_code?>=number_format($item['item_price'], 2)?> | +=currency_code?>=number_format($item['item_price']*$item['item_quantity'], 2)?> | +
| + | ||||
| Subtotal | +=currency_code?>=number_format($subtotal, 2)?> | +|||
| Shipping | +=currency_code?>=number_format($order['shipping_amount'], 2)?> | +|||
| Discount | +=currency_code?>=number_format(($order['payment_amount']+$order['shipping_amount'])-($subtotal), 2)?> | +|||
| VAT | +=currency_code?>=number_format($order['tax_amount'], 2)?> | +|||
| Total | +=currency_code?>=number_format($order['payment_amount'], 2)?> | +|||
| Giftcard | +Valid | +Value | +||
| There are no order items | +||||
| =$giftcard['discount_code']?> | +=$current_date >= strtotime($giftcard['start_date']) && $current_date <= strtotime($giftcard['end_date']) ? 'Yes' : 'No'?> | +=currency_code?>=number_format($giftcard['discount_value'], 2)?> | +||
| + | ||||
| + + | ++ + | ++ + | +
View, create, and search orders.
+=$success_msg?>
+ +| # | +Customer | +Products | +Total | +Method | +Status | +Date | +Actions | +|
| There are no orders | +||||||||
| =$i['id']?> | +=htmlspecialchars($i['first_name'], ENT_QUOTES)?> =htmlspecialchars($i['last_name'], ENT_QUOTES)?> | +=htmlspecialchars($i['payer_email'], ENT_QUOTES)?> | +=$i['total_products']?> | +=currency_code?>=number_format($i['payment_amount'], 2)?> | +=$i['payment_method']?> | +=$i['payment_status']?> | +=date('F j, Y', strtotime($i['created']))?> | +View Edit | +
View, manage, and search products.
+=$success_msg?>
+ +| # | +Productcode | +Name | +Price | +Quantity | +Images | +Date Added | +Status | +Actions | +|
| There are no products | +|||||||||
| =$product['id']?> | +=$product['productcode']?> | +=$product['name']?> | + +=currency_code?>=number_format($product['price'], 2)?> | + +=currency_code?>=number_format($product['price'], 2)?> |
+
+ =$product['quantity']==-1?'--':number_format($product['quantity'])?> | +
+
+
+ |
+ =date('F j, Y', strtotime($product['date_added']))?> | +=$product['status'] ? 'Enabled' : 'Disabled'?> | +Edit | +
View, create, and edit shipping methods.
+=$success_msg?>
+ +| # | +Name | +Type | +Countries | +Price Range | +Weight Range | +Total Shipping Price | +Actions | +
| There are no shipping methods | +|||||||
| =$s['id']?> | +=$s['name']?> | +=$s['type']?> | +=$s['countries'] ? str_replace(',', ', ', $s['countries']) : 'all'?> | +=currency_code?>=number_format($s['price_from'], 2)?> - =currency_code?>=number_format($s['price_to'], 2)?> | +=number_format($s['weight_from'], 2)?> lbs - =number_format($s['weight_to'], 2)?> lbs | +=currency_code?>=number_format($s['price'], 2)?> | +Edit | +
View, create, and edit taxes.
+=$success_msg?>
+ +| # | +Country | +Tax Rate | +Actions | +
| There are no taxes | +|||
| =$tax['id']?> | +=$tax['country']?> | +=$tax['rate']?>% | +Edit | +
=implode('
', $errors)?>
=$account_available?> =$account_log_in?>
+ + + + +|
+
+ |
+
+ $company_name + $company_adres + $company_postal $company_city + $company_country + + $company_email + + Bank: $company_bank + KvK: $company_kvk + |
+
|
+ =$address_name?> + =$address_street?> + =$address_zip?>, =$address_city?> + =$address_country?> + |
+ |
+ =$order_email_message_1?>+=$order_email_message_2?> |
+
+ Order: =$order_id?> +Date: |
+
| =$tr_product?> | +Opties | +=$tr_quantity?> | +=$tr_price?> | +=$tr_total?> | +
| =$product['meta']['name']?> | +=$product['options']?> | +=$product['quantity']?> | +=currency_code?>=number_format($product['final_price'],2)?> | +=number_format($product['final_price'] * $product['quantity'],2)?> | +
| + | ||||
| =$total_subtotal?> | +=currency_code?>=number_format($subtotal,2)?> | +|||
| =$total_discount?> | +=currency_code?>=number_format($discounttotal,2)?> | +|||
| =$total_shipping?> | +=currency_code?>=number_format($shippingtotal,2)?> | +|||
| =$total_vat?> | +=currency_code?>=number_format($taxtotal,2)?> | +|||
| =$total_total?> | +=currency_code?>=number_format($total,2)?> | +|||
Date:
| =$tr_product?> | +Options | +=$tr_quantity?> | +=$tr_price?> | +=$tr_total?> | +
| =$product['name']?> | +=$product['item_options']?> | +=$product['item_quantity']?> | +=currency_code?> =number_format($product['item_price'],2)?> | +=currency_code?> =number_format($product['item_price'] * $product['item_quantity'],2)?> | +
| + | ||||
| =$total_subtotal?> | +=currency_code?> =number_format($subtotal,2)?> | +|||
| =$total_discount?> | +=currency_code?> =number_format($total-($subtotal+$shippingtotal),2)?> | +|||
| =$total_shipping?> | +=currency_code?>=number_format($shippingtotal,2)?> | +|||
| =$total_vat?> | +=currency_code?>=number_format($taxtotal,2)?> | +|||
| =$total_total?> | +=currency_code?> =number_format($total,2)?> | +|||
Het totaalbedrag van deze factuur is betaald
+ '; + } + ?> \ No newline at end of file diff --git a/custom/email/order-notification-template.php b/custom/email/order-notification-template.php new file mode 100644 index 0000000..990cb5c --- /dev/null +++ b/custom/email/order-notification-template.php @@ -0,0 +1,79 @@ + + +=template_order_email_header()?> + + + +=$order_email_message_2?>
Order: =$order_id?>
+Date:
| =$tr_product?> | +Options | +=$tr_quantity?> | +=$tr_price?> | +=$tr_total?> | +
| =$product['meta']['name']?> | +=$product['options']?> | +=$product['quantity']?> | +=currency_code?>=number_format($product['final_price'],2)?> | +=number_format($product['final_price'] * $product['quantity'],2)?> | +
| + | ||||
| =$total_subtotal?> | +=currency_code?>=number_format($subtotal,2)?> | +|||
| =$total_discount?> | +=currency_code?>=number_format($discounttotal,2)?> | +|||
| =$total_shipping?> | +=currency_code?>=number_format($shippingtotal,2)?> | +|||
| =$total_vat?> | +=currency_code?>=number_format($taxtotal,2)?> | +|||
| =$total_total?> | +=currency_code?>=number_format($total,2)?> | +|||
+ =$about_1_p?> +
++ =$about_2_p?> +
++ =$about_morval_1_p?> +
++ =$about_morval_2_p?> +
++Your watch will be shipped within three business Days of placing your order. Most customers receive their watches 3-7 business days after their order. In case your watch isn’t in stock, you will receive an e-mail with the expected date of delivery. +
+ ++Shipping is free of charge! +
+ ++We ship anywhere in the world. +
++For orders shipped outside of Europe, the customs office or other authorities in the destination country may apply import duties, custom fees, or other fees, duties, or taxes to the order. Customers are solely responsible for these charges. +
+ ++You may return any item for a refund or exchange, for any reason, within 14 days of receipt. Refunds or exchanges will only be issued for items returned in new, undamaged, re-sellable condition, with all original packaging and protective coverings. Return shipping and costs is the responsibility of the customer. +
+ ++Morval watches are guaranteed to be free of defects and work well for at least two years. If you have any problems within two years of the date of purchase, we will repair or replace your watch for free. Damage caused by accidents, mishandling, tampering, etc., will void your warranty. +
+E-mail us with any questions or warranty claims.
+ ++ For proper use please see our user guide. +
+ ++You don’t need to do much. Every few years, it’s a good idea to bring it for a tune up and lubrication check at your local watch repair shop. Clean with soap and water or a dry cloth as needed. Avoid getting leather straps wet. +
+ +Please connect with us
+ +Let us know what you have in mind.
+ +If you have other questions, please contact us by e-mail
+MorvalWatches can be reached via https://www.morvalwatches.com
+Mr R. van Wezel is the Data Protection Officer of MorvalWatches. He can be reached via info@morvalwatches.com
+ +
MorvalWatches processes your personal data because you use our services and/or because you provide it to us yourself.
+Below you will find an overview of the personal data we process:
+Our website and/or service does not intend to collect data about website visitors under the age of 16. Unless they have parental or guardian consent. However, we cannot check whether a visitor is older than 16. We therefore advise parents to be involved in the online activities of their children, in order to prevent data about children from being collected without parental consent. If you are convinced that we have collected personal information about a minor without this permission, please contact us at info@morvalwatches.com and we will delete this information.
+ +MorvalWatches processes your personal data for the following purposes:
+MorvalWatches does not store your personal data longer than is strictly necessary to achieve the purposes for which your data is collected. We use a retention period of 3 years for personal data.
+ +MorvalWatches only provides to third parties and only if this is necessary for the execution of our agreement with you or to comply with a legal obligation.
+ +MorvalWatches does not use cookies or similar techniques.
+ + +You have the right to view, correct or delete your personal data. In addition, you have the right to withdraw your consent to the data processing or to object to the processing of your personal data by MorvalWatches and you have the right to data portability. This means that you can submit a request to us to send the personal data we hold about you in a computer file to you or another organization mentioned by you.
+You can send a request for inspection, correction, deletion, data transfer of your personal data or request for withdrawal of your consent or objection to the processing of your personal data to info@morvalwatches.com.
+To ensure that the request for inspection has been made by you, we ask you to enclose a copy of your proof of identity with the request. Make your passport photo, MRZ (machine readable zone, the strip with numbers at the bottom of the passport), passport number and citizen service number (BSN) black in this copy. This is to protect your privacy. We will respond to your request as quickly as possible, but within four weeks.
+MorvalWatches would also like to point out that you have the option of submitting a complaint to the national supervisory authority, the Dutch Data Protection Authority. This can be done via the following link: https://autoriteitpersoonsgegevens.nl/nl/contact-met-de-autoriteit-persoonsgegevens/tip-ons
+ +MorvalWatches takes the protection of your data seriously and takes appropriate measures to prevent misuse, loss, unauthorized access, unwanted disclosure and unauthorized modification. If you have the impression that your data is not properly secured or there are indications of abuse, please contact our customer service or via info@morvalwatches.com
+ +'.$delivery_status.'
+ '.$type.'>'; +} + +function consent() + { + include './custom/translations_'.strtoupper(language_code).'.php'; + + $age_consent = ' + + '; + + return $age_consent; + } + + function banner() + { + include './custom/translations_'.strtoupper(language_code).'.php'; + + $banner = ' + + '; + + return $banner; + } + +function maintenanceMode() +{ +include './custom/translations_'.strtoupper(language_code).'.php'; + +$maintenanceMode = ' + +'; + +return $maintenanceMode; +} +//++++++++++++++++++++++++++++++++++++++++ +//HomePage Products +//++++++++++++++++++++++++++++++++++++++++ +function getPictureID($pdo,$id,$config){ + $stmt = $pdo->prepare('SELECT * FROM products_media where product_id = :product_id ORDER BY position ASC'); + $stmt->bindValue(':product_id', $id, PDO::PARAM_INT); + $stmt->execute(); + $product_media = $stmt->fetchAll(PDO::FETCH_ASSOC); + + //Search for option_id + $option_profile = json_decode($config,true) ?? ''; + if (!empty($option_profile) && $option_profile !=''){ + foreach ($option_profile as $option){ + if ($option['IMG_large_id'] == $product_media[0]['media_id']){ + return $option['option_id']; + } + } + } +} + +//++++++++++++++++++++++++++++++++++++++++ +//HomePage Products +//++++++++++++++++++++++++++++++++++++++++ +function highlightedProducts($pdo,$categoryID,$range){ + + include './custom/translations_'.strtoupper(language_code).'.php'; + + $stmt = $pdo->prepare('SELECT p.*, (SELECT m.full_path FROM products_media pm JOIN media m ON m.id = pm.media_id WHERE pm.product_id = p.id ORDER BY pm.position ASC LIMIT 1) AS img FROM products p JOIN products_categories pc ON pc.category_id = :category_id AND pc.product_id = p.id JOIN categories c ON c.id = pc.category_id WHERE p.status = 1'); + $stmt->bindValue(':category_id', $categoryID, PDO::PARAM_INT); + $stmt->execute(); + $products = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $view = ' +'.$stock_status.'
+ '; + + + //Remove first characters from Productname + if (product_truncate_text != ''){ + $productname = str_replace(product_truncate_text,'',$product['name']); + $productname = (product_truncate !=0)? substr($productname,product_truncate):$productname; + } else { + $productname = $product['name']; + } + + //ADD related optionID when configuration is found + if (empty($product['product_config'])){ + $option_id = ''; + }else { + $option_id = '/'.getPictureID($pdo,$product['id'],$product['product_config']); + } + $view .= ' + + '.$productname.' + '.currency_code.number_format($product['price'],2); + if ($product['rrp'] > 0) { + $view .= ''.currency_code.number_format($product['rrp'],2).''; + } + $view .= ' + + ++
+'.show_offer_home_text.'
+ + '; + } + ?> ++ Morval Watches are unique, robust, stylish and timeless timepieces that will last for generations! +
+ shop now > ++ Morval watches meet the highest quality requirements and can compete with the well-known Swiss brands. The parts are supplied by renowned manufacturers from Europe and beyond. A Morval contains a Swiss-made caliber (STP) that is known for its reliable quality. +
+ shop now > ++ Morval stands for an excellent price-quality ratio +
+ shop now > +| + | This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and distributed for any purpose and without charge, with or without modification, provided that all copyright notices are retained; that the AFM files are not distributed without this file; that all modifications to this file or any of the AFM files are prominently noted in the modified file(s); and that this paragraph is not modified. Adobe Systems has no responsibility or obligation to support the use of the AFM files. Col | +
Source http://www.adobe.com/devnet/font/#pcfi
+ + \ No newline at end of file diff --git a/lib/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.png b/lib/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.png new file mode 100644 index 0000000..771a1a3 Binary files /dev/null and b/lib/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.png differ diff --git a/lib/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.svg b/lib/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.svg new file mode 100644 index 0000000..83ba7e7 --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.svg @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/lib/dompdf/vendor/dompdf/dompdf/lib/res/html.css b/lib/dompdf/vendor/dompdf/dompdf/lib/res/html.css new file mode 100644 index 0000000..b3aae08 --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/lib/res/html.css @@ -0,0 +1,518 @@ +/** + * dompdf default stylesheet. + * + * @package dompdf + * @link https://github.com/dompdf/dompdf + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * + * Portions from Mozilla + * @link https://dxr.mozilla.org/mozilla-central/source/layout/style/res/html.css + * @license http://mozilla.org/MPL/2.0/ Mozilla Public License, v. 2.0 + * + * Portions from W3C + * @link https://www.w3.org/TR/css-ui-3/#default-style-sheet + * + */ + +@page { + margin: 1.2cm; +} + +html { + display: -dompdf-page !important; + counter-reset: page; +} + +/* blocks */ + +article, +aside, +details, +div, +dt, +figcaption, +footer, +form, +header, +hgroup, +main, +nav, +noscript, +section, +summary { + display: block; +} + +body { + page-break-before: avoid; + display: block !important; + counter-increment: page; +} + +p, dl, multicol { + display: block; + margin: 1em 0; +} + +dd { + display: block; + margin-left: 40px; +} + +blockquote, figure { + display: block; + margin: 1em 40px; +} + +address { + display: block; + font-style: italic; +} + +center { + display: block; + text-align: center; +} + +blockquote[type=cite] { + display: block; + margin: 1em 0; + padding-left: 1em; + border-left: solid; + border-color: blue; + border-width: thin; +} + +h1, h2, h3, h4, h5, h6 { + display: block; + font-weight: bold; +} + +h1 { + font-size: 2em; + margin: .67em 0; +} + +h2 { + font-size: 1.5em; + margin: .83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.67em; + margin: 2.33em 0; +} + +listing { + display: block; + font-family: fixed; + font-size: medium; + white-space: pre; + margin: 1em 0; +} + +plaintext, pre, xmp { + display: block; + font-family: fixed; + white-space: pre; + margin: 1em 0; +} + +/* tables */ + +table { + display: table; + border-spacing: 2px; + border-collapse: separate; + margin-top: 0; + margin-bottom: 0; + text-indent: 0; + text-align: left; /* quirk */ +} + +table[border] { + border: outset gray; +} + +table[border] td, +table[border] th { + border: 1px inset gray; +} + +table[border="0"] td, +table[border="0"] th { + border-width: 0; +} + +/* make sure backgrounds are inherited in tables -- see bug 4510 */ +td, th, tr { + background: inherit; +} + +/* caption inherits from table not table-outer */ +caption { + display: table-caption; + text-align: center; +} + +tr { + display: table-row; + vertical-align: inherit; +} + +col { + display: table-column; +} + +colgroup { + display: table-column-group; +} + +tbody { + display: table-row-group; + vertical-align: middle; +} + +thead { + display: table-header-group; + vertical-align: middle; +} + +tfoot { + display: table-footer-group; + vertical-align: middle; +} + +/* To simulate tbody auto-insertion */ +table > tr { + vertical-align: middle; +} + +td { + display: table-cell; + vertical-align: inherit; + text-align: inherit; + padding: 1px; +} + +th { + display: table-cell; + vertical-align: inherit; + text-align: center; + font-weight: bold; + padding: 1px; +} + +/* inlines */ + +q::before { + content: open-quote; +} + +q::after { + content: close-quote; +} + +:link { + color: #00c; + text-decoration: underline; +} + +b, strong { + font-weight: bolder; +} + +i, cite, em, var, dfn { + font-style: italic; +} + +tt, code, kbd, samp { + font-family: fixed; +} + +u, ins { + text-decoration: underline; +} + +s, strike, del { + text-decoration: line-through; +} + +big { + font-size: larger; +} + +small { + font-size: smaller; +} + +sub { + vertical-align: sub; + font-size: smaller; + line-height: normal; +} + +sup { + vertical-align: super; + font-size: smaller; + line-height: normal; +} + +nobr { + white-space: nowrap; +} + +mark { + background: yellow; + color: black; +} + +/* titles */ + +abbr[title], acronym[title] { + text-decoration: dotted underline; +} + +/* lists */ + +ul, menu, dir { + display: block; + list-style-type: disc; + margin: 1em 0; + padding-left: 40px; +} + +ol { + display: block; + list-style-type: decimal; + margin: 1em 0; + padding-left: 40px; +} + +li { + display: list-item; +} + +/*li::before { + display: -dompdf-list-bullet !important; + content: counter(-dompdf-default-counter) ". "; + padding-right: 0.5em; +}*/ + +/* nested lists have no top/bottom margins */ +:matches(ul, ol, dir, menu, dl) ul, +:matches(ul, ol, dir, menu, dl) ol, +:matches(ul, ol, dir, menu, dl) dir, +:matches(ul, ol, dir, menu, dl) menu, +:matches(ul, ol, dir, menu, dl) dl { + margin-top: 0; + margin-bottom: 0; +} + +/* 2 deep unordered lists use a circle */ +:matches(ul, ol, dir, menu) ul, +:matches(ul, ol, dir, menu) ul, +:matches(ul, ol, dir, menu) ul, +:matches(ul, ol, dir, menu) ul { + list-style-type: circle; +} + +/* 3 deep (or more) unordered lists use a square */ +:matches(ul, ol, dir, menu) :matches(ul, ol, dir, menu) ul, +:matches(ul, ol, dir, menu) :matches(ul, ol, dir, menu) menu, +:matches(ul, ol, dir, menu) :matches(ul, ol, dir, menu) dir { + list-style-type: square; +} + +/* forms */ +/* From https://www.w3.org/TR/css-ui-3/#default-style-sheet */ +form { + display: block; +} + +input, button, select { + display: inline-block; + font-family: sans-serif; +} + +input[type=text], +input[type=password], +select { + width: 12em; +} + +input[type=text], +input[type=password], +input[type=button], +input[type=submit], +input[type=reset], +input[type=file], +button, +textarea, +select { + background: #FFF; + border: 1px solid #999; + padding: 2px; + margin: 2px; +} + +input[type=button], +input[type=submit], +input[type=reset], +input[type=file], +button { + background: #CCC; + text-align: center; +} + +input[type=file] { + width: 8em; +} + +input[type=text]::before, +input[type=button]::before, +input[type=submit]::before, +input[type=reset]::before { + content: attr(value); +} + +input[type=file]::before { + content: "Choose a file"; +} + +input[type=password][value]::before { + font-family: "DejaVu Sans" !important; + content: "\2022\2022\2022\2022\2022\2022\2022\2022"; + line-height: 1em; +} + +input[type=checkbox], +input[type=radio], +select::after { + font-family: "DejaVu Sans" !important; + font-size: 18px; + line-height: 1; +} + +input[type=checkbox]::before { + content: "\2610"; +} + +input[type=checkbox][checked]::before { + content: "\2611"; +} + +input[type=radio]::before { + content: "\25CB"; +} + +input[type=radio][checked]::before { + content: "\25C9"; +} + +textarea { + display: block; + height: 3em; + overflow: hidden; + font-family: monospace; + white-space: pre-wrap; + word-wrap: break-word; +} + +select { + position: relative!important; + overflow: hidden!important; +} + +select::after { + position: absolute; + right: 0; + top: 0; + height: 5em; + width: 1.4em; + text-align: center; + background: #CCC; + content: "\25BE"; +} + +select option { + display: none; +} + +select option[selected] { + display: inline; +} + +fieldset { + display: block; + margin: 0.6em 2px 2px; + padding: 0.75em; + border: 1pt groove #666; + position: relative; +} + +fieldset > legend { + position: absolute; + top: -0.6em; + left: 0.75em; + padding: 0 0.3em; + background: white; +} + +legend { + display: inline-block; +} + +/* leafs */ + +hr { + display: block; + height: 0; + border: 1px inset; + margin: 0.5em auto 0.5em auto; +} + +hr[size="1"] { + border-style: solid none none none; +} + +iframe { + border: 2px inset; +} + +noframes { + display: block; +} + +br { + display: -dompdf-br; +} + +img, img_generated { + display: -dompdf-image !important; +} + +dompdf_generated { + display: inline; +} + +/* hidden elements */ +area, base, basefont, head, meta, script, style, title, +noembed, param { + display: none; + -dompdf-keep: yes; +} diff --git a/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/CPDF.php b/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/CPDF.php new file mode 100644 index 0000000..13802b6 --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/CPDF.php @@ -0,0 +1,944 @@ + [0.0, 0.0, 4767.87, 6740.79], + "2a0" => [0.0, 0.0, 3370.39, 4767.87], + "a0" => [0.0, 0.0, 2383.94, 3370.39], + "a1" => [0.0, 0.0, 1683.78, 2383.94], + "a2" => [0.0, 0.0, 1190.55, 1683.78], + "a3" => [0.0, 0.0, 841.89, 1190.55], + "a4" => [0.0, 0.0, 595.28, 841.89], + "a5" => [0.0, 0.0, 419.53, 595.28], + "a6" => [0.0, 0.0, 297.64, 419.53], + "a7" => [0.0, 0.0, 209.76, 297.64], + "a8" => [0.0, 0.0, 147.40, 209.76], + "a9" => [0.0, 0.0, 104.88, 147.40], + "a10" => [0.0, 0.0, 73.70, 104.88], + "b0" => [0.0, 0.0, 2834.65, 4008.19], + "b1" => [0.0, 0.0, 2004.09, 2834.65], + "b2" => [0.0, 0.0, 1417.32, 2004.09], + "b3" => [0.0, 0.0, 1000.63, 1417.32], + "b4" => [0.0, 0.0, 708.66, 1000.63], + "b5" => [0.0, 0.0, 498.90, 708.66], + "b6" => [0.0, 0.0, 354.33, 498.90], + "b7" => [0.0, 0.0, 249.45, 354.33], + "b8" => [0.0, 0.0, 175.75, 249.45], + "b9" => [0.0, 0.0, 124.72, 175.75], + "b10" => [0.0, 0.0, 87.87, 124.72], + "c0" => [0.0, 0.0, 2599.37, 3676.54], + "c1" => [0.0, 0.0, 1836.85, 2599.37], + "c2" => [0.0, 0.0, 1298.27, 1836.85], + "c3" => [0.0, 0.0, 918.43, 1298.27], + "c4" => [0.0, 0.0, 649.13, 918.43], + "c5" => [0.0, 0.0, 459.21, 649.13], + "c6" => [0.0, 0.0, 323.15, 459.21], + "c7" => [0.0, 0.0, 229.61, 323.15], + "c8" => [0.0, 0.0, 161.57, 229.61], + "c9" => [0.0, 0.0, 113.39, 161.57], + "c10" => [0.0, 0.0, 79.37, 113.39], + "ra0" => [0.0, 0.0, 2437.80, 3458.27], + "ra1" => [0.0, 0.0, 1729.13, 2437.80], + "ra2" => [0.0, 0.0, 1218.90, 1729.13], + "ra3" => [0.0, 0.0, 864.57, 1218.90], + "ra4" => [0.0, 0.0, 609.45, 864.57], + "sra0" => [0.0, 0.0, 2551.18, 3628.35], + "sra1" => [0.0, 0.0, 1814.17, 2551.18], + "sra2" => [0.0, 0.0, 1275.59, 1814.17], + "sra3" => [0.0, 0.0, 907.09, 1275.59], + "sra4" => [0.0, 0.0, 637.80, 907.09], + "letter" => [0.0, 0.0, 612.00, 792.00], + "half-letter" => [0.0, 0.0, 396.00, 612.00], + "legal" => [0.0, 0.0, 612.00, 1008.00], + "ledger" => [0.0, 0.0, 1224.00, 792.00], + "tabloid" => [0.0, 0.0, 792.00, 1224.00], + "executive" => [0.0, 0.0, 521.86, 756.00], + "folio" => [0.0, 0.0, 612.00, 936.00], + "commercial #10 envelope" => [0.0, 0.0, 684.00, 297.00], + "catalog #10 1/2 envelope" => [0.0, 0.0, 648.00, 864.00], + "8.5x11" => [0.0, 0.0, 612.00, 792.00], + "8.5x14" => [0.0, 0.0, 612.00, 1008.00], + "11x17" => [0.0, 0.0, 792.00, 1224.00], + ]; + + /** + * The Dompdf object + * + * @var Dompdf + */ + protected $_dompdf; + + /** + * Instance of Cpdf class + * + * @var \Dompdf\Cpdf + */ + protected $_pdf; + + /** + * PDF width, in points + * + * @var float + */ + protected $_width; + + /** + * PDF height, in points + * + * @var float + */ + protected $_height; + + /** + * Current page number + * + * @var int + */ + protected $_page_number; + + /** + * Total number of pages + * + * @var int + */ + protected $_page_count; + + /** + * Array of pages for accessing after rendering is initially complete + * + * @var array + */ + protected $_pages; + + /** + * Currently-applied opacity level (0 - 1) + * + * @var float + */ + protected $_current_opacity = 1; + + public function __construct($paper = "letter", string $orientation = "portrait", ?Dompdf $dompdf = null) + { + if (is_array($paper)) { + $size = array_map("floatval", $paper); + } else { + $paper = strtolower($paper); + $size = self::$PAPER_SIZES[$paper] ?? self::$PAPER_SIZES["letter"]; + } + + if (strtolower($orientation) === "landscape") { + [$size[2], $size[3]] = [$size[3], $size[2]]; + } + + if ($dompdf === null) { + $this->_dompdf = new Dompdf(); + } else { + $this->_dompdf = $dompdf; + } + + $this->_pdf = new \Dompdf\Cpdf( + $size, + true, + $this->_dompdf->getOptions()->getFontCache(), + $this->_dompdf->getOptions()->getTempDir() + ); + + $this->_pdf->addInfo("Producer", sprintf("%s + CPDF", $this->_dompdf->version)); + $time = substr_replace(date('YmdHisO'), '\'', -2, 0) . '\''; + $this->_pdf->addInfo("CreationDate", "D:$time"); + $this->_pdf->addInfo("ModDate", "D:$time"); + + $this->_width = $size[2] - $size[0]; + $this->_height = $size[3] - $size[1]; + + $this->_page_number = $this->_page_count = 1; + + $this->_pages = [$this->_pdf->getFirstPageId()]; + } + + public function get_dompdf() + { + return $this->_dompdf; + } + + /** + * Returns the Cpdf instance + * + * @return \Dompdf\Cpdf + */ + public function get_cpdf() + { + return $this->_pdf; + } + + public function add_info(string $label, string $value): void + { + $this->_pdf->addInfo($label, $value); + } + + /** + * Opens a new 'object' + * + * While an object is open, all drawing actions are recorded in the object, + * as opposed to being drawn on the current page. Objects can be added + * later to a specific page or to several pages. + * + * The return value is an integer ID for the new object. + * + * @see CPDF::close_object() + * @see CPDF::add_object() + * + * @return int + */ + public function open_object() + { + $ret = $this->_pdf->openObject(); + $this->_pdf->saveState(); + return $ret; + } + + /** + * Reopens an existing 'object' + * + * @see CPDF::open_object() + * @param int $object the ID of a previously opened object + */ + public function reopen_object($object) + { + $this->_pdf->reopenObject($object); + $this->_pdf->saveState(); + } + + /** + * Closes the current 'object' + * + * @see CPDF::open_object() + */ + public function close_object() + { + $this->_pdf->restoreState(); + $this->_pdf->closeObject(); + } + + /** + * Adds a specified 'object' to the document + * + * $object int specifying an object created with {@link + * CPDF::open_object()}. $where can be one of: + * - 'add' add to current page only + * - 'all' add to every page from the current one onwards + * - 'odd' add to all odd numbered pages from now on + * - 'even' add to all even numbered pages from now on + * - 'next' add the object to the next page only + * - 'nextodd' add to all odd numbered pages from the next one + * - 'nexteven' add to all even numbered pages from the next one + * + * @see Cpdf::addObject() + * + * @param int $object + * @param string $where + */ + public function add_object($object, $where = 'all') + { + $this->_pdf->addObject($object, $where); + } + + /** + * Stops the specified 'object' from appearing in the document. + * + * The object will stop being displayed on the page following the current + * one. + * + * @param int $object + */ + public function stop_object($object) + { + $this->_pdf->stopObject($object); + } + + /** + * Serialize the pdf object's current state for retrieval later + */ + public function serialize_object($id) + { + return $this->_pdf->serializeObject($id); + } + + public function reopen_serialized_object($obj) + { + return $this->_pdf->restoreSerializedObject($obj); + } + + //........................................................................ + + public function get_width() + { + return $this->_width; + } + + public function get_height() + { + return $this->_height; + } + + public function get_page_number() + { + return $this->_page_number; + } + + public function get_page_count() + { + return $this->_page_count; + } + + /** + * Sets the current page number + * + * @param int $num + */ + public function set_page_number($num) + { + $this->_page_number = $num; + } + + public function set_page_count($count) + { + $this->_page_count = $count; + } + + /** + * Sets the stroke color + * + * See {@link Style::set_color()} for the format of the color array. + * + * @param array $color + */ + protected function _set_stroke_color($color) + { + $this->_pdf->setStrokeColor($color); + $alpha = isset($color["alpha"]) ? $color["alpha"] : 1; + $alpha *= $this->_current_opacity; + $this->_set_line_transparency("Normal", $alpha); + } + + /** + * Sets the fill colour + * + * See {@link Style::set_color()} for the format of the colour array. + * + * @param array $color + */ + protected function _set_fill_color($color) + { + $this->_pdf->setColor($color); + $alpha = isset($color["alpha"]) ? $color["alpha"] : 1; + $alpha *= $this->_current_opacity; + $this->_set_fill_transparency("Normal", $alpha); + } + + /** + * Sets line transparency + * @see Cpdf::setLineTransparency() + * + * Valid blend modes are (case-sensitive): + * + * Normal, Multiply, Screen, Overlay, Darken, Lighten, + * ColorDodge, ColorBurn, HardLight, SoftLight, Difference, + * Exclusion + * + * @param string $mode the blending mode to use + * @param float $opacity 0.0 fully transparent, 1.0 fully opaque + */ + protected function _set_line_transparency($mode, $opacity) + { + $this->_pdf->setLineTransparency($mode, $opacity); + } + + /** + * Sets fill transparency + * @see Cpdf::setFillTransparency() + * + * Valid blend modes are (case-sensitive): + * + * Normal, Multiply, Screen, Overlay, Darken, Lighten, + * ColorDogde, ColorBurn, HardLight, SoftLight, Difference, + * Exclusion + * + * @param string $mode the blending mode to use + * @param float $opacity 0.0 fully transparent, 1.0 fully opaque + */ + protected function _set_fill_transparency($mode, $opacity) + { + $this->_pdf->setFillTransparency($mode, $opacity); + } + + /** + * Sets the line style + * + * @see Cpdf::setLineStyle() + * + * @param float $width + * @param string $cap + * @param string $join + * @param array $dash + */ + protected function _set_line_style($width, $cap, $join, $dash) + { + $this->_pdf->setLineStyle($width, $cap, $join, $dash); + } + + public function set_opacity(float $opacity, string $mode = "Normal"): void + { + $this->_set_line_transparency($mode, $opacity); + $this->_set_fill_transparency($mode, $opacity); + $this->_current_opacity = $opacity; + } + + public function set_default_view($view, $options = []) + { + array_unshift($options, $view); + call_user_func_array([$this->_pdf, "openHere"], $options); + } + + /** + * Remaps y coords from 4th to 1st quadrant + * + * @param float $y + * @return float + */ + protected function y($y) + { + return $this->_height - $y; + } + + public function line($x1, $y1, $x2, $y2, $color, $width, $style = [], $cap = "butt") + { + $this->_set_stroke_color($color); + $this->_set_line_style($width, $cap, "", $style); + + $this->_pdf->line($x1, $this->y($y1), + $x2, $this->y($y2)); + $this->_set_line_transparency("Normal", $this->_current_opacity); + } + + public function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt") + { + $this->_set_stroke_color($color); + $this->_set_line_style($width, $cap, "", $style); + + $this->_pdf->ellipse($x, $this->y($y), $r1, $r2, 0, 8, $astart, $aend, false, false, true, false); + $this->_set_line_transparency("Normal", $this->_current_opacity); + } + + public function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt") + { + $this->_set_stroke_color($color); + $this->_set_line_style($width, $cap, "", $style); + $this->_pdf->rectangle($x1, $this->y($y1) - $h, $w, $h); + $this->_set_line_transparency("Normal", $this->_current_opacity); + } + + public function filled_rectangle($x1, $y1, $w, $h, $color) + { + $this->_set_fill_color($color); + $this->_pdf->filledRectangle($x1, $this->y($y1) - $h, $w, $h); + $this->_set_fill_transparency("Normal", $this->_current_opacity); + } + + public function clipping_rectangle($x1, $y1, $w, $h) + { + $this->_pdf->clippingRectangle($x1, $this->y($y1) - $h, $w, $h); + } + + public function clipping_roundrectangle($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL) + { + $this->_pdf->clippingRectangleRounded($x1, $this->y($y1) - $h, $w, $h, $rTL, $rTR, $rBR, $rBL); + } + + public function clipping_polygon(array $points): void + { + // Adjust y values + for ($i = 1; $i < count($points); $i += 2) { + $points[$i] = $this->y($points[$i]); + } + + $this->_pdf->clippingPolygon($points); + } + + public function clipping_end() + { + $this->_pdf->clippingEnd(); + } + + public function save() + { + $this->_pdf->saveState(); + } + + public function restore() + { + $this->_pdf->restoreState(); + } + + public function rotate($angle, $x, $y) + { + $this->_pdf->rotate($angle, $x, $y); + } + + public function skew($angle_x, $angle_y, $x, $y) + { + $this->_pdf->skew($angle_x, $angle_y, $x, $y); + } + + public function scale($s_x, $s_y, $x, $y) + { + $this->_pdf->scale($s_x, $s_y, $x, $y); + } + + public function translate($t_x, $t_y) + { + $this->_pdf->translate($t_x, $t_y); + } + + public function transform($a, $b, $c, $d, $e, $f) + { + $this->_pdf->transform([$a, $b, $c, $d, $e, $f]); + } + + public function polygon($points, $color, $width = null, $style = [], $fill = false) + { + $this->_set_fill_color($color); + $this->_set_stroke_color($color); + + if (!$fill && isset($width)) { + $this->_set_line_style($width, "square", "miter", $style); + } + + // Adjust y values + for ($i = 1; $i < count($points); $i += 2) { + $points[$i] = $this->y($points[$i]); + } + + $this->_pdf->polygon($points, $fill); + + $this->_set_fill_transparency("Normal", $this->_current_opacity); + $this->_set_line_transparency("Normal", $this->_current_opacity); + } + + public function circle($x, $y, $r, $color, $width = null, $style = [], $fill = false) + { + $this->_set_fill_color($color); + $this->_set_stroke_color($color); + + if (!$fill && isset($width)) { + $this->_set_line_style($width, "round", "round", $style); + } + + $this->_pdf->ellipse($x, $this->y($y), $r, 0, 0, 8, 0, 360, 1, $fill); + + $this->_set_fill_transparency("Normal", $this->_current_opacity); + $this->_set_line_transparency("Normal", $this->_current_opacity); + } + + /** + * Convert image to a PNG image + * + * @param string $image_url + * @param string $type + * + * @return string|null The url of the newly converted image + */ + protected function _convert_to_png($image_url, $type) + { + $filename = Cache::getTempImage($image_url); + + if ($filename !== null && file_exists($filename)) { + return $filename; + } + + $func_name = "imagecreatefrom$type"; + + set_error_handler([Helpers::class, "record_warnings"]); + + if (!function_exists($func_name)) { + if (!method_exists(Helpers::class, $func_name)) { + throw new Exception("Function $func_name() not found. Cannot convert $type image: $image_url. Please install the image PHP extension."); + } + $func_name = [Helpers::class, $func_name]; + } + + try { + $im = call_user_func($func_name, $image_url); + + if ($im) { + imageinterlace($im, false); + + $tmp_dir = $this->_dompdf->getOptions()->getTempDir(); + $tmp_name = @tempnam($tmp_dir, "{$type}_dompdf_img_"); + @unlink($tmp_name); + $filename = "$tmp_name.png"; + + imagepng($im, $filename); + imagedestroy($im); + } else { + $filename = null; + } + } finally { + restore_error_handler(); + } + + if ($filename !== null) { + Cache::addTempImage($image_url, $filename); + } + + return $filename; + } + + public function image($img, $x, $y, $w, $h, $resolution = "normal") + { + [$width, $height, $type] = Helpers::dompdf_getimagesize($img, $this->get_dompdf()->getHttpContext()); + + $debug_png = $this->_dompdf->getOptions()->getDebugPng(); + + if ($debug_png) { + print "[image:$img|$width|$height|$type]"; + } + + switch ($type) { + case "jpeg": + if ($debug_png) { + print '!!!jpg!!!'; + } + $this->_pdf->addJpegFromFile($img, $x, $this->y($y) - $h, $w, $h); + break; + + case "webp": + /** @noinspection PhpMissingBreakStatementInspection */ + case "gif": + /** @noinspection PhpMissingBreakStatementInspection */ + case "bmp": + if ($debug_png) print "!!!{$type}!!!"; + $img = $this->_convert_to_png($img, $type); + if ($img === null) { + if ($debug_png) print '!!!conversion to PDF failed!!!'; + $this->image(Cache::$broken_image, $x, $y, $w, $h, $resolution); + break; + } + + case "png": + if ($debug_png) print '!!!png!!!'; + + $this->_pdf->addPngFromFile($img, $x, $this->y($y) - $h, $w, $h); + break; + + case "svg": + if ($debug_png) print '!!!SVG!!!'; + + $this->_pdf->addSvgFromFile($img, $x, $this->y($y) - $h, $w, $h); + break; + + default: + if ($debug_png) print '!!!unknown!!!'; + } + } + + public function select($x, $y, $w, $h, $font, $size, $color = [0, 0, 0], $opts = []) + { + $pdf = $this->_pdf; + + $font .= ".afm"; + $pdf->selectFont($font); + + if (!isset($pdf->acroFormId)) { + $pdf->addForm(); + } + + $ft = \Dompdf\Cpdf::ACROFORM_FIELD_CHOICE; + $ff = \Dompdf\Cpdf::ACROFORM_FIELD_CHOICE_COMBO; + + $id = $pdf->addFormField($ft, rand(), $x, $this->y($y) - $h, $x + $w, $this->y($y), $ff, $size, $color); + $pdf->setFormFieldOpt($id, $opts); + } + + public function textarea($x, $y, $w, $h, $font, $size, $color = [0, 0, 0]) + { + $pdf = $this->_pdf; + + $font .= ".afm"; + $pdf->selectFont($font); + + if (!isset($pdf->acroFormId)) { + $pdf->addForm(); + } + + $ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT; + $ff = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT_MULTILINE; + + $pdf->addFormField($ft, rand(), $x, $this->y($y) - $h, $x + $w, $this->y($y), $ff, $size, $color); + } + + public function input($x, $y, $w, $h, $type, $font, $size, $color = [0, 0, 0]) + { + $pdf = $this->_pdf; + + $font .= ".afm"; + $pdf->selectFont($font); + + if (!isset($pdf->acroFormId)) { + $pdf->addForm(); + } + + $ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT; + $ff = 0; + + switch ($type) { + case 'text': + $ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT; + break; + case 'password': + $ft = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT; + $ff = \Dompdf\Cpdf::ACROFORM_FIELD_TEXT_PASSWORD; + break; + case 'submit': + $ft = \Dompdf\Cpdf::ACROFORM_FIELD_BUTTON; + break; + } + + $pdf->addFormField($ft, rand(), $x, $this->y($y) - $h, $x + $w, $this->y($y), $ff, $size, $color); + } + + public function text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0) + { + $pdf = $this->_pdf; + + $this->_set_fill_color($color); + + $is_font_subsetting = $this->_dompdf->getOptions()->getIsFontSubsettingEnabled(); + $pdf->selectFont($font . '.afm', '', true, $is_font_subsetting); + + $pdf->addText($x, $this->y($y) - $pdf->getFontHeight($size), $size, $text, $angle, $word_space, $char_space); + + $this->_set_fill_transparency("Normal", $this->_current_opacity); + } + + public function javascript($code) + { + $this->_pdf->addJavascript($code); + } + + //........................................................................ + + public function add_named_dest($anchorname) + { + $this->_pdf->addDestination($anchorname, "Fit"); + } + + public function add_link($url, $x, $y, $width, $height) + { + $y = $this->y($y) - $height; + + if (strpos($url, '#') === 0) { + // Local link + $name = substr($url, 1); + if ($name) { + $this->_pdf->addInternalLink($name, $x, $y, $x + $width, $y + $height); + } + } else { + $this->_pdf->addLink($url, $x, $y, $x + $width, $y + $height); + } + } + + /** + * @throws FontNotFoundException + */ + public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0) + { + $this->_pdf->selectFont($font, '', true, $this->_dompdf->getOptions()->getIsFontSubsettingEnabled()); + return $this->_pdf->getTextWidth($size, $text, $word_spacing, $char_spacing); + } + + /** + * @throws FontNotFoundException + */ + public function get_font_height($font, $size) + { + $options = $this->_dompdf->getOptions(); + $this->_pdf->selectFont($font, '', true, $options->getIsFontSubsettingEnabled()); + + return $this->_pdf->getFontHeight($size) * $options->getFontHeightRatio(); + } + + /*function get_font_x_height($font, $size) { + $this->_pdf->selectFont($font); + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + return $this->_pdf->getFontXHeight($size) * $ratio; + }*/ + + /** + * @throws FontNotFoundException + */ + public function get_font_baseline($font, $size) + { + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + return $this->get_font_height($font, $size) / $ratio; + } + + /** + * Processes a callback or script on every page. + * + * The callback function receives the four parameters `int $pageNumber`, + * `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics`, in + * that order. If a script is passed as string, the variables `$PAGE_NUM`, + * `$PAGE_COUNT`, `$pdf`, and `$fontMetrics` are available instead. Passing + * a script as string is deprecated and will be removed in a future version. + * + * This function can be used to add page numbers to all pages after the + * first one, for example. + * + * @param callable|string $callback The callback function or PHP script to process on every page + */ + public function page_script($callback): void + { + if (is_string($callback)) { + $this->processPageScript(function ( + int $PAGE_NUM, + int $PAGE_COUNT, + self $pdf, + FontMetrics $fontMetrics + ) use ($callback) { + eval($callback); + }); + return; + } + + $this->processPageScript($callback); + } + + public function page_text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0) + { + $this->processPageScript(function (int $pageNumber, int $pageCount) use ($x, $y, $text, $font, $size, $color, $word_space, $char_space, $angle) { + $text = str_replace( + ["{PAGE_NUM}", "{PAGE_COUNT}"], + [$pageNumber, $pageCount], + $text + ); + $this->text($x, $y, $text, $font, $size, $color, $word_space, $char_space, $angle); + }); + } + + public function page_line($x1, $y1, $x2, $y2, $color, $width, $style = []) + { + $this->processPageScript(function () use ($x1, $y1, $x2, $y2, $color, $width, $style) { + $this->line($x1, $y1, $x2, $y2, $color, $width, $style); + }); + } + + /** + * @return int + */ + public function new_page() + { + $this->_page_number++; + $this->_page_count++; + + $ret = $this->_pdf->newPage(); + $this->_pages[] = $ret; + return $ret; + } + + protected function processPageScript(callable $callback): void + { + $pageNumber = 1; + + foreach ($this->_pages as $pid) { + $this->reopen_object($pid); + + $fontMetrics = $this->_dompdf->getFontMetrics(); + $callback($pageNumber, $this->_page_count, $this, $fontMetrics); + + $this->close_object(); + $pageNumber++; + } + } + + public function stream($filename = "document.pdf", $options = []) + { + if (headers_sent()) { + die("Unable to stream pdf: headers already sent"); + } + + if (!isset($options["compress"])) $options["compress"] = true; + if (!isset($options["Attachment"])) $options["Attachment"] = true; + + $debug = !$options['compress']; + $tmp = ltrim($this->_pdf->output($debug)); + + header("Cache-Control: private"); + header("Content-Type: application/pdf"); + header("Content-Length: " . mb_strlen($tmp, "8bit")); + + $filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf"; + $attachment = $options["Attachment"] ? "attachment" : "inline"; + header(Helpers::buildContentDispositionHeader($attachment, $filename)); + + echo $tmp; + flush(); + } + + public function output($options = []) + { + if (!isset($options["compress"])) $options["compress"] = true; + + $debug = !$options['compress']; + + return $this->_pdf->output($debug); + } + + /** + * Returns logging messages generated by the Cpdf class + * + * @return string + */ + public function get_messages() + { + return $this->_pdf->messages; + } +} diff --git a/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php b/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php new file mode 100644 index 0000000..269beb9 --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php @@ -0,0 +1,929 @@ +_dompdf = new Dompdf(); + } else { + $this->_dompdf = $dompdf; + } + + $this->dpi = $this->get_dompdf()->getOptions()->getDpi(); + + if ($aa_factor < 1) { + $aa_factor = 1; + } + + $this->_aa_factor = $aa_factor; + + $size[2] *= $aa_factor; + $size[3] *= $aa_factor; + + $this->_width = $size[2] - $size[0]; + $this->_height = $size[3] - $size[1]; + + $this->_actual_width = $this->_upscale($this->_width); + $this->_actual_height = $this->_upscale($this->_height); + + $this->_page_number = $this->_page_count = 0; + + if (is_null($bg_color) || !is_array($bg_color)) { + // Pure white bg + $bg_color = [1, 1, 1, 0]; + } + + $this->_bg_color_array = $bg_color; + + $this->new_page(); + } + + public function get_dompdf() + { + return $this->_dompdf; + } + + /** + * Return the GD image resource + * + * @return \GdImage|resource + */ + public function get_image() + { + return $this->_img; + } + + /** + * Return the image's width in pixels + * + * @return int + */ + public function get_width() + { + return round($this->_width / $this->_aa_factor); + } + + /** + * Return the image's height in pixels + * + * @return int + */ + public function get_height() + { + return round($this->_height / $this->_aa_factor); + } + + public function get_page_number() + { + return $this->_page_number; + } + + public function get_page_count() + { + return $this->_page_count; + } + + /** + * Sets the current page number + * + * @param int $num + */ + public function set_page_number($num) + { + $this->_page_number = $num; + } + + public function set_page_count($count) + { + $this->_page_count = $count; + } + + public function set_opacity(float $opacity, string $mode = "Normal"): void + { + // FIXME + } + + /** + * Allocate a new color. Allocate with GD as needed and store + * previously allocated colors in $this->_colors. + * + * @param array $color The new current color + * @return int The allocated color + */ + protected function _allocate_color($color) + { + $a = isset($color["alpha"]) ? $color["alpha"] : 1; + + if (isset($color["c"])) { + $color = Helpers::cmyk_to_rgb($color); + } + + list($r, $g, $b) = $color; + + $r = round($r * 255); + $g = round($g * 255); + $b = round($b * 255); + $a = round(127 - ($a * 127)); + + // Clip values + $r = $r > 255 ? 255 : $r; + $g = $g > 255 ? 255 : $g; + $b = $b > 255 ? 255 : $b; + $a = $a > 127 ? 127 : $a; + + $r = $r < 0 ? 0 : $r; + $g = $g < 0 ? 0 : $g; + $b = $b < 0 ? 0 : $b; + $a = $a < 0 ? 0 : $a; + + $key = sprintf("#%02X%02X%02X%02X", $r, $g, $b, $a); + + if (isset($this->_colors[$key])) { + return $this->_colors[$key]; + } + + if ($a != 0) { + $this->_colors[$key] = imagecolorallocatealpha($this->get_image(), $r, $g, $b, $a); + } else { + $this->_colors[$key] = imagecolorallocate($this->get_image(), $r, $g, $b); + } + + return $this->_colors[$key]; + } + + /** + * Scales value up to the current canvas DPI from 72 DPI + * + * @param float $length + * @return int + */ + protected function _upscale($length) + { + return round(($length * $this->dpi) / 72 * $this->_aa_factor); + } + + /** + * Scales value down from the current canvas DPI to 72 DPI + * + * @param float $length + * @return float + */ + protected function _downscale($length) + { + return round(($length / $this->dpi * 72) / $this->_aa_factor); + } + + protected function convertStyle(array $style, int $color, int $width): array + { + $gdStyle = []; + + if (count($style) === 1) { + $style[] = $style[0]; + } + + foreach ($style as $index => $s) { + $d = $this->_upscale($s); + + for ($i = 0; $i < $d; $i++) { + for ($j = 0; $j < $width; $j++) { + $gdStyle[] = $index % 2 === 0 + ? $color + : IMG_COLOR_TRANSPARENT; + } + } + } + + return $gdStyle; + } + + public function line($x1, $y1, $x2, $y2, $color, $width, $style = [], $cap = "butt") + { + // Account for the fact that round and square caps are expected to + // extend outwards + if ($cap === "round" || $cap === "square") { + // Shift line by half width + $w = $width / 2; + $a = $x2 - $x1; + $b = $y2 - $y1; + $c = sqrt($a ** 2 + $b ** 2); + $dx = $a * $w / $c; + $dy = $b * $w / $c; + + $x1 -= $dx; + $x2 -= $dx; + $y1 -= $dy; + $y2 -= $dy; + + // Adapt dash pattern + if (is_array($style)) { + foreach ($style as $index => &$s) { + $s = $index % 2 === 0 ? $s + $width : $s - $width; + } + } + } + + // Scale by the AA factor and DPI + $x1 = $this->_upscale($x1); + $y1 = $this->_upscale($y1); + $x2 = $this->_upscale($x2); + $y2 = $this->_upscale($y2); + $width = $this->_upscale($width); + + $c = $this->_allocate_color($color); + + // Convert the style array if required + if (is_array($style) && count($style) > 0) { + $gd_style = $this->convertStyle($style, $c, $width); + + if (!empty($gd_style)) { + imagesetstyle($this->get_image(), $gd_style); + $c = IMG_COLOR_STYLED; + } + } + + imagesetthickness($this->get_image(), $width); + + imageline($this->get_image(), $x1, $y1, $x2, $y2, $c); + } + + public function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt") + { + // Account for the fact that round and square caps are expected to + // extend outwards + if ($cap === "round" || $cap === "square") { + // Adapt dash pattern + if (is_array($style)) { + foreach ($style as $index => &$s) { + $s = $index % 2 === 0 ? $s + $width : $s - $width; + } + } + } + + // Scale by the AA factor and DPI + $x = $this->_upscale($x); + $y = $this->_upscale($y); + $w = $this->_upscale($r1 * 2); + $h = $this->_upscale($r2 * 2); + $width = $this->_upscale($width); + + // Adapt angles as imagearc counts clockwise + $start = 360 - $aend; + $end = 360 - $astart; + + $c = $this->_allocate_color($color); + + // Convert the style array if required + if (is_array($style) && count($style) > 0) { + $gd_style = $this->convertStyle($style, $c, $width); + + if (!empty($gd_style)) { + imagesetstyle($this->get_image(), $gd_style); + $c = IMG_COLOR_STYLED; + } + } + + imagesetthickness($this->get_image(), $width); + + imagearc($this->get_image(), $x, $y, $w, $h, $start, $end, $c); + } + + public function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt") + { + // Account for the fact that round and square caps are expected to + // extend outwards + if ($cap === "round" || $cap === "square") { + // Adapt dash pattern + if (is_array($style)) { + foreach ($style as $index => &$s) { + $s = $index % 2 === 0 ? $s + $width : $s - $width; + } + } + } + + // Scale by the AA factor and DPI + $x1 = $this->_upscale($x1); + $y1 = $this->_upscale($y1); + $w = $this->_upscale($w); + $h = $this->_upscale($h); + $width = $this->_upscale($width); + + $c = $this->_allocate_color($color); + + // Convert the style array if required + if (is_array($style) && count($style) > 0) { + $gd_style = $this->convertStyle($style, $c, $width); + + if (!empty($gd_style)) { + imagesetstyle($this->get_image(), $gd_style); + $c = IMG_COLOR_STYLED; + } + } + + imagesetthickness($this->get_image(), $width); + + if ($c === IMG_COLOR_STYLED) { + imagepolygon($this->get_image(), [ + $x1, $y1, + $x1 + $w, $y1, + $x1 + $w, $y1 + $h, + $x1, $y1 + $h + ], $c); + } else { + imagerectangle($this->get_image(), $x1, $y1, $x1 + $w, $y1 + $h, $c); + } + } + + public function filled_rectangle($x1, $y1, $w, $h, $color) + { + // Scale by the AA factor and DPI + $x1 = $this->_upscale($x1); + $y1 = $this->_upscale($y1); + $w = $this->_upscale($w); + $h = $this->_upscale($h); + + $c = $this->_allocate_color($color); + + imagefilledrectangle($this->get_image(), $x1, $y1, $x1 + $w, $y1 + $h, $c); + } + + public function clipping_rectangle($x1, $y1, $w, $h) + { + // @todo + } + + public function clipping_roundrectangle($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL) + { + // @todo + } + + public function clipping_polygon(array $points): void + { + // @todo + } + + public function clipping_end() + { + // @todo + } + + public function save() + { + $this->get_dompdf()->getOptions()->setDpi(72); + } + + public function restore() + { + $this->get_dompdf()->getOptions()->setDpi($this->dpi); + } + + public function rotate($angle, $x, $y) + { + // @todo + } + + public function skew($angle_x, $angle_y, $x, $y) + { + // @todo + } + + public function scale($s_x, $s_y, $x, $y) + { + // @todo + } + + public function translate($t_x, $t_y) + { + // @todo + } + + public function transform($a, $b, $c, $d, $e, $f) + { + // @todo + } + + public function polygon($points, $color, $width = null, $style = [], $fill = false) + { + // Scale each point by the AA factor and DPI + foreach (array_keys($points) as $i) { + $points[$i] = $this->_upscale($points[$i]); + } + + $width = isset($width) ? $this->_upscale($width) : null; + + $c = $this->_allocate_color($color); + + // Convert the style array if required + if (is_array($style) && count($style) > 0 && isset($width) && !$fill) { + $gd_style = $this->convertStyle($style, $c, $width); + + if (!empty($gd_style)) { + imagesetstyle($this->get_image(), $gd_style); + $c = IMG_COLOR_STYLED; + } + } + + imagesetthickness($this->get_image(), isset($width) ? $width : 0); + + if ($fill) { + imagefilledpolygon($this->get_image(), $points, $c); + } else { + imagepolygon($this->get_image(), $points, $c); + } + } + + public function circle($x, $y, $r, $color, $width = null, $style = [], $fill = false) + { + // Scale by the AA factor and DPI + $x = $this->_upscale($x); + $y = $this->_upscale($y); + $d = $this->_upscale(2 * $r); + $width = isset($width) ? $this->_upscale($width) : null; + + $c = $this->_allocate_color($color); + + // Convert the style array if required + if (is_array($style) && count($style) > 0 && isset($width) && !$fill) { + $gd_style = $this->convertStyle($style, $c, $width); + + if (!empty($gd_style)) { + imagesetstyle($this->get_image(), $gd_style); + $c = IMG_COLOR_STYLED; + } + } + + imagesetthickness($this->get_image(), isset($width) ? $width : 0); + + if ($fill) { + imagefilledellipse($this->get_image(), $x, $y, $d, $d, $c); + } else { + imageellipse($this->get_image(), $x, $y, $d, $d, $c); + } + } + + /** + * @throws \Exception + */ + public function image($img, $x, $y, $w, $h, $resolution = "normal") + { + $img_type = Cache::detect_type($img, $this->get_dompdf()->getHttpContext()); + + if (!$img_type) { + return; + } + + $func_name = "imagecreatefrom$img_type"; + if (!function_exists($func_name)) { + if (!method_exists(Helpers::class, $func_name)) { + throw new \Exception("Function $func_name() not found. Cannot convert $img_type image: $img. Please install the image PHP extension."); + } + $func_name = [Helpers::class, $func_name]; + } + $src = @call_user_func($func_name, $img); + + if (!$src) { + return; // Probably should add to $_dompdf_errors or whatever here + } + + // Scale by the AA factor and DPI + $x = $this->_upscale($x); + $y = $this->_upscale($y); + + $w = $this->_upscale($w); + $h = $this->_upscale($h); + + $img_w = imagesx($src); + $img_h = imagesy($src); + + imagecopyresampled($this->get_image(), $src, $x, $y, 0, 0, $w, $h, $img_w, $img_h); + } + + public function text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_spacing = 0.0, $char_spacing = 0.0, $angle = 0.0) + { + // Scale by the AA factor and DPI + $x = $this->_upscale($x); + $y = $this->_upscale($y); + $size = $this->_upscale($size) * self::FONT_SCALE; + + $h = round($this->get_font_height_actual($font, $size)); + $c = $this->_allocate_color($color); + + // imagettftext() converts numeric entities to their respective + // character. Preserve any originally double encoded entities to be + // represented as is. + // eg:   will render rather than its character. + $text = preg_replace('/&(#(?:x[a-fA-F0-9]+|[0-9]+);)/', '&\1', $text); + + $text = mb_encode_numericentity($text, [0x0080, 0xff, 0, 0xff], 'UTF-8'); + + $font = $this->get_ttf_file($font); + + // FIXME: word spacing + imagettftext($this->get_image(), $size, $angle, $x, $y + $h, $c, $font, $text); + } + + public function javascript($code) + { + // Not implemented + } + + public function add_named_dest($anchorname) + { + // Not implemented + } + + public function add_link($url, $x, $y, $width, $height) + { + // Not implemented + } + + public function add_info(string $label, string $value): void + { + // N/A + } + + public function set_default_view($view, $options = []) + { + // N/A + } + + public function get_text_width($text, $font, $size, $word_spacing = 0.0, $char_spacing = 0.0) + { + $font = $this->get_ttf_file($font); + $size = $this->_upscale($size) * self::FONT_SCALE; + + // imagettfbbox() converts numeric entities to their respective + // character. Preserve any originally double encoded entities to be + // represented as is. + // eg:   will render rather than its character. + $text = preg_replace('/&(#(?:x[a-fA-F0-9]+|[0-9]+);)/', '&\1', $text); + + $text = mb_encode_numericentity($text, [0x0080, 0xffff, 0, 0xffff], 'UTF-8'); + + // FIXME: word spacing + list($x1, , $x2) = imagettfbbox($size, 0, $font, $text); + + // Add additional 1pt to prevent text overflow issues + return $this->_downscale($x2 - $x1) + 1; + } + + /** + * @param string|null $font + * @return string + */ + public function get_ttf_file($font) + { + if ($font === null) { + $font = ""; + } + + if ( stripos($font, ".ttf") === false ) { + $font .= ".ttf"; + } + + if (!file_exists($font)) { + $font_metrics = $this->_dompdf->getFontMetrics(); + $font = $font_metrics->getFont($this->_dompdf->getOptions()->getDefaultFont()) . ".ttf"; + if (!file_exists($font)) { + if (strpos($font, "mono")) { + $font = $font_metrics->getFont("DejaVu Mono") . ".ttf"; + } elseif (strpos($font, "sans") !== false) { + $font = $font_metrics->getFont("DejaVu Sans") . ".ttf"; + } elseif (strpos($font, "serif")) { + $font = $font_metrics->getFont("DejaVu Serif") . ".ttf"; + } else { + $font = $font_metrics->getFont("DejaVu Sans") . ".ttf"; + } + } + } + + return $font; + } + + public function get_font_height($font, $size) + { + $size = $this->_upscale($size) * self::FONT_SCALE; + + $height = $this->get_font_height_actual($font, $size); + + return $this->_downscale($height); + } + + /** + * @param string $font + * @param float $size + * + * @return float + */ + protected function get_font_height_actual($font, $size) + { + $font = $this->get_ttf_file($font); + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + + // FIXME: word spacing + list(, $y2, , , , $y1) = imagettfbbox($size, 0, $font, "MXjpqytfhl"); // Test string with ascenders, descenders and caps + return ($y2 - $y1) * $ratio; + } + + public function get_font_baseline($font, $size) + { + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + return $this->get_font_height($font, $size) / $ratio; + } + + public function new_page() + { + $this->_page_number++; + $this->_page_count++; + + $this->_img = imagecreatetruecolor($this->_actual_width, $this->_actual_height); + + $this->_bg_color = $this->_allocate_color($this->_bg_color_array); + imagealphablending($this->_img, true); + imagesavealpha($this->_img, true); + imagefill($this->_img, 0, 0, $this->_bg_color); + + $this->_imgs[] = $this->_img; + } + + public function open_object() + { + // N/A + } + + public function close_object() + { + // N/A + } + + public function add_object() + { + // N/A + } + + public function page_script($callback): void + { + // N/A + } + + public function page_text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0) + { + // N/A + } + + public function page_line($x1, $y1, $x2, $y2, $color, $width, $style = []) + { + // N/A + } + + /** + * Streams the image to the client. + * + * @param string $filename The filename to present to the client. + * @param array $options Associative array: 'type' => jpeg|jpg|png; 'quality' => 0 - 100 (JPEG only); + * 'page' => Number of the page to output (defaults to the first); 'Attachment': 1 or 0 (default 1). + */ + public function stream($filename, $options = []) + { + if (headers_sent()) { + die("Unable to stream image: headers already sent"); + } + + if (!isset($options["type"])) $options["type"] = "png"; + if (!isset($options["Attachment"])) $options["Attachment"] = true; + $type = strtolower($options["type"]); + + switch ($type) { + case "jpg": + case "jpeg": + $contentType = "image/jpeg"; + $extension = ".jpg"; + break; + case "png": + default: + $contentType = "image/png"; + $extension = ".png"; + break; + } + + header("Cache-Control: private"); + header("Content-Type: $contentType"); + + $filename = str_replace(["\n", "'"], "", basename($filename, ".$type")) . $extension; + $attachment = $options["Attachment"] ? "attachment" : "inline"; + header(Helpers::buildContentDispositionHeader($attachment, $filename)); + + $this->_output($options); + flush(); + } + + /** + * Returns the image as a string. + * + * @param array $options Associative array: 'type' => jpeg|jpg|png; 'quality' => 0 - 100 (JPEG only); + * 'page' => Number of the page to output (defaults to the first). + * @return string + */ + public function output($options = []) + { + ob_start(); + + $this->_output($options); + + return ob_get_clean(); + } + + /** + * Outputs the image stream directly. + * + * @param array $options Associative array: 'type' => jpeg|jpg|png; 'quality' => 0 - 100 (JPEG only); + * 'page' => Number of the page to output (defaults to the first). + */ + protected function _output($options = []) + { + if (!isset($options["type"])) $options["type"] = "png"; + if (!isset($options["page"])) $options["page"] = 1; + $type = strtolower($options["type"]); + + if (isset($this->_imgs[$options["page"] - 1])) { + $img = $this->_imgs[$options["page"] - 1]; + } else { + $img = $this->_imgs[0]; + } + + // Perform any antialiasing + if ($this->_aa_factor != 1) { + $dst_w = round($this->_actual_width / $this->_aa_factor); + $dst_h = round($this->_actual_height / $this->_aa_factor); + $dst = imagecreatetruecolor($dst_w, $dst_h); + imagecopyresampled($dst, $img, 0, 0, 0, 0, + $dst_w, $dst_h, + $this->_actual_width, $this->_actual_height); + } else { + $dst = $img; + } + + switch ($type) { + case "jpg": + case "jpeg": + if (!isset($options["quality"])) { + $options["quality"] = 75; + } + + imagejpeg($dst, null, $options["quality"]); + break; + case "png": + default: + imagepng($dst); + break; + } + + if ($this->_aa_factor != 1) { + imagedestroy($dst); + } + } +} diff --git a/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/PDFLib.php b/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/PDFLib.php new file mode 100644 index 0000000..6467cf1 --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/src/Adapter/PDFLib.php @@ -0,0 +1,1450 @@ + "Courier", + "courier-bold" => "Courier-Bold", + "courier-oblique" => "Courier-Oblique", + "courier-boldoblique" => "Courier-BoldOblique", + "helvetica" => "Helvetica", + "helvetica-bold" => "Helvetica-Bold", + "helvetica-oblique" => "Helvetica-Oblique", + "helvetica-boldoblique" => "Helvetica-BoldOblique", + "times" => "Times-Roman", + "times-roman" => "Times-Roman", + "times-bold" => "Times-Bold", + "times-italic" => "Times-Italic", + "times-bolditalic" => "Times-BoldItalic", + "symbol" => "Symbol", + "zapfdinbats" => "ZapfDingbats", + "zapfdingbats" => "ZapfDingbats", + ]; + + /** + * @var \Dompdf\Dompdf + */ + protected $_dompdf; + + /** + * Instance of PDFLib class + * + * @var \PDFLib + */ + protected $_pdf; + + /** + * Name of temporary file used for PDFs created on disk + * + * @var string + */ + protected $_file; + + /** + * PDF width, in points + * + * @var float + */ + protected $_width; + + /** + * PDF height, in points + * + * @var float + */ + protected $_height; + + /** + * Last fill color used + * + * @var array + */ + protected $_last_fill_color; + + /** + * Last stroke color used + * + * @var array + */ + protected $_last_stroke_color; + + /** + * The current opacity level + * + * @var float|null + */ + protected $_current_opacity; + + /** + * Cache of image handles + * + * @var array + */ + protected $_imgs; + + /** + * Cache of font handles + * + * @var array + */ + protected $_fonts; + + /** + * Cache of fontFile checks + * + * @var array + */ + protected $_fontsFiles; + + /** + * List of objects (templates) to add to multiple pages + * + * @var array + */ + protected $_objs; + + /** + * List of gstate objects created for this PDF (for reuse) + * + * @var array + */ + protected $_gstates = []; + + /** + * Current page number + * + * @var int + */ + protected $_page_number; + + /** + * Total number of pages + * + * @var int + */ + protected $_page_count; + + /** + * Array of pages for accessing after rendering is initially complete + * + * @var array + */ + protected $_pages; + + public function __construct($paper = "letter", string $orientation = "portrait", ?Dompdf $dompdf = null) + { + if (is_array($paper)) { + $size = array_map("floatval", $paper); + } else { + $paper = strtolower($paper); + $size = self::$PAPER_SIZES[$paper] ?? self::$PAPER_SIZES["letter"]; + } + + if (strtolower($orientation) === "landscape") { + [$size[2], $size[3]] = [$size[3], $size[2]]; + } + + $this->_width = $size[2] - $size[0]; + $this->_height = $size[3] - $size[1]; + + if ($dompdf === null) { + $this->_dompdf = new Dompdf(); + } else { + $this->_dompdf = $dompdf; + } + + $this->_pdf = new \PDFLib(); + + $license = $dompdf->getOptions()->getPdflibLicense(); + if (strlen($license) > 0) { + $this->setPDFLibParameter("license", $license); + } + + if ($this->getPDFLibMajorVersion() < 10) { + $this->setPDFLibParameter("textformat", "utf8"); + } + if ($this->getPDFLibMajorVersion() >= 7) { + $this->setPDFLibParameter("errorpolicy", "return"); + // $this->_pdf->set_option('logging={filename=' . \APP_PATH . '/logs/pdflib.log classes={api=1 warning=2}}'); + // $this->_pdf->set_option('errorpolicy=exception'); + } else { + $this->setPDFLibParameter("fontwarning", "false"); + } + + $searchPath = $this->_dompdf->getOptions()->getFontDir(); + if (empty($searchPath) === false) { + $this->_pdf->set_option('searchpath={' . $searchPath . '}'); + } + + // fetch PDFLib version information for the producer field + $this->_pdf->set_info("Producer Addendum", sprintf("%s + PDFLib %s", $dompdf->version, $this->getPDFLibMajorVersion())); + + // Silence pedantic warnings about missing TZ settings + $tz = @date_default_timezone_get(); + date_default_timezone_set("UTC"); + $this->_pdf->set_info("Date", date("Y-m-d")); + date_default_timezone_set($tz); + + if (self::$IN_MEMORY) { + $this->_pdf->begin_document("", ""); + } else { + $tmp_dir = $this->_dompdf->getOptions()->getTempDir(); + $tmp_name = @tempnam($tmp_dir, "libdompdf_pdf_"); + @unlink($tmp_name); + $this->_file = "$tmp_name.pdf"; + $this->_pdf->begin_document($this->_file, ""); + } + + $this->_pdf->begin_page_ext($this->_width, $this->_height, ""); + + $this->_page_number = $this->_page_count = 1; + + $this->_imgs = []; + $this->_fonts = []; + $this->_objs = []; + } + + function get_dompdf() + { + return $this->_dompdf; + } + + /** + * Close the pdf + */ + protected function _close() + { + $this->_place_objects(); + + // Close all pages + $this->_pdf->suspend_page(""); + for ($p = 1; $p <= $this->_page_count; $p++) { + $this->_pdf->resume_page("pagenumber=$p"); + $this->_pdf->end_page_ext(""); + } + + $this->_pdf->end_document(""); + } + + + /** + * Returns the PDFLib instance + * + * @return PDFLib + */ + public function get_pdflib() + { + return $this->_pdf; + } + + public function add_info(string $label, string $value): void + { + $this->_pdf->set_info($label, $value); + } + + /** + * Opens a new 'object' (template in PDFLib-speak) + * + * While an object is open, all drawing actions are recorded to the + * object instead of being drawn on the current page. Objects can + * be added later to a specific page or to several pages. + * + * The return value is an integer ID for the new object. + * + * @see PDFLib::close_object() + * @see PDFLib::add_object() + * + * @return int + */ + public function open_object() + { + $this->_pdf->suspend_page(""); + if ($this->getPDFLibMajorVersion() >= 7) { + $ret = $this->_pdf->begin_template_ext($this->_width, $this->_height, null); + } else { + $ret = $this->_pdf->begin_template($this->_width, $this->_height); + } + $this->_pdf->save(); + $this->_objs[$ret] = ["start_page" => $this->_page_number]; + + return $ret; + } + + /** + * Reopen an existing object (NOT IMPLEMENTED) + * PDFLib does not seem to support reopening templates. + * + * @param int $object the ID of a previously opened object + * + * @throws Exception + * @return void + */ + public function reopen_object($object) + { + throw new Exception("PDFLib does not support reopening objects."); + } + + /** + * Close the current template + * + * @see PDFLib::open_object() + */ + public function close_object() + { + $this->_pdf->restore(); + if ($this->getPDFLibMajorVersion() >= 7) { + $this->_pdf->end_template_ext($this->_width, $this->_height); + } else { + $this->_pdf->end_template(); + } + $this->_pdf->resume_page("pagenumber=" . $this->_page_number); + } + + /** + * Adds the specified object to the document + * + * $where can be one of: + * - 'add' add to current page only + * - 'all' add to every page from the current one onwards + * - 'odd' add to all odd numbered pages from now on + * - 'even' add to all even numbered pages from now on + * - 'next' add the object to the next page only + * - 'nextodd' add to all odd numbered pages from the next one + * - 'nexteven' add to all even numbered pages from the next one + * + * @param int $object the object handle returned by open_object() + * @param string $where + */ + public function add_object($object, $where = 'all') + { + + if (mb_strpos($where, "next") !== false) { + $this->_objs[$object]["start_page"]++; + $where = str_replace("next", "", $where); + if ($where == "") { + $where = "add"; + } + } + + $this->_objs[$object]["where"] = $where; + } + + /** + * Stops the specified template from appearing in the document. + * + * The object will stop being displayed on the page following the + * current one. + * + * @param int $object + */ + public function stop_object($object) + { + + if (!isset($this->_objs[$object])) { + return; + } + + $start = $this->_objs[$object]["start_page"]; + $where = $this->_objs[$object]["where"]; + + // Place the object on this page if required + if ($this->_page_number >= $start && + (($this->_page_number % 2 == 0 && $where === "even") || + ($this->_page_number % 2 == 1 && $where === "odd") || + ($where === "all")) + ) { + $this->_pdf->fit_image($object, 0, 0, ""); + } + + $this->_objs[$object] = null; + unset($this->_objs[$object]); + } + + /** + * Add all active objects to the current page + */ + protected function _place_objects() + { + + foreach ($this->_objs as $obj => $props) { + $start = $props["start_page"]; + $where = $props["where"]; + + // Place the object on this page if required + if ($this->_page_number >= $start && + (($this->_page_number % 2 == 0 && $where === "even") || + ($this->_page_number % 2 == 1 && $where === "odd") || + ($where === "all")) + ) { + $this->_pdf->fit_image($obj, 0, 0, ""); + } + } + } + + public function get_width() + { + return $this->_width; + } + + public function get_height() + { + return $this->_height; + } + + public function get_page_number() + { + return $this->_page_number; + } + + public function get_page_count() + { + return $this->_page_count; + } + + /** + * @param $num + */ + public function set_page_number($num) + { + $this->_page_number = (int)$num; + } + + public function set_page_count($count) + { + $this->_page_count = (int)$count; + } + + /** + * Sets the line style + * + * @param float $width + * @param string $cap + * @param string $join + * @param array $dash + */ + protected function _set_line_style($width, $cap, $join, $dash) + { + if (!is_array($dash)) { + $dash = []; + } + + // Work around PDFLib limitation with 0 dash length: + // Value 0 for option 'dasharray' is too small (minimum 1.5e-05) + foreach ($dash as &$d) { + if ($d == 0) { + $d = 1.5e-5; + } + } + + if (count($dash) === 1) { + $dash[] = $dash[0]; + } + + if ($this->getPDFLibMajorVersion() >= 9) { + if (count($dash) > 1) { + $this->_pdf->set_graphics_option("dasharray={" . implode(" ", $dash) . "}"); + } else { + $this->_pdf->set_graphics_option("dasharray=none"); + } + } else { + if (count($dash) > 1) { + $this->_pdf->setdashpattern("dasharray={" . implode(" ", $dash) . "}"); + } else { + $this->_pdf->setdash(0, 0); + } + } + + switch ($join) { + case "miter": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linejoin=0'); + } else { + $this->_pdf->setlinejoin(0); + } + break; + + case "round": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linejoin=1'); + } else { + $this->_pdf->setlinejoin(1); + } + break; + + case "bevel": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linejoin=2'); + } else { + $this->_pdf->setlinejoin(2); + } + break; + + default: + break; + } + + switch ($cap) { + case "butt": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linecap=0'); + } else { + $this->_pdf->setlinecap(0); + } + break; + + case "round": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linecap=1'); + } else { + $this->_pdf->setlinecap(1); + } + break; + + case "square": + if ($this->getPDFLibMajorVersion() >= 9) { + $this->_pdf->set_graphics_option('linecap=2'); + } else { + $this->_pdf->setlinecap(2); + } + break; + + default: + break; + } + + $this->_pdf->setlinewidth($width); + } + + /** + * Sets the line color + * + * @param array $color array(r,g,b) + */ + protected function _set_stroke_color($color) + { + // TODO: we should check the current PDF stroke color + // instead of the cached value + if ($this->_last_stroke_color == $color) { + // FIXME: do nothing, this optimization is broken by the + // stroke being set as a side effect of other operations + //return; + } + + $alpha = isset($color["alpha"]) ? $color["alpha"] : 1; + if (isset($this->_current_opacity)) { + $alpha *= $this->_current_opacity; + } + + $this->_last_stroke_color = $color; + + if (isset($color[3])) { + $type = "cmyk"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], $color[3]]; + } elseif (isset($color[2])) { + $type = "rgb"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], null]; + } else { + $type = "gray"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], null, null]; + } + + $this->_set_stroke_opacity($alpha, "Normal"); + $this->_pdf->setcolor("stroke", $type, $c1, $c2, $c3, $c4); + } + + /** + * Sets the fill color + * + * @param array $color array(r,g,b) + */ + protected function _set_fill_color($color) + { + // TODO: we should check the current PDF fill color + // instead of the cached value + if ($this->_last_fill_color == $color) { + // FIXME: do nothing, this optimization is broken by the + // fill being set as a side effect of other operations + //return; + } + + $alpha = isset($color["alpha"]) ? $color["alpha"] : 1; + if (isset($this->_current_opacity)) { + $alpha *= $this->_current_opacity; + } + + $this->_last_fill_color = $color; + + if (isset($color[3])) { + $type = "cmyk"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], $color[3]]; + } elseif (isset($color[2])) { + $type = "rgb"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], $color[2], null]; + } else { + $type = "gray"; + list($c1, $c2, $c3, $c4) = [$color[0], $color[1], null, null]; + } + + $this->_set_fill_opacity($alpha, "Normal"); + $this->_pdf->setcolor("fill", $type, $c1, $c2, $c3, $c4); + } + + /** + * Sets the fill opacity + * + * @param float $opacity + * @param string $mode + */ + public function _set_fill_opacity($opacity, $mode = "Normal") + { + if ($mode === "Normal" && isset($opacity)) { + $this->_set_gstate("opacityfill=$opacity"); + } + } + + /** + * Sets the stroke opacity + * + * @param float $opacity + * @param string $mode + */ + public function _set_stroke_opacity($opacity, $mode = "Normal") + { + if ($mode === "Normal" && isset($opacity)) { + $this->_set_gstate("opacitystroke=$opacity"); + } + } + + public function set_opacity(float $opacity, string $mode = "Normal"): void + { + if ($mode === "Normal") { + $this->_set_gstate("opacityfill=$opacity opacitystroke=$opacity"); + $this->_current_opacity = $opacity; + } + } + + /** + * Sets the gstate + * + * @param $gstate_options + * @return int + */ + public function _set_gstate($gstate_options) + { + if (($gstate = array_search($gstate_options, $this->_gstates)) === false) { + $gstate = $this->_pdf->create_gstate($gstate_options); + $this->_gstates[$gstate] = $gstate_options; + } + + return $this->_pdf->set_gstate($gstate); + } + + public function set_default_view($view, $options = []) + { + // TODO + // http://www.pdflib.com/fileadmin/pdflib/pdf/manuals/PDFlib-8.0.2-API-reference.pdf + /** + * fitheight Fit the page height to the window, with the x coordinate left at the left edge of the window. + * fitrect Fit the rectangle specified by left, bottom, right, and top to the window. + * fitvisible Fit the visible contents of the page (the ArtBox) to the window. + * fitvisibleheight Fit the visible contents of the page to the window with the x coordinate left at the left edge of the window. + * fitvisiblewidth Fit the visible contents of the page to the window with the y coordinate top at the top edge of the window. + * fitwidth Fit the page width to the window, with the y coordinate top at the top edge of the window. + * fitwindow Fit the complete page to the window. + * fixed + */ + //$this->setPDFLibParameter("openaction", $view); + } + + /** + * Loads a specific font and stores the corresponding descriptor. + * + * @param string $font + * @param string $encoding + * @param string $options + * + * @return int the font descriptor for the font + */ + protected function _load_font($font, $encoding = null, $options = "") + { + // Fix for PDFLibs case-sensitive font names + $baseFont = basename($font); + $isNativeFont = false; + if (isset(self::$nativeFontsTpPDFLib[$baseFont])) { + $font = self::$nativeFontsTpPDFLib[$baseFont]; + $isNativeFont = true; + } + + // Check if the font is a native PDF font + // Embed non-native fonts + $test = strtolower($baseFont); + if (in_array($test, DOMPDF::$nativeFonts)) { + $font = basename($font); + } else { + // Embed non-native fonts + $options .= " embedding=true"; + } + + $options .= " autosubsetting=" . ($this->_dompdf->getOptions()->getIsFontSubsettingEnabled() === false ? "false" : "true"); + + if (is_null($encoding)) { + // Unicode encoding is only available for the commerical + // version of PDFlib and not PDFlib-Lite + if (strlen($this->_dompdf->getOptions()->getPdflibLicense()) > 0) { + $encoding = "unicode"; + } else { + $encoding = "auto"; + } + } + + $key = "$font:$encoding:$options"; + if (isset($this->_fonts[$key])) { + return $this->_fonts[$key]; + } + + // Native fonts are build in, just load it + if ($isNativeFont) { + $this->_fonts[$key] = $this->_pdf->load_font($font, $encoding, $options); + + return $this->_fonts[$key]; + } + + $fontOutline = $this->getPDFLibParameter("FontOutline", 1); + if ($fontOutline === "" || $fontOutline <= 0) { + $families = $this->_dompdf->getFontMetrics()->getFontFamilies(); + foreach ($families as $files) { + foreach ($files as $file) { + $face = basename($file); + $afm = null; + + if (isset($this->_fontsFiles[$face])) { + continue; + } + + // Prefer ttfs to afms + if (file_exists("$file.ttf")) { + $outline = "$file.ttf"; + } elseif (file_exists("$file.TTF")) { + $outline = "$file.TTF"; + } elseif (file_exists("$file.pfb")) { + $outline = "$file.pfb"; + if (file_exists("$file.afm")) { + $afm = "$file.afm"; + } + } elseif (file_exists("$file.PFB")) { + $outline = "$file.PFB"; + if (file_exists("$file.AFM")) { + $afm = "$file.AFM"; + } + } else { + continue; + } + + $this->_fontsFiles[$face] = true; + + if ($this->getPDFLibMajorVersion() >= 9) { + $this->setPDFLibParameter("FontOutline", '{' . "$face=$outline" . '}'); + } else { + $this->setPDFLibParameter("FontOutline", "\{$face\}=\{$outline\}"); + } + + if (is_null($afm)) { + continue; + } + if ($this->getPDFLibMajorVersion() >= 9) { + $this->setPDFLibParameter("FontAFM", '{' . "$face=$afm" . '}'); + } else { + $this->setPDFLibParameter("FontAFM", "\{$face\}=\{$afm\}"); + } + } + } + } + + $this->_fonts[$key] = $this->_pdf->load_font($font, $encoding, $options); + + return $this->_fonts[$key]; + } + + /** + * Remaps y coords from 4th to 1st quadrant + * + * @param float $y + * @return float + */ + protected function y($y) + { + return $this->_height - $y; + } + + public function line($x1, $y1, $x2, $y2, $color, $width, $style = [], $cap = "butt") + { + $this->_set_line_style($width, $cap, "", $style); + $this->_set_stroke_color($color); + + $y1 = $this->y($y1); + $y2 = $this->y($y2); + + $this->_pdf->moveto($x1, $y1); + $this->_pdf->lineto($x2, $y2); + $this->_pdf->stroke(); + + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function arc($x, $y, $r1, $r2, $astart, $aend, $color, $width, $style = [], $cap = "butt") + { + $this->_set_line_style($width, $cap, "", $style); + $this->_set_stroke_color($color); + + $y = $this->y($y); + + $this->_pdf->arc($x, $y, $r1, $astart, $aend); + $this->_pdf->stroke(); + + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function rectangle($x1, $y1, $w, $h, $color, $width, $style = [], $cap = "butt") + { + $this->_set_stroke_color($color); + $this->_set_line_style($width, $cap, "", $style); + + $y1 = $this->y($y1) - $h; + + $this->_pdf->rect($x1, $y1, $w, $h); + $this->_pdf->stroke(); + + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function filled_rectangle($x1, $y1, $w, $h, $color) + { + $this->_set_fill_color($color); + + $y1 = $this->y($y1) - $h; + + $this->_pdf->rect(floatval($x1), floatval($y1), floatval($w), floatval($h)); + $this->_pdf->fill(); + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + } + + public function clipping_rectangle($x1, $y1, $w, $h) + { + $this->_pdf->save(); + + $y1 = $this->y($y1) - $h; + + $this->_pdf->rect(floatval($x1), floatval($y1), floatval($w), floatval($h)); + $this->_pdf->clip(); + } + + public function clipping_roundrectangle($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL) + { + if ($this->getPDFLibMajorVersion() < 9) { + //TODO: add PDFLib7 support + $this->clipping_rectangle($x1, $y1, $w, $h); + return; + } + + $this->_pdf->save(); + + // we use 0,0 for the base coordinates for the path points + // since we're drawing the path at the $x1,$y1 coordinates + + $path = 0; + //start: left edge, top end + $path = $this->_pdf->add_path_point($path, 0, 0 - $rTL + $h, "move", ""); + // line: left edge, bottom end + $path = $this->_pdf->add_path_point($path, 0, 0 + $rBL, "line", ""); + // curve: bottom-left corner + if ($rBL > 0) { + $path = $this->_pdf->add_path_point($path, 0 + $rBL, 0, "elliptical", "radius=$rBL clockwise=false"); + } + // line: bottom edge, left end + $path = $this->_pdf->add_path_point($path, 0 - $rBR + $w, 0, "line", ""); + // curve: bottom-right corner + if ($rBR > 0) { + $path = $this->_pdf->add_path_point($path, 0 + $w, 0 + $rBR, "elliptical", "radius=$rBR clockwise=false"); + } + // line: right edge, top end + $path = $this->_pdf->add_path_point($path, 0 + $w, 0 - $rTR + $h, "line", ""); + // curve: top-right corner + if ($rTR > 0) { + $path = $this->_pdf->add_path_point($path, 0 - $rTR + $w, 0 + $h, "elliptical", "radius=$rTR clockwise=false"); + } + // line: top edge, left end + $path = $this->_pdf->add_path_point($path, 0 + $rTL, 0 + $h, "line", ""); + // curve: top-left corner + if ($rTL > 0) { + $path = $this->_pdf->add_path_point($path, 0, 0 - $rTL + $h, "elliptical", "radius=$rTL clockwise=false"); + } + $this->_pdf->draw_path($path, $x1, $this->_height-$y1-$h, "clip=true"); + } + + public function clipping_polygon(array $points): void + { + $this->_pdf->save(); + + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->moveto($x, $y); + + while (count($points) > 1) { + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->lineto($x, $y); + } + + $this->_pdf->closepath(); + $this->_pdf->clip(); + } + + public function clipping_end() + { + $this->_pdf->restore(); + } + + public function save() + { + $this->_pdf->save(); + } + + function restore() + { + $this->_pdf->restore(); + } + + public function rotate($angle, $x, $y) + { + $pdf = $this->_pdf; + $pdf->translate($x, $this->_height - $y); + $pdf->rotate(-$angle); + $pdf->translate(-$x, -$this->_height + $y); + } + + public function skew($angle_x, $angle_y, $x, $y) + { + $pdf = $this->_pdf; + $pdf->translate($x, $this->_height - $y); + $pdf->skew($angle_y, $angle_x); // Needs to be inverted + $pdf->translate(-$x, -$this->_height + $y); + } + + public function scale($s_x, $s_y, $x, $y) + { + $pdf = $this->_pdf; + $pdf->translate($x, $this->_height - $y); + $pdf->scale($s_x, $s_y); + $pdf->translate(-$x, -$this->_height + $y); + } + + public function translate($t_x, $t_y) + { + $this->_pdf->translate($t_x, -$t_y); + } + + public function transform($a, $b, $c, $d, $e, $f) + { + $this->_pdf->concat($a, $b, $c, $d, $e, $f); + } + + public function polygon($points, $color, $width = null, $style = [], $fill = false) + { + $this->_set_fill_color($color); + $this->_set_stroke_color($color); + + if (!$fill && isset($width)) { + $this->_set_line_style($width, "square", "miter", $style); + } + + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->moveto($x, $y); + + while (count($points) > 1) { + $y = $this->y(array_pop($points)); + $x = array_pop($points); + $this->_pdf->lineto($x, $y); + } + + if ($fill) { + $this->_pdf->fill(); + } else { + $this->_pdf->closepath_stroke(); + } + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function circle($x, $y, $r, $color, $width = null, $style = [], $fill = false) + { + $this->_set_fill_color($color); + $this->_set_stroke_color($color); + + if (!$fill && isset($width)) { + $this->_set_line_style($width, "round", "round", $style); + } + + $y = $this->y($y); + + $this->_pdf->circle($x, $y, $r); + + if ($fill) { + $this->_pdf->fill(); + } else { + $this->_pdf->stroke(); + } + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + $this->_set_stroke_opacity($this->_current_opacity, "Normal"); + } + + public function image($img, $x, $y, $w, $h, $resolution = "normal") + { + $w = (int)$w; + $h = (int)$h; + + $img_type = Cache::detect_type($img, $this->get_dompdf()->getHttpContext()); + + // Strip file:// prefix + if (substr($img, 0, 7) === "file://") { + $img = substr($img, 7); + } + + if (!isset($this->_imgs[$img])) { + if (strtolower($img_type) === "svg") { + //FIXME: PDFLib loads SVG but returns error message "Function must not be called in 'page' scope" + $image_load_response = $this->_pdf->load_graphics($img_type, $img, ""); + } else { + $image_load_response = $this->_pdf->load_image($img_type, $img, ""); + } + if ($image_load_response === 0) { + //TODO: should do something with the error message + $error = $this->_pdf->get_errmsg(); + return; + } + $this->_imgs[$img] = $image_load_response; + } + + $img = $this->_imgs[$img]; + + $y = $this->y($y) - $h; + if (strtolower($img_type) === "svg") { + $this->_pdf->fit_graphics($img, $x, $y, 'boxsize={' . "$w $h" . '} fitmethod=entire'); + } else { + $this->_pdf->fit_image($img, $x, $y, 'boxsize={' . "$w $h" . '} fitmethod=entire'); + } + } + + public function text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_spacing = 0, $char_spacing = 0, $angle = 0) + { + if ($size == 0) { + return; + } + + $fh = $this->_load_font($font); + + $this->_pdf->setfont($fh, $size); + $this->_set_fill_color($color); + + $y = $this->y($y) - $this->get_font_height($font, $size); + + $word_spacing = (float)$word_spacing; + $char_spacing = (float)$char_spacing; + $angle = -(float)$angle; + + $this->_pdf->fit_textline($text, $x, $y, "rotate=$angle wordspacing=$word_spacing charspacing=$char_spacing "); + + $this->_set_fill_opacity($this->_current_opacity, "Normal"); + } + + public function javascript($code) + { + if (strlen($this->_dompdf->getOptions()->getPdflibLicense()) > 0) { + $this->_pdf->create_action("JavaScript", $code); + } + } + + public function add_named_dest($anchorname) + { + $this->_pdf->add_nameddest($anchorname, ""); + } + + public function add_link($url, $x, $y, $width, $height) + { + $y = $this->y($y) - $height; + if (strpos($url, '#') === 0) { + // Local link + $name = substr($url, 1); + if ($name) { + $this->_pdf->create_annotation($x, $y, $x + $width, $y + $height, 'Link', + "contents={$url} destname=" . substr($url, 1) . " linewidth=0"); + } + } else { + //TODO: PDFLib::create_action does not permit non-HTTP links for URI actions + $action = $this->_pdf->create_action("URI", "url={{$url}}"); + // add the annotation only if the action was created + if ($action !== 0) { + $this->_pdf->create_annotation($x, $y, $x + $width, $y + $height, 'Link', "contents={{$url}} action={activate=$action} linewidth=0"); + } + } + } + + public function get_text_width($text, $font, $size, $word_spacing = 0.0, $letter_spacing = 0.0) + { + if ($size == 0) { + return 0.0; + } + + $fh = $this->_load_font($font); + + // Determine the additional width due to extra spacing + $num_spaces = mb_substr_count($text, " "); + $delta = $word_spacing * $num_spaces; + + if ($letter_spacing) { + $num_chars = mb_strlen($text); + $delta += $num_chars * $letter_spacing; + } + + return $this->_pdf->stringwidth($text, $fh, $size) + $delta; + } + + public function get_font_height($font, $size) + { + if ($size == 0) { + return 0.0; + } + + $fh = $this->_load_font($font); + + $this->_pdf->setfont($fh, $size); + + $asc = $this->_pdf->info_font($fh, "ascender", "fontsize=$size"); + $desc = $this->_pdf->info_font($fh, "descender", "fontsize=$size"); + + // $desc is usually < 0, + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + + return (abs($asc) + abs($desc)) * $ratio; + } + + public function get_font_baseline($font, $size) + { + $ratio = $this->_dompdf->getOptions()->getFontHeightRatio(); + + return $this->get_font_height($font, $size) / $ratio * 1.1; + } + + /** + * Processes a callback or script on every page. + * + * The callback function receives the four parameters `int $pageNumber`, + * `int $pageCount`, `Canvas $canvas`, and `FontMetrics $fontMetrics`, in + * that order. If a script is passed as string, the variables `$PAGE_NUM`, + * `$PAGE_COUNT`, `$pdf`, and `$fontMetrics` are available instead. Passing + * a script as string is deprecated and will be removed in a future version. + * + * This function can be used to add page numbers to all pages after the + * first one, for example. + * + * @param callable|string $callback The callback function or PHP script to process on every page + */ + public function page_script($callback): void + { + if (is_string($callback)) { + $this->processPageScript(function ( + int $PAGE_NUM, + int $PAGE_COUNT, + self $pdf, + FontMetrics $fontMetrics + ) use ($callback) { + eval($callback); + }); + return; + } + + $this->processPageScript($callback); + } + + public function page_text($x, $y, $text, $font, $size, $color = [0, 0, 0], $word_space = 0.0, $char_space = 0.0, $angle = 0.0) + { + $this->processPageScript(function (int $pageNumber, int $pageCount) use ($x, $y, $text, $font, $size, $color, $word_space, $char_space, $angle) { + $text = str_replace( + ["{PAGE_NUM}", "{PAGE_COUNT}"], + [$pageNumber, $pageCount], + $text + ); + $this->text($x, $y, $text, $font, $size, $color, $word_space, $char_space, $angle); + }); + } + + public function page_line($x1, $y1, $x2, $y2, $color, $width, $style = []) + { + $this->processPageScript(function () use ($x1, $y1, $x2, $y2, $color, $width, $style) { + $this->line($x1, $y1, $x2, $y2, $color, $width, $style); + }); + } + + public function new_page() + { + // Add objects to the current page + $this->_place_objects(); + + $this->_pdf->suspend_page(""); + $this->_pdf->begin_page_ext($this->_width, $this->_height, ""); + $this->_page_number = ++$this->_page_count; + } + + protected function processPageScript(callable $callback): void + { + $this->_pdf->suspend_page(""); + + for ($p = 1; $p <= $this->_page_count; $p++) { + $this->_pdf->resume_page("pagenumber=$p"); + + $fontMetrics = $this->_dompdf->getFontMetrics(); + $callback($p, $this->_page_count, $this, $fontMetrics); + + $this->_pdf->suspend_page(""); + } + + $this->_pdf->resume_page("pagenumber=" . $this->_page_number); + } + + /** + * @throws Exception + */ + public function stream($filename = "document.pdf", $options = []) + { + if (headers_sent()) { + die("Unable to stream pdf: headers already sent"); + } + + if (!isset($options["compress"])) { + $options["compress"] = true; + } + if (!isset($options["Attachment"])) { + $options["Attachment"] = true; + } + + if ($options["compress"]) { + $this->setPDFLibValue("compress", 6); + } else { + $this->setPDFLibValue("compress", 0); + } + + $this->_close(); + + $data = ""; + + if (self::$IN_MEMORY) { + $data = $this->_pdf->get_buffer(); + $size = mb_strlen($data, "8bit"); + } else { + $size = filesize($this->_file); + } + + header("Cache-Control: private"); + header("Content-Type: application/pdf"); + header("Content-Length: " . $size); + + $filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf"; + $attachment = $options["Attachment"] ? "attachment" : "inline"; + header(Helpers::buildContentDispositionHeader($attachment, $filename)); + + if (self::$IN_MEMORY) { + echo $data; + } else { + // Chunked readfile() + $chunk = (1 << 21); // 2 MB + $fh = fopen($this->_file, "rb"); + if (!$fh) { + throw new Exception("Unable to load temporary PDF file: " . $this->_file); + } + + while (!feof($fh)) { + echo fread($fh, $chunk); + } + fclose($fh); + + //debugpng + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[pdflib stream unlink ' . $this->_file . ']'; + } + if (!$this->_dompdf->getOptions()->getDebugKeepTemp()) { + unlink($this->_file); + } + $this->_file = null; + unset($this->_file); + } + + flush(); + } + + public function output($options = []) + { + if (!isset($options["compress"])) { + $options["compress"] = true; + } + + if ($options["compress"]) { + $this->setPDFLibValue("compress", 6); + } else { + $this->setPDFLibValue("compress", 0); + } + + $this->_close(); + + if (self::$IN_MEMORY) { + $data = $this->_pdf->get_buffer(); + } else { + $data = file_get_contents($this->_file); + + //debugpng + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[pdflib output unlink ' . $this->_file . ']'; + } + if (!$this->_dompdf->getOptions()->getDebugKeepTemp()) { + unlink($this->_file); + } + $this->_file = null; + unset($this->_file); + } + + return $data; + } + + /** + * @param string $keyword + * @param string $optlist + * @return mixed + */ + protected function getPDFLibParameter($keyword, $optlist = "") + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->_pdf->get_option($keyword, ""); + } + + return $this->_pdf->get_parameter($keyword, $optlist); + } + + /** + * @param string $keyword + * @param string $value + * @return mixed + */ + protected function setPDFLibParameter($keyword, $value) + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->_pdf->set_option($keyword . "=" . $value); + } + + return $this->_pdf->set_parameter($keyword, $value); + } + + /** + * @param string $keyword + * @param string $optlist + * @return mixed + */ + protected function getPDFLibValue($keyword, $optlist = "") + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->getPDFLibParameter($keyword, $optlist); + } + + return $this->_pdf->get_value($keyword); + } + + /** + * @param string $keyword + * @param string $value + * @return mixed + */ + protected function setPDFLibValue($keyword, $value) + { + if ($this->getPDFLibMajorVersion() >= 9) { + return $this->setPDFLibParameter($keyword, $value); + } + + return $this->_pdf->set_value($keyword, $value); + } + + /** + * @return int + */ + protected function getPDFLibMajorVersion() + { + if (is_null(self::$MAJOR_VERSION)) { + if (method_exists($this->_pdf, "get_option")) { + self::$MAJOR_VERSION = abs(intval($this->_pdf->get_option("major", ""))); + } else { + self::$MAJOR_VERSION = abs(intval($this->_pdf->get_value("major", ""))); + } + } + + return self::$MAJOR_VERSION; + } +} + +// Workaround for idiotic limitation on statics... +PDFLib::$PAPER_SIZES = CPDF::$PAPER_SIZES; diff --git a/lib/dompdf/vendor/dompdf/dompdf/src/Canvas.php b/lib/dompdf/vendor/dompdf/dompdf/src/Canvas.php new file mode 100644 index 0000000..a3b486c --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/src/Canvas.php @@ -0,0 +1,477 @@ + 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 = []); +} diff --git a/lib/dompdf/vendor/dompdf/dompdf/src/CanvasFactory.php b/lib/dompdf/vendor/dompdf/dompdf/src/CanvasFactory.php new file mode 100644 index 0000000..d89320c --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/src/CanvasFactory.php @@ -0,0 +1,58 @@ +getOptions()->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); + } +} diff --git a/lib/dompdf/vendor/dompdf/dompdf/src/Cellmap.php b/lib/dompdf/vendor/dompdf/dompdf/src/Cellmap.php new file mode 100644 index 0000000..e6c1c68 --- /dev/null +++ b/lib/dompdf/vendor/dompdf/dompdf/src/Cellmap.php @@ -0,0 +1,999 @@ + 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[get_font_family:";
+ print '(' . $computed . '.' . $font_style . '.' . $weight . '.' . $subtype . ')';
+ print '(' . $font . ")get_font_family]\n";
+ }
+ return $font;
+ }
+ }
+
+ $family = null;
+ if ($DEBUGCSS) {
+ print '(default)';
+ }
+ $font = $fontMetrics->getFont($family, $subtype);
+
+ if ($font) {
+ if ($DEBUGCSS) {
+ print '(' . $font . ")get_font_family]\n";
+ }
+ return $font;
+ }
+
+ throw new Exception("Unable to find a suitable font replacement for: '" . $computed . "'");
+ }
+
+ /**
+ * @param float|string $computed
+ * @return float
+ *
+ * @link https://www.w3.org/TR/css-text-4/#word-spacing-property
+ */
+ protected function _get_word_spacing($computed)
+ {
+ if (\is_float($computed)) {
+ return $computed;
+ }
+
+ // Resolve percentage values
+ $font_size = $this->__get("font_size");
+ return $this->single_length_in_pt($computed, $font_size);
+ }
+
+ /**
+ * @param float|string $computed
+ * @return float
+ *
+ * @link https://www.w3.org/TR/css-text-4/#letter-spacing-property
+ */
+ protected function _get_letter_spacing($computed)
+ {
+ if (\is_float($computed)) {
+ return $computed;
+ }
+
+ // Resolve percentage values
+ $font_size = $this->__get("font_size");
+ return $this->single_length_in_pt($computed, $font_size);
+ }
+
+ /**
+ * @param float|string $computed
+ * @return float
+ *
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-line-height
+ */
+ protected function _get_line_height($computed)
+ {
+ // Lengths have been computed to float, number values to string
+ if (\is_float($computed)) {
+ return $computed;
+ }
+
+ $font_size = $this->__get("font_size");
+ $factor = $computed === "normal"
+ ? self::$default_line_height
+ : (float) $computed;
+
+ return $factor * $font_size;
+ }
+
+ /**
+ * @param string $computed
+ * @param bool $current_is_parent
+ *
+ * @return array|string
+ */
+ protected function get_color_value($computed, bool $current_is_parent = false)
+ {
+ if ($computed === "currentcolor") {
+ // https://www.w3.org/TR/css-color-4/#resolving-other-colors
+ if ($current_is_parent) {
+ // Use the `color` value from the parent for the `color`
+ // property itself
+ return isset($this->parent_style)
+ ? $this->parent_style->__get("color")
+ : $this->munge_color(self::$_defaults["color"]);
+ }
+
+ return $this->__get("color");
+ }
+
+ return $this->munge_color($computed) ?? "transparent";
+ }
+
+ /**
+ * Returns the color as an array
+ *
+ * The array has the following format:
+ * `array(r, g, b, "r" => r, "g" => g, "b" => b, "alpha" => alpha, "hex" => "#rrggbb")`
+ *
+ * @param string $computed
+ * @return array|string
+ *
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-color
+ */
+ protected function _get_color($computed)
+ {
+ return $this->get_color_value($computed, true);
+ }
+
+ /**
+ * Returns the background color as an array
+ *
+ * See {@link Style::_get_color()} for format of the color array.
+ *
+ * @param string $computed
+ * @return array|string
+ *
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-color
+ */
+ protected function _get_background_color($computed)
+ {
+ return $this->get_color_value($computed);
+ }
+
+ /**
+ * Returns the background image URI, or "none"
+ *
+ * @param string $computed
+ * @return string
+ *
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-image
+ */
+ protected function _get_background_image($computed): string
+ {
+ return $this->_stylesheet->resolve_url($computed);
+ }
+
+ /**
+ * Returns the border color as an array
+ *
+ * See {@link Style::_get_color()} for format of the color array.
+ *
+ * @param string $computed
+ * @return array|string
+ *
+ * @link https://www.w3.org/TR/CSS21/box.html#border-color-properties
+ */
+ protected function _get_border_top_color($computed)
+ {
+ return $this->get_color_value($computed);
+ }
+
+ /**
+ * @param string $computed
+ * @return array|string
+ */
+ protected function _get_border_right_color($computed)
+ {
+ return $this->get_color_value($computed);
+ }
+
+ /**
+ * @param string $computed
+ * @return array|string
+ */
+ protected function _get_border_bottom_color($computed)
+ {
+ return $this->get_color_value($computed);
+ }
+
+ /**
+ * @param string $computed
+ * @return array|string
+ */
+ protected function _get_border_left_color($computed)
+ {
+ return $this->get_color_value($computed);
+ }
+
+ /**
+ * Return an array of all border properties.
+ *
+ * The returned array has the following structure:
+ *
+ * ```
+ * array("top" => array("width" => [border-width],
+ * "style" => [border-style],
+ * "color" => [border-color (array)]),
+ * "bottom" ... )
+ * ```
+ *
+ * @return array
+ */
+ public function get_border_properties(): array
+ {
+ return [
+ "top" => [
+ "width" => $this->__get("border_top_width"),
+ "style" => $this->__get("border_top_style"),
+ "color" => $this->__get("border_top_color"),
+ ],
+ "bottom" => [
+ "width" => $this->__get("border_bottom_width"),
+ "style" => $this->__get("border_bottom_style"),
+ "color" => $this->__get("border_bottom_color"),
+ ],
+ "right" => [
+ "width" => $this->__get("border_right_width"),
+ "style" => $this->__get("border_right_style"),
+ "color" => $this->__get("border_right_color"),
+ ],
+ "left" => [
+ "width" => $this->__get("border_left_width"),
+ "style" => $this->__get("border_left_style"),
+ "color" => $this->__get("border_left_color"),
+ ],
+ ];
+ }
+
+ /**
+ * Return a single border-side property
+ *
+ * @param string $side
+ * @return string
+ */
+ protected function get_border_side(string $side): string
+ {
+ $color = $this->__get("border_{$side}_color");
+
+ return $this->__get("border_{$side}_width") . " " .
+ $this->__get("border_{$side}_style") . " " .
+ (\is_array($color) ? $color["hex"] : $color);
+ }
+
+ /**
+ * Return full border properties as a string
+ *
+ * Border properties are returned just as specified in CSS:
+ * `[width] [style] [color]`
+ * e.g. "1px solid blue"
+ *
+ * @return string
+ *
+ * @link https://www.w3.org/TR/CSS21/box.html#border-shorthand-properties
+ */
+ protected function _get_border_top(): string
+ {
+ return $this->get_border_side("top");
+ }
+
+ /**
+ * @return string
+ */
+ protected function _get_border_right(): string
+ {
+ return $this->get_border_side("right");
+ }
+
+ /**
+ * @return string
+ */
+ protected function _get_border_bottom(): string
+ {
+ return $this->get_border_side("bottom");
+ }
+
+ /**
+ * @return string
+ */
+ protected function _get_border_left(): string
+ {
+ return $this->get_border_side("left");
+ }
+
+ public function has_border_radius(): bool
+ {
+ if (isset($this->has_border_radius_cache)) {
+ return $this->has_border_radius_cache;
+ }
+
+ // Use a fixed ref size here. We don't know the border-box width here
+ // and font size might be 0. Since we are only interested in whether
+ // there is any border radius at all, this should do
+ $tl = (float) $this->length_in_pt($this->border_top_left_radius, 12);
+ $tr = (float) $this->length_in_pt($this->border_top_right_radius, 12);
+ $br = (float) $this->length_in_pt($this->border_bottom_right_radius, 12);
+ $bl = (float) $this->length_in_pt($this->border_bottom_left_radius, 12);
+
+ $this->has_border_radius_cache = $tl + $tr + $br + $bl > 0;
+ return $this->has_border_radius_cache;
+ }
+
+ /**
+ * Get the final border-radius values to use.
+ *
+ * Percentage values are resolved relative to the width of the border box.
+ * The border radius is additionally scaled for the given render box, and
+ * constrained by its width and height.
+ *
+ * @param float[] $border_box The border box of the frame.
+ * @param float[]|null $render_box The box to resolve the border radius for.
+ *
+ * @return float[] A 4-tuple of top-left, top-right, bottom-right, and bottom-left radius.
+ */
+ public function resolve_border_radius(
+ array $border_box,
+ ?array $render_box = null
+ ): array {
+ $render_box = $render_box ?? $border_box;
+ $use_cache = $render_box === $border_box;
+
+ if ($use_cache && isset($this->resolved_border_radius)) {
+ return $this->resolved_border_radius;
+ }
+
+ [$x, $y, $w, $h] = $border_box;
+
+ // Resolve percentages relative to width, as long as we have no support
+ // for per-axis radii
+ $tl = (float) $this->length_in_pt($this->border_top_left_radius, $w);
+ $tr = (float) $this->length_in_pt($this->border_top_right_radius, $w);
+ $br = (float) $this->length_in_pt($this->border_bottom_right_radius, $w);
+ $bl = (float) $this->length_in_pt($this->border_bottom_left_radius, $w);
+
+ if ($tl + $tr + $br + $bl > 0) {
+ [$rx, $ry, $rw, $rh] = $render_box;
+
+ $t_offset = $y - $ry;
+ $r_offset = $rx + $rw - $x - $w;
+ $b_offset = $ry + $rh - $y - $h;
+ $l_offset = $x - $rx;
+
+ if ($tl > 0) {
+ $tl = max($tl + ($t_offset + $l_offset) / 2, 0);
+ }
+ if ($tr > 0) {
+ $tr = max($tr + ($t_offset + $r_offset) / 2, 0);
+ }
+ if ($br > 0) {
+ $br = max($br + ($b_offset + $r_offset) / 2, 0);
+ }
+ if ($bl > 0) {
+ $bl = max($bl + ($b_offset + $l_offset) / 2, 0);
+ }
+
+ if ($tl + $bl > $rh) {
+ $f = $rh / ($tl + $bl);
+ $tl = $f * $tl;
+ $bl = $f * $bl;
+ }
+ if ($tr + $br > $rh) {
+ $f = $rh / ($tr + $br);
+ $tr = $f * $tr;
+ $br = $f * $br;
+ }
+ if ($tl + $tr > $rw) {
+ $f = $rw / ($tl + $tr);
+ $tl = $f * $tl;
+ $tr = $f * $tr;
+ }
+ if ($bl + $br > $rw) {
+ $f = $rw / ($bl + $br);
+ $bl = $f * $bl;
+ $br = $f * $br;
+ }
+ }
+
+ $values = [$tl, $tr, $br, $bl];
+
+ if ($use_cache) {
+ $this->resolved_border_radius = $values;
+ }
+
+ return $values;
+ }
+
+ /**
+ * Returns the outline color as an array
+ *
+ * See {@link Style::_get_color()} for format of the color array.
+ *
+ * @param string $computed
+ * @return array|string
+ *
+ * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-color
+ */
+ protected function _get_outline_color($computed)
+ {
+ return $this->get_color_value($computed);
+ }
+
+ /**
+ * @param string $computed
+ * @return string
+ *
+ * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-style
+ */
+ protected function _get_outline_style($computed): string
+ {
+ return $computed === "auto" ? "solid" : $computed;
+ }
+
+ /**
+ * Return full outline properties as a string
+ *
+ * Outline properties are returned just as specified in CSS:
+ * `[width] [style] [color]`
+ * e.g. "1px solid blue"
+ *
+ * @return string
+ *
+ * @link https://www.w3.org/TR/CSS21/box.html#border-shorthand-properties
+ */
+ protected function _get_outline(): string
+ {
+ $color = $this->__get("outline_color");
+
+ return $this->__get("outline_width") . " " .
+ $this->__get("outline_style") . " " .
+ (\is_array($color) ? $color["hex"] : $color);
+ }
+
+ /**
+ * Returns the list style image URI, or "none"
+ *
+ * @param string $computed
+ * @return string
+ *
+ * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-image
+ */
+ protected function _get_list_style_image($computed): string
+ {
+ return $this->_stylesheet->resolve_url($computed);
+ }
+
+ /**
+ * @param string $value
+ * @param int $default
+ *
+ * @return array|string
+ */
+ protected function parse_counter_prop(string $value, int $default)
+ {
+ $ident = self::CSS_IDENTIFIER;
+ $integer = self::CSS_INTEGER;
+ $pattern = "/($ident)(?:\s+($integer))?/";
+
+ if (!preg_match_all($pattern, $value, $matches, PREG_SET_ORDER)) {
+ return "none";
+ }
+
+ $counters = [];
+
+ foreach ($matches as $match) {
+ $counter = $match[1];
+ $value = isset($match[2]) ? (int) $match[2] : $default;
+ $counters[$counter] = $value;
+ }
+
+ return $counters;
+ }
+
+ /**
+ * @param string $computed
+ * @return array|string
+ *
+ * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-increment
+ */
+ protected function _get_counter_increment($computed)
+ {
+ if ($computed === "none") {
+ return $computed;
+ }
+
+ return $this->parse_counter_prop($computed, 1);
+ }
+
+ /**
+ * @param string $computed
+ * @return array|string
+ *
+ * @link https://www.w3.org/TR/CSS21/generate.html#propdef-counter-reset
+ */
+ protected function _get_counter_reset($computed)
+ {
+ if ($computed === "none") {
+ return $computed;
+ }
+
+ return $this->parse_counter_prop($computed, 0);
+ }
+
+ /**
+ * @param string $computed
+ * @return string[]|string
+ *
+ * @link https://www.w3.org/TR/CSS21/generate.html#propdef-content
+ */
+ protected function _get_content($computed)
+ {
+ if ($computed === "normal" || $computed === "none") {
+ return $computed;
+ }
+
+ return $this->parse_property_value($computed);
+ }
+
+ /*==============================*/
+
+ /**
+ * Parse a property value into its components.
+ *
+ * @param string $value
+ *
+ * @return string[]
+ */
+ protected function parse_property_value(string $value): array
+ {
+ $ident = self::CSS_IDENTIFIER;
+ $number = self::CSS_NUMBER;
+
+ $pattern = "/\n" .
+ "\s* \" ( (?:[^\"]|\\\\[\"])* ) (?munge_color($val)
+ : $val;
+
+ if ($munged_color === null) {
+ return null;
+ }
+
+ return \is_array($munged_color) ? $munged_color["hex"] : $munged_color;
+ }
+
+ /**
+ * @param string $val
+ * @return int|null
+ */
+ protected function compute_integer(string $val): ?int
+ {
+ $integer = self::CSS_INTEGER;
+ return preg_match("/^$integer$/", $val)
+ ? (int) $val
+ : null;
+ }
+
+ /**
+ * @param string $val
+ * @return float|null
+ */
+ protected function compute_length(string $val): ?float
+ {
+ return mb_strpos($val, "%") === false
+ ? $this->single_length_in_pt($val)
+ : null;
+ }
+
+ /**
+ * @param string $val
+ * @return float|null
+ */
+ protected function compute_length_positive(string $val): ?float
+ {
+ $computed = $this->compute_length($val);
+ return $computed !== null && $computed >= 0 ? $computed : null;
+ }
+
+ /**
+ * @param string $val
+ * @return float|string|null
+ */
+ protected function compute_length_percentage(string $val)
+ {
+ // Compute with a fixed ref size to decide whether percentage values
+ // are valid
+ $computed = $this->single_length_in_pt($val, 12);
+
+ if ($computed === null) {
+ return null;
+ }
+
+ // Retain valid percentage declarations
+ return mb_strpos($val, "%") === false ? $computed : $val;
+ }
+
+ /**
+ * @param string $val
+ * @return float|string|null
+ */
+ protected function compute_length_percentage_positive(string $val)
+ {
+ // Compute with a fixed ref size to decide whether percentage values
+ // are valid
+ $computed = $this->single_length_in_pt($val, 12);
+
+ if ($computed === null || $computed < 0) {
+ return null;
+ }
+
+ // Retain valid percentage declarations
+ return mb_strpos($val, "%") === false ? $computed : $val;
+ }
+
+ /**
+ * @param string $val
+ * @param string $style_prop The corresponding border-/outline-style property.
+ *
+ * @return float|null
+ *
+ * @link https://www.w3.org/TR/css-backgrounds-3/#typedef-line-width
+ */
+ protected function compute_line_width(string $val, string $style_prop): ?float
+ {
+ // Border-width keywords
+ if ($val === "thin") {
+ $computed = 0.5;
+ } elseif ($val === "medium") {
+ $computed = 1.5;
+ } elseif ($val === "thick") {
+ $computed = 2.5;
+ } else {
+ $computed = $this->compute_length_positive($val);
+ }
+
+ if ($computed === null) {
+ return null;
+ }
+
+ // Computed width is 0 if the line style is `none` or `hidden`
+ // https://www.w3.org/TR/css-backgrounds-3/#border-width
+ // https://www.w3.org/TR/css-ui-4/#outline-width
+ $lineStyle = $this->__get($style_prop);
+ $hasLineStyle = $lineStyle !== "none" && $lineStyle !== "hidden";
+
+ return $hasLineStyle ? $computed : 0.0;
+ }
+
+ /**
+ * @param string $val
+ * @return string|null
+ */
+ protected function compute_border_style(string $val): ?string
+ {
+ return \in_array($val, self::BORDER_STYLES, true) ? $val : null;
+ }
+
+ /**
+ * Parse a property value with 1 to 4 components into 4 values, as required
+ * by shorthand properties such as `margin`, `padding`, and `border-radius`.
+ *
+ * @param string $prop The shorthand property with exactly 4 sub-properties to handle.
+ * @param string $value The property value to parse.
+ *
+ * @return string[]
+ */
+ protected function set_quad_shorthand(string $prop, string $value): array
+ {
+ $v = $this->parse_property_value($value);
+
+ switch (\count($v)) {
+ case 1:
+ $values = [$v[0], $v[0], $v[0], $v[0]];
+ break;
+ case 2:
+ $values = [$v[0], $v[1], $v[0], $v[1]];
+ break;
+ case 3:
+ $values = [$v[0], $v[1], $v[2], $v[1]];
+ break;
+ case 4:
+ $values = [$v[0], $v[1], $v[2], $v[3]];
+ break;
+ default:
+ return [];
+ }
+
+ return array_combine(self::$_props_shorthand[$prop], $values);
+ }
+
+ /*======================*/
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visuren.html#display-prop
+ */
+ protected function _compute_display(string $val)
+ {
+ // Make sure that common valid, but unsupported display types have an
+ // appropriate fallback display type
+ switch ($val) {
+ case "flow-root":
+ case "flex":
+ case "grid":
+ case "table-caption":
+ $val = "block";
+ break;
+ case "inline-flex":
+ case "inline-grid":
+ $val = "inline-block";
+ break;
+ }
+
+ if (!isset(self::$valid_display_types[$val])) {
+ return null;
+ }
+
+ // https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
+ if ($this->is_in_flow()) {
+ return $val;
+ } else {
+ switch ($val) {
+ case "inline":
+ case "inline-block":
+ // case "table-row-group":
+ // case "table-header-group":
+ // case "table-footer-group":
+ // case "table-row":
+ // case "table-cell":
+ // case "table-column-group":
+ // case "table-column":
+ // case "table-caption":
+ return "block";
+ case "inline-table":
+ return "table";
+ default:
+ return $val;
+ }
+ }
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-color
+ */
+ protected function _compute_color(string $color)
+ {
+ return $this->compute_color_value($color);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-color
+ */
+ protected function _compute_background_color(string $color)
+ {
+ return $this->compute_color_value($color);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-image
+ */
+ protected function _compute_background_image(string $val)
+ {
+ $parsed_val = $this->_stylesheet->resolve_url($val);
+
+ if ($parsed_val === "none") {
+ return "none";
+ } else {
+ return "url($parsed_val)";
+ }
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-repeat
+ */
+ protected function _compute_background_repeat(string $val)
+ {
+ $keywords = ["repeat", "repeat-x", "repeat-y", "no-repeat"];
+ return \in_array($val, $keywords, true) ? $val : null;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-attachment
+ */
+ protected function _compute_background_attachment(string $val)
+ {
+ $keywords = ["scroll", "fixed"];
+ return \in_array($val, $keywords, true) ? $val : null;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/colors.html#propdef-background-position
+ */
+ protected function _compute_background_position(string $val)
+ {
+ $parts = preg_split("/\s+/", $val);
+
+ if (\count($parts) > 2) {
+ return null;
+ }
+
+ switch ($parts[0]) {
+ case "left":
+ $x = "0%";
+ break;
+
+ case "right":
+ $x = "100%";
+ break;
+
+ case "top":
+ $y = "0%";
+ break;
+
+ case "bottom":
+ $y = "100%";
+ break;
+
+ case "center":
+ $x = "50%";
+ $y = "50%";
+ break;
+
+ default:
+ $x = $parts[0];
+ break;
+ }
+
+ if (isset($parts[1])) {
+ switch ($parts[1]) {
+ case "left":
+ $x = "0%";
+ break;
+
+ case "right":
+ $x = "100%";
+ break;
+
+ case "top":
+ $y = "0%";
+ break;
+
+ case "bottom":
+ $y = "100%";
+ break;
+
+ case "center":
+ if ($parts[0] === "left" || $parts[0] === "right" || $parts[0] === "center") {
+ $y = "50%";
+ } else {
+ $x = "50%";
+ }
+ break;
+
+ default:
+ $y = $parts[1];
+ break;
+ }
+ } else {
+ $y = "50%";
+ }
+
+ if (!isset($x)) {
+ $x = "0%";
+ }
+
+ if (!isset($y)) {
+ $y = "0%";
+ }
+
+ return [$x, $y];
+ }
+
+ /**
+ * Compute `background-size`.
+ *
+ * Computes to one of the following values:
+ * * `cover`
+ * * `contain`
+ * * `[width, height]`, each being a length, percentage, or `auto`
+ *
+ * @link https://www.w3.org/TR/css-backgrounds-3/#background-size
+ */
+ protected function _compute_background_size(string $val)
+ {
+ if ($val === "cover" || $val === "contain") {
+ return $val;
+ }
+
+ $parts = preg_split("/\s+/", $val);
+
+ if (\count($parts) > 2) {
+ return null;
+ }
+
+ $width = $parts[0];
+ if ($width !== "auto") {
+ $width = $this->compute_length_percentage_positive($width);
+ }
+
+ $height = $parts[1] ?? "auto";
+ if ($height !== "auto") {
+ $height = $this->compute_length_percentage_positive($height);
+ }
+
+ if ($width === null || $height === null) {
+ return null;
+ }
+
+ return [$width, $height];
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-background
+ */
+ protected function _set_background(string $value): array
+ {
+ $components = $this->parse_property_value($value);
+ $props = [];
+ $pos_size = [];
+
+ foreach ($components as $val) {
+ if ($val === "none" || mb_substr($val, 0, 4) === "url(") {
+ $props["background_image"] = $val;
+ } elseif ($val === "scroll" || $val === "fixed") {
+ $props["background_attachment"] = $val;
+ } elseif ($val === "repeat" || $val === "repeat-x" || $val === "repeat-y" || $val === "no-repeat") {
+ $props["background_repeat"] = $val;
+ } elseif ($this->is_color_value($val)) {
+ $props["background_color"] = $val;
+ } else {
+ $pos_size[] = $val;
+ }
+ }
+
+ if (\count($pos_size)) {
+ // Split value list at "/"
+ $index = array_search("/", $pos_size, true);
+
+ if ($index !== false) {
+ $pos = \array_slice($pos_size, 0, $index);
+ $size = \array_slice($pos_size, $index + 1);
+ } else {
+ $pos = $pos_size;
+ $size = [];
+ }
+
+ $props["background_position"] = implode(" ", $pos);
+
+ if (\count($size)) {
+ $props["background_size"] = implode(" ", $size);
+ }
+ }
+
+ return $props;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/fonts.html#propdef-font-size
+ */
+ protected function _compute_font_size(string $size)
+ {
+ $parent_font_size = isset($this->parent_style)
+ ? $this->parent_style->__get("font_size")
+ : self::$default_font_size;
+
+ switch ($size) {
+ case "xx-small":
+ case "x-small":
+ case "small":
+ case "medium":
+ case "large":
+ case "x-large":
+ case "xx-large":
+ $fs = self::$default_font_size * self::$font_size_keywords[$size];
+ break;
+
+ case "smaller":
+ $fs = 8 / 9 * $parent_font_size;
+ break;
+
+ case "larger":
+ $fs = 6 / 5 * $parent_font_size;
+ break;
+
+ default:
+ $fs = $this->single_length_in_pt($size, $parent_font_size, $parent_font_size);
+ break;
+ }
+
+ return $fs;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/fonts.html#font-boldness
+ */
+ protected function _compute_font_weight(string $weight)
+ {
+ $computed_weight = $weight;
+
+ if ($weight === "bolder") {
+ //TODO: One font weight heavier than the parent element (among the available weights of the font).
+ $computed_weight = "bold";
+ } elseif ($weight === "lighter") {
+ //TODO: One font weight lighter than the parent element (among the available weights of the font).
+ $computed_weight = "normal";
+ }
+
+ return $computed_weight;
+ }
+
+ /**
+ * Handle the `font` shorthand property.
+ *
+ * `[ font-style || font-variant || font-weight ] font-size [ / line-height ] font-family`
+ *
+ * @link https://www.w3.org/TR/CSS21/fonts.html#font-shorthand
+ */
+ protected function _set_font(string $value): array
+ {
+ $components = $this->parse_property_value($value);
+ $props = [];
+
+ $number = self::CSS_NUMBER;
+ $unit = "pt|px|pc|rem|em|ex|in|cm|mm|%";
+ $sizePattern = "/^(xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger|$number(?:$unit))$/";
+ $sizeIndex = null;
+
+ // Find index of font-size to split the component list
+ foreach ($components as $i => $val) {
+ if (preg_match($sizePattern, $val)) {
+ $sizeIndex = $i;
+ $props["font_size"] = $val;
+ break;
+ }
+ }
+
+ // `font-size` is mandatory
+ if ($sizeIndex === null) {
+ return [];
+ }
+
+ // `font-style`, `font-variant`, `font-weight` in any order
+ $styleVariantWeight = \array_slice($components, 0, $sizeIndex);
+ $stylePattern = "/^(italic|oblique)$/";
+ $variantPattern = "/^(small-caps)$/";
+ $weightPattern = "/^(bold|bolder|lighter|100|200|300|400|500|600|700|800|900)$/";
+
+ if (\count($styleVariantWeight) > 3) {
+ return [];
+ }
+
+ foreach ($styleVariantWeight as $val) {
+ if ($val === "normal") {
+ // Ignore any `normal` value, as it is valid and the initial
+ // value for all three properties
+ } elseif (!isset($props["font_style"]) && preg_match($stylePattern, $val)) {
+ $props["font_style"] = $val;
+ } elseif (!isset($props["font_variant"]) && preg_match($variantPattern, $val)) {
+ $props["font_variant"] = $val;
+ } elseif (!isset($props["font_weight"]) && preg_match($weightPattern, $val)) {
+ $props["font_weight"] = $val;
+ } else {
+ // Duplicates and other values disallowed here
+ return [];
+ }
+ }
+
+ // Optional slash + `line-height` followed by mandatory `font-family`
+ $lineFamily = \array_slice($components, $sizeIndex + 1);
+ $hasLineHeight = $lineFamily !== [] && $lineFamily[0] === "/";
+ $lineHeight = $hasLineHeight ? \array_slice($lineFamily, 1, 1) : [];
+ $fontFamily = $hasLineHeight ? \array_slice($lineFamily, 2) : $lineFamily;
+ $lineHeightPattern = "/^(normal|$number(?:$unit)?)$/";
+
+ // Missing `font-family` or `line-height` after slash
+ if ($fontFamily === []
+ || ($hasLineHeight && !preg_match($lineHeightPattern, $lineHeight[0]))
+ ) {
+ return [];
+ }
+
+ if ($hasLineHeight) {
+ $props["line_height"] = $lineHeight[0];
+ }
+
+ $props["font_family"] = implode("", $fontFamily);
+
+ return $props;
+ }
+
+ /**
+ * Compute `text-align`.
+ *
+ * If no alignment is set on the element and the direction is rtl then
+ * the property is set to "right", otherwise it is set to "left".
+ *
+ * @link https://www.w3.org/TR/CSS21/text.html#propdef-text-align
+ */
+ protected function _compute_text_align(string $val)
+ {
+ $alignment = $val;
+ if ($alignment === "") {
+ $alignment = "left";
+ if ($this->__get("direction") === "rtl") {
+ $alignment = "right";
+ }
+ }
+
+ if (!\in_array($alignment, self::TEXT_ALIGN_KEYWORDS, true)) {
+ return null;
+ }
+
+ return $alignment;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-text-4/#word-spacing-property
+ */
+ protected function _compute_word_spacing(string $val)
+ {
+ if ($val === "normal") {
+ return 0.0;
+ }
+
+ return $this->compute_length_percentage($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-text-4/#letter-spacing-property
+ */
+ protected function _compute_letter_spacing(string $val)
+ {
+ if ($val === "normal") {
+ return 0.0;
+ }
+
+ return $this->compute_length_percentage($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-line-height
+ */
+ protected function _compute_line_height(string $val)
+ {
+ if ($val === "normal") {
+ return $val;
+ }
+
+ // Compute number values to string and lengths to float (in pt)
+ if (is_numeric($val)) {
+ return (string) $val;
+ }
+
+ $font_size = $this->__get("font_size");
+ $computed = $this->single_length_in_pt($val, $font_size);
+ return $computed !== null && $computed >= 0 ? $computed : null;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-text-3/#text-indent-property
+ */
+ protected function _compute_text_indent(string $val)
+ {
+ return $this->compute_length_percentage($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-before
+ */
+ protected function _compute_page_break_before(string $break)
+ {
+ if ($break === "left" || $break === "right") {
+ $break = "always";
+ }
+
+ return $break;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/page.html#propdef-page-break-after
+ */
+ protected function _compute_page_break_after(string $break)
+ {
+ if ($break === "left" || $break === "right") {
+ $break = "always";
+ }
+
+ return $break;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-width
+ */
+ protected function _compute_width(string $val)
+ {
+ if ($val === "auto") {
+ return $val;
+ }
+
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-height
+ */
+ protected function _compute_height(string $val)
+ {
+ if ($val === "auto") {
+ return $val;
+ }
+
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-width
+ */
+ protected function _compute_min_width(string $val)
+ {
+ // Legacy support for `none`, not covered by spec
+ if ($val === "auto" || $val === "none") {
+ return "auto";
+ }
+
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-min-height
+ */
+ protected function _compute_min_height(string $val)
+ {
+ // Legacy support for `none`, not covered by spec
+ if ($val === "auto" || $val === "none") {
+ return "auto";
+ }
+
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-width
+ */
+ protected function _compute_max_width(string $val)
+ {
+ // Legacy support for `auto`, not covered by spec
+ if ($val === "none" || $val === "auto") {
+ return "none";
+ }
+
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/visudet.html#propdef-max-height
+ */
+ protected function _compute_max_height(string $val)
+ {
+ // Legacy support for `auto`, not covered by spec
+ if ($val === "none" || $val === "auto") {
+ return "none";
+ }
+
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-position-3/#inset-properties
+ * @link https://www.w3.org/TR/css-position-3/#propdef-inset
+ */
+ protected function _set_inset(string $val): array
+ {
+ return $this->set_quad_shorthand("inset", $val);
+ }
+
+ /**
+ * @param string $val
+ * @return float|string|null
+ */
+ protected function compute_box_inset(string $val)
+ {
+ if ($val === "auto") {
+ return $val;
+ }
+
+ return $this->compute_length_percentage($val);
+ }
+
+ protected function _compute_top(string $val)
+ {
+ return $this->compute_box_inset($val);
+ }
+
+ protected function _compute_right(string $val)
+ {
+ return $this->compute_box_inset($val);
+ }
+
+ protected function _compute_bottom(string $val)
+ {
+ return $this->compute_box_inset($val);
+ }
+
+ protected function _compute_left(string $val)
+ {
+ return $this->compute_box_inset($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/box.html#margin-properties
+ * @link https://www.w3.org/TR/CSS21/box.html#propdef-margin
+ */
+ protected function _set_margin(string $val): array
+ {
+ return $this->set_quad_shorthand("margin", $val);
+ }
+
+ /**
+ * @param string $val
+ * @return float|string|null
+ */
+ protected function compute_margin(string $val)
+ {
+ // Legacy support for `none` keyword, not covered by spec
+ if ($val === "none") {
+ return 0.0;
+ }
+
+ if ($val === "auto") {
+ return $val;
+ }
+
+ return $this->compute_length_percentage($val);
+ }
+
+ protected function _compute_margin_top(string $val)
+ {
+ return $this->compute_margin($val);
+ }
+
+ protected function _compute_margin_right(string $val)
+ {
+ return $this->compute_margin($val);
+ }
+
+ protected function _compute_margin_bottom(string $val)
+ {
+ return $this->compute_margin($val);
+ }
+
+ protected function _compute_margin_left(string $val)
+ {
+ return $this->compute_margin($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/box.html#padding-properties
+ * @link https://www.w3.org/TR/CSS21/box.html#propdef-padding
+ */
+ protected function _set_padding(string $val): array
+ {
+ return $this->set_quad_shorthand("padding", $val);
+ }
+
+ /**
+ * @param string $val
+ * @return float|string|null
+ */
+ protected function compute_padding(string $val)
+ {
+ // Legacy support for `none` keyword, not covered by spec
+ if ($val === "none") {
+ return 0.0;
+ }
+
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ protected function _compute_padding_top(string $val)
+ {
+ return $this->compute_padding($val);
+ }
+
+ protected function _compute_padding_right(string $val)
+ {
+ return $this->compute_padding($val);
+ }
+
+ protected function _compute_padding_bottom(string $val)
+ {
+ return $this->compute_padding($val);
+ }
+
+ protected function _compute_padding_left(string $val)
+ {
+ return $this->compute_padding($val);
+ }
+
+ /**
+ * @param string $value `width || style || color`
+ * @param string[] $styles The list of border styles to accept.
+ *
+ * @return array Array of `[width, style, color]`, or `null` if the declaration is invalid.
+ */
+ protected function parse_border_side(string $value, array $styles = self::BORDER_STYLES): ?array
+ {
+ $components = $this->parse_property_value($value);
+ $width = null;
+ $style = null;
+ $color = null;
+
+ foreach ($components as $val) {
+ if ($style === null && \in_array($val, $styles, true)) {
+ $style = $val;
+ } elseif ($color === null && $this->is_color_value($val)) {
+ $color = $val;
+ } elseif ($width === null) {
+ // Assume width
+ $width = $val;
+ } else {
+ // Duplicates are not allowed
+ return null;
+ }
+ }
+
+ return [$width, $style, $color];
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/box.html#border-properties
+ * @link https://www.w3.org/TR/CSS21/box.html#propdef-border
+ */
+ protected function _set_border(string $value): array
+ {
+ $values = $this->parse_border_side($value);
+
+ if ($values === null) {
+ return [];
+ }
+
+ return array_merge(
+ array_combine(self::$_props_shorthand["border_top"], $values),
+ array_combine(self::$_props_shorthand["border_right"], $values),
+ array_combine(self::$_props_shorthand["border_bottom"], $values),
+ array_combine(self::$_props_shorthand["border_left"], $values)
+ );
+ }
+
+ /**
+ * @param string $prop
+ * @param string $value
+ * @return array
+ */
+ protected function set_border_side(string $prop, string $value): array
+ {
+ $values = $this->parse_border_side($value);
+
+ if ($values === null) {
+ return [];
+ }
+
+ return array_combine(self::$_props_shorthand[$prop], $values);
+ }
+
+ protected function _set_border_top(string $val): array
+ {
+ return $this->set_border_side("border_top", $val);
+ }
+
+ protected function _set_border_right(string $val): array
+ {
+ return $this->set_border_side("border_right", $val);
+ }
+
+ protected function _set_border_bottom(string $val): array
+ {
+ return $this->set_border_side("border_bottom", $val);
+ }
+
+ protected function _set_border_left(string $val): array
+ {
+ return $this->set_border_side("border_left", $val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-color
+ */
+ protected function _set_border_color(string $val): array
+ {
+ return $this->set_quad_shorthand("border_color", $val);
+ }
+
+ protected function _compute_border_top_color(string $val)
+ {
+ return $this->compute_color_value($val);
+ }
+
+ protected function _compute_border_right_color(string $val)
+ {
+ return $this->compute_color_value($val);
+ }
+
+ protected function _compute_border_bottom_color(string $val)
+ {
+ return $this->compute_color_value($val);
+ }
+
+ protected function _compute_border_left_color(string $val)
+ {
+ return $this->compute_color_value($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-style
+ */
+ protected function _set_border_style(string $val): array
+ {
+ return $this->set_quad_shorthand("border_style", $val);
+ }
+
+ protected function _compute_border_top_style(string $val)
+ {
+ return $this->compute_border_style($val);
+ }
+
+ protected function _compute_border_right_style(string $val)
+ {
+ return $this->compute_border_style($val);
+ }
+
+ protected function _compute_border_bottom_style(string $val)
+ {
+ return $this->compute_border_style($val);
+ }
+
+ protected function _compute_border_left_style(string $val)
+ {
+ return $this->compute_border_style($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/box.html#propdef-border-width
+ */
+ protected function _set_border_width(string $val): array
+ {
+ return $this->set_quad_shorthand("border_width", $val);
+ }
+
+ protected function _compute_border_top_width(string $val)
+ {
+ return $this->compute_line_width($val, "border_top_style");
+ }
+
+ protected function _compute_border_right_width(string $val)
+ {
+ return $this->compute_line_width($val, "border_right_style");
+ }
+
+ protected function _compute_border_bottom_width(string $val)
+ {
+ return $this->compute_line_width($val, "border_bottom_style");
+ }
+
+ protected function _compute_border_left_width(string $val)
+ {
+ return $this->compute_line_width($val, "border_left_style");
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-backgrounds-3/#corners
+ * @link https://www.w3.org/TR/css-backgrounds-3/#propdef-border-radius
+ */
+ protected function _set_border_radius(string $val): array
+ {
+ return $this->set_quad_shorthand("border_radius", $val);
+ }
+
+ protected function _compute_border_top_left_radius(string $val)
+ {
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ protected function _compute_border_top_right_radius(string $val)
+ {
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ protected function _compute_border_bottom_right_radius(string $val)
+ {
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ protected function _compute_border_bottom_left_radius(string $val)
+ {
+ return $this->compute_length_percentage_positive($val);
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-ui-4/#outline-props
+ * @link https://www.w3.org/TR/css-ui-4/#propdef-outline
+ */
+ protected function _set_outline(string $value): array
+ {
+ $values = $this->parse_border_side($value, self::OUTLINE_STYLES);
+
+ if ($values === null) {
+ return [];
+ }
+
+ return array_combine(self::$_props_shorthand["outline"], $values);
+ }
+
+ protected function _compute_outline_color(string $val)
+ {
+ return $this->compute_color_value($val);
+ }
+
+ protected function _compute_outline_style(string $val)
+ {
+ return \in_array($val, self::OUTLINE_STYLES, true) ? $val : null;
+ }
+
+ protected function _compute_outline_width(string $val)
+ {
+ return $this->compute_line_width($val, "outline_style");
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-ui-4/#propdef-outline-offset
+ */
+ protected function _compute_outline_offset(string $val)
+ {
+ return $this->compute_length($val);
+ }
+
+ /**
+ * Compute `border-spacing` to two lengths of the form
+ * `[horizontal, vertical]`.
+ *
+ * @link https://www.w3.org/TR/CSS21/tables.html#propdef-border-spacing
+ */
+ protected function _compute_border_spacing(string $val)
+ {
+ $parts = preg_split("/\s+/", $val);
+
+ if (\count($parts) > 2) {
+ return null;
+ }
+
+ $h = $this->compute_length_positive($parts[0]);
+ $v = isset($parts[1])
+ ? $this->compute_length_positive($parts[1])
+ : $h;
+
+ if ($h === null || $v === null) {
+ return null;
+ }
+
+ return [$h, $v];
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style-image
+ */
+ protected function _compute_list_style_image(string $val)
+ {
+ $parsed_val = $this->_stylesheet->resolve_url($val);
+
+ if ($parsed_val === "none") {
+ return "none";
+ } else {
+ return "url($parsed_val)";
+ }
+ }
+
+ /**
+ * @link https://www.w3.org/TR/CSS21/generate.html#propdef-list-style
+ */
+ protected function _set_list_style(string $value): array
+ {
+ static $positions = ["inside", "outside"];
+ static $types = [
+ "disc", "circle", "square",
+ "decimal-leading-zero", "decimal", "1",
+ "lower-roman", "upper-roman", "a", "A",
+ "lower-greek",
+ "lower-latin", "upper-latin",
+ "lower-alpha", "upper-alpha",
+ "armenian", "georgian", "hebrew",
+ "cjk-ideographic", "hiragana", "katakana",
+ "hiragana-iroha", "katakana-iroha", "none"
+ ];
+
+ $components = $this->parse_property_value($value);
+ $props = [];
+
+ foreach ($components as $val) {
+ /* https://www.w3.org/TR/CSS21/generate.html#list-style
+ * A value of 'none' for the 'list-style' property sets both 'list-style-type' and 'list-style-image' to 'none'
+ */
+ if ($val === "none") {
+ $props["list_style_type"] = $val;
+ $props["list_style_image"] = $val;
+ continue;
+ }
+
+ //On setting or merging or inheriting list_style_image as well as list_style_type,
+ //and url exists, then url has precedence, otherwise fall back to list_style_type
+ //Firefox is wrong here (list_style_image gets overwritten on explicit list_style_type)
+ //Internet Explorer 7/8 and dompdf is right.
+
+ if (mb_substr($val, 0, 4) === "url(") {
+ $props["list_style_image"] = $val;
+ continue;
+ }
+
+ if (\in_array($val, $types, true)) {
+ $props["list_style_type"] = $val;
+ } elseif (\in_array($val, $positions, true)) {
+ $props["list_style_position"] = $val;
+ }
+ }
+
+ return $props;
+ }
+
+ /**
+ * @link https://www.w3.org/TR/css-page-3/#page-size-prop
+ */
+ protected function _compute_size(string $val)
+ {
+ if ($val === "auto") {
+ return $val;
+ }
+
+ $parts = $this->parse_property_value($val);
+ $count = \count($parts);
+
+ if ($count === 0 || $count > 3) {
+ return null;
+ }
+
+ $size = null;
+ $orientation = null;
+ $lengths = [];
+
+ foreach ($parts as $part) {
+ if ($size === null && isset(CPDF::$PAPER_SIZES[$part])) {
+ $size = $part;
+ } elseif ($orientation === null && ($part === "portrait" || $part === "landscape")) {
+ $orientation = $part;
+ } else {
+ $lengths[] = $part;
+ }
+ }
+
+ if ($size !== null && $lengths !== []) {
+ return null;
+ }
+
+ if ($size !== null) {
+ // Standard paper size
+ [$l1, $l2] = \array_slice(CPDF::$PAPER_SIZES[$size], 2, 2);
+ } elseif ($lengths === []) {
+ // Orientation only, use default paper size
+ $dims = $this->_stylesheet->get_dompdf()->getPaperSize();
+ [$l1, $l2] = \array_slice($dims, 2, 2);
+ } else {
+ // Custom paper size
+ $l1 = $this->compute_length_positive($lengths[0]);
+ $l2 = isset($lengths[1]) ? $this->compute_length_positive($lengths[1]) : $l1;
+
+ if ($l1 === null || $l2 === null) {
+ return null;
+ }
+ }
+
+ if (($orientation === "portrait" && $l1 > $l2)
+ || ($orientation === "landscape" && $l2 > $l1)
+ ) {
+ return [$l2, $l1];
+ }
+
+ return [$l1, $l2];
+ }
+
+ /**
+ * @param string $computed
+ * @return array
+ *
+ * @link https://www.w3.org/TR/css-transforms-1/#transform-property
+ */
+ protected function _get_transform($computed)
+ {
+ //TODO: should be handled in setter (lengths set to absolute)
+
+ $number = "\s*([^,\s]+)\s*";
+ $tr_value = "\s*([^,\s]+)\s*";
+ $angle = "\s*([^,\s]+(?:deg|rad)?)\s*";
+
+ if (!preg_match_all("/[a-z]+\([^\)]+\)/i", $computed, $parts, PREG_SET_ORDER)) {
+ return [];
+ }
+
+ $functions = [
+ //"matrix" => "\($number,$number,$number,$number,$number,$number\)",
+
+ "translate" => "\($tr_value(?:,$tr_value)?\)",
+ "translateX" => "\($tr_value\)",
+ "translateY" => "\($tr_value\)",
+
+ "scale" => "\($number(?:,$number)?\)",
+ "scaleX" => "\($number\)",
+ "scaleY" => "\($number\)",
+
+ "rotate" => "\($angle\)",
+
+ "skew" => "\($angle(?:,$angle)?\)",
+ "skewX" => "\($angle\)",
+ "skewY" => "\($angle\)",
+ ];
+
+ $transforms = [];
+
+ foreach ($parts as $part) {
+ $t = $part[0];
+
+ foreach ($functions as $name => $pattern) {
+ if (preg_match("/$name\s*$pattern/i", $t, $matches)) {
+ $values = \array_slice($matches, 1);
+
+ switch ($name) {
+ // ';
+ 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") .
+ "'" . mb_substr($tmp, 0, 70) . + (mb_strlen($tmp) > 70 ? "..." : "") . "'"; + } elseif ($css_class = $this->_node->getAttribute("class")) { + $str .= "CSS class: '$css_class'
" . $this->_style->__toString() . ""; + + if ($this->_decorator instanceof FrameDecorator\Block) { + $str .= "Lines:
";
+ foreach ($this->_decorator->get_line_boxes() as $line) {
+ foreach ($line->get_frames() as $frame) {
+ if ($frame instanceof FrameDecorator\Text) {
+ $str .= "\ntext: ";
+ $str .= "'" . htmlspecialchars($frame->get_text()) . "'";
+ } else {
+ $str .= "\nBlock: " . $frame->get_node()->nodeName . " (" . spl_object_hash($frame->get_node()) . ")";
+ }
+ }
+
+ $str .=
+ "\ny => " . $line->y . "\n" .
+ "w => " . $line->w . "\n" .
+ "h => " . $line->h . "\n" .
+ "left => " . $line->left . "\n" .
+ "right => " . $line->right . "\n";
+ }
+ $str .= "";
+ }
+
+ $str .= "\n";
+ if (php_sapi_name() === "cli") {
+ $str = strip_tags(str_replace(["" . 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