Bladeren bron

Merge branch 'develop' of github.com:Dolibarr/dolibarr into develop

# Conflicts:
#	htdocs/compta/facture/card.php
#	htdocs/fourn/facture/card.php
Laurent Destailleur 2 jaren geleden
bovenliggende
commit
7be8ea782d
100 gewijzigde bestanden met toevoegingen van 8470 en 629 verwijderingen
  1. 6 0
      .travis.yml
  2. 3 1
      COPYRIGHT
  3. 86 2
      ChangeLog
  4. 1 0
      htdocs/adherents/admin/member_emails.php
  5. 1 1
      htdocs/adherents/canvas/actions_adherentcard_common.class.php
  6. 7 1
      htdocs/adherents/class/adherent.class.php
  7. 1 1
      htdocs/adherents/subscription.php
  8. 1 1
      htdocs/adherents/subscription/list.php
  9. 4 4
      htdocs/admin/dav.php
  10. 15 8
      htdocs/admin/emailcollector_card.php
  11. 11 8
      htdocs/admin/pdf_other.php
  12. 4 4
      htdocs/api/class/api_setup.class.php
  13. 3 3
      htdocs/categories/index.php
  14. 8 4
      htdocs/categories/viewcat.php
  15. 8 2
      htdocs/compta/bank/card.php
  16. 6 2
      htdocs/compta/bank/class/account.class.php
  17. 1 1
      htdocs/compta/facture/card.php
  18. 3 3
      htdocs/compta/facture/class/facture.class.php
  19. 294 166
      htdocs/compta/prelevement/list.php
  20. 244 119
      htdocs/compta/prelevement/orders_list.php
  21. 14 7
      htdocs/compta/prelevement/stats.php
  22. 1 1
      htdocs/contact/canvas/actions_contactcard_common.class.php
  23. 8 1
      htdocs/contrat/class/contrat.class.php
  24. 1 1
      htdocs/core/boxes/box_members_by_tags.php
  25. 2 2
      htdocs/core/class/CMailFile.class.php
  26. 15 0
      htdocs/core/class/commonobject.class.php
  27. 12 5
      htdocs/core/class/extrafields.class.php
  28. 85 28
      htdocs/core/class/html.form.class.php
  29. 6 6
      htdocs/core/class/notify.class.php
  30. 251 86
      htdocs/core/customreports.php
  31. 1 1
      htdocs/core/lib/admin.lib.php
  32. 1 1
      htdocs/core/lib/files.lib.php
  33. 68 13
      htdocs/core/lib/functions.lib.php
  34. 39 0
      htdocs/core/lib/functions2.lib.php
  35. 109 0
      htdocs/core/lib/modulebuilder.lib.php
  36. 4 4
      htdocs/core/menus/standard/eldy.lib.php
  37. 32 13
      htdocs/core/modules/facture/doc/pdf_crabe.modules.php
  38. 36 21
      htdocs/core/modules/facture/doc/pdf_sponge.modules.php
  39. 155 0
      htdocs/core/modules/facture/modules_facture.php
  40. 4 2
      htdocs/core/modules/modEmailCollector.class.php
  41. 1 1
      htdocs/core/modules/modFournisseur.class.php
  42. 1 35
      htdocs/core/modules/modWebhook.class.php
  43. 2 1
      htdocs/core/modules/modWorkstation.class.php
  44. 5 2
      htdocs/core/modules/supplier_invoice/mod_facture_fournisseur_cactus.php
  45. 1 1
      htdocs/core/tpl/list_print_total.tpl.php
  46. 13 10
      htdocs/emailcollector/class/emailcollector.class.php
  47. 1 1
      htdocs/expedition/card.php
  48. 6 6
      htdocs/expedition/dispatch.php
  49. 1 1
      htdocs/fourn/facture/card.php
  50. 1 43
      htdocs/holiday/class/holiday.class.php
  51. 6 5
      htdocs/holiday/list.php
  52. 22 0
      htdocs/includes/bacon/bacon-qr-code/LICENSE
  53. 39 0
      htdocs/includes/bacon/bacon-qr-code/README.md
  54. 44 0
      htdocs/includes/bacon/bacon-qr-code/composer.json.disabled
  55. 372 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/BitArray.php
  56. 313 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/BitMatrix.php
  57. 41 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/BitUtils.php
  58. 183 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/CharacterSetEci.php
  59. 49 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlock.php
  60. 74 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlocks.php
  61. 63 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/ErrorCorrectionLevel.php
  62. 203 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/FormatInformation.php
  63. 79 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/Mode.php
  64. 468 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/ReedSolomonCodec.php
  65. 596 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/Version.php
  66. 58 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/BlockPair.php
  67. 150 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/ByteMatrix.php
  68. 668 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/Encoder.php
  69. 271 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/MaskUtil.php
  70. 513 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/MatrixUtil.php
  71. 141 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/QrCode.php
  72. 10 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/ExceptionInterface.php
  73. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/InvalidArgumentException.php
  74. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/OutOfBoundsException.php
  75. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/RuntimeException.php
  76. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/UnexpectedValueException.php
  77. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/WriterException.php
  78. 57 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Alpha.php
  79. 103 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Cmyk.php
  80. 22 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/ColorInterface.php
  81. 46 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Gray.php
  82. 88 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Rgb.php
  83. 38 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/CompositeEye.php
  84. 26 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/EyeInterface.php
  85. 54 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/ModuleEye.php
  86. 54 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SimpleCircleEye.php
  87. 53 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SquareEye.php
  88. 376 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/EpsImageBackEnd.php
  89. 87 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImageBackEndInterface.php
  90. 336 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImagickImageBackEnd.php
  91. 369 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/SvgImageBackEnd.php
  92. 68 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/TransformationMatrix.php
  93. 152 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/ImageRenderer.php
  94. 63 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/DotsModule.php
  95. 100 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/Edge.php
  96. 169 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/EdgeIterator.php
  97. 18 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/ModuleInterface.php
  98. 129 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/RoundnessModule.php
  99. 47 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/SquareModule.php
  100. 29 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Path/Close.php

+ 6 - 0
.travis.yml

@@ -221,6 +221,8 @@ before_script:
       pgloader mysql://root@127.0.0.1/travis postgresql://postgres@/travis
       echo 'ALTER SEQUENCE llx_accountingaccount_rowid_seq RENAME TO llx_accounting_account_rowid_seq' | psql -U postgres travis
       echo 'ALTER SEQUENCE llx_accounting_account_rowid_seq RESTART WITH 1000001;' | psql -U postgres travis
+      # Create pgsql compatibility functions
+      psql -U postgres travis < htdocs/install/pgsql/functions/functions.sql
     fi
     echo
 
@@ -459,6 +461,9 @@ script:
   php upgrade2.php 17.0.0 18.0.0 > $TRAVIS_BUILD_DIR/upgrade17001800-2.log
   php step5.php 17.0.0 18.0.0 > $TRAVIS_BUILD_DIR/upgrade17001800-3.log
   ls -alrt $TRAVIS_BUILD_DIR/
+  echo
+  #cat $TRAVIS_BUILD_DIR/upgrade17001800.log
+  #cat $TRAVIS_BUILD_DIR/upgrade17001800-2.log
 
 - |
   echo "Enabling new modules"
@@ -468,6 +473,7 @@ script:
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_WEBSITE,MAIN_MODULE_TICKET,MAIN_MODULE_ACCOUNTING,MAIN_MODULE_MRP >> $TRAVIS_BUILD_DIR/enablemodule.log
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_RECEPTION,MAIN_MODULE_RECRUITMENT >> $TRAVIS_BUILD_DIR/enablemodule.log
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_KNOWLEDGEMANAGEMENT,MAIN_MODULE_EVENTORGANIZATION,MAIN_MODULE_PARTNERSHIP >> $TRAVIS_BUILD_DIR/enablemodule.log
+  php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_EMAILCOLLECTOR >> $TRAVIS_BUILD_DIR/enablemodule.log
   echo $?
   cd -
   set +e

+ 3 - 1
COPYRIGHT

@@ -40,13 +40,15 @@ PHPPrintIPP            1.3           GPL-2+                      Yes
 PSR/Logs               1.0           MIT License                 Yes             Library for logs (used by DebugBar)
 PSR/simple-cache       ?             MIT License                 Yes             Library for cache (used by PHPSpreadSheet)
 Restler                3.1.1         LGPL-3+                     Yes             Library to develop REST Web services (+ swagger-ui js lib into dir explorer)
-Sabre                  3.2.2         BSD                         Yes             DAV support
+Sabre                  4.0.2         BSD                         Yes             DAV support
 Swift Mailer           5.4.2-DEV     MIT License                 Yes             Comprehensive mailing tools for PHP
 Symfony/var-dumper     ??? 			 MIT License                 Yes             Library to make var dump (used by DebugBar)
 Stripe                 10.7.0        MIT Licence                 Yes             Library for Stripe module
 TCPDF                  6.3.2         LGPL-3+                     Yes             PDF generation
 TCPDI                  1.0.0         LGPL-3+ / Apache 2.0        Yes             FPDI replacement
 
+bacon, dasprid, swiss-qr-bill, kmukku, symfony/validator
+
 JS libraries:
 Ace                    1.4.14        BSD                         Yes             JS library to get code syntaxique coloration in a textarea.
 ChartJS                3.7.1         MIT License                 Yes             JS library for graph

+ 86 - 2
ChangeLog

@@ -9,6 +9,8 @@ For uses:
 ---------
 
 NEW: PHP 8.2 compatibility (test not yet completed).
+NEW: Module Workstations Management upgraded to stable status
+NEW: Module Webhook upgraded to stable status
 NEW: #23436 Group social networks fields
 NEW: Accountancy - Add specific page to export accounting data rather than the journals page
 NEW: Accountancy - Add sub-account balance FPC22
@@ -211,8 +213,90 @@ Following changes may create regressions for some external modules, but were nec
 * The type 'text' in ->fields property dos not accept html content anymore. Use the type 'html' for that.
 * The module for WebService SOAP API have been deprecated. Use instead the Webservice REST API module.
 * The method htmlPrintOnlinePaymentFooter() used for public footer pages has been renamed into htmlPrintOnlineFooter() and moved into company.lib.php
-
-
+* The method getCheckOption() and deleteCPUser() of class Holiday has been removed (it was not used)
+
+
+***** ChangeLog for 17.0.2 compared to 17.0.1 *****
+
+FIX: #24414
+FIX: #24798 Deleting member subscription is not possible
+FIX: add a test for updating date on task update in tab time consummed pro…
+FIX: add charchesociales in security.lib.php
+FIX: Add Missing rights check on holiday calendar
+FIX: Add the possibility to events owner to check their events from the list when the perm "Read the actions (events or tasks) of others" is not active
+FIX: Authorize '0' subprice in supplier proposal line
+FIX: avoid error when computed property of extrafields is used
+FIX: avoid warnings php8
+FIX: Back to cancel on reception creation
+FIX: Bad deletion of email when there is several emails processed
+FIX: bank account not visible on credit transfer
+FIX: Better support for option MAIN_NO_INPUT_PRICE_WITH_TAX
+FIX: broken feature : send private message by email
+FIX: categorie compatibility with actioncomm
+FIX: Click on "NEW" in simple POS was broken
+FIX: Compress in xz for better debian old version compatibility
+FIX: Correct Evaluation for extrafields
+FIX: Count only attendee draft of validated.
+FIX: Creation of thumbs when images.lib.php was already included
+FIX: delete of warehouse
+FIX: deletion of a line of time spent (backport e3aa438d2a582313dfd5178b8cc5975e0c912c25)
+FIX: Deployment of external module failed with copy dir error.
+FIX: edit field value of url
+FIX: error management on emoji and utf8 validity by emailcollector
+FIX: expense report autofill ttc input if force ttc conf is enabled
+FIX: filter missing id on consumption contact card
+FIX: Filter on member status
+FIX: Filter status orders in list no invoiced if validated + in progress + delivered
+FIX: Fiscal year list ref display
+FIX: for empty shipping
+FIX: INVOICE_SHOW_SHIPPING_ADDRESS
+FIX: jump to direct record on member search was broken
+FIX: label of columns must be short into invoice PDF table
+FIX: making search in takepos broken when TAKEPOS_PRODUCT_IN_STOCK is set
+FIX: Margin calculation for credit notes on margin reports
+FIX: message MAIN_MESSAGE_INSTALL_MODULES_DISABLED_CONTACT_US
+FIX: missing checking if file is uploaded
+FIX: missing constant and avoid submit button conflict
+FIX: missing entity field in unique index (since v16)
+FIX: Missing error message display on insertExtrafields()
+FIX: missing mrp module dependency
+FIX: More complete fix for #24411
+FIX: No error message because  $price_ht_devise is equal to '0' if not filled because of price2num
+FIX: No usage of the function updateProduction in the update function
+FIX: On company change, we must reuse the company parameters
+FIX: Operator to search category Knowledge
+FIX: Pb in install when password start with some special char like !
+FIX: pb in sending email when mail contains data src image.
+FIX: PDF Font for turkish language
+FIX: product notes rights
+FIX: Propagate correct origin/origin_id when creating order from proposal
+FIX: Propagate extrafields from supplier order to reception
+FIX: reassortlot search categorie + add inithooks
+FIX: Reception process loose some lines on first error.
+FIX: redesign of the function : updateProduction
+FIX: ref_client on Project Overview for propale
+FIX: reference id in getnomurl function.
+FIX: regression Undefined $datepaid
+FIX: remove NOTOKENRENOWAL (backport commit v17 7c316229db8060781ee50f4465b1133b5aeef156)
+FIX: Remove warning on lettering - Impossible to write in ledger v16 v17 v18
+FIX: Report of date of task suggested only if there is tasks
+FIX: Rounding on total margin on invoice list
+FIX: Search List Select Extrafields with condition
+FIX: Search when criteria start with !
+FIX: Sending email from attendee list
+FIX: Shipping address same third party
+FIX: special chars in generated file name from build doc mass action
+FIX: supplier invoice status on bank transfer line
+FIX: supplier link on bank transfer line
+FIX: Task events not displayed
+FIX: token errors on public interface
+FIX: Transfer between accounts with different currencies was broken
+FIX: Update hour of intervention line
+FIX: Upload of files on public ticket interface
+FIX: Use max parameters of widget graph product distribution
+FIX: Warehouse total line
+FIX: When salary module is not enabled, bad permission check on user list
+FIX: wrong colspan for tasks list
 
 ***** ChangeLog for 17.0.1 compared to 17.0.0 *****
 

+ 1 - 0
htdocs/adherents/admin/member_emails.php

@@ -63,6 +63,7 @@ $constantes = array(
 	'ADHERENT_EMAIL_TEMPLATE_CANCELATION'			=>array('type'=>'emailtemplate:member'),
 	'ADHERENT_EMAIL_TEMPLATE_EXCLUSION'				=>array('type'=>'emailtemplate:member'),
 	'ADHERENT_MAIL_FROM'							=>array('type'=>'string'),
+	'ADHERENT_CC_MAIL_FROM'							=>array('type'=>'string'),
 	'ADHERENT_AUTOREGISTER_NOTIF_MAIL_SUBJECT'		=>array('type'=>'string'),
 	'ADHERENT_AUTOREGISTER_NOTIF_MAIL'				=>array('type'=>'html', 'tooltip'=>$helptext)
 );

+ 1 - 1
htdocs/adherents/canvas/actions_adherentcard_common.class.php

@@ -241,7 +241,7 @@ abstract class ActionsAdherentCardCommon
 	/**
 	 *  Assign POST values into object
 	 *
-	 *  @return		string					HTML output
+	 *  @return		void
 	 */
 	private function assign_post()
 	{

+ 7 - 1
htdocs/adherents/class/adherent.class.php

@@ -64,6 +64,11 @@ class Adherent extends CommonObject
 	 */
 	public $ismultientitymanaged = 1;
 
+	/**
+	 * @var int  Does object support extrafields ? 0=No, 1=Yes
+	 */
+	public $isextrafieldmanaged = 1;
+
 	/**
 	 * @var string picto
 	 */
@@ -3146,12 +3151,13 @@ class Adherent extends CommonObject
 							$msg = make_substitutions($arraydefaultmessage->content, $substitutionarray, $outputlangs);
 							$from = getDolGlobalString('ADHERENT_MAIL_FROM');
 							$to = $adherent->email;
+							$cc = getDolGlobalString('ADHERENT_CC_MAIL_FROM');
 
 							$trackid = 'mem'.$adherent->id;
 							$moreinheader = 'X-Dolibarr-Info: sendReminderForExpiredSubscription'."\r\n";
 
 							include_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
-							$cmail = new CMailFile($subject, $to, $from, $msg, array(), array(), array(), '', '', 0, 1, '', '', $trackid, $moreinheader);
+							$cmail = new CMailFile($subject, $to, $from, $msg, array(), array(), array(), $cc, '', 0, 1, '', '', $trackid, $moreinheader);
 							$result = $cmail->sendfile();
 							if (!$result) {
 								$error++;

+ 1 - 1
htdocs/adherents/subscription.php

@@ -995,7 +995,7 @@ if ($rowid > 0) {
 
 		if ($adht->subscription) {
 			// Amount
-			print '<tr><td class="fieldrequired">'.$langs->trans("Amount").'</td><td><input type="text" name="subscription" size="6" value="'. price(GETPOSTISSET('subscription') ? GETPOST('subscription') : $adht->amount).'"> '.$langs->trans("Currency".$conf->currency) .'</td></tr>';
+			print '<tr><td class="fieldrequired">'.$langs->trans("Amount").'</td><td><input type="text" name="subscription" size="6" value="'.(GETPOSTISSET('subscription') ? GETPOST('subscription') : price($adht->amount, 0, '', 0)).'"> '.$langs->trans("Currency".$conf->currency) .'</td></tr>';
 
 			// Label
 			print '<tr><td>'.$langs->trans("Label").'</td>';

+ 1 - 1
htdocs/adherents/subscription/list.php

@@ -715,7 +715,7 @@ while ($i < $imaxinloop) {
 
 		// Label
 		if (!empty($arrayfields['t.libelle']['checked'])) {
-			print '<td class="tdoverflowmax400" title="'.dol_escape_htmltag($obj->note).'">';
+			print '<td class="tdoverflowmax400" title="'.dol_escape_htmltag($obj->note_private).'">';
 			print dol_escape_htmltag(dolGetFirstLineOfText($obj->note_private));
 			print '</td>';
 			if (!$i) {

+ 4 - 4
htdocs/admin/dav.php

@@ -104,7 +104,7 @@ if ($action == 'edit') {
 		if ($key == 'DAV_ALLOW_PRIVATE_DIR') {
 			print $langs->trans("AlwaysActive");
 		} elseif ($key == 'DAV_ALLOW_PUBLIC_DIR' || $key == 'DAV_ALLOW_ECM_DIR') {
-			print $form->selectyesno($key, $conf->global->$key, 1);
+			print $form->selectyesno($key, getDolGlobalString($key), 1);
 		} else {
 			print '<input name="'.$key.'"  class="flat '.(empty($val['css']) ? 'minwidth200' : $val['css']).'" value="'.getDolGlobalString($key).'">';
 		}
@@ -140,9 +140,9 @@ if ($action == 'edit') {
 		if ($key == 'DAV_ALLOW_PRIVATE_DIR') {
 			print $langs->trans("AlwaysActive");
 		} elseif ($key == 'DAV_ALLOW_PUBLIC_DIR' || $key == 'DAV_ALLOW_ECM_DIR') {
-			print yn($conf->global->$key);
+			print yn(getDolGlobalString($key));
 		} else {
-			print $conf->global->$key;
+			print getDolGlobalString($key);
 		}
 		print '</td></tr>';
 	}
@@ -189,7 +189,7 @@ $message .= '</div>';
 $message .= ajax_autoselect('webdavpublicurl');
 
 $message .= '<br>';
-if (!empty($conf->global->DAV_ALLOW_PUBLIC_DIR)) {
+if (!empty(getDolGlobalString('DAV_ALLOW_PUBLIC_DIR'))) {
 	$urlEntity = (isModEnabled('multicompany') ? '?entity=' . $conf->entity : '');
 	$url = '<a href="' . $urlwithroot . '/dav/fileserver.php/public/' . $urlEntity . '" target="_blank" rel="noopener noreferrer">' . $urlwithroot . '/dav/fileserver.php/public/' . $urlEntity . '</a>';
 

+ 15 - 8
htdocs/admin/emailcollector_card.php

@@ -395,9 +395,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
 	$connectstringtarget = '';
 
 	// Note: $object->host has been loaded by the fetch
-	$usessl = 1;
-
-	$connectstringserver = $object->getConnectStringIMAP($usessl);
+	$connectstringserver = $object->getConnectStringIMAP();
 
 	if ($action == 'scan') {
 		if (!empty($conf->global->MAIN_IMAP_USE_PHPIMAP)) {
@@ -645,9 +643,13 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
         console.log("We change a filter");
         if (jQuery("#filtertype option:selected").attr("data-noparam")) {
             jQuery("#rulevalue").attr("placeholder", "");
-            jQuery("#rulevalue").text(""); jQuery("#rulevalue").prop("disabled", true);
-        }
-        else { jQuery("#rulevalue").prop("disabled", false); }
+            jQuery("#rulevalue").text("");
+			jQuery("#rulevalue").prop("disabled", true);
+			jQuery("#rulevaluehelp").addClass("unvisible");
+        } else {
+			jQuery("#rulevalue").prop("disabled", false);
+			jQuery("#rulevaluehelp").removeClass("unvisible");
+		}
         jQuery("#rulevalue").attr("placeholder", (jQuery("#filtertype option:selected").attr("data-placeholder")));
     ';
 	/*$noparam = array();
@@ -658,8 +660,13 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
 	print '})';
 	print '</script>'."\n";
 
-	print '</td><td>';
-	print '<input type="text" name="rulevalue" id="rulevalue">';
+	print '</td><td class="nowraponall">';
+	print '<div class="nowraponall">';
+	print '<input type="text" name="rulevalue" id="rulevalue" class="inline-block valignmiddle">';
+	print '<div class="inline-block valignmiddle unvisible" id="rulevaluehelp">';
+	print img_warning($langs->trans("FilterSearchImapHelp"), '', 'pictowarning classfortooltip');
+	print '</div>';
+	print '</div>';
 	print '</td>';
 	print '<td class="right"><input type="submit" name="addfilter" id="addfilter" class="flat button smallpaddingimp" value="'.$langs->trans("Add").'"></td>';
 	print '</tr>';

+ 11 - 8
htdocs/admin/pdf_other.php

@@ -50,6 +50,9 @@ $action = GETPOST('action', 'aZ09');
  */
 
 if ($action == 'update') {
+	if (GETPOSTISSET('MAIN_PDF_PROPAL_USE_ELECTRONIC_SIGNING')) {
+		dolibarr_set_const($db, "MAIN_PDF_PROPAL_USE_ELECTRONIC_SIGNING", GETPOST("MAIN_PDF_PROPAL_USE_ELECTRONIC_SIGNING"), 'chaine', 0, '', $conf->entity);
+	}
 	if (GETPOSTISSET('PROPOSAL_PDF_HIDE_PAYMENTTERM')) {
 		dolibarr_set_const($db, "PROPOSAL_PDF_HIDE_PAYMENTTERM", GETPOST("PROPOSAL_PDF_HIDE_PAYMENTTERM"), 'chaine', 0, '', $conf->entity);
 	}
@@ -67,7 +70,7 @@ if ($action == 'update') {
 		dolibarr_del_const($db, "INVOICE_ADD_SWISS_QR_CODE", $conf->entity);
 	}
 	if (GETPOSTISSET('INVOICE_ADD_SWISS_QR_CODE')) {
-		dolibarr_set_const($db, "INVOICE_ADD_SWISS_QR_CODE", GETPOST("INVOICE_ADD_SWISS_QR_CODE", 'int'), 'chaine', 0, '', $conf->entity);
+		dolibarr_set_const($db, "INVOICE_ADD_SWISS_QR_CODE", GETPOST("INVOICE_ADD_SWISS_QR_CODE", 'alpha'), 'chaine', 0, '', $conf->entity);
 		dolibarr_del_const($db, "INVOICE_ADD_ZATCA_QR_CODE", $conf->entity);
 	}
 	if (GETPOSTISSET('INVOICE_CATEGORY_OF_OPERATION')) {
@@ -165,14 +168,14 @@ if (isModEnabled('facture')) {
 	print '</td></tr>';
 
 	print '<tr class="oddeven"><td>';
-	print $form->textwithpicto($langs->trans("INVOICE_ADD_SWISS_QR_CODE"), '');
+	print $form->textwithpicto($langs->trans("INVOICE_ADD_SWISS_QR_CODE"), $langs->trans("INVOICE_ADD_SWISS_QR_CODEMore"));
 	print '</td><td>';
-	if ($conf->use_javascript_ajax) {
-		print ajax_constantonoff('INVOICE_ADD_SWISS_QR_CODE');
-	} else {
-		$arrval = array('0' => $langs->trans("No"), '1' => $langs->trans("Yes"));
-		print $form->selectarray("INVOICE_ADD_SWISS_QR_CODE", $arrval, $conf->global->INVOICE_ADD_SWISS_QR_CODE);
-	}
+	//if ($conf->use_javascript_ajax) {
+	//	print ajax_constantonoff('INVOICE_ADD_SWISS_QR_CODE');
+	//} else {
+	$arrval = array('0' => $langs->trans("No"), '1' => $langs->trans("Yes"), 'bottom' => $langs->trans("AtBottomOfPage").' ('.$langs->trans("Experimental").' - Need PHP 8.1+)');
+	print $form->selectarray("INVOICE_ADD_SWISS_QR_CODE", $arrval, $conf->global->INVOICE_ADD_SWISS_QR_CODE);
+	//}
 	print '</td></tr>';
 
 	// Mention category of operations

+ 4 - 4
htdocs/api/class/api_setup.class.php

@@ -310,8 +310,8 @@ class Setup extends DolibarrApi
 	/**
 	 * Get region by ID.
 	 *
-	 * @param int       $id        ID of region
-	 * @return array    		   Array of cleaned object properties
+	 * @param 	int       $id       ID of region
+	 * @return 	Object 				Object with cleaned properties
 	 *
 	 * @url     GET dictionary/regions/{id}
 	 *
@@ -325,8 +325,8 @@ class Setup extends DolibarrApi
 	/**
 	 * Get region by Code.
 	 *
-	 * @param string    $code      Code of region
-	 * @return array 			   Array of cleaned object properties
+	 * @param 	string    $code     Code of region
+	 * @return 	Object 				Object with cleaned properties
 	 *
 	 * @url     GET dictionary/regions/byCode/{code}
 	 *

+ 3 - 3
htdocs/categories/index.php

@@ -249,13 +249,13 @@ if ($morethan1level && !empty($conf->use_javascript_ajax)) {
 print '</td></tr>';
 
 if ($nbofentries > 0) {
-	print '<tr class="pair"><td colspan="3">';
+	print '<tr class="oddeven"><td colspan="3">';
 	tree_recur($data, $data[0], 0);
 	print '</td></tr>';
 } else {
-	print '<tr class="pair">';
+	print '<tr class="oddeven">';
 	print '<td colspan="3"><table class="nobordernopadding"><tr class="nobordernopadding"><td>'.img_picto_common('', 'treemenu/branchbottom.gif').'</td>';
-	print '<td valign="middle">';
+	print '<td class="valignmiddle">';
 	print $langs->trans("NoCategoryYet");
 	print '</td>';
 	print '<td>&nbsp;</td>';

+ 8 - 4
htdocs/categories/viewcat.php

@@ -1,4 +1,6 @@
 <?php
+use Stripe\BankAccount;
+
 /* Copyright (C) 2005       Matthieu Valleton	<mv@seeschloss.org>
  * Copyright (C) 2006-2020  Laurent Destailleur  <eldy@users.sourceforge.net>
  * Copyright (C) 2007       Patrick Raguin		<patrick.raguin@gmail.com>
@@ -215,12 +217,14 @@ if ($elemid && $action == 'addintocategory' &&
 		$elementtype = 'user';
 	} elseif ($type == Categorie::TYPE_ACCOUNT) {
 		require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
-		$newobject = new User($db);
+		$newobject = new Account($db);
 		$elementtype = 'bank_account';
+	} else {
+		dol_print_error("Not supported value of type = ".$type);
 	}
 	$result = $newobject->fetch($elemid);
 
-	// TODO Add into categ
+	// Add into categ
 	$result = $object->add_type($newobject, $elementtype);
 	if ($result >= 0) {
 		setEventMessages($langs->trans("WasAddedSuccessfully", $newobject->ref), null, 'mesgs');
@@ -925,8 +929,8 @@ if ($type == Categorie::TYPE_ACCOUNT) {
 				print '<input type="hidden" name="action" value="addintocategory">';
 				print '<table class="noborder centpercent">';
 				print '<tr class="liste_titre"><td>';
-					print $langs->trans("AddAccountIntoCategory").' &nbsp;';
-				$form->select_comptes('', 'elemid');
+				print $langs->trans("AddObjectIntoCategory").' &nbsp;';
+				print $form->select_comptes('', 'elemid', 0, '', 0, '', 0, '', 1);
 				print '<input type="submit" class="button buttongen" value="'.$langs->trans("ClassifyInCategory").'"></td>';
 				print '</tr>';
 				print '</table>';

+ 8 - 2
htdocs/compta/bank/card.php

@@ -1096,11 +1096,17 @@ if ($action == 'create') {
 			}
 
 			// IBAN
-			print '<tr><td>'.$langs->trans($ibankey).'</td>';
+			print '<tr><td>';
+			$tooltip = $langs->trans("Example").':<br>LT12 1000 0111 0100 1000<br>FR14 2004 1010 0505 0001 3M02 606<br>LU28 0019 4006 4475 0000<br>DE89 3704 0044 0532 0130 00';
+			print $form->textwithpicto($langs->trans($ibankey), $tooltip);
+			print '</td>';
 			print '<td><input class="minwidth300 maxwidth200onsmartphone" maxlength="34" type="text" class="flat" name="iban" value="'.(GETPOSTISSET('iban') ? GETPOST('iban',  'alphanohtml') : $object->iban).'"></td></tr>';
 
 			// BIC
-			print '<tr><td>'.$langs->trans($bickey).'</td>';
+			print '<tr><td>';
+			$tooltip = $langs->trans("Example").': LIABLT2XXXX';
+			print $form->textwithpicto($langs->trans($bickey), $tooltip);
+			print '</td>';
 			print '<td><input class="minwidth150 maxwidth200onsmartphone" maxlength="11" type="text" class="flat" name="bic" value="'.(GETPOSTISSET('bic') ? GETPOST('bic',  'alphanohtml') : $object->bic).'"></td></tr>';
 
 			// Show fields of bank account

+ 6 - 2
htdocs/compta/bank/class/account.class.php

@@ -1338,7 +1338,7 @@ class Account extends CommonObject
 	 *      Charge indicateurs this->nb de tableau de bord
 	 *
 	 *		@param		int			$filteraccountid	To get info for a particular account id
-	 *      @return     int         <0 if ko, >0 if ok
+	 *      @return     int         <0 if KO, >0 if OK
 	 */
 	public function load_state_board($filteraccountid = 0)
 	{
@@ -1366,6 +1366,7 @@ class Account extends CommonObject
 				$this->nb["banklines"] = $obj->nb;
 			}
 			$this->db->free($resql);
+			return 1;
 		} else {
 			dol_print_error($this->db);
 			$this->error = $this->db->error();
@@ -2611,7 +2612,8 @@ class AccountLine extends CommonObjectLine
 	public function LibStatut($status, $mode = 0)
 	{
 		// phpcs:enable
-		global $langs;
+		//global $langs;
+
 		//$langs->load('companies');
 		/*
 		if ($mode == 0)
@@ -2644,6 +2646,8 @@ class AccountLine extends CommonObjectLine
 			if ($status==0) return $langs->trans("ActivityCeased").' '.img_picto($langs->trans("ActivityCeased"),'statut5', 'class="pictostatus"');
 			if ($status==1) return $langs->trans("InActivity").' '.img_picto($langs->trans("InActivity"),'statut4', 'class="pictostatus"');
 		}*/
+
+		return '';
 	}
 
 

+ 1 - 1
htdocs/compta/facture/card.php

@@ -5212,7 +5212,7 @@ if ($action == 'create') {
 				print '<td class="right"><span class="amount">'.price($obj->amount_ttc).'</span></td>';
 				print '<td class="right">';
 				print '<a href="'.$_SERVER["PHP_SELF"].'?facid='.$object->id.'&action=unlinkdiscount&token='.newToken().'&discountid='.$obj->rowid.'">';
-				print img_picto($langs->trans("RemoveDiscount"), 'unlink');
+				print img_picto($langs->transnoentitiesnoconv("RemoveDiscount"), 'unlink');
 				print '</a>';
 				print '</td></tr>';
 				$i++;

+ 3 - 3
htdocs/compta/facture/class/facture.class.php

@@ -5629,11 +5629,11 @@ class Facture extends CommonInvoice
 	 *  @param	int			$nbdays				Delay before due date (or after if delay is negative)
 	 *  @param	string		$paymentmode		'' or 'all' by default (no filter), or 'LIQ', 'CHQ', CB', ...
 	 *  @param	int|string	$template			Name (or id) of email template (Must be a template of type 'facture_send')
-	 *  @param	string		$forcerecipient		Force email of recipient (for example to send the email to an accountant supervisor instead of the customer)
 	 *  @param	string		$datetouse			'duedate' (default) or 'invoicedate'
+	 *  @param	string		$forcerecipient		Force email of recipient (for example to send the email to an accountant supervisor instead of the customer)
 	 *  @return int         					0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK)
 	 */
-	public function sendEmailsRemindersOnInvoiceDueDate($nbdays = 0, $paymentmode = 'all', $template = '', $forcerecipient = '', $datetouse = 'duedate')
+	public function sendEmailsRemindersOnInvoiceDueDate($nbdays = 0, $paymentmode = 'all', $template = '', $datetouse = 'duedate', $forcerecipient = '')
 	{
 		global $conf, $langs, $user;
 
@@ -5829,7 +5829,7 @@ class Facture extends CommonInvoice
 								$actioncomm->contact_id = 0;
 
 								$actioncomm->code = 'AC_EMAIL';
-								$actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateOK (nbdays='.$nbdays.' paymentmode='.$paymentmode.' template='.$template.' forcerecipient='.$forcerecipient.' datetouse='.$datetouse.')';
+								$actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateOK (nbdays='.$nbdays.' paymentmode='.$paymentmode.' template='.$template.' datetouse='.$datetouse.' forcerecipient='.$forcerecipient.')';
 								$actioncomm->note_private = $sendContent;
 								$actioncomm->fk_project = $tmpinvoice->fk_project;
 								$actioncomm->datep = dol_now();

+ 294 - 166
htdocs/compta/prelevement/list.php

@@ -22,7 +22,7 @@
 /**
  *      \file       htdocs/compta/prelevement/list.php
  *      \ingroup    prelevement
- *      \brief      Page list of direct debit orders or credit transfers orders
+ *      \brief      Page to list direct debit orders or credit transfers orders
  */
 
 // Load Dolibarr environment
@@ -34,7 +34,7 @@ require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
 // Load translation files required by the page
 $langs->loadLangs(array('banks', 'withdrawals', 'companies', 'categories'));
 
-$action     = GETPOST('action', 'aZ09') ?GETPOST('action', 'aZ09') : 'view'; // The action 'add', 'create', 'edit', 'update', 'view', ...
+$action     = GETPOST('action', 'aZ09') ? GETPOST('action', 'aZ09') : 'view'; // The action 'add', 'create', 'edit', 'update', 'view', ...
 $massaction = GETPOST('massaction', 'alpha'); // The bulk action (combo box choice into lists)
 $show_files = GETPOST('show_files', 'int'); // Show files area generated by bulk actions ?
 $confirm    = GETPOST('confirm', 'alpha'); // Result of a confirmation
@@ -43,17 +43,19 @@ $toselect   = GETPOST('toselect', 'array'); // Array of ids of elements selected
 $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'directdebitcredittransferlinelist'; // To manage different context of search
 $backtopage = GETPOST('backtopage', 'alpha'); // Go back to a dedicated page
 $optioncss  = GETPOST('optioncss', 'aZ'); // Option for the css output (always '' except when 'print')
-$mode       = GETPOST('mode', 'alpha');
+$mode       = GETPOST('mode', 'aZ'); // The output mode ('list', 'kanban', 'hierarchy', 'calendar', ...)
 
 $type = GETPOST('type', 'aZ09');
 
+// Load variable for pagination
 $limit = GETPOST('limit', 'int') ?GETPOST('limit', 'int') : $conf->liste_limit;
 $sortfield = GETPOST('sortfield', 'aZ09comma');
 $sortorder = GETPOST('sortorder', 'aZ09comma');
 $page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
-if (empty($page) || $page == -1) {
+if (empty($page) || $page < 0 || GETPOST('button_search', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
+	// If $page is not defined, or '' or -1 or if we click on clear filters
 	$page = 0;
-}     // If $page is not defined, or '' or -1
+}
 $offset = $limit * $page;
 $pageprev = $page - 1;
 $pagenext = $page + 1;
@@ -107,7 +109,11 @@ if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x'
 
 $form = new Form($db);
 
-llxHeader('', $langs->trans("WithdrawalsLines"));
+$title = $langs->trans("WithdrawalsLines");
+if ($type == 'bank-transfer') {
+	$title = $langs->trans("CreditTransferLines");
+}
+$help_url = '';
 
 $sql = "SELECT p.rowid, p.ref, p.statut as status, p.datec";
 $sql .= " , f.rowid as facid, f.ref as invoiceref, f.total_ttc";
@@ -170,193 +176,315 @@ if (!getDolGlobalInt('MAIN_DISABLE_FULL_SCANLIST')) {
 		dol_print_error($db);
 	}
 
-	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller then paging size (filtering), goto and load page 0
+	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller than the paging size (filtering), goto and load page 0
 		$page = 0;
 		$offset = 0;
 	}
 	$db->free($resql);
 }
 
+// Complete request and execute it with limit
 $sql .= $db->order($sortfield, $sortorder);
 if ($limit) {
 	$sql .= $db->plimit($limit + 1, $offset);
 }
 
-$result = $db->query($sql);
-if ($result) {
-	$num = $db->num_rows($result);
-	$i = 0;
+$resql = $db->query($sql);
+if (!$resql) {
+	dol_print_error($db);
+	exit;
+}
 
-	$param = "&amp;statut=".urlencode($statut);
-	$param .= "&amp;search_bon=".urlencode($search_bon);
-	if (!empty($mode)) {
-		$param .= '&mode='.urlencode($mode);
-	}
-	if ($type == 'bank-transfer') {
-		$param .= '&amp;type=bank-transfer';
-	}
-	if ($limit > 0 && $limit != $conf->liste_limit) {
-		$param .= '&limit='.((int) $limit);
-	}
-	$newcardbutton = '';
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
-
-	print"\n<!-- debut table -->\n";
-	print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">'."\n";
-	if ($optioncss != '') {
-		print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
-	}
-	print '<input type="hidden" name="token" value="'.newToken().'">';
-	print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
-	print '<input type="hidden" name="action" value="list">';
-	print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
-	print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
-	print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
-	print '<input type="hidden" name="mode" value="'.$mode.'">';
-
-	if ($type != '') {
-		print '<input type="hidden" name="type" value="'.$type.'">';
-	}
+$num = $db->num_rows($resql);
 
-	$title = $langs->trans("WithdrawalsLines");
-	if ($type == 'bank-transfer') {
-		$title = $langs->trans("CreditTransferLines");
-	}
-	print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
-
-	$moreforfilter = '';
-
-	print '<div class="div-table-responsive">';
-	print '<table class="tagtable liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
-
-	print '<tr class="liste_titre">';
-	print '<td class="liste_titre"><input type="text" class="flat" name="search_line" value="'.dol_escape_htmltag($search_line).'" size="6"></td>';
-	print '<td class="liste_titre"><input type="text" class="flat" name="search_bon" value="'.dol_escape_htmltag($search_bon).'" size="6"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre"><input type="text" class="flat" name="search_company" value="'.dol_escape_htmltag($search_company).'" size="6"></td>';
-	print '<td class="liste_titre center"><input type="text" class="flat" name="search_code" value="'.dol_escape_htmltag($search_code).'" size="6"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre maxwidthsearch">';
+// Output page
+// --------------------------------------------------------------------
+
+llxHeader('', $title, $help_url);
+
+$arrayofselected = is_array($toselect) ? $toselect : array();
+
+$param = '';
+$param .= "&statut=".urlencode($statut);
+$param .= "&search_bon=".urlencode($search_bon);
+if ($type == 'bank-transfer') {
+	$param .= '&type=bank-transfer';
+}
+if (!empty($mode)) {
+	$param .= '&mode='.urlencode($mode);
+}
+if (!empty($contextpage) && $contextpage != $_SERVER["PHP_SELF"]) {
+	$param .= '&contextpage='.urlencode($contextpage);
+}
+if ($limit > 0 && $limit != $conf->liste_limit) {
+	$param .= '&limit='.((int) $limit);
+}
+if ($optioncss != '') {
+	$param .= '&optioncss='.urlencode($optioncss);
+}
+
+print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">'."\n";
+print '<input type="hidden" name="token" value="'.newToken().'">';
+if ($optioncss != '') {
+	print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
+}
+print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
+print '<input type="hidden" name="action" value="list">';
+print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
+print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
+print '<input type="hidden" name="page" value="'.$page.'">';
+print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
+print '<input type="hidden" name="page_y" value="">';
+print '<input type="hidden" name="mode" value="'.$mode.'">';
+
+if ($type != '') {
+	print '<input type="hidden" name="type" value="'.$type.'">';
+}
+
+$newcardbutton = '';
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
+
+print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, $massactionbutton, $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
+
+$moreforfilter = '';
+/*$moreforfilter.='<div class="divsearchfield">';
+ $moreforfilter.= $langs->trans('MyFilter') . ': <input type="text" name="search_myfield" value="'.dol_escape_htmltag($search_myfield).'">';
+ $moreforfilter.= '</div>';*/
+
+$parameters = array();
+$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+if (empty($reshook)) {
+	$moreforfilter .= $hookmanager->resPrint;
+} else {
+	$moreforfilter = $hookmanager->resPrint;
+}
+
+if (!empty($moreforfilter)) {
+	print '<div class="liste_titre liste_titre_bydiv centpercent">';
+	print $moreforfilter;
+	$parameters = array();
+	$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+	print $hookmanager->resPrint;
+	print '</div>';
+}
+
+$varpage = empty($contextpage) ? $_SERVER["PHP_SELF"] : $contextpage;
+$selectedfields = ($mode != 'kanban' ? $form->multiSelectArrayWithCheckbox('selectedfields', $arrayfields, $varpage, getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN', '')) : ''); // This also change content of $arrayfields
+$selectedfields .= (count($arrayofmassactions) ? $form->showCheckAddButtons('checkforselect', 1) : '');
+
+print '<div class="div-table-responsive">'; // You can use div-table-responsive-no-min if you dont need reserved height for your table
+print '<table class="tagtable nobottomiftotal liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
+
+// Fields title search
+// --------------------------------------------------------------------
+print '<tr class="liste_titre_filter">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
+	$searchpicto = $form->showFilterButtons('left');
+	print $searchpicto;
+	print '</td>';
+}
+print '<td class="liste_titre"><input type="text" class="flat" name="search_line" value="'.dol_escape_htmltag($search_line).'" size="6"></td>';
+print '<td class="liste_titre"><input type="text" class="flat" name="search_bon" value="'.dol_escape_htmltag($search_bon).'" size="6"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+print '<td class="liste_titre"><input type="text" class="flat" name="search_company" value="'.dol_escape_htmltag($search_company).'" size="6"></td>';
+print '<td class="liste_titre center"><input type="text" class="flat" name="search_code" value="'.dol_escape_htmltag($search_code).'" size="6"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+print '<td class="liste_titre">&nbsp;</td>';
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
 	$searchpicto = $form->showFilterButtons();
 	print $searchpicto;
 	print '</td>';
-	print '</tr>';
-
-	$columntitle = "WithdrawalsReceipts";
-	$columntitlethirdparty = "CustomerCode";
-	$columncodethirdparty = "s.code_client";
-	if ($type == 'bank-transfer') {
-		$columntitle = "BankTransferReceipts";
-		$columntitlethirdparty = "SupplierCode";
-		$columncodethirdparty = "s.code_fournisseur";
-	}
+}
+print '</tr>'."\n";
 
-	print '<tr class="liste_titre">';
-	print_liste_field_titre($columntitle, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Line", $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Bill", $_SERVER["PHP_SELF"], "f.ref", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Company", $_SERVER["PHP_SELF"], "s.nom", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre($columntitlethirdparty, $_SERVER["PHP_SELF"], $columncodethirdparty, '', $param, '', $sortfield, $sortorder, 'center ');
-	print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
-	print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "pl.amount", "", $param, '', $sortfield, $sortorder, 'right ');
-	print_liste_field_titre('');
-	print "</tr>\n";
-
-	if ($num) {
-		$imaxinloop = ($limit ? min($num, $limit) : $num);
-		while ($i < $imaxinloop) {
-			$obj = $db->fetch_object($result);
-
-			$bon->id = $obj->rowid;
-			$bon->ref = $obj->ref;
-			$bon->statut = $obj->status;
-			$bon->date_echeance = $obj->datec;
-			$bon->total = $obj->amount;
-
-			$company->id = $obj->socid;
-			$company->name = $obj->name;
-			$company->email = $obj->email;
-			$company->code_client = $obj->code_client;
-
-			if ($mode == 'kanban') {
-				if ($i == 0) {
-					print '<tr><td colspan="12">';
-					print '<div class="box-flex-container kanban">';
-				}
-				// Output Kanban
-				print $bon->getKanbanView('', array('selected' => in_array($bon->id, $arrayofselected)));
-				if ($i == ($imaxinloop - 1)) {
-					print '</div>';
-					print '</td></tr>';
-				}
-			} else {
-				print '<tr class="oddeven">';
-
-				print '<td>';
-				print $bon->getNomUrl(1);
-				print "</td>\n";
-
-				print '<td>';
-				print $line->LibStatut($obj->statut_ligne, 2);
-				print "&nbsp;";
-				print '<a href="'.DOL_URL_ROOT.'/compta/prelevement/line.php?id='.$obj->rowid_ligne.'">';
-				print substr('000000'.$obj->rowid_ligne, -6);
-				print '</a></td>';
-
-				print '<td>';
-				$link_to_bill = '/compta/facture/card.php?facid=';
-				$link_title = 'Invoice';
-				$link_picto = 'bill';
-				if ($type == 'bank-transfer') {
-					$link_to_bill = '/fourn/facture/card.php?facid=';
-					$link_title = 'SupplierInvoice';
-					$link_picto = 'supplier_invoice';
-				}
-				print '<a href="'.DOL_URL_ROOT.$link_to_bill.$obj->facid.'">';
-				print img_object($langs->trans($link_title), $link_picto);
-				print '&nbsp;'.$obj->invoiceref."</td>\n";
-				print '</a>';
-				print '</td>';
-
-				print '<td>';
-				print $company->getNomUrl(1);
-				print "</td>\n";
-
-
-				print '<td class="center">';
-				$link_to_tab = '/comm/card.php?socid=';
-				$link_code = $obj->code_client;
-				if ($type == 'bank-transfer') {
-					$link_to_tab = '/fourn/card.php?socid=';
-					$link_code = $obj->code_fournisseur;
-				}
-				print '<a href="'.DOL_URL_ROOT.$link_to_tab.$company->id.'">'.$link_code."</a></td>\n";
+$totalarray = array();
+$totalarray['nbfield'] = 0;
 
-				print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
+$columntitle = "WithdrawalsReceipts";
+$columntitlethirdparty = "CustomerCode";
+$columncodethirdparty = "s.code_client";
+if ($type == 'bank-transfer') {
+	$columntitle = "BankTransferReceipts";
+	$columntitlethirdparty = "SupplierCode";
+	$columncodethirdparty = "s.code_fournisseur";
+}
 
-				print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+// Fields title label
+// --------------------------------------------------------------------
+print '<tr class="liste_titre">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print_liste_field_titre($columntitle, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Line", $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Bill", $_SERVER["PHP_SELF"], "f.ref", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Company", $_SERVER["PHP_SELF"], "s.nom", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre($columntitlethirdparty, $_SERVER["PHP_SELF"], $columncodethirdparty, '', $param, '', $sortfield, $sortorder, 'center ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "pl.amount", "", $param, '', $sortfield, $sortorder, 'right ');
+$totalarray['nbfield']++;
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print '</tr>'."\n";
+
+// Loop on record
+// --------------------------------------------------------------------
+$i = 0;
+$savnbfield = $totalarray['nbfield'];
+$totalarray = array();
+$totalarray['nbfield'] = 0;
+
+$imaxinloop = ($limit ? min($num, $limit) : $num);
+while ($i < $imaxinloop) {
+	$obj = $db->fetch_object($resql);
+
+	$bon->id = $obj->rowid;
+	$bon->ref = $obj->ref;
+	$bon->statut = $obj->status;
+	$bon->date_echeance = $obj->datec;
+	$bon->total = $obj->amount;
+
+	$object = $bon;
+
+	$company->id = $obj->socid;
+	$company->name = $obj->name;
+	$company->email = $obj->email;
+	$company->code_client = $obj->code_client;
+
+	if ($mode == 'kanban') {
+		if ($i == 0) {
+			print '<tr><td colspan="'.$savnbfield.'">';
+			print '<div class="box-flex-container kanban">';
+		}
+		// Output Kanban
+		if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+			$selected = 0;
+			if (in_array($object->id, $arrayofselected)) {
+				$selected = 1;
+			}
+		}
+		print $object->getKanbanView('', array('selected' => in_array($bon->id, $arrayofselected)));
+		if ($i == ($imaxinloop - 1)) {
+			print '</div>';
+			print '</td></tr>';
+		}
+	} else {
+		// Show line of result
+		$j = 0;
+		print '<tr data-rowid="'.$object->id.'" class="oddeven">';
+
+		// Action column
+		if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
+			}
+		}
+		print '<td>';
+		print $bon->getNomUrl(1);
+		print "</td>\n";
+
+		print '<td>';
+		print $line->LibStatut($obj->statut_ligne, 2);
+		print "&nbsp;";
+		print '<a href="'.DOL_URL_ROOT.'/compta/prelevement/line.php?id='.$obj->rowid_ligne.'">';
+		print substr('000000'.$obj->rowid_ligne, -6);
+		print '</a></td>';
+
+		print '<td>';
+		$link_to_bill = '/compta/facture/card.php?facid=';
+		$link_title = 'Invoice';
+		$link_picto = 'bill';
+		if ($type == 'bank-transfer') {
+			$link_to_bill = '/fourn/facture/card.php?facid=';
+			$link_title = 'SupplierInvoice';
+			$link_picto = 'supplier_invoice';
+		}
+		print '<a href="'.DOL_URL_ROOT.$link_to_bill.$obj->facid.'">';
+		print img_object($langs->trans($link_title), $link_picto);
+		print '&nbsp;'.$obj->invoiceref."</td>\n";
+		print '</a>';
+		print '</td>';
+
+		print '<td>';
+		print $company->getNomUrl(1);
+		print "</td>\n";
+
+
+		print '<td class="center">';
+		$link_to_tab = '/comm/card.php?socid=';
+		$link_code = $obj->code_client;
+		if ($type == 'bank-transfer') {
+			$link_to_tab = '/fourn/card.php?socid=';
+			$link_code = $obj->code_fournisseur;
+		}
+		print '<a href="'.DOL_URL_ROOT.$link_to_tab.$company->id.'">'.$link_code."</a></td>\n";
 
-				print '<td>&nbsp;</td>';
+		print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
 
-				print "</tr>\n";
+		print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+
+		// Action column
+		if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
 			}
-			$i++;
 		}
-	} else {
-		print '<tr><td colspan="8"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
-	}
-	print "</table>";
-	print '</div>';
 
-	print '</form>';
+		print '</tr>'."\n";
+	}
+	$i++;
+}
 
-	$db->free($result);
-} else {
-	dol_print_error($db);
+if ($num == 0) {
+	print '<tr><td colspan="8"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
 }
 
+$db->free($result);
+
+$parameters = array('arrayfields' => $arrayfields, 'sql' => $sql);
+$reshook = $hookmanager->executeHooks('printFieldListFooter', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+print $hookmanager->resPrint;
+
+print '</table>'."\n";
+print '</div>'."\n";
+
+print '</form>'."\n";
+
+
 // End of page
 llxFooter();
 $db->close();

+ 244 - 119
htdocs/compta/prelevement/orders_list.php

@@ -32,17 +32,27 @@ require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
 // Load translation files required by the page
 $langs->loadLangs(array('banks', 'categories', 'withdrawals'));
 
+$action     = GETPOST('action', 'aZ09') ? GETPOST('action', 'aZ09') : 'view'; // The action 'add', 'create', 'edit', 'update', 'view', ...
+$massaction = GETPOST('massaction', 'alpha'); // The bulk action (combo box choice into lists)
+$confirm    = GETPOST('confirm', 'alpha'); // Result of a confirmation
+$cancel     = GETPOST('cancel', 'alpha'); // We click on a Cancel button
+$toselect   = GETPOST('toselect', 'array'); // Array of ids of elements selected into a list
 $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'directdebitcredittransferlist'; // To manage different context of search
+$backtopage = GETPOST('backtopage', 'alpha'); // Go back to a dedicated page
+$optioncss = GETPOST('optioncss', 'alpha');
+$mode = GETPOST('mode', 'alpha');
 
 $type = GETPOST('type', 'aZ09');
 
+// Load variable for pagination
 $limit = GETPOST('limit', 'int') ? GETPOST('limit', 'int') : $conf->liste_limit;
 $sortfield = GETPOST('sortfield', 'aZ09comma');
 $sortorder = GETPOST('sortorder', 'aZ09comma');
 $page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
-if (empty($page) || $page == -1) {
+if (empty($page) || $page < 0 || GETPOST('button_search', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
+	// If $page is not defined, or '' or -1 or if we click on clear filters
 	$page = 0;
-}     // If $page is not defined, or '' or -1
+}
 $offset = $limit * $page;
 $pageprev = $page - 1;
 $pagenext = $page + 1;
@@ -53,10 +63,6 @@ if (!$sortfield) {
 	$sortfield = "p.datec";
 }
 
-$optioncss = GETPOST('optioncss', 'alpha');
-$mode = GETPOST('mode', 'alpha');
-
-
 // Get supervariables
 $statut = GETPOST('statut', 'int');
 $search_ref = GETPOST('search_ref', 'alpha');
@@ -96,7 +102,16 @@ if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x'
  * View
  */
 
-llxHeader('', $langs->trans("WithdrawalsReceipts"));
+$directdebitorder = new BonPrelevement($db);
+
+$titlekey = "WithdrawalsReceipts";
+$title = $langs->trans("WithdrawalsReceipts");
+if ($type == 'bank-transfer') {
+	$titlekey = "BankTransferReceipts";
+	$title = $langs->trans("BankTransferReceipts");
+}
+$help_url = '';
+
 
 $sql = "SELECT p.rowid, p.ref, p.amount, p.statut, p.datec";
 
@@ -130,7 +145,7 @@ if (!getDolGlobalInt('MAIN_DISABLE_FULL_SCANLIST')) {
 		dol_print_error($db);
 	}
 
-	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller then paging size (filtering), goto and load page 0
+	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller than the paging size (filtering), goto and load page 0
 		$page = 0;
 		$offset = 0;
 	}
@@ -143,142 +158,252 @@ if ($limit) {
 	$sql .= $db->plimit($limit + 1, $offset);
 }
 
-$result = $db->query($sql);
-if ($result) {
-	$num = $db->num_rows($result);
-	$i = 0;
+$resql = $db->query($sql);
+if (!$resql) {
+	dol_print_error($db);
+	exit;
+}
 
-	$param = '';
-	if (!empty($mode)) {
-		$param .= '&mode='.urlencode($mode);
-	}
-	if (!empty($contextpage) && $contextpage != $_SERVER["PHP_SELF"]) {
-		$param .= '&contextpage='.urlencode($contextpage);
-	}
-	if ($type == 'bank-transfer') {
-		$param .= '&amp;type=bank-transfer';
-	}
-	if ($limit > 0 && $limit != $conf->liste_limit) {
-		$param .= '&limit='.((int) $limit);
-	}
-	$param .= "&statut=".urlencode($statut);
+$num = $db->num_rows($resql);
 
-	$selectedfields = '';
+// Output page
+// --------------------------------------------------------------------
 
-	$newcardbutton = '';
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
-	if ($usercancreate) {
-		$newcardbutton .= dolGetButtonTitle($langs->trans('NewStandingOrder'), '', 'fa fa-plus-circle', DOL_URL_ROOT.'/compta/prelevement/create.php?type='.urlencode($type));
-	}
+llxHeader('', $title, $help_url);
 
-	// Lines of title fields
-	print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">';
-	print '<input type="hidden" name="token" value="'.newToken().'">';
-	if ($optioncss != '') {
-		print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
-	}
-	print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
-	print '<input type="hidden" name="action" value="list">';
-	print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
-	print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
-	print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
-	print '<input type="hidden" name="mode" value="'.$mode.'">';
-
-	if ($type != '') {
-		print '<input type="hidden" name="type" value="'.$type.'">';
-	}
-	$titlekey = "WithdrawalsReceipts";
-	$title = $langs->trans("WithdrawalsReceipts");
-	if ($type == 'bank-transfer') {
-		$titlekey = "BankTransferReceipts";
-		$title = $langs->trans("BankTransferReceipts");
-	}
+$arrayofselected = is_array($toselect) ? $toselect : array();
+
+$param = '';
+$param .= "&statut=".urlencode($statut);
+if ($type == 'bank-transfer') {
+	$param .= '&type=bank-transfer';
+}
+if (!empty($mode)) {
+	$param .= '&mode='.urlencode($mode);
+}
+if (!empty($contextpage) && $contextpage != $_SERVER["PHP_SELF"]) {
+	$param .= '&contextpage='.urlencode($contextpage);
+}
+if ($limit > 0 && $limit != $conf->liste_limit) {
+	$param .= '&limit='.((int) $limit);
+}
+if ($optioncss != '') {
+	$param .= '&optioncss='.urlencode($optioncss);
+}
+
+
+print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">'."\n";
+print '<input type="hidden" name="token" value="'.newToken().'">';
+if ($optioncss != '') {
+	print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
+}
+print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
+print '<input type="hidden" name="action" value="list">';
+print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
+print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
+print '<input type="hidden" name="page" value="'.$page.'">';
+print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
+print '<input type="hidden" name="page_y" value="">';
+print '<input type="hidden" name="mode" value="'.$mode.'">';
+
+if ($type != '') {
+	print '<input type="hidden" name="type" value="'.$type.'">';
+}
+
+$newcardbutton = '';
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
+if ($usercancreate) {
+	$newcardbutton .= dolGetButtonTitle($langs->trans('NewStandingOrder'), '', 'fa fa-plus-circle', DOL_URL_ROOT.'/compta/prelevement/create.php?type='.urlencode($type));
+}
+
+print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
+
+$moreforfilter = '';
+/*$moreforfilter.='<div class="divsearchfield">';
+ $moreforfilter.= $langs->trans('MyFilter') . ': <input type="text" name="search_myfield" value="'.dol_escape_htmltag($search_myfield).'">';
+ $moreforfilter.= '</div>';*/
+
+$parameters = array();
+$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+if (empty($reshook)) {
+	$moreforfilter .= $hookmanager->resPrint;
+} else {
+	$moreforfilter = $hookmanager->resPrint;
+}
 
-	print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
+if (!empty($moreforfilter)) {
+	print '<div class="liste_titre liste_titre_bydiv centpercent">';
+	print $moreforfilter;
+	$parameters = array();
+	$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+	print $hookmanager->resPrint;
+	print '</div>';
+}
 
-	$moreforfilter = '';
+$varpage = empty($contextpage) ? $_SERVER["PHP_SELF"] : $contextpage;
+$selectedfields = ($mode != 'kanban' ? $form->multiSelectArrayWithCheckbox('selectedfields', $arrayfields, $varpage, getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN', '')) : ''); // This also change content of $arrayfields
+$selectedfields .= (count($arrayofmassactions) ? $form->showCheckAddButtons('checkforselect', 1) : '');
 
-	print '<div class="div-table-responsive">';
-	print '<table class="tagtable liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
+print '<div class="div-table-responsive">'; // You can use div-table-responsive-no-min if you dont need reserved height for your table
+print '<table class="tagtable nobottomiftotal liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
 
-	print '<tr class="liste_titre">';
-	print '<td class="liste_titre"><input type="text" class="flat maxwidth100" name="search_ref" value="'.dol_escape_htmltag($search_ref).'"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre right"><input type="text" class="flat maxwidth100" name="search_amount" value="'.dol_escape_htmltag($search_amount).'"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre maxwidthsearch">';
+// Fields title search
+// --------------------------------------------------------------------
+print '<tr class="liste_titre_filter">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
+	$searchpicto = $form->showFilterButtons('left');
+	print $searchpicto;
+	print '</td>';
+}
+print '<td class="liste_titre"><input type="text" class="flat maxwidth100" name="search_ref" value="'.dol_escape_htmltag($search_ref).'"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+print '<td class="liste_titre right"><input type="text" class="flat maxwidth100" name="search_amount" value="'.dol_escape_htmltag($search_amount).'"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
 	$searchpicto = $form->showFilterButtons();
 	print $searchpicto;
 	print '</td>';
-	print '</tr>';
-
-	print '<tr class="liste_titre">';
-	print_liste_field_titre($titlekey, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
-	print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "p.amount", "", $param, '', $sortfield, $sortorder, 'right ');
-	print_liste_field_titre("Status", $_SERVER["PHP_SELF"], "", "", $param, '', $sortfield, $sortorder, 'right ');
-	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], "", '', $param, '', $sortfield, $sortorder, 'maxwidthsearch center ')."\n";
-	print "</tr>\n";
-
-	$directdebitorder = new BonPrelevement($db);
-
-	if ($num) {
-		while ($i < min($num, $limit)) {
-			$obj = $db->fetch_object($result);
-
-			$directdebitorder->id = $obj->rowid;
-			$directdebitorder->ref = $obj->ref;
-			$directdebitorder->date_echeance = $obj->datec;
-			$directdebitorder->total = $obj->amount;
-			$directdebitorder->statut = $obj->statut;
-
-			if ($mode == 'kanban') {
-				if ($i == 0) {
-					print '<tr><td colspan="12">';
-					print '<div class="box-flex-container kanban">';
-				}
-				// Output Kanban
-				print $directdebitorder->getKanbanView('', array('selected' => in_array($obj->id, $arrayofselected)));
-				if ($i == (min($num, $limit) - 1)) {
-					print '</div>';
-					print '</td></tr>';
-				}
-			} else {
-				print '<tr class="oddeven">';
+}
+print '</tr>'."\n";
+
+$totalarray = array();
+$totalarray['nbfield'] = 0;
+
+// Fields title label
+// --------------------------------------------------------------------
+print '<tr class="liste_titre">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print_liste_field_titre($titlekey, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "p.amount", "", $param, '', $sortfield, $sortorder, 'right ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Status", $_SERVER["PHP_SELF"], "", "", $param, '', $sortfield, $sortorder, 'right ');
+$totalarray['nbfield']++;
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print '</tr>'."\n";
 
-				print '<td>';
-				print $directdebitorder->getNomUrl(1);
-				print "</td>\n";
+// Loop on record
+// --------------------------------------------------------------------
 
-				print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
+$i = 0;
+$savnbfield = $totalarray['nbfield'];
+$totalarray = array();
+$totalarray['nbfield'] = 0;
 
-				print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+$imaxinloop = ($limit ? min($num, $limit) : $num);
+while ($i < $imaxinloop) {
+	$obj = $db->fetch_object($resql);
 
-				print '<td class="right">';
-				print $bon->LibStatut($obj->statut, 5);
-				print '</td>';
+	$directdebitorder->id = $obj->rowid;
+	$directdebitorder->ref = $obj->ref;
+	$directdebitorder->date_echeance = $obj->datec;
+	$directdebitorder->total = $obj->amount;
+	$directdebitorder->statut = $obj->statut;
 
-				print '<td class="right"></td>'."\n";
+	$object = $directdebitorder;
 
-				print "</tr>\n";
+	if ($mode == 'kanban') {
+		if ($i == 0) {
+			print '<tr><td colspan="'.$savnbfield.'">';
+			print '<div class="box-flex-container kanban">';
+		}
+		// Output Kanban
+		if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+			$selected = 0;
+			if (in_array($object->id, $arrayofselected)) {
+				$selected = 1;
 			}
-			$i++;
+		}
+		print $directdebitorder->getKanbanView('', array('selected' => in_array($obj->id, $arrayofselected)));
+		if ($i == ($imaxinloop - 1)) {
+			print '</div>';
+			print '</td></tr>';
 		}
 	} else {
-		print '<tr><td colspan="5"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
-	}
+		// Show line of result
+		$j = 0;
+		print '<tr data-rowid="'.$object->id.'" class="oddeven">';
+
+		// Action column
+		if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
+			}
+		}
 
-	print "</table>";
-	print '</div>';
+		print '<td>';
+		print $directdebitorder->getNomUrl(1);
+		print "</td>\n";
 
-	print '</form>';
+		print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
 
-	$db->free($result);
-} else {
-	dol_print_error($db);
+		print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+
+		print '<td class="right">';
+		print $bon->LibStatut($obj->statut, 5);
+		print '</td>';
+
+		// Action column
+		if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
+			}
+		}
+
+		print '</tr>'."\n";
+	}
+	$i++;
 }
 
+if ($num == 0) {
+	print '<tr><td colspan="5"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
+}
+
+$db->free($result);
+
+$parameters = array('arrayfields' => $arrayfields, 'sql' => $sql);
+$reshook = $hookmanager->executeHooks('printFieldListFooter', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+print $hookmanager->resPrint;
+
+print '</table>'."\n";
+print '</div>'."\n";
+
+print '</form>'."\n";
+
+
 // End of page
 llxFooter();
 $db->close();

+ 14 - 7
htdocs/compta/prelevement/stats.php

@@ -118,23 +118,30 @@ if ($resql) {
 	while ($i < $num) {
 		$row = $db->fetch_row($resql);
 
-		print '<tr class="oddeven"><td>';
+		print '<tr class="oddeven">';
 
+		print '<td>';
 		print $ligne->LibStatut($row[2], 1);
 		//print $st[$row[2]];
-		print '</td><td align="center">';
+		print '</td>';
+
+		print '<td align="center">';
 		print $row[1];
+		print '</td>';
 
-		print '</td><td class="right">';
+		print '<td class="right">';
 		print round($row[1] / $nbtotal * 100, 2)." %";
+		print '</td>';
 
-		print '</td><td class="right">';
-
+		print '<td class="right amount">';
 		print price($row[0]);
+		print '</td>';
 
-		print '</td><td class="right">';
+		print '<td class="right">';
 		print round($row[0] / $total * 100, 2)." %";
-		print '</td></tr>';
+		print '</td>';
+
+		print '</tr>';
 
 		$i++;
 	}

+ 1 - 1
htdocs/contact/canvas/actions_contactcard_common.class.php

@@ -278,7 +278,7 @@ abstract class ActionsContactCardCommon
 	/**
 	 *  Assign POST values into object
 	 *
-	 *  @return		string					HTML output
+	 *  @return		void
 	 */
 	private function assign_post()
 	{

+ 8 - 1
htdocs/contrat/class/contrat.class.php

@@ -738,6 +738,10 @@ class Contrat extends CommonObject
 					}
 
 					return $this->id;
+				} else {
+					dol_syslog(get_class($this)."::fetch Contract failed");
+					$this->error = "Fetch contract failed";
+					return -1;
 				}
 			} else {
 				dol_syslog(get_class($this)."::fetch Contract not found");
@@ -1664,7 +1668,7 @@ class Contrat extends CommonObject
 	 *  @param	array		$array_options		extrafields array
 	 * 	@param 	string		$fk_unit 			Code of the unit to use. Null to use the default one
 	 * 	@param 	string		$rang 				Position
-	 *  @return int              				< 0 si erreur, > 0 si ok
+	 *  @return int              				<0 if KO, >0 if OK
 	 */
 	public function updateline($rowid, $desc, $pu, $qty, $remise_percent, $date_start, $date_end, $tvatx, $localtax1tx = 0.0, $localtax2tx = 0.0, $date_start_real = '', $date_end_real = '', $price_base_type = 'HT', $info_bits = 0, $fk_fournprice = null, $pa_ht = 0, $array_options = 0, $fk_unit = null, $rang = 0)
 	{
@@ -1819,6 +1823,9 @@ class Contrat extends CommonObject
 
 				$this->db->commit();
 				return 1;
+			} else {
+				$this->db->rollback();
+				return -1;
 			}
 		} else {
 			$this->db->rollback();

+ 1 - 1
htdocs/core/boxes/box_members_by_tags.php

@@ -160,7 +160,7 @@ class box_members_by_tags extends ModeleBoxes
 
 					$this->info_box_contents[$line][] = array(
 						'td' => 'class="tdoverflowmax150 maxwidth150onsmartphone"',
-						'text' => '<a href="'.$DOL_MAIN_URL_ROOT.'/adherents/list.php?search_categ='.$adhtag->id.'&sortfield=d.datefin,t.subscription&sortorder=desc,desc&backtopage='.urlencode($_SERVER['PHP_SELF']).'">'.dol_trunc(($adhtag->ref ? $adhtag->ref : $adhtag->label), dol_size(32)).'</a>',
+						'text' => '<a href="'.DOL_MAIN_URL_ROOT.'/adherents/list.php?search_categ='.$adhtag->id.'&sortfield=d.datefin,t.subscription&sortorder=desc,desc&backtopage='.urlencode($_SERVER['PHP_SELF']).'">'.dol_trunc(($adhtag->ref ? $adhtag->ref : $adhtag->label), dol_size(32)).'</a>',
 						'asis' => 1,
 					);
 					$this->info_box_contents[$line][] = array(

+ 2 - 2
htdocs/core/class/CMailFile.class.php

@@ -164,7 +164,7 @@ class CMailFile
 	 *	@param	string	$css                 Css option
 	 *	@param	string	$trackid             Tracking string (contains type and id of related element)
 	 *  @param  string  $moreinheader        More in header. $moreinheader must contains the "\r\n" (TODO not supported for other MAIL_SEND_MODE different than 'mail' and 'smtps' for the moment)
-	 *  @param  string  $sendcontext      	 'standard', 'emailing', ... (used to define which sending mode and parameters to use)
+	 *  @param  string  $sendcontext      	 'standard', 'emailing', 'ticket', 'password', ... (used to define which sending mode and parameters to use)
 	 *  @param	string	$replyto			 Reply-to email (will be set to same value than From by default if not provided)
 	 *  @param	string	$upload_dir_tmp		 Temporary directory (used to convert images embedded as img src=data:image)
 	 */
@@ -187,7 +187,7 @@ class CMailFile
 
 		$this->sendcontext = $sendcontext;
 
-		// Define this->sendmode ('mail', 'smtps', 'siwftmailer', ...) according to $sendcontext ('standard', 'emailing', 'ticket')
+		// Define this->sendmode ('mail', 'smtps', 'swiftmailer', ...) according to $sendcontext ('standard', 'emailing', 'ticket', 'password')
 		$this->sendmode = '';
 		if (!empty($this->sendcontext)) {
 			$smtpContextKey = strtoupper($this->sendcontext);

+ 15 - 0
htdocs/core/class/commonobject.class.php

@@ -9888,6 +9888,21 @@ abstract class CommonObject
 				$this->{$key} = $value;
 			}
 		}
+
+		// Force values to default values when known
+		if (property_exists($this, 'fields')) {
+			foreach ($this->fields as $key => $value) {
+				// If fields are already set, do nothing
+				if (array_key_exists($key, $fields)) {
+					continue;
+				}
+
+				if (!empty($value['default'])) {
+					$this->$key = $value['default'];
+				}
+			}
+		}
+
 		return 1;
 	}
 

+ 12 - 5
htdocs/core/class/extrafields.class.php

@@ -823,9 +823,9 @@ class ExtraFields
 
 	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
 	/**
-	 * 	Load array this->attributes
+	 * 	Load the array of extrafields defintion $this->attributes
 	 *
-	 * 	@param	string		$elementtype		Type of element ('' = all or $object->table_element like 'adherent', 'commande', 'thirdparty', 'facture', 'propal', 'product', ...).
+	 * 	@param	string		$elementtype		Type of element ('all' = all or $object->table_element like 'adherent', 'commande', 'thirdparty', 'facture', 'propal', 'product', ...).
 	 * 	@param	boolean		$forceload			Force load of extra fields whatever is status of cache.
 	 * 	@return	array							Array of attributes keys+label for all extra fields.
 	 */
@@ -848,6 +848,9 @@ class ExtraFields
 			$elementtype = 'commande_fournisseur';
 		}
 
+		// Test cache $this->attributes[$elementtype]['loaded'] to see if we must do something
+		// TODO
+
 		$array_name_label = array();
 
 		// We should not have several time this request. If we have, there is some optimization to do by calling a simple $extrafields->fetch_optionals() in top of code and not into subcode
@@ -855,7 +858,7 @@ class ExtraFields
 		$sql .= " css, cssview, csslist";
 		$sql .= " FROM ".$this->db->prefix()."extrafields";
 		//$sql.= " WHERE entity IN (0,".$conf->entity.")";    // Filter is done later
-		if ($elementtype) {
+		if ($elementtype && $elementtype != 'all') {
 			$sql .= " WHERE elementtype = '".$this->db->escape($elementtype)."'"; // Filed with object->table_element
 		}
 		$sql .= " ORDER BY pos";
@@ -906,7 +909,7 @@ class ExtraFields
 				}
 			}
 			if ($elementtype) {
-				$this->attributes[$elementtype]['loaded'] = 1; // If nothing found, we also save tag 'loaded'
+				$this->attributes[$elementtype]['loaded'] = 1; // Note: If nothing is found, we also set the key 'loaded' to 1.
 				$this->attributes[$elementtype]['count'] = $count;
 			}
 		} else {
@@ -2136,8 +2139,12 @@ class ExtraFields
 						|| (is_array($_POST["options_".$key]) && empty($_POST["options_".$key]))) {
 						//print 'ccc'.$value.'-'.$this->attributes[$object->table_element]['required'][$key];
 
-						// Field is not defined. We mark this as a problem. We may fix it later if there is a default value and $todefaultifmissing is set.
+						// Field is not defined. We mark this as an error. We may fix it later if there is a default value and $todefaultifmissing is set.
+
 						$nofillrequired++;
+						if (!empty($this->attributes[$object->table_element]['langfile'][$key])) {
+							$langs->load($this->attributes[$object->table_element]['langfile'][$key]);
+						}
 						$error_field_required[$key] = $langs->transnoentitiesnoconv($value);
 					}
 				}

+ 85 - 28
htdocs/core/class/html.form.class.php

@@ -4791,16 +4791,16 @@ class Form
 	/**
 	 *  Return a HTML select list of bank accounts
 	 *
-	 * @param string $selected Id account pre-selected
-	 * @param string $htmlname Name of select zone
-	 * @param int $status Status of searched accounts (0=open, 1=closed, 2=both)
-	 * @param string $filtre To filter list. This parameter must not come from input of users
-	 * @param int $useempty 1=Add an empty value in list, 2=Add an empty value in list only if there is more than 2 entries.
-	 * @param string $moreattrib To add more attribute on select
-	 * @param int $showcurrency Show currency in label
-	 * @param string $morecss More CSS
-	 * @param int $nooutput 1=Return string, do not send to output
-	 * @return    int                            <0 if error, Num of bank account found if OK (0, 1, 2, ...)
+	 * @param string 	$selected 		Id account pre-selected
+	 * @param string 	$htmlname 		Name of select zone
+	 * @param int 		$status 		Status of searched accounts (0=open, 1=closed, 2=both)
+	 * @param string 	$filtre 		To filter list. This parameter must not come from input of users
+	 * @param int 		$useempty 		1=Add an empty value in list, 2=Add an empty value in list only if there is more than 2 entries.
+	 * @param string 	$moreattrib 	To add more attribute on select
+	 * @param int 		$showcurrency 	Show currency in label
+	 * @param string 	$morecss 		More CSS
+	 * @param int 		$nooutput 		1=Return string, do not send to output
+	 * @return int                   	<0 if error, Num of bank account found if OK (0, 1, 2, ...)
 	 */
 	public function select_comptes($selected = '', $htmlname = 'accountid', $status = 0, $filtre = '', $useempty = 0, $moreattrib = '', $showcurrency = 0, $morecss = '', $nooutput = 0)
 	{
@@ -10431,23 +10431,25 @@ class Form
 	/**
 	 * Output the component to make advanced search criteries
 	 *
-	 * @param array $arrayofcriterias Array of available search criterias. Example: array($object->element => $object->fields, 'otherfamily' => otherarrayoffields, ...)
-	 * @param array $search_component_params Array of selected search criterias
-	 * @param array $arrayofinputfieldsalreadyoutput Array of input fields already inform. The component will not generate a hidden input field if it is in this list.
-	 * @param string $search_component_params_hidden String with $search_component_params criterias
-	 * @return    string                                              HTML component for advanced search
+	 * @param 	array 	$arrayofcriterias 					Array of available search criterias. Example: array($object->element => $object->fields, 'otherfamily' => otherarrayoffields, ...)
+	 * @param 	array 	$search_component_params 			Array of selected search criterias
+	 * @param 	array 	$arrayofinputfieldsalreadyoutput 	Array of input fields already inform. The component will not generate a hidden input field if it is in this list.
+	 * @param 	string 	$search_component_params_hidden 	String with $search_component_params criterias
+	 * @return	string                                    	HTML component for advanced search
 	 */
 	public function searchComponent($arrayofcriterias, $search_component_params, $arrayofinputfieldsalreadyoutput = array(), $search_component_params_hidden = '')
 	{
 		global $langs;
 
+		if ($search_component_params_hidden != '' && !preg_match('/^\(.*\)$/', $search_component_params_hidden)) {    // If $search_component_params_hidden does not start and end with ()
+			$search_component_params_hidden = '(' . $search_component_params_hidden . ')';
+		}
+
 		$ret = '';
 
 		$ret .= '<div class="divadvancedsearchfieldcomp inline-block">';
-		//$ret .= '<button type="submit" class="liste_titre button_removefilter" name="button_removefilter_x" value="x"><span class="fa fa-remove"></span></button>';
 		$ret .= '<a href="#" class="dropdownsearch-toggle unsetcolor">';
 		$ret .= '<span class="fas fa-filter linkobject boxfilter paddingright pictofixedwidth" title="' . dol_escape_htmltag($langs->trans("Filters")) . '" id="idsubimgproductdistribution"></span>';
-		//$ret .= $langs->trans("Filters");
 		$ret .= '</a>';
 
 		$ret .= '<div class="divadvancedsearchfieldcompinput inline-block minwidth500 maxwidth300onsmartphone">';
@@ -10456,16 +10458,58 @@ class Form
 		$ret .= '<div name="divsearch_component_params" class="noborderbottom search_component_params inline-block valignmiddle">';
 
 		if ($search_component_params_hidden) {
-			if (!preg_match('/^\(.*\)$/', $search_component_params_hidden)) {    // If $search_component_params_hidden does not start and end with ()
-				$search_component_params_hidden .= '(' . $search_component_params_hidden . ')';
+			// Split the criteria on each AND
+			//var_dump($search_component_params_hidden);
+
+			$nbofchars = dol_strlen($search_component_params_hidden);
+			$arrayofandtags = array();
+			$i = 0; $s = '';
+			$countparenthesis = 0;
+			while ($i < $nbofchars) {
+				$char = dol_substr($search_component_params_hidden, $i, 1);
+
+				if ($char == '(') {
+					$countparenthesis++;
+				} elseif ($char == ')') {
+					$countparenthesis--;
+				}
+
+				if ($countparenthesis == 0) {
+					$char2 = dol_substr($search_component_params_hidden, $i+1, 1);
+					$char3 = dol_substr($search_component_params_hidden, $i+2, 1);
+					if ($char == 'A' && $char2 == 'N' && $char3 == 'D') {
+						// We found a AND
+						$arrayofandtags[] = trim($s);
+						$s = '';
+						$i+=2;
+					} else {
+						$s .= $char;
+					}
+				} else {
+					$s .= $char;
+				}
+				$i++;
+			}
+			if ($s) {
+				$arrayofandtags[] = trim($s);
 			}
-			$errormessage = '';
-			$searchtags = forgeSQLFromUniversalSearchCriteria($search_component_params_hidden, $errormessage);
-			if ($errormessage) {
-				print 'ERROR in parsing search string: ' . dol_escape_htmltag($errormessage);
+
+			// Show each AND part
+			foreach ($arrayofandtags as $tmpkey => $tmpval) {
+				$errormessage = '';
+				$searchtags = forgeSQLFromUniversalSearchCriteria($tmpval, $errormessage, 1, 1);
+				if ($errormessage) {
+					$this->error = 'ERROR in parsing search string: '.$errormessage;
+				}
+				// Remove first and last parenthesis but only if first is the opening and last the closing of the same group
+				include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
+				$searchtags = removeGlobalParenthesis($searchtags);
+
+				$ret .= '<span class="marginleftonlyshort valignmiddle tagsearch" data-ufilterid="'.($tmpkey+1).'" data-ufilter="'.dol_escape_htmltag($tmpval).'">';
+				$ret .= '<span class="tagsearchdelete select2-selection__choice__remove" data-ufilterid="'.($tmpkey+1).'">x</span> ';
+				$ret .= dol_escape_htmltag($searchtags);
+				$ret .= '</span>';
 			}
-			//var_dump($searchtags);
-			$ret .= '<span class="marginleftonlyshort valignmiddle tagsearch"><span class="tagsearchdelete select2-selection__choice__remove">x</span> ' . dol_escape_htmltag($searchtags) . '</span>';
 		}
 
 		//$ret .= '<button type="submit" class="liste_titre button_search paddingleftonly" name="button_search_x" value="x"><span class="fa fa-search"></span></button>';
@@ -10478,9 +10522,11 @@ class Form
 		if ($show_search_component_params_hidden) {
 			$ret .= '<input type="hidden" name="show_search_component_params_hidden" value="1">';
 		}
-		$ret .= "<!-- We store the full search string into this field. For example: (t.ref:like:'SO-%') and ((t.ref:like:'CO-%') or (t.ref:like:'AA%')) -->";
+		$ret .= "<!-- We store the full Universal Search String into this field. For example: (t.ref:like:'SO-%') AND ((t.ref:like:'CO-%') OR (t.ref:like:'AA%')) -->";
 		$ret .= '<input type="hidden" name="search_component_params_hidden" value="' . dol_escape_htmltag($search_component_params_hidden) . '">';
-		// For compatibility with forms that show themself the search criteria in addition of this component, we output the fields
+		// $ret .= "<!-- sql= ".forgeSQLFromUniversalSearchCriteria($search_component_params_hidden, $errormessage)." -->";
+
+		// For compatibility with forms that show themself the search criteria in addition of this component, we output these fields
 		foreach ($arrayofcriterias as $criterias) {
 			foreach ($criterias as $criteriafamilykey => $criteriafamilyval) {
 				if (in_array('search_' . $criteriafamilykey, $arrayofinputfieldsalreadyoutput)) {
@@ -10506,12 +10552,23 @@ class Form
 
 		$ret .= '</div>';
 
-		$ret .= "<!-- Syntax of Generic filter string: t.ref:like:'SO-%', t.date_creation:<:'20160101', t.date_creation:<:'2016-01-01 12:30:00', t.nature:is:NULL, t.field2:isnot:NULL -->\n";
+		$ret .= "<!-- Field to enter a generic filter string: t.ref:like:'SO-%', t.date_creation:<:'20160101', t.date_creation:<:'2016-01-01 12:30:00', t.nature:is:NULL, t.field2:isnot:NULL -->\n";
 		$ret .= '<input type="text" placeholder="' . $langs->trans("Search") . '" name="search_component_params_input" class="noborderbottom search_component_input" value="">';
 
 		$ret .= '</div>';
 		$ret .= '</div>';
 
+		$ret .= '<script>
+		jQuery(".tagsearchdelete").click(function() {
+			var filterid = $(this).parents().data("ufilterid");
+			console.log("We click to delete a criteria nb "+filterid);
+			// TODO Update the search_component_params_hidden with all data-ufilter except the one delete and post page
+
+		});
+		</script>
+		';
+
+
 		return $ret;
 	}
 

+ 6 - 6
htdocs/core/class/notify.class.php

@@ -547,7 +547,7 @@ class Notify
 								break;
 							case 'ORDER_SUPPLIER_VALIDATE':
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-								$dir_output = $conf->fournisseur->commande->dir_output;
+								$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 								$object_type = 'order_supplier';
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderValidatedBy", $link, $user->getFullName($outputlangs));
@@ -555,7 +555,7 @@ class Notify
 								break;
 							case 'ORDER_SUPPLIER_APPROVE':
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-								$dir_output = $conf->fournisseur->commande->dir_output;
+								$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 								$object_type = 'order_supplier';
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($outputlangs));
@@ -563,7 +563,7 @@ class Notify
 								break;
 							case 'ORDER_SUPPLIER_REFUSE':
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-								$dir_output = $conf->fournisseur->commande->dir_output;
+								$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 								$object_type = 'order_supplier';
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderRefusedBy", $link, $user->getFullName($outputlangs));
@@ -815,7 +815,7 @@ class Notify
 						break;
 					case 'ORDER_SUPPLIER_VALIDATE':
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-						$dir_output = $conf->fournisseur->commande->dir_output;
+						$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 						$object_type = 'order_supplier';
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderValidatedBy", $link, $user->getFullName($langs));
@@ -823,7 +823,7 @@ class Notify
 						break;
 					case 'ORDER_SUPPLIER_APPROVE':
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-						$dir_output = $conf->fournisseur->commande->dir_output;
+						$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 						$object_type = 'order_supplier';
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($langs));
@@ -831,7 +831,7 @@ class Notify
 						break;
 					case 'ORDER_SUPPLIER_APPROVE2':
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-						$dir_output = $conf->fournisseur->commande->dir_output;
+						$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 						$object_type = 'order_supplier';
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($langs));

+ 251 - 86
htdocs/core/customreports.php

@@ -152,7 +152,7 @@ if ($user->socid > 0) {	// Protection if external user
 }
 
 // Fetch optionals attributes and labels
-$extrafields->fetch_name_optionals_label($object->table_element);
+$extrafields->fetch_name_optionals_label('all');	// We load all extrafields definitions for all objects
 //$extrafields->fetch_name_optionals_label($object->table_element_line);
 
 $search_array_options = $extrafields->getOptionalsFromPost($object->table_element, '', 'search_');
@@ -277,8 +277,18 @@ if (is_array($search_groupby) && count($search_groupby)) {
 
 		$sql = "SELECT DISTINCT ".$fieldtocount." as val";
 
-		if (strpos($fieldtocount, 'te.') === 0) {
-			$sql .= " FROM ".MAIN_DB_PREFIX.$object->table_element."_extrafields as te";
+		if (strpos($fieldtocount, 'te') === 0) {
+			$tabletouse = $object->table_element;
+			$tablealiastouse = 'te';
+			if (!empty($arrayofgroupby[$gval])) {
+				$tmpval = explode('.', $gval);
+				$tabletouse = $arrayofgroupby[$gval]['table'];
+				$tablealiastouse = $tmpval[0];
+			}
+			//var_dump($tablealiastouse);exit;
+
+			//$sql .= " FROM ".MAIN_DB_PREFIX.$object->table_element."_extrafields as te";
+			$sql .= " FROM ".MAIN_DB_PREFIX.$tabletouse."_extrafields as ".$tablealiastouse;
 		} else {
 			$tabletouse = $object->table_element;
 			$tablealiastouse = 't';
@@ -290,9 +300,9 @@ if (is_array($search_groupby) && count($search_groupby)) {
 			$sql .= " FROM ".MAIN_DB_PREFIX.$tabletouse." as ".$tablealiastouse;
 		}
 
-		// Add the where here
-		/*
-		$sqlfilters = GETPOST('search_component_params_hidden', 'alphanohtml');
+		// Add a where here keeping only the citeria on $tabletouse
+		// TODO
+		/*$sqlfilters = ... GETPOST('search_component_params_hidden', 'alphanohtml');
 		if ($sqlfilters) {
 			$errormessage = '';
 			$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
@@ -358,7 +368,8 @@ if (is_array($search_groupby) && count($search_groupby)) {
 		// Add a protection/error to refuse the request if number of differentr values for the group by is higher than $MAXUNIQUEVALFORGROUP
 		if (count($arrayofvaluesforgroupby['g_'.$gkey]) > $MAXUNIQUEVALFORGROUP) {
 			$langs->load("errors");
-			if (strpos($fieldtocount, 'te.') === 0) {			// This is an extrafield
+
+			if (strpos($fieldtocount, 'te') === 0) {			// This is a field of an extrafield
 				//if (!empty($extrafields->attributes[$object->table_element]['langfile'][$gvalwithoutprefix])) {
 				//      $langs->load($extrafields->attributes[$object->table_element]['langfile'][$gvalwithoutprefix]);
 				//}
@@ -367,10 +378,13 @@ if (is_array($search_groupby) && count($search_groupby)) {
 			} elseif (strpos($fieldtocount, 't__') === 0) {		// This is a field of a foreign key
 				$reg = array();
 				if (preg_match('/^(.*)\.(.*)/', $gvalwithoutprefix, $reg)) {
+					/*
 					$gvalwithoutprefix = preg_replace('/\..*$/', '', $gvalwithoutprefix);
-					$gvalwithoutprefix = preg_replace('/t__/', '', $gvalwithoutprefix);
+					$gvalwithoutprefix = preg_replace('/^t__/', '', $gvalwithoutprefix);
 					$keyforlabeloffield = $object->fields[$gvalwithoutprefix]['label'];
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield).'-'.$reg[2];
+					*/
+					$labeloffield = $arrayofgroupby[$fieldtocount]['labelnohtml'];
 				} else {
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield);
 				}
@@ -385,7 +399,6 @@ if (is_array($search_groupby) && count($search_groupby)) {
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield);
 				}
 			}
-			//var_dump($gkey.' '.$gval.' '.$gvalwithoutprefix.' '.$fieldtocount.' '.$keyforlabeloffield);
 			//var_dump($object->fields);
 			setEventMessages($langs->trans("ErrorTooManyDifferentValueForSelectedGroupBy", $MAXUNIQUEVALFORGROUP, $labeloffield), null, 'warnings');
 			$search_groupby = array();
@@ -405,7 +418,7 @@ $startyear = $endyear - 2;
 
 $param = '';
 
-print '<form method="post" action="'.$_SERVER['PHP_SELF'].'">';
+print '<form method="post" action="'.$_SERVER['PHP_SELF'].'" autocomplete="off">';
 print '<input type="hidden" name="token" value="'.newToken().'">';
 print '<input type="hidden" name="action" value="viewgraph">';
 print '<input type="hidden" name="tabfamily" value="'.$tabfamily.'">';
@@ -452,12 +465,12 @@ if (empty($conf->use_javascript_ajax)) {
 }
 print '</div><div class="clearboth"></div>';
 
-// Add Filter (you can use param &show_search_component_params_hidden=1 for debug)
+// Filter (you can use param &show_search_component_params_hidden=1 for debug)
 print '<div class="divadvancedsearchfield quatrevingtpercent">';
 print $form->searchComponent(array($object->element => $object->fields), $search_component_params, array(), $search_component_params_hidden);
 print '</div>';
 
-// Add measures into array
+// YAxis (add measures into array)
 $count = 0;
 //var_dump($arrayofmesures);
 print '<div class="divadvancedsearchfield clearboth">';
@@ -559,6 +572,8 @@ print '</form>';
 // Generate the SQL request
 $sql = '';
 if (!empty($search_measures) && !empty($search_xaxis)) {
+	$errormessage = '';
+
 	$fieldid = 'rowid';
 
 	$sql = "SELECT ";
@@ -613,7 +628,7 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	if ($object->isextrafieldmanaged) {
 		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$object->table_element."_extrafields as te ON te.fk_object = t.".$fieldid;
 	}
-	// Add table for link for multientity
+	// Add table for link on multientity
 	if ($object->ismultientitymanaged) {	// 0=No test on entity, 1=Test with field entity, 'field@table'=Test with link by field@table
 		if ($object->ismultientitymanaged == 1) {
 			// No table to add here
@@ -624,6 +639,7 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 		}
 	}
 
+	// Init the list of tables added. We include by default always the main table.
 	$listoftablesalreadyadded = array($object->table_element => $object->table_element);
 
 	// Add LEFT JOIN for all parent tables mentionned into the Xaxis
@@ -631,13 +647,25 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	foreach ($search_xaxis as $key => $val) {
 		if (!empty($arrayofxaxis[$val])) {
 			$tmpval = explode('.', $val);
-			//var_dump($arrayofxaxis[$val]['table']);
-			if (! in_array($arrayofxaxis[$val]['table'], $listoftablesalreadyadded)) {	// We do not add join for main table already added
-				$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$arrayofxaxis[$val]['table']." as ".$db->sanitize($tmpval[0])." ON t.".str_replace('t__', '', $db->sanitize($tmpval[0]))." = ".$db->sanitize($tmpval[0]).".rowid";
-				$listoftablesalreadyadded[$arrayofxaxis[$val]['table']] = $arrayofxaxis[$val]['table'];
+			//var_dump($arrayofgroupby);
+			$tmpforloop = dolExplodeIntoArray($arrayofxaxis[$val]['tablefromt'], ',');
+			foreach ($tmpforloop as $tmptable => $tmptablealias) {
+				if (! in_array($tmptable, $listoftablesalreadyadded)) {	// We do not add join for main table and tables already added
+					$tmpforexplode = explode('__', $tmptablealias);
+					$endpart = end($tmpforexplode);
+					$parenttableandfield = preg_replace('/__'.$endpart.'$/', '', $tmptablealias).'.'.$endpart;
+
+					$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable." as ".$db->sanitize($tmptablealias)." ON ".$db->sanitize($parenttableandfield)." = ".$db->sanitize($tmptablealias).".rowid";
+					$listoftablesalreadyadded[$tmptable] = $tmptable;
+
+					if (preg_match('/^te/', $tmpval[0]) && preg_replace('/^t_/', 'te_', $tmptablealias) == $tmpval[0]) {
+						$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable."_extrafields as ".$db->sanitize($tmpval[0])." ON ".$db->sanitize($tmpval[0]).".fk_object = ".$db->sanitize($tmptablealias).".rowid";
+						$listoftablesalreadyadded[$tmptable] = $tmptable;
+					}
+				}
 			}
 		} else {
-			dol_print_error($db, 'Found a key into search_xaxis not found into arrayofxaxis');
+			$errormessage = 'Found a key into search_xaxis not found into arrayofxaxis';
 		}
 	}
 
@@ -646,13 +674,25 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	foreach ($search_groupby as $key => $val) {
 		if (!empty($arrayofgroupby[$val])) {
 			$tmpval = explode('.', $val);
-			//var_dump($arrayofxaxis[$val]['table']);
-			if (! in_array($arrayofgroupby[$val]['table'], $listoftablesalreadyadded)) {	// We do not add join for main table already added
-				$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$arrayofgroupby[$val]['table']." as ".$tmpval[0]." ON t.".str_replace('t__', '', $tmpval[0])." = ".$tmpval[0].".rowid";
-				$listoftablesalreadyadded[$arrayofgroupby[$val]['table']] = $arrayofgroupby[$val]['table'];
+			//var_dump($arrayofgroupby[$val]); var_dump($tmpval);
+			$tmpforloop = dolExplodeIntoArray($arrayofgroupby[$val]['tablefromt'], ',');
+			foreach ($tmpforloop as $tmptable => $tmptablealias) {
+				if (! in_array($tmptable, $listoftablesalreadyadded)) {	// We do not add join for main table and tables already added
+					$tmpforexplode = explode('__', $tmptablealias);
+					$endpart = end($tmpforexplode);
+					$parenttableandfield = preg_replace('/__'.$endpart.'$/', '', $tmptablealias).'.'.$endpart;
+
+					$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable." as ".$db->sanitize($tmptablealias)." ON ".$db->sanitize($parenttableandfield)." = ".$db->sanitize($tmptablealias).".rowid";
+					$listoftablesalreadyadded[$tmptable] = $tmptable;
+
+					if (preg_match('/^te/', $tmpval[0]) && preg_replace('/^t_/', 'te_', $tmptablealias) == $tmpval[0]) {
+						$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable."_extrafields as ".$db->sanitize($tmpval[0])." ON ".$db->sanitize($tmpval[0]).".fk_object = ".$db->sanitize($tmptablealias).".rowid";
+						$listoftablesalreadyadded[$tmptable] = $tmptable;
+					}
+				}
 			}
 		} else {
-			dol_print_error($db, 'Found a key into search_groupby not found into arrayofgroupby');
+			$errormessage = 'Found a key into search_groupby not found into arrayofgroupby';
 		}
 	}
 
@@ -661,13 +701,25 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	foreach ($search_measures as $key => $val) {
 		if (!empty($arrayofmesures[$val])) {
 			$tmpval = explode('.', $val);
-			//var_dump($arrayofxaxis[$val]['table']);
-			if (! in_array($arrayofmesures[$val]['table'], $listoftablesalreadyadded)) {	// We do not add join for main table already added
-				$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$arrayofmesures[$val]['table']." as ".$tmpval[0]." ON t.".str_replace('t__', '', $tmpval[0])." = ".$tmpval[0].".rowid";
-				$listoftablesalreadyadded[$arrayofmesures[$val]['table']] = $arrayofmesures[$val]['table'];
+			//var_dump($arrayofgroupby);
+			$tmpforloop = dolExplodeIntoArray($arrayofmesures[$val]['tablefromt'], ',');
+			foreach ($tmpforloop as $tmptable => $tmptablealias) {
+				if (! in_array($tmptable, $listoftablesalreadyadded)) {	// We do not add join for main table and tables already added
+					$tmpforexplode = explode('__', $tmptablealias);
+					$endpart = end($tmpforexplode);
+					$parenttableandfield = preg_replace('/__'.$endpart.'$/', '', $tmptablealias).'.'.$endpart;
+
+					$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable." as ".$db->sanitize($tmptablealias)." ON ".$db->sanitize($parenttableandfield)." = ".$db->sanitize($tmptablealias).".rowid";
+					$listoftablesalreadyadded[$tmptable] = $tmptable;
+
+					if (preg_match('/^te/', $tmpval[0]) && preg_replace('/^t_/', 'te_', $tmptablealias) == $tmpval[0]) {
+						$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable."_extrafields as ".$db->sanitize($tmpval[0])." ON ".$db->sanitize($tmpval[0]).".fk_object = ".$db->sanitize($tmptablealias).".rowid";
+						$listoftablesalreadyadded[$tmptable] = $tmptable;
+					}
+				}
 			}
 		} else {
-			dol_print_error($db, 'Found a key into search_measures not found into arrayofmesures');
+			$errormessage = 'Found a key into search_measures not found into arrayofmesures';
 		}
 	}
 
@@ -678,11 +730,7 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	// Add the where here
 	$sqlfilters = $search_component_params_hidden;
 	if ($sqlfilters) {
-		$errormessage = '';
-		$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
-		if ($errormessage) {
-			print dol_escape_htmltag($errormessage);
-		}
+		$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage, 0, 0, 1);
 	}
 	$sql .= " GROUP BY ";
 	foreach ($search_xaxis as $key => $val) {
@@ -747,6 +795,11 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 }
 //print $sql;
 
+if ($errormessage) {
+	print dol_escape_htmltag($errormessage);
+	$sql = '';
+}
+
 $legend = array();
 foreach ($search_measures as $key => $val) {
 	$legend[] = $langs->trans($arrayofmesures[$val]['label']);
@@ -952,7 +1005,6 @@ $db->close();
 
 
 
-
 /**
  * Fill arrayofmesures for an object
  *
@@ -962,9 +1014,10 @@ $db->close();
  * @param	array		$arrayofmesures	Array of mesures already filled
  * @param	int			$level 			Level
  * @param	int			$count			Count
+ * @param	string		$tablepath		Path of all tables ('t' or 't,contract' or 't,contract,societe'...)
  * @return 	array						Array of mesures
  */
-function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesures, $level = 0, &$count = 0)
+function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesures, $level = 0, &$count = 0, &$tablepath = '')
 {
 	global $langs, $extrafields, $db;
 
@@ -972,38 +1025,56 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
 		return $arrayofmesures;
 	}
 
+	if (empty($tablepath)) {
+		$tablepath = $object->table_element.'='.$tablealias;
+	} else {
+		$tablepath .= ','.$object->table_element.'='.$tablealias;
+	}
+
 	if ($level == 0) {
 		// Add the count of record only for the main/first level object. Parents are necessarly unique for each record.
 		$arrayofmesures[$tablealias.'.count'] = array(
 			'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': Count',
+			'labelnohtml' => $labelofobject.': Count',
 			'position' => 0,
-			'table' => $object->table_element
+			'table' => $object->table_element,
+			'tablefromt' => $tablepath
 		);
 	}
 
+	// Note: here $tablealias can be 't' or 't__fk_contract' or 't_fk_contract_fk_soc'
+
 	// Add main fields of object
 	foreach ($object->fields as $key => $val) {
 		if (!empty($val['isameasure']) && (!isset($val['enabled']) || dol_eval($val['enabled'], 1, 1, '1'))) {
 			$position = (empty($val['position']) ? 0 : intVal($val['position']));
 			$arrayofmesures[$tablealias.'.'.$key.'-sum'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Sum").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.1',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 			$arrayofmesures[$tablealias.'.'.$key.'-average'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Average").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.2',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 			$arrayofmesures[$tablealias.'.'.$key.'-min'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Minimum").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.3',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 			$arrayofmesures[$tablealias.'.'.$key.'-max'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Maximum").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.4',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 		}
 	}
@@ -1012,25 +1083,33 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
 		foreach ($extrafields->attributes[$object->table_element]['label'] as $key => $val) {
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key]) && (!isset($extrafields->attributes[$object->table_element]['enabled'][$key]) || dol_eval($extrafields->attributes[$object->table_element]['enabled'][$key], 1, 1, '1'))) {
 				$position = (!empty($val['position']) ? $val['position'] : 0);
-				$arrayofmesures[$tablealias.'e.'.$key.'-sum'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-sum'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Sum").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.1',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
-				$arrayofmesures[$tablealias.'e.'.$key.'-average'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-average'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Average").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.2',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
-				$arrayofmesures[$tablealias.'e.'.$key.'-min'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-min'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Minimum").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.3',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
-				$arrayofmesures[$tablealias.'e.'.$key.'-max'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-max'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Maximum").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.4',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 			}
 		}
@@ -1039,14 +1118,14 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
 	foreach ($object->fields as $key => $val) {
 		if (preg_match('/^[^:]+:[^:]+:/', $val['type'])) {
 			$tmptype = explode(':', $val['type'], 4);
-			if ($tmptype[0] == 'integer' && $tmptype[1] && $tmptype[2]) {
+			if ($tmptype[0] == 'integer' && !empty($tmptype[1]) && !empty($tmptype[2])) {
 				$newobject = $tmptype[1];
 				dol_include_once($tmptype[2]);
 				if (class_exists($newobject)) {
 					$tmpobject = new $newobject($db);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					$count++;
-					$arrayofmesures = fillArrayOfMeasures($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofmesures, $level + 1, $count);
+					$arrayofmesures = fillArrayOfMeasures($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofmesures, $level + 1, $count, $tablepath);
 				} else {
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 				}
@@ -1067,18 +1146,21 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
  * @param	array		$arrayofxaxis	Array of xaxis already filled
  * @param	int			$level 			Level
  * @param	int			$count			Count
+ * @param	string		$tablepath		Path of all tables ('t' or 't,contract' or 't,contract,societe'...)
  * @return 	array						Array of xaxis
  */
-function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis, $level = 0, &$count = 0)
+function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis, $level = 0, &$count = 0, &$tablepath = '')
 {
 	global $langs, $extrafields, $db;
 
-	if ($level > 10) {	// Protection against infinite loop
+	if ($level >= 3) {	// Limit scan on 2 levels max
 		return $arrayofxaxis;
 	}
 
-	if ($level >= 2) {
-		return $arrayofxaxis;
+	if (empty($tablepath)) {
+		$tablepath = $object->table_element.'='.$tablealias;
+	} else {
+		$tablepath .= ','.$object->table_element.'='.$tablealias;
 	}
 
 	$YYYY = substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1);
@@ -1088,6 +1170,12 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 	$MI = substr($langs->trans("Minute"), 0, 1).substr($langs->trans("Minute"), 0, 1);
 	$SS = substr($langs->trans("Second"), 0, 1).substr($langs->trans("Second"), 0, 1);
 
+	/*if ($level > 0) {
+		var_dump($object->element.' '.$object->isextrafieldmanaged);
+	}*/
+
+	// Note: here $tablealias can be 't' or 't__fk_contract' or 't_fk_contract_fk_soc'
+
 	// Add main fields of object
 	foreach ($object->fields as $key => $val) {
 		if (empty($val['measure'])) {
@@ -1115,25 +1203,33 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofxaxis[$tablealias.'.'.$key.'-year'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)).'.1',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				$arrayofxaxis[$tablealias.'.'.$key.'-month'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)).'.2',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				$arrayofxaxis[$tablealias.'.'.$key.'-day'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)).'.3',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 			} else {
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofxaxis[$tablealias.'.'.$key] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)),
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 			}
 		}
@@ -1148,11 +1244,39 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key])) {
 				continue;
 			}
-			$arrayofxaxis[$tablealias.'e.'.$key] = array(
-				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]),
-				'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
-				'table' => $object->table_element
-			);
+
+			if (in_array($extrafields->attributes[$object->table_element]['type'][$key], array('timestamp', 'date', 'datetime'))) {
+				$position = (empty($extrafields->attributes[$object->table_element]['pos'][$key]) ? 0 : intVal($extrafields->attributes[$object->table_element]['pos'][$key]));
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-year'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.1',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-month'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.2',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-day'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.3',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			} else {
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			}
 		}
 	}
 
@@ -1167,7 +1291,7 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 					$tmpobject = new $newobject($db);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					$count++;
-					$arrayofxaxis = fillArrayOfXAxis($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofxaxis, $level + 1, $count);
+					$arrayofxaxis = fillArrayOfXAxis($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofxaxis, $level + 1, $count, $tablepath);
 				} else {
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 				}
@@ -1188,18 +1312,21 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
  * @param	array		$arrayofgroupby	Array of groupby already filled
  * @param	int			$level 			Level
  * @param	int			$count			Count
+ * @param	string		$tablepath		Path of all tables ('t' or 't,contract' or 't,contract,societe'...)
  * @return 	array						Array of groupby
  */
-function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroupby, $level = 0, &$count = 0)
+function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroupby, $level = 0, &$count = 0, &$tablepath = '')
 {
 	global $langs, $extrafields, $db;
 
-	if ($level > 10) {	// Protection against infinite loop
+	if ($level >= 3) {
 		return $arrayofgroupby;
 	}
 
-	if ($level >= 2) {
-		return $arrayofgroupby;
+	if (empty($tablepath)) {
+		$tablepath = $object->table_element.'='.$tablealias;
+	} else {
+		$tablepath .= ','.$object->table_element.'='.$tablealias;
 	}
 
 	$YYYY = substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1);
@@ -1209,6 +1336,8 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 	$MI = substr($langs->trans("Minute"), 0, 1).substr($langs->trans("Minute"), 0, 1);
 	$SS = substr($langs->trans("Second"), 0, 1).substr($langs->trans("Second"), 0, 1);
 
+	// Note: here $tablealias can be 't' or 't__fk_contract' or 't_fk_contract_fk_soc'
+
 	// Add main fields of object
 	foreach ($object->fields as $key => $val) {
 		if (empty($val['isameasure'])) {
@@ -1235,26 +1364,34 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 			if (in_array($val['type'], array('timestamp', 'date', 'datetime'))) {
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofgroupby[$tablealias.'.'.$key.'-year'] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.')</span>', 'position' => ($position + ($count * 100000)).'.1',
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)).'.1',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				$arrayofgroupby[$tablealias.'.'.$key.'-month'] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>', 'position' => ($position + ($count * 100000)).'.2',
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)).'.2',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				$arrayofgroupby[$tablealias.'.'.$key.'-day'] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>', 'position' => ($position + ($count * 100000)).'.3',
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)).'.3',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 			} else {
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofgroupby[$tablealias.'.'.$key] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']), 'position' => ($position + ($count * 100000)),
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)),
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 			}
 		}
@@ -1269,11 +1406,39 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key])) {
 				continue;
 			}
-			$arrayofgroupby[$tablealias.'e.'.$key] = array(
-				'label' => img_picto('', $object->picto,
-				'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]), 'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
-				'table' => $object->table_element
-			);
+
+			if (in_array($extrafields->attributes[$object->table_element]['type'][$key], array('timestamp', 'date', 'datetime'))) {
+				$position = (empty($extrafields->attributes[$object->table_element]['pos'][$key]) ? 0 : intVal($extrafields->attributes[$object->table_element]['pos'][$key]));
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-year'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.1',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-month'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.2',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-day'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.3',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			} else {
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			}
 		}
 	}
 
@@ -1288,7 +1453,7 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 					$tmpobject = new $newobject($db);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					$count++;
-					$arrayofgroupby = fillArrayOfGroupBy($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofgroupby, $level + 1, $count);
+					$arrayofgroupby = fillArrayOfGroupBy($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofgroupby, $level + 1, $count, $tablepath);
 				} else {
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 				}

+ 1 - 1
htdocs/core/lib/admin.lib.php

@@ -1766,7 +1766,7 @@ function form_constantes($tableau, $strictw3c = 0, $helptext = '', $text = 'Valu
 				}
 				print '</div>';
 				//print 'http://lists.example.com/cgi-bin/mailman/admin/%LISTE%/members/remove?adminpw=%MAILMAN_ADMINPW%&unsubscribees=%EMAIL%';
-			} elseif ($const == 'ADHERENT_MAIL_FROM') {
+			} elseif (in_array($const, ['ADHERENT_MAIL_FROM', 'ADHERENT_CC_MAIL_FROM'])) {
 				print ' '.img_help(1, $langs->trans("EMailHelpMsgSPFDKIM"));
 			}
 

+ 1 - 1
htdocs/core/lib/files.lib.php

@@ -2606,7 +2606,7 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
 		$original_file = $conf->medias->multidir_output[$entity].'/'.$original_file;
 	} elseif ($modulepart == 'logs' && !empty($dolibarr_main_data_root)) {
 		// Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
-		$accessallowed = ($user->admin && basename($original_file) == $original_file && preg_match('/^dolibarr.*\.log$/', basename($original_file)));
+		$accessallowed = ($user->admin && basename($original_file) == $original_file && preg_match('/^dolibarr.*\.(log|json)$/', basename($original_file)));
 		$original_file = $dolibarr_main_data_root.'/'.$original_file;
 	} elseif ($modulepart == 'doctemplates' && !empty($dolibarr_main_data_root)) {
 		// Wrapping for doctemplates

+ 68 - 13
htdocs/core/lib/functions.lib.php

@@ -45,7 +45,7 @@
 
 include_once DOL_DOCUMENT_ROOT.'/core/lib/json.lib.php';
 
-
+// Function for better PHP x compatibility
 if (!function_exists('utf8_encode')) {
 	/**
 	 * Implement utf8_encode for PHP that does not support it.
@@ -71,6 +71,46 @@ if (!function_exists('utf8_decode')) {
 		return mb_convert_encoding($elements, 'ISO-8859-1', 'UTF-8');
 	}
 }
+if (!function_exists('str_starts_with')) {
+	/**
+	 * str_starts_with
+	 *
+	 * @param string $haystack	haystack
+	 * @param string $needle	needle
+	 * @return boolean
+	 */
+	function str_starts_with($haystack, $needle)
+	{
+		return (string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0;
+	}
+}
+if (!function_exists('str_ends_with')) {
+	/**
+	 * str_ends_with
+	 *
+	 * @param string $haystack	haystack
+	 * @param string $needle	needle
+	 * @return boolean
+	 */
+	function str_ends_with($haystack, $needle)
+	{
+		return $needle !== '' && substr($haystack, -strlen($needle)) === (string) $needle;
+	}
+}
+if (!function_exists('str_contains')) {
+	/**
+	 * str_contains
+	 *
+	 * @param string $haystack	haystack
+	 * @param string $needle	needle
+	 * @return boolean
+	 */
+	function str_contains($haystack, $needle)
+	{
+		return $needle !== '' && mb_strpos($haystack, $needle) !== false;
+	}
+}
+
 
 /**
  * Return the full path of the directory where a module (or an object of a module) stores its files. Path may depends on the entity if a multicompany module is enabled.
@@ -11846,19 +11886,30 @@ function jsonOrUnserialize($stringtodecode)
 /**
  * forgeSQLFromUniversalSearchCriteria
  *
- * @param 	string		$filter		String with universal search string. Must be  (aaa:bbb:...) with
+ * @param 	string		$filter		String with universal search string. Must be '(aaa:bbb:...) OR (ccc:ddd:...) ...' with
  * 									aaa is a field name (with alias or not) and
  * 									bbb is one of this operator '=', '<', '>', '<=', '>=', '!=', 'in', 'notin', 'like', 'notlike', 'is', 'isnot'.
- * @param	string		$error		Error message
- * @param	int			$noand		0=Default, 1=Do not add the AND before the condition string.
+ * 									Example: '((client:=:1) OR ((client:>=:2) AND (client:<=:3))) AND (client:!=:8) AND (nom:like:'a%')'
+ * @param	string		$errorstr	Error message string
+ * @param	int			$noand		1=Do not add the AND before the condition string.
+ * @param	int			$nopar		1=Do not add the perenthesis around the condition string.
+ * @param	int			$noerror	1=If search criteria is not valid, does not return an error string but invalidate the SQL
  * @return	string					Return forged SQL string
  */
-function forgeSQLFromUniversalSearchCriteria($filter, &$error = '', $noand = 0)
+function forgeSQLFromUniversalSearchCriteria($filter, &$errorstr = '', $noand = 0, $nopar = 0, $noerror = 0)
 {
+	if (!preg_match('/^\(.*\)$/', $filter)) {    // If $filter does not start and end with ()
+		$filter = '(' . $filter . ')';
+	}
+
 	$regexstring = '\(([a-zA-Z0-9_\.]+:[<>!=insotlke]+:[^\(\)]+)\)';	// Must be  (aaa:bbb:...) with aaa is a field name (with alias or not) and bbb is one of this operator '=', '<', '>', '<=', '>=', '!=', 'in', 'notin', 'like', 'notlike', 'is', 'isnot'
 
-	if (!dolCheckFilters($filter, $error)) {
-		return '1 = 2';		// Bad balance of parenthesis, we force a SQL not found
+	if (!dolCheckFilters($filter, $errorstr)) {
+		if ($noerror) {
+			return '1 = 2';
+		} else {
+			return 'Filter syntax error - '.$errorstr;		// Bad balance of parenthesis, we return an error message or force a SQL not found
+		}
 	}
 
 	// Test the filter syntax
@@ -11866,11 +11917,15 @@ function forgeSQLFromUniversalSearchCriteria($filter, &$error = '', $noand = 0)
 	$t = str_replace(array('and','or','AND','OR',' '), '', $t);		// Remove the only strings allowed between each () criteria
 	// If the string result contains something else than '()', the syntax was wrong
 	if (preg_match('/[^\(\)]/', $t)) {
-		$error = 'Bad syntax of the search string, filter criteria is invalidated';
-		return 'Filter syntax error';		// Bad syntax of the search string, we force a SQL not found
+		$errorstr = 'Bad syntax of the search string';
+		if ($noerror) {
+			return '1 = 2';
+		} else {
+			return 'Filter syntax error - '.$errorstr;		// Bad syntax of the search string, we return an error message or force a SQL not found
+		}
 	}
 
-	return ($noand ? "" : " AND ")."(".preg_replace_callback('/'.$regexstring.'/i', 'dolForgeCriteriaCallback', $filter).")";
+	return ($noand ? "" : " AND ").($nopar ? "" : '(').preg_replace_callback('/'.$regexstring.'/i', 'dolForgeCriteriaCallback', $filter).($nopar ? "" : ')');
 }
 
 /**
@@ -11909,7 +11964,7 @@ function dolCheckFilters($sqlfilters, &$error = '')
  * This method is called by forgeSQLFromUniversalSearchCriteria()
  *
  * @param  array    $matches       Array of found string by regex search. Example: "t.ref:like:'SO-%'" or "t.date_creation:<:'20160101'" or "t.nature:is:NULL"
- * @return string                  Forged criteria. Example: "t.field like 'abc%'"
+ * @return string                  Forged criteria. Example: "" or "()"
  */
 function dolForgeDummyCriteriaCallback($matches)
 {
@@ -11931,7 +11986,7 @@ function dolForgeDummyCriteriaCallback($matches)
  *
  * @param  array    $matches       	Array of found string by regex search.
  * 									Example: "t.ref:like:'SO-%'" or "t.date_creation:<:'20160101'" or "t.date_creation:<:'2016-01-01 12:30:00'" or "t.nature:is:NULL"
- * @return string                  	Forged criteria. Example: "t.field like 'abc%'"
+ * @return string                  	Forged criteria. Example: "t.field LIKE 'abc%'"
  */
 function dolForgeCriteriaCallback($matches)
 {
@@ -11987,7 +12042,7 @@ function dolForgeCriteriaCallback($matches)
 		}
 	}
 
-	return $db->escape($operand).' '.strtoupper($operator).' '.$tmpescaped;
+	return '('.$db->escape($operand).' '.strtoupper($operator).' '.$tmpescaped.')';
 }
 
 

+ 39 - 0
htdocs/core/lib/functions2.lib.php

@@ -2909,3 +2909,42 @@ function acceptLocalLinktoMedia()
 	//return 1;
 	return $acceptlocallinktomedia;
 }
+
+
+/**
+ * Remove first and last parenthesis but only if first is the opening and last the closing of the same group
+ *
+ * @param 	string	$string		String to sanitize
+ * @return 	string				String without global parenthesis
+ */
+function removeGlobalParenthesis($string)
+{
+	$string = trim($string);
+
+	// If string does not start and end with parenthesis, we return $string as is.
+	if (! preg_match('/^\(.*\)$/', $string)) {
+		return $string;
+	}
+
+	$nbofchars = dol_strlen($string);
+	$i = 0; $g = 0;
+	$countparenthesis = 0;
+	while ($i < $nbofchars) {
+		$char = dol_substr($string, $i, 1);
+		if ($char == '(') {
+			$countparenthesis++;
+		} elseif ($char == ')') {
+			$countparenthesis--;
+			if ($countparenthesis <= 0) {	// We reach the end of an independent group of parenthesis
+				$g++;
+			}
+		}
+		$i++;
+	}
+
+	if ($g <= 1) {
+		return preg_replace('/^\(/', '', preg_replace('/\)$/', '', $string));
+	}
+
+	return $string;
+}

+ 109 - 0
htdocs/core/lib/modulebuilder.lib.php

@@ -950,3 +950,112 @@ function removeObjectFromApiFile($file, $objectname, $modulename)
 	}
 	return 1;
 }
+
+/**
+ * Compare menus by their object
+ * @param  mixed  $a  first value
+ * @param  mixed  $b  seconde value
+ * @return int        1 if OK, -1 if KO
+ */
+function compareMenus($a, $b)
+{
+	return strcmp($a['fk_menu'], $b['fk_menu']);
+}
+
+/**
+ * @param    string         $file       path of filename
+ * @param    mixed          $menus      all menus for module
+ * @param    mixed|null     $menuWantTo  menu get for do actions
+ * @param    int|null       $key        key for the concerned menu
+ * @param    int            $action     for specify what action (0 = delete, 1 = add, 2 = update, -1 = when delete object)
+ * @return   int            1 if OK, -1 if KO
+ */
+function reWriteAllMenus($file, $menus, $menuWantTo, $key, $action)
+{
+	$errors =0;
+	$counter = 0;
+	if (!file_exists($file)) {
+		return -1;
+	}
+	if ($action == 0 && !empty($key)) {
+		// delete menu manuelly
+		array_splice($menus, array_search($menus[$key], $menus), 1);
+	} elseif ($action == 1) {
+		// add menu manualy
+		array_push($menus, $menuWantTo);
+	} elseif ($action == 2 && !empty($key) && !empty($menuWantTo)) {
+		// update right from permissions array
+
+		// check if the values already exists
+		foreach ($menus as $index => $menu) {
+			if ($index !== $key) {
+				if ($menu['type'] === $menuWantTo['type']) {
+					if (strcasecmp(str_replace(' ', '', $menu['titre']), str_replace(' ', '', $menuWantTo['titre'])) === 0) {
+						$counter++;
+					}
+					if (strcasecmp(str_replace(' ', '', $menu['url']), str_replace(' ', '', $menuWantTo['url'])) === 0) {
+						$counter++;
+					}
+				}
+			}
+		}
+		if (!$counter) {
+			$menus[$key] = $menuWantTo;
+		} else {
+			$errors++;
+		}
+	} elseif ($action == -1 && !empty($menuWantTo)) {
+		// delete menus when delete Object
+		foreach ($menus as $index => $menu) {
+			if ((strpos(strtolower($menu['fk_menu']), strtolower($menuWantTo)) !== false) || (strpos(strtolower($menu['leftmenu']), strtolower($menuWantTo)) !== false)) {
+				array_splice($menus, array_search($menu, $menus), 1);
+			}
+		}
+	} else {
+		$errors++;
+	}
+	if (!$errors) {
+		// delete All LEFT Menus
+		$beginMenu = '/* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */';
+		$endMenu = '/* END MODULEBUILDER LEFTMENU MYOBJECT */';
+		$allMenus = getFromFile($file, $beginMenu, $endMenu);
+		dolReplaceInFile($file, array($allMenus => ''));
+
+		// orders menu with other menus that have the same object
+		usort($menus, 'compareMenus');
+
+		//prepare each menu and stock them in string
+		$str_menu = "";
+		foreach ($menus as $index =>$menu) {
+			$menu['position'] = "1000 + \$r";
+			if ($menu['type'] === 'left') {
+				$start = "\t\t".'/* LEFTMENU '.strtoupper($menu['titre']).' */';
+				$end   = "\t\t".'/* END LEFTMENU '.strtoupper($menu['titre']).' */';
+				$val_actuel = $menu;
+				$next_val = $menus[$index + 1];
+				$str_menu .= $start."\n";
+				$str_menu.= "\t\t\$this->menu[\$r++]=array(\n";
+				$str_menu.= "\t\t\t 'fk_menu' =>'".$menu['fk_menu']."',\n";
+				$str_menu.= "\t\t\t 'type' =>'".$menu['type']."',\n";
+				$str_menu.= "\t\t\t 'titre' =>'".$menu['titre']."',\n";
+				$str_menu.= "\t\t\t 'mainmenu' =>'".$menu['mainmenu']."',\n";
+				$str_menu.= "\t\t\t 'leftmenu' =>'".$menu['leftmenu']."',\n";
+				$str_menu.= "\t\t\t 'url' =>'".$menu['url']."',\n";
+				$str_menu.= "\t\t\t 'langs' =>'".$menu['langs']."',\n";
+				$str_menu.= "\t\t\t 'position' =>".$menu['position'].",\n";
+				$str_menu.= "\t\t\t 'enabled' =>'".$menu['enabled']."',\n";
+				$str_menu.= "\t\t\t 'perms' =>'".$menu['perms']."',\n";
+				$str_menu.= "\t\t\t 'target' =>'".$menu['target']."',\n";
+				$str_menu.= "\t\t\t 'user' =>".$menu['user'].",\n";
+				$str_menu.= "\t\t);\n";
+
+				if ($val_actuel['leftmenu'] !== $next_val['leftmenu']) {
+					$str_menu .= $end."\n";
+				}
+			}
+		}
+
+		dolReplaceInFile($file, array($beginMenu => $beginMenu."\n".$str_menu."\n"));
+		return 1;
+	}return -1;
+}

+ 4 - 4
htdocs/core/menus/standard/eldy.lib.php

@@ -1169,16 +1169,16 @@ function get_left_menu_home($mainmenu, &$newmenu, $usemenuhider = 1, $leftmenu =
 		if ($user->hasRight('user', 'user', 'read')) {
 			if ($usemenuhider || empty($leftmenu) || $leftmenu == "users") {
 				$newmenu->add("", $langs->trans("Users"), 1, $user->hasRight('user',  'user', 'lire') || $user->admin);
-				$newmenu->add("/user/card.php?leftmenu=users&action=create", $langs->trans("NewUser"), 2, ($user->hasRight("user", "user", "write") || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE), '', 'home');
+				$newmenu->add("/user/card.php?leftmenu=users&action=create", $langs->trans("NewUser"), 2, ($user->hasRight("user", "user", "write") || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)), '', 'home');
 				$newmenu->add("/user/list.php?leftmenu=users", $langs->trans("ListOfUsers"), 2, $user->hasRight('user',  'user', 'lire') || $user->admin);
 				$newmenu->add("/user/hierarchy.php?leftmenu=users", $langs->trans("HierarchicView"), 2, $user->hasRight('user',  'user', 'lire') || $user->admin);
 				if (isModEnabled('categorie')) {
 					$langs->load("categories");
 					$newmenu->add("/categories/index.php?leftmenu=users&type=7", $langs->trans("UsersCategoriesShort"), 2, $user->hasRight('categorie',  'lire'), '', $mainmenu, 'cat');
 				}
-				$newmenu->add("", $langs->trans("Groups"), 1, ($user->hasRight('user',  'user', 'lire') || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE));
-				$newmenu->add("/user/group/card.php?leftmenu=users&action=create", $langs->trans("NewGroup"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight("user", "group_advance", "create") : $user->hasRight("user", "user", "create")) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE));
-				$newmenu->add("/user/group/list.php?leftmenu=users", $langs->trans("ListOfGroups"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight('user',  'group_advance', 'read') : $user->hasRight('user',  'user', 'lire')) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE));
+				$newmenu->add("", $langs->trans("Groups"), 1, ($user->hasRight('user',  'user', 'lire') || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)));
+				$newmenu->add("/user/group/card.php?leftmenu=users&action=create", $langs->trans("NewGroup"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight("user", "group_advance", "create") : $user->hasRight("user", "user", "create")) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)));
+				$newmenu->add("/user/group/list.php?leftmenu=users", $langs->trans("ListOfGroups"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight('user',  'group_advance', 'read') : $user->hasRight('user',  'user', 'lire')) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)));
 			}
 		}
 	}

+ 32 - 13
htdocs/core/modules/facture/doc/pdf_crabe.modules.php

@@ -354,6 +354,15 @@ class pdf_crabe extends ModelePDFFactures
 					$heightforfooter += 6;
 				}
 
+				$heightforqrinvoice_firstpage = 0;
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					$heightforqrinvoice_firstpage = $this->getHeightForQRInvoice(1, $object, $langs);
+					if ($heightforqrinvoice_firstpage > 0) {
+						// Shrink infotot to a base 30
+						$heightforinfotot = 30 + (4 * $nbpayments); // Height reserved to output the info and total part and payment part
+					}
+				}
+
 				if (class_exists('TCPDF')) {
 					$pdf->setPrintHeader(false);
 					$pdf->setPrintFooter(false);
@@ -487,9 +496,10 @@ class pdf_crabe extends ModelePDFFactures
 				$qrcodestring = '';
 				if (!empty($conf->global->INVOICE_ADD_ZATCA_QR_CODE)) {
 					$qrcodestring = $object->buildZATCAQRString();
-				} elseif (!empty($conf->global->INVOICE_ADD_SWISS_QR_CODE)) {
+				} elseif (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == '1') {
 					$qrcodestring = $object->buildSwitzerlandQRString();
 				}
+
 				if ($qrcodestring) {
 					$qrcodecolor = array('25', '25', '25');
 					// set style for QR-code
@@ -597,7 +607,8 @@ class pdf_crabe extends ModelePDFFactures
 					}
 
 					$pdf->setTopMargin($tab_top_newpage);
-					$pdf->setPageOrientation('', 1, $heightforfooter + $heightforfreetext + $heightforinfotot); // The only function to edit the bottom margin of current page to set it.
+					$page_bottom_margin = $heightforfooter + $heightforfreetext + $heightforinfotot + $this->getHeightForQRInvoice($pdf->getPage(), $object, $langs);
+					$pdf->setPageOrientation('', 1, $page_bottom_margin);
 					$pageposbefore = $pdf->getPage();
 
 					$showpricebeforepagebreak = 1;
@@ -605,7 +616,7 @@ class pdf_crabe extends ModelePDFFactures
 					$posYAfterDescription = 0;
 
 					// We start with Photo of product line
-					if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - ($heightforfooter + $heightforfreetext + $heightforinfotot))) {	// If photo too high, we moved completely on new page
+					if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - $page_bottom_margin)) {	// If photo too high, we moved completely on new page
 						$pdf->AddPage('', '', true);
 						if (!empty($tplidx)) {
 							$pdf->useTemplate($tplidx);
@@ -648,7 +659,7 @@ class pdf_crabe extends ModelePDFFactures
 						$pageposafter = $pdf->getPage();
 						$posyafter = $pdf->GetY();
 						//var_dump($posyafter); var_dump(($this->page_hauteur - ($heightforfooter+$heightforfreetext+$heightforinfotot))); exit;
-						if ($posyafter > ($this->page_hauteur - ($heightforfooter + $heightforfreetext + $heightforinfotot))) {	// There is no space left for total+free text
+						if ($posyafter > ($this->page_hauteur - $page_bottom_margin)) {	// There is no space left for total+free text
 							if ($i == ($nblines - 1)) {	// No more lines, and no space left to show total, so we create a new page
 								$pdf->AddPage('', '', true);
 								if (!empty($tplidx)) {
@@ -832,11 +843,11 @@ class pdf_crabe extends ModelePDFFactures
 					while ($pagenb < $pageposafter) {
 						$pdf->setPage($pagenb);
 						if ($pagenb == 1) {
-							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter, 0, $outputlangs, 0, 1, $object->multicurrency_code);
+							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter - $heightforqrinvoice_firstpage, 0, $outputlangs, 0, 1, $object->multicurrency_code);
 						} else {
 							$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code);
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pagenb, $object, $langs));
 						$pagenb++;
 						$pdf->setPage($pagenb);
 						$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
@@ -850,11 +861,11 @@ class pdf_crabe extends ModelePDFFactures
 					}
 					if (isset($object->lines[$i + 1]->pagebreak) && $object->lines[$i + 1]->pagebreak) {
 						if ($pagenb == 1) {
-							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter, 0, $outputlangs, 0, 1, $object->multicurrency_code);
+							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter - $heightforqrinvoice_firstpage, 0, $outputlangs, 0, 1, $object->multicurrency_code);
 						} else {
 							$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code);
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pagenb, $object, $langs));
 						// New page
 						$pdf->AddPage();
 						if (!empty($tplidx)) {
@@ -870,8 +881,8 @@ class pdf_crabe extends ModelePDFFactures
 
 				// Show square
 				if ($pagenb == 1) {
-					$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforinfotot - $heightforfreetext - $heightforfooter, 0, $outputlangs, 0, 0, $object->multicurrency_code);
-					$bottomlasttab = $this->page_hauteur - $heightforinfotot - $heightforfreetext - $heightforfooter + 1;
+					$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforinfotot - $heightforfreetext - $heightforfooter - $heightforqrinvoice_firstpage, 0, $outputlangs, 0, 0, $object->multicurrency_code);
+					$bottomlasttab = $this->page_hauteur - $heightforinfotot - $heightforfreetext - $heightforfooter - $heightforqrinvoice_firstpage + 1;
 				} else {
 					$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforinfotot - $heightforfreetext - $heightforfooter, 0, $outputlangs, 1, 0, $object->multicurrency_code);
 					$bottomlasttab = $this->page_hauteur - $heightforinfotot - $heightforfreetext - $heightforfooter + 1;
@@ -890,11 +901,18 @@ class pdf_crabe extends ModelePDFFactures
 				}
 
 				// Pagefoot
-				$this->_pagefoot($pdf, $object, $outputlangs);
+				$this->_pagefoot($pdf, $object, $outputlangs, 0, $this->getHeightForQRInvoice($pageposbefore, $object, $langs));
 				if (method_exists($pdf, 'AliasNbPages')) {
 					$pdf->AliasNbPages();
 				}
 
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					$result = $this->addBottomQRInvoice($pdf, $object, $outputlangs);
+					if (!$result) {
+						$pdf->Close();
+						return 0;
+					}
+				}
 				$pdf->Close();
 
 				$pdf->Output($file, 'F');
@@ -2190,11 +2208,12 @@ class pdf_crabe extends ModelePDFFactures
 	 * 		@param	Facture		$object				Object to show
 	 *      @param	Translate	$outputlangs		Object lang for output
 	 *      @param	int			$hidefreetext		1=Hide free text
+	 *      @param	int			$heightforqrinvoice	Height for QR invoices
 	 *      @return	int								Return height of bottom margin including footer text
 	 */
-	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0)
+	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0, $heightforqrinvoice = 0)
 	{
 		$showdetails = getDolGlobalInt('MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS', 0);
-		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
+		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $heightforqrinvoice + $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
 	}
 }

+ 36 - 21
htdocs/core/modules/facture/doc/pdf_sponge.modules.php

@@ -382,6 +382,14 @@ class pdf_sponge extends ModelePDFFactures
 				$this->heightforfreetext = (isset($conf->global->MAIN_PDF_FREETEXT_HEIGHT) ? $conf->global->MAIN_PDF_FREETEXT_HEIGHT : 5); // Height reserved to output the free text on last page
 				$this->heightforfooter = $this->marge_basse + (empty($conf->global->MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS) ? 12 : 22); // Height reserved to output the footer (value include bottom margin)
 
+				$heightforqrinvoice = $heightforqrinvoice_firstpage = 0;
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					if ($this->getHeightForQRInvoice(1, $object, $langs) > 0) {
+						// Shrink infotot to a base 30
+						$this->heightforinfotot = 30 + (4 * $nbpayments); // Height reserved to output the info and total part and payment part
+					}
+				}
+
 				if (class_exists('TCPDF')) {
 					$pdf->setPrintHeader(false);
 					$pdf->setPrintFooter(false);
@@ -498,7 +506,7 @@ class pdf_sponge extends ModelePDFFactures
 				$qrcodestring = '';
 				if (!empty($conf->global->INVOICE_ADD_ZATCA_QR_CODE)) {
 					$qrcodestring = $object->buildZATCAQRString();
-				} elseif (!empty($conf->global->INVOICE_ADD_SWISS_QR_CODE)) {
+				} elseif (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == '1') {
 					$qrcodestring = $object->buildSwitzerlandQRString();
 				}
 				if ($qrcodestring) {
@@ -534,7 +542,7 @@ class pdf_sponge extends ModelePDFFactures
 
 
 				// Define heigth of table for lines (for first page)
-				$tab_height = $this->page_hauteur - $this->tab_top - $this->heightforfooter - $this->heightforfreetext;
+				$tab_height = $this->page_hauteur - $this->tab_top - $this->heightforfooter - $this->heightforfreetext - $this->getHeightForQRInvoice(1, $object, $langs);
 
 				$nexY = $this->tab_top - 1;
 
@@ -612,7 +620,6 @@ class pdf_sponge extends ModelePDFFactures
 							if (!getDolGlobalInt('MAIN_PDF_DONOTREPEAT_HEAD')) {
 								$this->_pagehead($pdf, $object, 0, $outputlangs, $outputlangsbis);
 							}
-							// $this->_pagefoot($pdf,$object,$outputlangs,1);
 							$pdf->setTopMargin($this->tab_top_newpage);
 							// The only function to edit the bottom margin of current page to set it.
 							$pdf->setPageOrientation('', 1, $this->heightforfooter + $this->heightforfreetext);
@@ -657,7 +664,7 @@ class pdf_sponge extends ModelePDFFactures
 
 							// Add footer
 							$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
-							$this->_pagefoot($pdf, $object, $outputlangs, 1);
+							$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 
 							$i++;
 						}
@@ -728,7 +735,8 @@ class pdf_sponge extends ModelePDFFactures
 					}
 
 					$pdf->setTopMargin($this->tab_top_newpage);
-					$pdf->setPageOrientation('', 1, $this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot); // The only function to edit the bottom margin of current page to set it.
+					$page_bottom_margin =  $this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot + $this->getHeightForQRInvoice($pdf->getPage(), $object, $langs);
+					$pdf->setPageOrientation('', 1, $page_bottom_margin);
 					$pageposbefore = $pdf->getPage();
 
 					$showpricebeforepagebreak = 1;
@@ -737,7 +745,7 @@ class pdf_sponge extends ModelePDFFactures
 
 					if ($this->getColumnStatus('photo')) {
 						// We start with Photo of product line
-						if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - ($this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot))) {	// If photo too high, we moved completely on new page
+						if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - $page_bottom_margin)) {	// If photo too high, we moved completely on new page
 							$pdf->AddPage('', '', true);
 							if (!empty($tplidx)) {
 								$pdf->useTemplate($tplidx);
@@ -778,7 +786,7 @@ class pdf_sponge extends ModelePDFFactures
 							$pageposafter = $pdf->getPage();
 							$posyafter = $pdf->GetY();
 							//var_dump($posyafter); var_dump(($this->page_hauteur - ($this->heightforfooter+$this->heightforfreetext+$this->heightforinfotot))); exit;
-							if ($posyafter > ($this->page_hauteur - ($this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot))) {	// There is no space left for total+free text
+							if ($posyafter > ($this->page_hauteur - $page_bottom_margin)) {	// There is no space left for total+free text
 								if ($i == ($nblines - 1)) {	// No more lines, and no space left to show total, so we create a new page
 									$pdf->AddPage('', '', true);
 									if (!empty($tplidx)) {
@@ -983,12 +991,13 @@ class pdf_sponge extends ModelePDFFactures
 					// Detect if some page were added automatically and output _tableau for past pages
 					while ($pagenb < $pageposafter) {
 						$pdf->setPage($pagenb);
+						$heightforqrinvoice = $this->getHeightForQRInvoice($pagenb, $object, $langs);
 						if ($pagenb == $pageposbeforeprintlines) {
-							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
 						} else {
-							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 						$pagenb++;
 						$pdf->setPage($pagenb);
 						$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
@@ -1001,12 +1010,13 @@ class pdf_sponge extends ModelePDFFactures
 					}
 
 					if (isset($object->lines[$i + 1]->pagebreak) && $object->lines[$i + 1]->pagebreak) {
+						$heightforqrinvoice = $this->getHeightForQRInvoice($pagenb, $object, $langs);
 						if ($pagenb == $pageposafter) {
-							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
 						} else {
-							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 						// New page
 						$pdf->AddPage();
 						if (!empty($tplidx)) {
@@ -1020,12 +1030,13 @@ class pdf_sponge extends ModelePDFFactures
 				}
 
 				// Show square
+				$heightforqrinvoice = $this->getHeightForQRInvoice($pagenb, $object, $langs);
 				if ($pagenb == $pageposbeforeprintlines) {
-					$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter, 0, $outputlangs, $hidetop, 0, $object->multicurrency_code, $outputlangsbis);
-					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter + 1;
+					$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, $hidetop, 0, $object->multicurrency_code, $outputlangsbis);
+					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice + 1;
 				} else {
-					$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter, 0, $outputlangs, 1, 0, $object->multicurrency_code, $outputlangsbis);
-					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter + 1;
+					$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, 1, 0, $object->multicurrency_code, $outputlangsbis);
+					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice + 1;
 				}
 
 				// Display infos area
@@ -1040,11 +1051,14 @@ class pdf_sponge extends ModelePDFFactures
 				}
 
 				// Pagefoot
-				$this->_pagefoot($pdf, $object, $outputlangs);
+				$this->_pagefoot($pdf, $object, $outputlangs, 0, $this->getHeightForQRInvoice($pageposbefore, $object, $langs));
 				if (method_exists($pdf, 'AliasNbPages')) {
 					$pdf->AliasNbPages();
 				}
 
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					$this->addBottomQRInvoice($pdf, $object, $outputlangs);
+				}
 				$pdf->Close();
 
 				$pdf->Output($file, 'F');
@@ -1530,7 +1544,7 @@ class pdf_sponge extends ModelePDFFactures
 
 			foreach ($TPreviousIncoice as &$fac) {
 				if ($posy > $this->page_hauteur - 4 - $this->heightforfooter) {
-					$this->_pagefoot($pdf, $object, $outputlangs, 1);
+					$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 					$pdf->addPage();
 					if (!getDolGlobalInt('MAIN_PDF_DONOTREPEAT_HEAD')) {
 						$this->_pagehead($pdf, $object, 0, $outputlangs, $outputlangsbis);
@@ -2442,12 +2456,13 @@ class pdf_sponge extends ModelePDFFactures
 	 * 		@param	Facture		$object				Object to show
 	 *      @param	Translate	$outputlangs		Object lang for output
 	 *      @param	int			$hidefreetext		1=Hide free text
+	 *      @param	int			$heightforqrinvoice	Height for QR invoices
 	 *      @return	int								Return height of bottom margin including footer text
 	 */
-	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0)
+	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0, $heightforqrinvoice = 0)
 	{
 		$showdetails = getDolGlobalInt('MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS', 0);
-		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
+		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $heightforqrinvoice + $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
 	}
 
 	/**

+ 155 - 0
htdocs/core/modules/facture/modules_facture.php

@@ -31,6 +31,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/class/commondocgenerator.class.php';
 require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
 require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php'; // Required because used in classes that inherit
 
+use \Sprain\SwissQrBill;
 
 /**
  *	Parent class of invoice document generators
@@ -78,6 +79,160 @@ abstract class ModelePDFFactures extends CommonDocGenerator
 
 		return $list;
 	}
+
+	/**
+	 * Get the SwissQR object, including validation
+	 *
+	 * @param 	Facture 				$object  	Invoice object
+	 * @param 	Translate 				$langs 		Translation object
+	 * @return 	SwissQrBill\QrBill|bool 			The valid SwissQR object, or false
+	 */
+	private function getSwissQrBill(Facture $object, Translate $langs)
+	{
+		if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') != 'bottom') {
+			return false;
+		}
+
+		if ($object->mode_reglement_code != 'VIR') {
+			$this->error = $langs->transnoentities("SwissQrOnlyVIR");
+			return false;
+		}
+
+		if (empty($object->fk_account)) {
+			$this->error = 'Bank account must be defined to use this experimental feature';
+			return false;
+		}
+
+		require_once DOL_DOCUMENT_ROOT.'/includes/sprain/swiss-qr-bill/autoload.php';
+
+		// Create a new instance of SwissQrBill, containing default headers with fixed values
+		$qrBill = SwissQrBill\QrBill::create();
+
+		// First, check creditor address
+		$address = SwissQrBill\DataGroup\Element\CombinedAddress::create(
+			$this->emetteur->name,
+			$this->emetteur->address,
+			$this->emetteur->zip . " " . $this->emetteur->town,
+			$this->emetteur->country_code
+		);
+		if (!$address->isValid()) {
+			$this->error = $langs->transnoentities("SwissQrCreditorAddressInvalid", (string) $address->getViolations());
+			return false;
+		}
+		$qrBill->setCreditor($address);
+
+		// Get IBAN from account.
+		$account = new Account($this->db);
+		$account->fetch($object->fk_account);
+
+		$creditorInformation = SwissQrBill\DataGroup\Element\CreditorInformation::create($account->iban);
+		if (!$creditorInformation->isValid()) {
+			$this->error = $langs->transnoentities("SwissQrCreditorInformationInvalid", $account->iban, (string) $creditorInformation->getViolations());
+			return false;
+		}
+		$qrBill->setCreditorInformation($creditorInformation);
+
+		if ($creditorInformation->containsQrIban()) {
+			$this->error = $langs->transnoentities("SwissQrIbanNotImplementedYet", $account->iban);
+			return false;
+		}
+
+		// Add payment reference CLASSIC-IBAN
+		// This is what you will need to identify incoming payments.
+		$qrBill->setPaymentReference(
+			SwissQrBill\DataGroup\Element\PaymentReference::create(
+				SwissQrBill\DataGroup\Element\PaymentReference::TYPE_NON
+			)
+		);
+
+		// Add payment amount, with currency
+		$pai = SwissQrBill\DataGroup\Element\PaymentAmountInformation::create($object->multicurrency_code, $object->total_ttc);
+		if (!$pai->isValid()) {
+			$this->error = $langs->transnoentities("SwissQrPaymentInformationInvalid", $object->total_ttc, (string) $pai->getViolations());
+			return false;
+		}
+		$qrBill->setPaymentAmountInformation($pai);
+
+		// Add some human-readable information about what the bill is for.
+		$qrBill->setAdditionalInformation(
+			SwissQrBill\DataGroup\Element\AdditionalInformation::create(
+				$object->ref
+			)
+		);
+
+		// Check debtor address; We _know_ zip&town have to be filled, so skip that if unfilled.
+		if (!empty($object->thirdparty->zip) && !empty($object->thirdparty->town)) {
+			$address = SwissQrBill\DataGroup\Element\CombinedAddress::create(
+				$object->thirdparty->name,
+				$object->thirdparty->address,
+				$object->thirdparty->zip . " " . $object->thirdparty->town,
+				$object->thirdparty->country_code
+			);
+			if (!$address->isValid()) {
+				$this->error = $langs->transnoentities("SwissQrDebitorAddressInvalid", (string) $address->getViolations());
+				return false;
+			}
+			$qrBill->setUltimateDebtor($address);
+		}
+
+		return $qrBill;
+	}
+
+	/**
+	 * Get the height for bottom-page QR invoice in mm, depending on the page number.
+	 *
+	 * @param int       $pagenbr Page number
+	 * @param Facture   $object  Invoice object
+	 * @param Translate $langs   Translation object
+	 * @return int      Height in mm of the bottom-page QR invoice. Can be zero if not on right page; not enabled
+	 */
+	protected function getHeightForQRInvoice(int $pagenbr, \Facture $object, \Translate $langs) : int
+	{
+		if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+			// Keep it, to reset it after QRinvoice getter
+			$error = $this->error;
+
+			if (!$this->getSwissQrBill($object, $langs)) {
+				// Reset error to previous one if exists
+				if (!empty($error)) {
+					$this->error = $error;
+				}
+				return 0;
+			}
+			// SWIFT's requirementis 105, but we get more room with 100 and the page number is in a nice place.
+			return $pagenbr == 1 ? 100 : 0;
+		}
+
+		return 0;
+	}
+
+	/**
+	 * Add SwissQR invoice at bottom of page 1
+	 *
+	 * @param TCPDF     $pdf     TCPDF object
+	 * @param Facture   $object  Invoice object
+	 * @param Translate $langs   Translation object
+	 * @return bool for success
+	 */
+	public function addBottomQRInvoice(\TCPDF $pdf, \Facture $object, \Translate $langs) : bool
+	{
+		if (!($qrBill = $this->getSwissQrBill($object, $langs))) {
+			return false;
+		}
+
+		try {
+			$pdf->startTransaction();
+
+			$pdf->setPage(1);
+			$pdf->SetTextColor(0, 0, 0);
+			$output = new SwissQrBill\PaymentPart\Output\TcPdfOutput\TcPdfOutput($qrBill, in_array($langs->shortlang, ['de', 'fr', 'it']) ? $langs->shortlang : 'en', $pdf);
+			$output->setPrintable(false)->getPaymentPart();
+		} catch (Exception $e) {
+			$pdf->rollbackTransaction(true);
+			return false;
+		}
+		return true;
+	}
 }
 
 /**

+ 4 - 2
htdocs/core/modules/modEmailCollector.class.php

@@ -296,8 +296,9 @@ class modEmailCollector extends DolibarrModules
 				$sqlforexampleFilterC3 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectorfilter (fk_emailcollector, type, rulevalue, date_creation, fk_user_creat, status)";
 				$sqlforexampleFilterC3 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Leads' and entity = ".((int) $conf->entity)."), 'to', 'sales@example.com', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
+				$paramstring = 'tmp_from=EXTRACT:HEADER:^From:(.*)'."\n".'socid=SETIFEMPTY:1'."\n".'usage_opportunity=SET:1'."\n".'description=EXTRACT:BODY:(.*)'."\n".'title=SET:Lead or message from __tmp_from__ received by email';
 				$sqlforexampleActionC4 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectoraction (fk_emailcollector, type, actionparam, date_creation, fk_user_creat, status)";
-				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Leads' and entity = ".((int) $conf->entity)."), 'project', 'tmp_from=EXTRACT:HEADER:^From:(.*);socid=SETIFEMPTY:1;usage_opportunity=SET:1;description=EXTRACT:BODY:(.*);title=SET:Lead or message from __tmp_from__ received by email', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
+				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Leads' and entity = ".((int) $conf->entity)."), 'project', '".$this->db->escape($paramstring)."', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
 				$sql[] = $sqlforexampleC1;
 
@@ -327,8 +328,9 @@ class modEmailCollector extends DolibarrModules
 				$sqlforexampleFilterC3 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectorfilter (fk_emailcollector, type, rulevalue, date_creation, fk_user_creat, status)";
 				$sqlforexampleFilterC3 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Candidatures' and entity = ".((int) $conf->entity)."), 'to', 'jobs@example.com', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
+				$paramstring = 'tmp_from=EXTRACT:HEADER:^From:(.*)(<.*>)?'."\n".'fk_recruitmentjobposition=EXTRACT:HEADER:^To:[^\n]*\+([^\n]*)'."\n".'description=EXTRACT:BODY:(.*)'."\n".'lastname=SET:__tmp_from__';
 				$sqlforexampleActionC4 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectoraction (fk_emailcollector, type, actionparam, date_creation, fk_user_creat, status)";
-				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Candidatures' and entity = ".((int) $conf->entity)."), 'candidature', 'tmp_from=EXTRACT:HEADER:^From:(.*)(<.*>)?;fk_recruitmentjobposition=EXTRACT:HEADER:^To:[^\n]*\+([^\n]*);description=EXTRACT:BODY:(.*);lastname=SET:__tmp_from__', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
+				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Candidatures' and entity = ".((int) $conf->entity)."), 'candidature', '".$this->db->escape($paramstring)."', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
 				$sql[] = $sqlforexampleC1;
 

+ 1 - 1
htdocs/core/modules/modFournisseur.class.php

@@ -592,7 +592,7 @@ class modFournisseur extends DolibarrModules
 		if (empty($conf->multicurrency->enabled)) {
 			$this->import_fieldshidden_array[$r]['f.multicurrency_code'] = 'const-'.$conf->currency;
 		}
-		$this->import_regex_array[$r] = array('f.ref' => '(SI\d{4}-\d{4}|PROV.{1,32}$)', 'f.multicurrency_code' => 'code@'.MAIN_DB_PREFIX.'multicurrency');
+		$this->import_regex_array[$r] = array('f.multicurrency_code' => 'code@'.MAIN_DB_PREFIX.'multicurrency');
 		$import_sample = array(
 			'f.ref' => '(PROV001)',
 			'f.ref_supplier' => 'Supplier1',

+ 1 - 35
htdocs/core/modules/modWebhook.class.php

@@ -67,7 +67,7 @@ class modWebhook extends DolibarrModules
 		$this->descriptionlong = "WebhookDescription";
 
 		// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z'
-		$this->version = 'experimental';
+		$this->version = 'dolibarr';
 		// Url to the file with your last numberversion of this module
 		//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
 
@@ -473,40 +473,6 @@ class modWebhook extends DolibarrModules
 
 		$sql = array();
 
-		// Document templates
-		$moduledir = dol_sanitizeFileName('webhook');
-		$myTmpObjects = array();
-		$myTmpObjects['Webhook_target'] = array('includerefgeneration'=>0, 'includedocgeneration'=>0);
-
-		foreach ($myTmpObjects as $myTmpObjectKey => $myTmpObjectArray) {
-			if ($myTmpObjectKey == 'Webhook_target') {
-				continue;
-			}
-			if ($myTmpObjectArray['includerefgeneration']) {
-				$src = DOL_DOCUMENT_ROOT.'/install/doctemplates/'.$moduledir.'/template_webhook_targets.odt';
-				$dirodt = DOL_DATA_ROOT.'/doctemplates/'.$moduledir;
-				$dest = $dirodt.'/template_webhook_targets.odt';
-
-				if (file_exists($src) && !file_exists($dest)) {
-					require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
-					dol_mkdir($dirodt);
-					$result = dol_copy($src, $dest, 0, 0);
-					if ($result < 0) {
-						$langs->load("errors");
-						$this->error = $langs->trans('ErrorFailToCopyFile', $src, $dest);
-						return 0;
-					}
-				}
-
-				$sql = array_merge($sql, array(
-					"DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'standard_".strtolower($myTmpObjectKey)."' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
-					"INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity) VALUES('standard_".strtolower($myTmpObjectKey)."', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")",
-					"DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'generic_".strtolower($myTmpObjectKey)."_odt' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
-					"INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity) VALUES('generic_".strtolower($myTmpObjectKey)."_odt', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")"
-				));
-			}
-		}
-
 		return $this->_init($sql, $options);
 	}
 

+ 2 - 1
htdocs/core/modules/modWorkstation.class.php

@@ -41,6 +41,7 @@ class modWorkstation extends DolibarrModules
 	public function __construct($db)
 	{
 		global $langs, $conf;
+
 		$this->db = $db;
 
 		// Id for module (must be unique).
@@ -62,7 +63,7 @@ class modWorkstation extends DolibarrModules
 		// Used only if file README.md and README-LL.md not found.
 		$this->descriptionlong = "WorkstationsDescription";
 		// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z'
-		$this->version = 'experimental';
+		$this->version = 'dolibarr';
 		// Url to the file with your last numberversion of this module
 		//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
 

+ 5 - 2
htdocs/core/modules/supplier_invoice/mod_facture_fournisseur_cactus.php

@@ -88,7 +88,7 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 
 
 	/**
-	 * 	Tests if the numbers already in force in the database do not cause conflicts that would prevent this numbering.
+	 * 	Tests if the numbers already in the database do not cause conflicts that would prevent this numbering.
 	 *
 	 *  @return     boolean     false if conflict, true if ok
 	 */
@@ -164,6 +164,8 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 			$this->error = $langs->trans('ErrorNumRefModel', $max);
 			return false;
 		}
+
+		return true;
 	}
 
 	/**
@@ -172,7 +174,7 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 	 * @param	Societe		$objsoc     Object third party
 	 * @param  	Object		$object		Object invoice
 	 * @param   string		$mode       'next' for next value or 'last' for last value
-	 * @return 	string      			Value if OK, 0 if KO
+	 * @return 	string      			Value if OK, <=0 if KO
 	 */
 	public function getNextValue($objsoc, $object, $mode = 'next')
 	{
@@ -244,6 +246,7 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 			return $prefix.$yymm."-".$num;
 		} else {
 			dol_print_error('', 'Bad parameter for getNextValue');
+			retun -1;
 		}
 	}
 

+ 1 - 1
htdocs/core/tpl/list_print_total.tpl.php

@@ -13,7 +13,7 @@ if (isset($totalarray['pos'])) {
 	while ($i < $totalarray['nbfield']) {
 		$i++;
 		if (!empty($totalarray['pos'][$i])) {
-			switch ($totalarray['type'][$i]) {
+			switch ($totalarray['pos'][$i]) {
 				case 'duration';
 					print '<td class="right">';
 					print (!empty($totalarray['val'][$totalarray['pos'][$i]]) ? convertSecondToTime($totalarray['val'][$totalarray['pos'][$i]], 'allhourmin') : 0);

+ 13 - 10
htdocs/emailcollector/class/emailcollector.class.php

@@ -131,6 +131,8 @@ class EmailCollector extends CommonObject
 		'host'          => array('type'=>'varchar(255)', 'label'=>'EMailHost', 'visible'=>1, 'enabled'=>1, 'position'=>90, 'notnull'=>1, 'searchall'=>1, 'comment'=>"IMAP server", 'help'=>'Example: imap.gmail.com', 'csslist'=>'tdoverflowmax125'),
 		'port'          => array('type'=>'varchar(10)', 'label'=>'EMailHostPort', 'visible'=>1, 'enabled'=>1, 'position'=>91, 'notnull'=>1, 'searchall'=>0, 'comment'=>"IMAP server port", 'help'=>'Example: 993', 'csslist'=>'tdoverflowmax50', 'default'=>'993'),
 		'hostcharset'   => array('type'=>'varchar(16)', 'label'=>'HostCharset', 'visible'=>-1, 'enabled'=>1, 'position'=>92, 'notnull'=>0, 'searchall'=>0, 'comment'=>"IMAP server charset", 'help'=>'Example: "UTF-8" (May be "US-ASCII" with some Office365)', 'default'=>'UTF-8'),
+		'imap_encryption'  => array('type'=>'varchar(16)', 'label'=>'ImapEncryption', 'visible'=>-1, 'enabled'=>1, 'position'=>93, 'searchall'=>0, 'comment'=>"IMAP encryption", 'help'=>'ImapEncryptionHelp', 'arrayofkeyval'=> array('ssl'=>'SSL', 'tls' => 'TLS', 'notls' => 'NOTLS'), 'default'=>'ssl'),
+		'norsh'  => array('type'=>'integer', 'label'=>'NoRSH', 'visible'=>-1, 'enabled'=>"!getDolGlobalInt('MAIN_IMAP_USE_PHPIMAP')", 'position'=>94, 'searchall'=>0, 'help'=>'NoRSHHelp', 'arrayofkeyval'=> array(0 =>'No', 1 => 'Yes'), 'default'=> 0),
 		'acces_type'     => array('type'=>'integer', 'label'=>'accessType', 'visible'=>-1, 'enabled'=>"getDolGlobalInt('MAIN_IMAP_USE_PHPIMAP')", 'position'=>101, 'notnull'=>1, 'index'=>1, 'comment'=>"IMAP login type", 'arrayofkeyval'=>array('0'=>'loginPassword', '1'=>'oauthToken'), 'default'=>'0', 'help'=>''),
 		'login'         => array('type'=>'varchar(128)', 'label'=>'Login', 'visible'=>-1, 'enabled'=>1, 'position'=>102, 'notnull'=>-1, 'index'=>1, 'comment'=>"IMAP login", 'help'=>'Example: myaccount@gmail.com'),
 		'password'      => array('type'=>'password', 'label'=>'Password', 'visible'=>-1, 'enabled'=>"1", 'position'=>103, 'notnull'=>-1, 'comment'=>"IMAP password", 'help'=>'WithGMailYouCanCreateADedicatedPassword'),
@@ -213,6 +215,8 @@ class EmailCollector extends CommonObject
 	public $password;
 	public $acces_type;
 	public $oauth_service;
+	public $imap_encryption;
+	public $norsh;
 	public $source_directory;
 	public $target_directory;
 	public $maxemailpercollect;
@@ -775,11 +779,9 @@ class EmailCollector extends CommonObject
 	/**
 	 * Return the connectstring to use with IMAP connection function
 	 *
-	 * @param	int		$ssl		Add /ssl tag
-	 * @param	int		$norsh		Add /norsh to connectstring
 	 * @return string
 	 */
-	public function getConnectStringIMAP($ssl = 1, $norsh = 0)
+	public function getConnectStringIMAP()
 	{
 		global $conf;
 
@@ -787,15 +789,16 @@ class EmailCollector extends CommonObject
 		$flags = '/service=imap'; // IMAP
 		if (!empty($conf->global->IMAP_FORCE_TLS)) {
 			$flags .= '/tls';
-		} elseif (empty($conf->global->IMAP_FORCE_NOSSL)) {
-			if ($ssl) {
-				$flags .= '/ssl';
-			}
+		} elseif (empty($this->imap_encryption) || ($this->imap_encryption == 'ssl' && !empty($conf->global->IMAP_FORCE_NOSSL))) {
+			$flags .= '';
+		} else {
+			$flags .= '/' . $this->imap_encryption;
 		}
+
 		$flags .= '/novalidate-cert';
 		//$flags.='/readonly';
 		//$flags.='/debug';
-		if ($norsh || !empty($conf->global->IMAP_FORCE_NORSH)) {
+		if (!empty($this->norsh) || !empty($conf->global->IMAP_FORCE_NORSH)) {
 			$flags .= '/norsh';
 		}
 		//Used in shared mailbox from Office365
@@ -1191,7 +1194,7 @@ class EmailCollector extends CommonObject
 				$client = $cm->make([
 					'host'           => $this->host,
 					'port'           => $this->port,
-					'encryption'     => 'ssl',
+					'encryption'     => !empty($this->imap_encryption) ? $this->imap_encryption : false,
 					'validate_cert'  => true,
 					'protocol'       => 'imap',
 					'username'       => $this->login,
@@ -1204,7 +1207,7 @@ class EmailCollector extends CommonObject
 				$client = $cm->make([
 					'host'           => $this->host,
 					'port'           => $this->port,
-					'encryption'     => 'ssl',
+					'encryption'     => !empty($this->imap_encryption) ? $this->imap_encryption : false,
 					'validate_cert'  => true,
 					'protocol'       => 'imap',
 					'username'       => $this->login,

+ 1 - 1
htdocs/expedition/card.php

@@ -387,7 +387,7 @@ if (empty($reshook)) {
 								$entrepot_id = 0;
 							}
 
-							$ret = $object->addline($entrepot_id, GETPOST($idl, 'int'), GETPOST($qty, 'int'), $array_options[$i]);
+							$ret = $object->addline($entrepot_id, GETPOST($idl, 'int'), GETPOSTINT($qty), $array_options[$i]);
 							if ($ret < 0) {
 								setEventMessages($object->error, $object->errors, 'errors');
 								$error++;

+ 6 - 6
htdocs/expedition/dispatch.php

@@ -684,7 +684,7 @@ if ($id > 0 || !empty($ref)) {
 							print $linktoprod;
 							print "</td>";
 						}
-						
+
 						// Define unit price for PMP calculation
 						$up_ht_disc = $objp->subprice;
 						if (!empty($objp->remise_percent) && empty($conf->global->STOCK_EXCLUDE_DISCOUNT_FOR_PMP)) {
@@ -722,7 +722,7 @@ if ($id > 0 || !empty($ref)) {
 							while ($j < $numd) {
 								$suffix = "_".$j."_".$i;
 								$objd = $db->fetch_object($resultsql);
-								
+
 								if (isModEnabled('productbatch') && !empty($objd->batch)) {
 									$type = 'batch';
 
@@ -753,10 +753,10 @@ if ($id > 0 || !empty($ref)) {
 									print '<input id="fk_commandedet'.$suffix.'" name="fk_commandedet'.$suffix.'" type="hidden" value="'.$objp->rowid.'">';
 									print '<input id="idline'.$suffix.'" name="idline'.$suffix.'" type="hidden" value="'.$objd->rowid.'">';
 									print '<input name="product_batch'.$suffix.'" type="hidden" value="'.$objd->fk_product.'">';
-									
+
 									print '<!-- This is a U.P. (may include discount or not depending on STOCK_EXCLUDE_DISCOUNT_FOR_PMP. will be used for PMP calculation) -->';
 									print '<input class="maxwidth75" name="pu'.$suffix.'" type="hidden" value="'.price2num($up_ht_disc, 'MU').'">';
-									
+
 									print '</td>';
 
 									print '<td>';
@@ -866,7 +866,7 @@ if ($id > 0 || !empty($ref)) {
 							}
 							$suffix = "_".$j."_".$i;
 						}
-						
+
 						if ($j == 0) {
 							if (isModEnabled('productbatch') && !empty($objp->tobatch)) {
 								$type = 'batch';
@@ -972,7 +972,7 @@ if ($id > 0 || !empty($ref)) {
 							}
 
 							print '</td>';
-							
+
 							// Warehouse
 							print '<td class="right">';
 							if (count($listwarehouses) > 1) {

+ 1 - 1
htdocs/fourn/facture/card.php

@@ -3622,7 +3622,7 @@ if ($action == 'create') {
 					print '<td class="right">'.price($obj->amount_ttc).'</td>';
 					print '<td class="right">';
 					print '<a href="'.$_SERVER["PHP_SELF"].'?facid='.$object->id.'&action=unlinkdiscount&discountid='.$obj->rowid.'">';
-					print img_picto($langs->trans("RemoveDiscount"), 'unlink');
+					print img_picto($langs->transnoentitiesnoconv("RemoveDiscount"), 'unlink');
 					print '</a>';
 					print '</td></tr>';
 					$i++;

+ 1 - 43
htdocs/holiday/class/holiday.class.php

@@ -1695,33 +1695,7 @@ class Holiday extends CommonObject
 	}
 
 	/**
-	 *	Retourne un checked si vrai
-	 *
-	 *  @param	string	$name       name du paramètre de configuration
-	 *  @return string      		retourne checked si > 0
-	 */
-	public function getCheckOption($name)
-	{
-
-		$sql = "SELECT value";
-		$sql .= " FROM ".MAIN_DB_PREFIX."holiday_config";
-		$sql .= " WHERE name = '".$this->db->escape($name)."'";
-
-		$result = $this->db->query($sql);
-
-		if ($result) {
-			$obj = $this->db->fetch_object($result);
-
-			// Si la valeur est 1 on retourne checked
-			if ($obj->value) {
-				return 'checked';
-			}
-		}
-	}
-
-
-	/**
-	 *  Créer les entrées pour chaque utilisateur au moment de la configuration
+	 *  Create entries for each user at setup step
 	 *
 	 *  @param	boolean		$single		Single
 	 *  @param	int			$userid		Id user
@@ -1756,22 +1730,6 @@ class Holiday extends CommonObject
 		}
 	}
 
-	/**
-	 *  Supprime un utilisateur du module Congés Payés
-	 *
-	 *  @param	int		$user_id        ID de l'utilisateur à supprimer
-	 *  @return boolean      			Vrai si pas d'erreur, faut si Erreur
-	 */
-	public function deleteCPuser($user_id)
-	{
-
-		$sql = "DELETE FROM ".MAIN_DB_PREFIX."holiday_users";
-		$sql .= " WHERE fk_user = ".((int) $user_id);
-
-		$this->db->query($sql);
-	}
-
-
 	/**
 	 *  Return balance of holiday for one user
 	 *

+ 6 - 5
htdocs/holiday/list.php

@@ -511,7 +511,7 @@ if ($id > 0) {		// For user tab
 
 		print '<br>';
 
-		showMyBalance($object, $user_id);
+		print showMyBalance($object, $user_id);
 	}
 
 	print dol_get_fiche_end();
@@ -1109,9 +1109,9 @@ $db->close();
  */
 function showMyBalance($holiday, $user_id)
 {
-	global $conf, $langs;
+	global $langs;
 
-	$alltypeleaves = $holiday->getTypes(1, -1); // To have labels
+	//$alltypeleaves = $holiday->getTypes(1, -1); // To have labels
 
 	$out = '';
 	$nb_holiday = 0;
@@ -1121,6 +1121,7 @@ function showMyBalance($holiday, $user_id)
 		$nb_holiday += $nb_type;
 		$out .= ' - '.$val['label'].': <strong>'.($nb_type ?price2num($nb_type) : 0).'</strong><br>';
 	}
-	print $langs->trans('SoldeCPUser', round($nb_holiday, 5)).'<br>';
-	print $out;
+	$out = $langs->trans('SoldeCPUser', round($nb_holiday, 5)).'<br>'.$out;
+
+	return $out;
 }

+ 22 - 0
htdocs/includes/bacon/bacon-qr-code/LICENSE

@@ -0,0 +1,22 @@
+Copyright (c) 2017, Ben Scholzen 'DASPRiD'
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 39 - 0
htdocs/includes/bacon/bacon-qr-code/README.md

@@ -0,0 +1,39 @@
+# QR Code generator
+
+[![PHP CI](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml/badge.svg)](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
+[![codecov](https://codecov.io/gh/Bacon/BaconQrCode/branch/master/graph/badge.svg?token=rD0HcAiEEx)](https://codecov.io/gh/Bacon/BaconQrCode)
+[![Latest Stable Version](https://poser.pugx.org/bacon/bacon-qr-code/v/stable)](https://packagist.org/packages/bacon/bacon-qr-code)
+[![Total Downloads](https://poser.pugx.org/bacon/bacon-qr-code/downloads)](https://packagist.org/packages/bacon/bacon-qr-code)
+[![License](https://poser.pugx.org/bacon/bacon-qr-code/license)](https://packagist.org/packages/bacon/bacon-qr-code)
+
+
+## Introduction
+BaconQrCode is a port of QR code portion of the ZXing library. It currently
+only features the encoder part, but could later receive the decoder part as
+well.
+
+As the Reed Solomon codec implementation of the ZXing library performs quite
+slow in PHP, it was exchanged with the implementation by Phil Karn.
+
+
+## Example usage
+```php
+use BaconQrCode\Renderer\ImageRenderer;
+use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
+use BaconQrCode\Renderer\RendererStyle\RendererStyle;
+use BaconQrCode\Writer;
+
+$renderer = new ImageRenderer(
+    new RendererStyle(400),
+    new ImagickImageBackEnd()
+);
+$writer = new Writer($renderer);
+$writer->writeFile('Hello World!', 'qrcode.png');
+```
+
+## Available image renderer back ends
+BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
+
+- `ImagickImageBackEnd`: renders raster images using the Imagick library
+- `SvgImageBackEnd`: renders SVG files using XMLWriter
+- `EpsImageBackEnd`: renders EPS files

+ 44 - 0
htdocs/includes/bacon/bacon-qr-code/composer.json.disabled

@@ -0,0 +1,44 @@
+{
+    "name": "bacon/bacon-qr-code",
+    "description": "BaconQrCode is a QR code generator for PHP.",
+    "license" : "BSD-2-Clause",
+    "homepage": "https://github.com/Bacon/BaconQrCode",
+    "require": {
+        "php": "^7.1 || ^8.0",
+        "ext-iconv": "*",
+        "dasprid/enum": "^1.0.3"
+    },
+    "suggest": {
+        "ext-imagick": "to generate QR code images"
+    },
+    "authors": [
+        {
+            "name": "Ben Scholzen 'DASPRiD'",
+            "email": "mail@dasprids.de",
+            "homepage": "https://dasprids.de/",
+            "role": "Developer"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "BaconQrCode\\": "src/"
+        }
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^7 | ^8 | ^9",
+        "spatie/phpunit-snapshot-assertions": "^4.2.9",
+        "squizlabs/php_codesniffer": "^3.4",
+        "phly/keep-a-changelog": "^2.1"
+    },
+    "config": {
+        "allow-plugins": {
+            "ocramius/package-versions": true
+        }
+    },
+    "archive": {
+        "exclude": [
+            "/test",
+            "/phpunit.xml.dist"
+        ]
+    }
+}

+ 372 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/BitArray.php

@@ -0,0 +1,372 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use SplFixedArray;
+
+/**
+ * A simple, fast array of bits.
+ */
+final class BitArray
+{
+    /**
+     * Bits represented as an array of integers.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $bits;
+
+    /**
+     * Size of the bit array in bits.
+     *
+     * @var int
+     */
+    private $size;
+
+    /**
+     * Creates a new bit array with a given size.
+     */
+    public function __construct(int $size = 0)
+    {
+        $this->size = $size;
+        $this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
+    }
+
+    /**
+     * Gets the size in bits.
+     */
+    public function getSize() : int
+    {
+        return $this->size;
+    }
+
+    /**
+     * Gets the size in bytes.
+     */
+    public function getSizeInBytes() : int
+    {
+        return ($this->size + 7) >> 3;
+    }
+
+    /**
+     * Ensures that the array has a minimum capacity.
+     */
+    public function ensureCapacity(int $size) : void
+    {
+        if ($size > count($this->bits) << 5) {
+            $this->bits->setSize(($size + 31) >> 5);
+        }
+    }
+
+    /**
+     * Gets a specific bit.
+     */
+    public function get(int $i) : bool
+    {
+        return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
+    }
+
+    /**
+     * Sets a specific bit.
+     */
+    public function set(int $i) : void
+    {
+        $this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
+    }
+
+    /**
+     * Flips a specific bit.
+     */
+    public function flip(int $i) : void
+    {
+        $this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
+    }
+
+    /**
+     * Gets the next set bit position from a given position.
+     */
+    public function getNextSet(int $from) : int
+    {
+        if ($from >= $this->size) {
+            return $this->size;
+        }
+
+        $bitsOffset = $from >> 5;
+        $currentBits = $this->bits[$bitsOffset];
+        $bitsLength = count($this->bits);
+        $currentBits &= ~((1 << ($from & 0x1f)) - 1);
+
+        while (0 === $currentBits) {
+            if (++$bitsOffset === $bitsLength) {
+                return $this->size;
+            }
+
+            $currentBits = $this->bits[$bitsOffset];
+        }
+
+        $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
+        return $result > $this->size ? $this->size : $result;
+    }
+
+    /**
+     * Gets the next unset bit position from a given position.
+     */
+    public function getNextUnset(int $from) : int
+    {
+        if ($from >= $this->size) {
+            return $this->size;
+        }
+
+        $bitsOffset = $from >> 5;
+        $currentBits = ~$this->bits[$bitsOffset];
+        $bitsLength = count($this->bits);
+        $currentBits &= ~((1 << ($from & 0x1f)) - 1);
+
+        while (0 === $currentBits) {
+            if (++$bitsOffset === $bitsLength) {
+                return $this->size;
+            }
+
+            $currentBits = ~$this->bits[$bitsOffset];
+        }
+
+        $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
+        return $result > $this->size ? $this->size : $result;
+    }
+
+    /**
+     * Sets a bulk of bits.
+     */
+    public function setBulk(int $i, int $newBits) : void
+    {
+        $this->bits[$i >> 5] = $newBits;
+    }
+
+    /**
+     * Sets a range of bits.
+     *
+     * @throws InvalidArgumentException if end is smaller than start
+     */
+    public function setRange(int $start, int $end) : void
+    {
+        if ($end < $start) {
+            throw new InvalidArgumentException('End must be greater or equal to start');
+        }
+
+        if ($end === $start) {
+            return;
+        }
+
+        --$end;
+
+        $firstInt = $start >> 5;
+        $lastInt = $end >> 5;
+
+        for ($i = $firstInt; $i <= $lastInt; ++$i) {
+            $firstBit = $i > $firstInt ? 0 : $start & 0x1f;
+            $lastBit = $i < $lastInt ? 31 : $end & 0x1f;
+
+            if (0 === $firstBit && 31 === $lastBit) {
+                $mask = 0x7fffffff;
+            } else {
+                $mask = 0;
+
+                for ($j = $firstBit; $j < $lastBit; ++$j) {
+                    $mask |= 1 << $j;
+                }
+            }
+
+            $this->bits[$i] = $this->bits[$i] | $mask;
+        }
+    }
+
+    /**
+     * Clears the bit array, unsetting every bit.
+     */
+    public function clear() : void
+    {
+        $bitsLength = count($this->bits);
+
+        for ($i = 0; $i < $bitsLength; ++$i) {
+            $this->bits[$i] = 0;
+        }
+    }
+
+    /**
+     * Checks if a range of bits is set or not set.
+
+     * @throws InvalidArgumentException if end is smaller than start
+     */
+    public function isRange(int $start, int $end, bool $value) : bool
+    {
+        if ($end < $start) {
+            throw new InvalidArgumentException('End must be greater or equal to start');
+        }
+
+        if ($end === $start) {
+            return true;
+        }
+
+        --$end;
+
+        $firstInt = $start >> 5;
+        $lastInt = $end >> 5;
+
+        for ($i = $firstInt; $i <= $lastInt; ++$i) {
+            $firstBit = $i > $firstInt ? 0 : $start & 0x1f;
+            $lastBit = $i < $lastInt ? 31 : $end & 0x1f;
+
+            if (0 === $firstBit && 31 === $lastBit) {
+                $mask = 0x7fffffff;
+            } else {
+                $mask = 0;
+
+                for ($j = $firstBit; $j <= $lastBit; ++$j) {
+                    $mask |= 1 << $j;
+                }
+            }
+
+            if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Appends a bit to the array.
+     */
+    public function appendBit(bool $bit) : void
+    {
+        $this->ensureCapacity($this->size + 1);
+
+        if ($bit) {
+            $this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
+        }
+
+        ++$this->size;
+    }
+
+    /**
+     * Appends a number of bits (up to 32) to the array.
+
+     * @throws InvalidArgumentException if num bits is not between 0 and 32
+     */
+    public function appendBits(int $value, int $numBits) : void
+    {
+        if ($numBits < 0 || $numBits > 32) {
+            throw new InvalidArgumentException('Num bits must be between 0 and 32');
+        }
+
+        $this->ensureCapacity($this->size + $numBits);
+
+        for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
+            $this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
+        }
+    }
+
+    /**
+     * Appends another bit array to this array.
+     */
+    public function appendBitArray(self $other) : void
+    {
+        $otherSize = $other->getSize();
+        $this->ensureCapacity($this->size + $other->getSize());
+
+        for ($i = 0; $i < $otherSize; ++$i) {
+            $this->appendBit($other->get($i));
+        }
+    }
+
+    /**
+     * Makes an exclusive-or comparision on the current bit array.
+     *
+     * @throws InvalidArgumentException if sizes don't match
+     */
+    public function xorBits(self $other) : void
+    {
+        $bitsLength = count($this->bits);
+        $otherBits  = $other->getBitArray();
+
+        if ($bitsLength !== count($otherBits)) {
+            throw new InvalidArgumentException('Sizes don\'t match');
+        }
+
+        for ($i = 0; $i < $bitsLength; ++$i) {
+            $this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
+        }
+    }
+
+    /**
+     * Converts the bit array to a byte array.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
+    {
+        $bytes = new SplFixedArray($numBytes);
+
+        for ($i = 0; $i < $numBytes; ++$i) {
+            $byte = 0;
+
+            for ($j = 0; $j < 8; ++$j) {
+                if ($this->get($bitOffset)) {
+                    $byte |= 1 << (7 - $j);
+                }
+
+                ++$bitOffset;
+            }
+
+            $bytes[$i] = $byte;
+        }
+
+        return $bytes;
+    }
+
+    /**
+     * Gets the internal bit array.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function getBitArray() : SplFixedArray
+    {
+        return $this->bits;
+    }
+
+    /**
+     * Reverses the array.
+     */
+    public function reverse() : void
+    {
+        $newBits = new SplFixedArray(count($this->bits));
+
+        for ($i = 0; $i < $this->size; ++$i) {
+            if ($this->get($this->size - $i - 1)) {
+                $newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
+            }
+        }
+
+        $this->bits = $newBits;
+    }
+
+    /**
+     * Returns a string representation of the bit array.
+     */
+    public function __toString() : string
+    {
+        $result = '';
+
+        for ($i = 0; $i < $this->size; ++$i) {
+            if (0 === ($i & 0x07)) {
+                $result .= ' ';
+            }
+
+            $result .= $this->get($i) ? 'X' : '.';
+        }
+
+        return $result;
+    }
+}

+ 313 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/BitMatrix.php

@@ -0,0 +1,313 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use SplFixedArray;
+
+/**
+ * Bit matrix.
+ *
+ * Represents a 2D matrix of bits. In function arguments below, and throughout
+ * the common module, x is the column position, and y is the row position. The
+ * ordering is always x, y. The origin is at the top-left.
+ */
+class BitMatrix
+{
+    /**
+     * Width of the bit matrix.
+     *
+     * @var int
+     */
+    private $width;
+
+    /**
+     * Height of the bit matrix.
+     *
+     * @var int
+     */
+    private $height;
+
+    /**
+     * Size in bits of each individual row.
+     *
+     * @var int
+     */
+    private $rowSize;
+
+    /**
+     * Bits representation.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $bits;
+
+    /**
+     * @throws InvalidArgumentException if a dimension is smaller than zero
+     */
+    public function __construct(int $width, int $height = null)
+    {
+        if (null === $height) {
+            $height = $width;
+        }
+
+        if ($width < 1 || $height < 1) {
+            throw new InvalidArgumentException('Both dimensions must be greater than zero');
+        }
+
+        $this->width = $width;
+        $this->height = $height;
+        $this->rowSize = ($width + 31) >> 5;
+        $this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
+    }
+
+    /**
+     * Gets the requested bit, where true means black.
+     */
+    public function get(int $x, int $y) : bool
+    {
+        $offset = $y * $this->rowSize + ($x >> 5);
+        return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
+    }
+
+    /**
+     * Sets the given bit to true.
+     */
+    public function set(int $x, int $y) : void
+    {
+        $offset = $y * $this->rowSize + ($x >> 5);
+        $this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
+    }
+
+    /**
+     * Flips the given bit.
+     */
+    public function flip(int $x, int $y) : void
+    {
+        $offset = $y * $this->rowSize + ($x >> 5);
+        $this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
+    }
+
+    /**
+     * Clears all bits (set to false).
+     */
+    public function clear() : void
+    {
+        $max = count($this->bits);
+
+        for ($i = 0; $i < $max; ++$i) {
+            $this->bits[$i] = 0;
+        }
+    }
+
+    /**
+     * Sets a square region of the bit matrix to true.
+     *
+     * @throws InvalidArgumentException if left or top are negative
+     * @throws InvalidArgumentException if width or height are smaller than 1
+     * @throws InvalidArgumentException if region does not fit into the matix
+     */
+    public function setRegion(int $left, int $top, int $width, int $height) : void
+    {
+        if ($top < 0 || $left < 0) {
+            throw new InvalidArgumentException('Left and top must be non-negative');
+        }
+
+        if ($height < 1 || $width < 1) {
+            throw new InvalidArgumentException('Width and height must be at least 1');
+        }
+
+        $right = $left + $width;
+        $bottom = $top + $height;
+
+        if ($bottom > $this->height || $right > $this->width) {
+            throw new InvalidArgumentException('The region must fit inside the matrix');
+        }
+
+        for ($y = $top; $y < $bottom; ++$y) {
+            $offset = $y * $this->rowSize;
+
+            for ($x = $left; $x < $right; ++$x) {
+                $index = $offset + ($x >> 5);
+                $this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
+            }
+        }
+    }
+
+    /**
+     * A fast method to retrieve one row of data from the matrix as a BitArray.
+     */
+    public function getRow(int $y, BitArray $row = null) : BitArray
+    {
+        if (null === $row || $row->getSize() < $this->width) {
+            $row = new BitArray($this->width);
+        }
+
+        $offset = $y * $this->rowSize;
+
+        for ($x = 0; $x < $this->rowSize; ++$x) {
+            $row->setBulk($x << 5, $this->bits[$offset + $x]);
+        }
+
+        return $row;
+    }
+
+    /**
+     * Sets a row of data from a BitArray.
+     */
+    public function setRow(int $y, BitArray $row) : void
+    {
+        $bits = $row->getBitArray();
+
+        for ($i = 0; $i < $this->rowSize; ++$i) {
+            $this->bits[$y * $this->rowSize + $i] = $bits[$i];
+        }
+    }
+
+    /**
+     * This is useful in detecting the enclosing rectangle of a 'pure' barcode.
+     *
+     * @return int[]|null
+     */
+    public function getEnclosingRectangle() : ?array
+    {
+        $left = $this->width;
+        $top = $this->height;
+        $right = -1;
+        $bottom = -1;
+
+        for ($y = 0; $y < $this->height; ++$y) {
+            for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
+                $bits = $this->bits[$y * $this->rowSize + $x32];
+
+                if (0 !== $bits) {
+                    if ($y < $top) {
+                        $top = $y;
+                    }
+
+                    if ($y > $bottom) {
+                        $bottom = $y;
+                    }
+
+                    if ($x32 * 32 < $left) {
+                        $bit = 0;
+
+                        while (($bits << (31 - $bit)) === 0) {
+                            $bit++;
+                        }
+
+                        if (($x32 * 32 + $bit) < $left) {
+                            $left = $x32 * 32 + $bit;
+                        }
+                    }
+                }
+
+                if ($x32 * 32 + 31 > $right) {
+                    $bit = 31;
+
+                    while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
+                        --$bit;
+                    }
+
+                    if (($x32 * 32 + $bit) > $right) {
+                        $right = $x32 * 32 + $bit;
+                    }
+                }
+            }
+        }
+
+        $width = $right - $left;
+        $height = $bottom - $top;
+
+        if ($width < 0 || $height < 0) {
+            return null;
+        }
+
+        return [$left, $top, $width, $height];
+    }
+
+    /**
+     * Gets the most top left set bit.
+     *
+     * This is useful in detecting a corner of a 'pure' barcode.
+     *
+     * @return int[]|null
+     */
+    public function getTopLeftOnBit() : ?array
+    {
+        $bitsOffset = 0;
+
+        while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
+            ++$bitsOffset;
+        }
+
+        if (count($this->bits) === $bitsOffset) {
+            return null;
+        }
+
+        $x = intdiv($bitsOffset, $this->rowSize);
+        $y = ($bitsOffset % $this->rowSize) << 5;
+
+        $bits = $this->bits[$bitsOffset];
+        $bit = 0;
+
+        while (0 === ($bits << (31 - $bit))) {
+            ++$bit;
+        }
+
+        $x += $bit;
+
+        return [$x, $y];
+    }
+
+    /**
+     * Gets the most bottom right set bit.
+     *
+     * This is useful in detecting a corner of a 'pure' barcode.
+     *
+     * @return int[]|null
+     */
+    public function getBottomRightOnBit() : ?array
+    {
+        $bitsOffset = count($this->bits) - 1;
+
+        while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
+            --$bitsOffset;
+        }
+
+        if ($bitsOffset < 0) {
+            return null;
+        }
+
+        $x = intdiv($bitsOffset, $this->rowSize);
+        $y = ($bitsOffset % $this->rowSize) << 5;
+
+        $bits = $this->bits[$bitsOffset];
+        $bit  = 0;
+
+        while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
+            --$bit;
+        }
+
+        $x += $bit;
+
+        return [$x, $y];
+    }
+
+    /**
+     * Gets the width of the matrix,
+     */
+    public function getWidth() : int
+    {
+        return $this->width;
+    }
+
+    /**
+     * Gets the height of the matrix.
+     */
+    public function getHeight() : int
+    {
+        return $this->height;
+    }
+}

+ 41 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/BitUtils.php

@@ -0,0 +1,41 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+/**
+ * General bit utilities.
+ *
+ * All utility methods are based on 32-bit integers and also work on 64-bit
+ * systems.
+ */
+final class BitUtils
+{
+    private function __construct()
+    {
+    }
+
+    /**
+     * Performs an unsigned right shift.
+     *
+     * This is the same as the unsigned right shift operator ">>>" in other
+     * languages.
+     */
+    public static function unsignedRightShift(int $a, int $b) : int
+    {
+        return (
+            $a >= 0
+            ? $a >> $b
+            : (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
+        );
+    }
+
+    /**
+     * Gets the number of trailing zeros.
+     */
+    public static function numberOfTrailingZeros(int $i) : int
+    {
+        $lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
+        return $lastPos === false ? 32 : 31 - $lastPos;
+    }
+}

+ 183 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/CharacterSetEci.php

@@ -0,0 +1,183 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use DASPRiD\Enum\AbstractEnum;
+
+/**
+ * Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
+ *
+ * @method static self CP437()
+ * @method static self ISO8859_1()
+ * @method static self ISO8859_2()
+ * @method static self ISO8859_3()
+ * @method static self ISO8859_4()
+ * @method static self ISO8859_5()
+ * @method static self ISO8859_6()
+ * @method static self ISO8859_7()
+ * @method static self ISO8859_8()
+ * @method static self ISO8859_9()
+ * @method static self ISO8859_10()
+ * @method static self ISO8859_11()
+ * @method static self ISO8859_12()
+ * @method static self ISO8859_13()
+ * @method static self ISO8859_14()
+ * @method static self ISO8859_15()
+ * @method static self ISO8859_16()
+ * @method static self SJIS()
+ * @method static self CP1250()
+ * @method static self CP1251()
+ * @method static self CP1252()
+ * @method static self CP1256()
+ * @method static self UNICODE_BIG_UNMARKED()
+ * @method static self UTF8()
+ * @method static self ASCII()
+ * @method static self BIG5()
+ * @method static self GB18030()
+ * @method static self EUC_KR()
+ */
+final class CharacterSetEci extends AbstractEnum
+{
+    protected const CP437 = [[0, 2]];
+    protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
+    protected const ISO8859_2 = [[4], 'ISO-8859-2'];
+    protected const ISO8859_3 = [[5], 'ISO-8859-3'];
+    protected const ISO8859_4 = [[6], 'ISO-8859-4'];
+    protected const ISO8859_5 = [[7], 'ISO-8859-5'];
+    protected const ISO8859_6 = [[8], 'ISO-8859-6'];
+    protected const ISO8859_7 = [[9], 'ISO-8859-7'];
+    protected const ISO8859_8 = [[10], 'ISO-8859-8'];
+    protected const ISO8859_9 = [[11], 'ISO-8859-9'];
+    protected const ISO8859_10 = [[12], 'ISO-8859-10'];
+    protected const ISO8859_11 = [[13], 'ISO-8859-11'];
+    protected const ISO8859_12 = [[14], 'ISO-8859-12'];
+    protected const ISO8859_13 = [[15], 'ISO-8859-13'];
+    protected const ISO8859_14 = [[16], 'ISO-8859-14'];
+    protected const ISO8859_15 = [[17], 'ISO-8859-15'];
+    protected const ISO8859_16 = [[18], 'ISO-8859-16'];
+    protected const SJIS = [[20], 'Shift_JIS'];
+    protected const CP1250 = [[21], 'windows-1250'];
+    protected const CP1251 = [[22], 'windows-1251'];
+    protected const CP1252 = [[23], 'windows-1252'];
+    protected const CP1256 = [[24], 'windows-1256'];
+    protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
+    protected const UTF8 = [[26], 'UTF-8'];
+    protected const ASCII = [[27, 170], 'US-ASCII'];
+    protected const BIG5 = [[28]];
+    protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
+    protected const EUC_KR = [[30], 'EUC-KR'];
+
+    /**
+     * @var int[]
+     */
+    private $values;
+
+    /**
+     * @var string[]
+     */
+    private $otherEncodingNames;
+
+    /**
+     * @var array<int, self>|null
+     */
+    private static $valueToEci;
+
+    /**
+     * @var array<string, self>|null
+     */
+    private static $nameToEci;
+
+    /**
+     * @param int[] $values
+     */
+    public function __construct(array $values, string ...$otherEncodingNames)
+    {
+        $this->values = $values;
+        $this->otherEncodingNames = $otherEncodingNames;
+    }
+
+    /**
+     * Returns the primary value.
+     */
+    public function getValue() : int
+    {
+        return $this->values[0];
+    }
+
+    /**
+     * Gets character set ECI by value.
+     *
+     * Returns the representing ECI of a given value, or null if it is legal but unsupported.
+     *
+     * @throws InvalidArgumentException if value is not between 0 and 900
+     */
+    public static function getCharacterSetEciByValue(int $value) : ?self
+    {
+        if ($value < 0 || $value >= 900) {
+            throw new InvalidArgumentException('Value must be between 0 and 900');
+        }
+
+        $valueToEci = self::valueToEci();
+
+        if (! array_key_exists($value, $valueToEci)) {
+            return null;
+        }
+
+        return $valueToEci[$value];
+    }
+
+    /**
+     * Returns character set ECI by name.
+     *
+     * Returns the representing ECI of a given name, or null if it is legal but unsupported
+     */
+    public static function getCharacterSetEciByName(string $name) : ?self
+    {
+        $nameToEci = self::nameToEci();
+        $name = strtolower($name);
+
+        if (! array_key_exists($name, $nameToEci)) {
+            return null;
+        }
+
+        return $nameToEci[$name];
+    }
+
+    private static function valueToEci() : array
+    {
+        if (null !== self::$valueToEci) {
+            return self::$valueToEci;
+        }
+
+        self::$valueToEci = [];
+
+        foreach (self::values() as $eci) {
+            foreach ($eci->values as $value) {
+                self::$valueToEci[$value] = $eci;
+            }
+        }
+
+        return self::$valueToEci;
+    }
+
+    private static function nameToEci() : array
+    {
+        if (null !== self::$nameToEci) {
+            return self::$nameToEci;
+        }
+
+        self::$nameToEci = [];
+
+        foreach (self::values() as $eci) {
+            self::$nameToEci[strtolower($eci->name())] = $eci;
+
+            foreach ($eci->otherEncodingNames as $name) {
+                self::$nameToEci[strtolower($name)] = $eci;
+            }
+        }
+
+        return self::$nameToEci;
+    }
+}

+ 49 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlock.php

@@ -0,0 +1,49 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+/**
+ * Encapsulates the parameters for one error-correction block in one symbol version.
+ *
+ * This includes the number of data codewords, and the number of times a block with these parameters is used
+ * consecutively in the QR code version's format.
+ */
+final class EcBlock
+{
+    /**
+     * How many times the block is used.
+     *
+     * @var int
+     */
+    private $count;
+
+    /**
+     * Number of data codewords.
+     *
+     * @var int
+     */
+    private $dataCodewords;
+
+    public function __construct(int $count, int $dataCodewords)
+    {
+        $this->count = $count;
+        $this->dataCodewords = $dataCodewords;
+    }
+
+    /**
+     * Returns how many times the block is used.
+     */
+    public function getCount() : int
+    {
+        return $this->count;
+    }
+
+    /**
+     * Returns the number of data codewords.
+     */
+    public function getDataCodewords() : int
+    {
+        return $this->dataCodewords;
+    }
+}

+ 74 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlocks.php

@@ -0,0 +1,74 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+/**
+ * Encapsulates a set of error-correction blocks in one symbol version.
+ *
+ * Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each
+ * set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all
+ * blocks within one version.
+ */
+final class EcBlocks
+{
+    /**
+     * Number of EC codewords per block.
+     *
+     * @var int
+     */
+    private $ecCodewordsPerBlock;
+
+    /**
+     * List of EC blocks.
+     *
+     * @var EcBlock[]
+     */
+    private $ecBlocks;
+
+    public function __construct(int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
+    {
+        $this->ecCodewordsPerBlock = $ecCodewordsPerBlock;
+        $this->ecBlocks = $ecBlocks;
+    }
+
+    /**
+     * Returns the number of EC codewords per block.
+     */
+    public function getEcCodewordsPerBlock() : int
+    {
+        return $this->ecCodewordsPerBlock;
+    }
+
+    /**
+     * Returns the total number of EC block appearances.
+     */
+    public function getNumBlocks() : int
+    {
+        $total = 0;
+
+        foreach ($this->ecBlocks as $ecBlock) {
+            $total += $ecBlock->getCount();
+        }
+
+        return $total;
+    }
+
+    /**
+     * Returns the total count of EC codewords.
+     */
+    public function getTotalEcCodewords() : int
+    {
+        return $this->ecCodewordsPerBlock * $this->getNumBlocks();
+    }
+
+    /**
+     * Returns the EC blocks included in this collection.
+     *
+     * @return EcBlock[]
+     */
+    public function getEcBlocks() : array
+    {
+        return $this->ecBlocks;
+    }
+}

+ 63 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/ErrorCorrectionLevel.php

@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\OutOfBoundsException;
+use DASPRiD\Enum\AbstractEnum;
+
+/**
+ * Enum representing the four error correction levels.
+ *
+ * @method static self L() ~7% correction
+ * @method static self M() ~15% correction
+ * @method static self Q() ~25% correction
+ * @method static self H() ~30% correction
+ */
+final class ErrorCorrectionLevel extends AbstractEnum
+{
+    protected const L = [0x01];
+    protected const M = [0x00];
+    protected const Q = [0x03];
+    protected const H = [0x02];
+
+    /**
+     * @var int
+     */
+    private $bits;
+
+    protected function __construct(int $bits)
+    {
+        $this->bits = $bits;
+    }
+
+    /**
+     * @throws OutOfBoundsException if number of bits is invalid
+     */
+    public static function forBits(int $bits) : self
+    {
+        switch ($bits) {
+            case 0:
+                return self::M();
+
+            case 1:
+                return self::L();
+
+            case 2:
+                return self::H();
+
+            case 3:
+                return self::Q();
+        }
+
+        throw new OutOfBoundsException('Invalid number of bits');
+    }
+
+    /**
+     * Returns the two bits used to encode this error correction level.
+     */
+    public function getBits() : int
+    {
+        return $this->bits;
+    }
+}

+ 203 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/FormatInformation.php

@@ -0,0 +1,203 @@
+<?php
+/**
+ * BaconQrCode
+ *
+ * @link      http://github.com/Bacon/BaconQrCode For the canonical source repository
+ * @copyright 2013 Ben 'DASPRiD' Scholzen
+ * @license   http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
+ */
+
+namespace BaconQrCode\Common;
+
+/**
+ * Encapsulates a QR Code's format information, including the data mask used and error correction level.
+ */
+class FormatInformation
+{
+    /**
+     * Mask for format information.
+     */
+    private const FORMAT_INFO_MASK_QR = 0x5412;
+
+    /**
+     * Lookup table for decoding format information.
+     *
+     * See ISO 18004:2006, Annex C, Table C.1
+     */
+    private const FORMAT_INFO_DECODE_LOOKUP = [
+        [0x5412, 0x00],
+        [0x5125, 0x01],
+        [0x5e7c, 0x02],
+        [0x5b4b, 0x03],
+        [0x45f9, 0x04],
+        [0x40ce, 0x05],
+        [0x4f97, 0x06],
+        [0x4aa0, 0x07],
+        [0x77c4, 0x08],
+        [0x72f3, 0x09],
+        [0x7daa, 0x0a],
+        [0x789d, 0x0b],
+        [0x662f, 0x0c],
+        [0x6318, 0x0d],
+        [0x6c41, 0x0e],
+        [0x6976, 0x0f],
+        [0x1689, 0x10],
+        [0x13be, 0x11],
+        [0x1ce7, 0x12],
+        [0x19d0, 0x13],
+        [0x0762, 0x14],
+        [0x0255, 0x15],
+        [0x0d0c, 0x16],
+        [0x083b, 0x17],
+        [0x355f, 0x18],
+        [0x3068, 0x19],
+        [0x3f31, 0x1a],
+        [0x3a06, 0x1b],
+        [0x24b4, 0x1c],
+        [0x2183, 0x1d],
+        [0x2eda, 0x1e],
+        [0x2bed, 0x1f],
+    ];
+
+    /**
+     * Offset i holds the number of 1 bits in the binary representation of i.
+     *
+     * @var int[]
+     */
+    private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
+
+    /**
+     * Error correction level.
+     *
+     * @var ErrorCorrectionLevel
+     */
+    private $ecLevel;
+
+    /**
+     * Data mask.
+     *
+     * @var int
+     */
+    private $dataMask;
+
+    protected function __construct(int $formatInfo)
+    {
+        $this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3);
+        $this->dataMask = $formatInfo & 0x7;
+    }
+
+    /**
+     * Checks how many bits are different between two integers.
+     */
+    public static function numBitsDiffering(int $a, int $b) : int
+    {
+        $a ^= $b;
+
+        return (
+            self::BITS_SET_IN_HALF_BYTE[$a & 0xf]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
+        );
+    }
+
+    /**
+     * Decodes format information.
+     */
+    public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
+    {
+        $formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
+
+        if (null !== $formatInfo) {
+            return $formatInfo;
+        }
+
+        // Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the
+        // pattern first.
+        return self::doDecodeFormatInformation(
+            $maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
+            $maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
+        );
+    }
+
+    /**
+     * Internal method for decoding format information.
+     */
+    private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
+    {
+        $bestDifference = PHP_INT_MAX;
+        $bestFormatInfo = 0;
+
+        foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) {
+            $targetInfo = $decodeInfo[0];
+
+            if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
+                // Found an exact match
+                return new self($decodeInfo[1]);
+            }
+
+            $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
+
+            if ($bitsDifference < $bestDifference) {
+                $bestFormatInfo = $decodeInfo[1];
+                $bestDifference = $bitsDifference;
+            }
+
+            if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
+                // Also try the other option
+                $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
+
+                if ($bitsDifference < $bestDifference) {
+                    $bestFormatInfo = $decodeInfo[1];
+                    $bestDifference = $bitsDifference;
+                }
+            }
+        }
+
+        // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match.
+        if ($bestDifference <= 3) {
+            return new self($bestFormatInfo);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the error correction level.
+     */
+    public function getErrorCorrectionLevel() : ErrorCorrectionLevel
+    {
+        return $this->ecLevel;
+    }
+
+    /**
+     * Returns the data mask.
+     */
+    public function getDataMask() : int
+    {
+        return $this->dataMask;
+    }
+
+    /**
+     * Hashes the code of the EC level.
+     */
+    public function hashCode() : int
+    {
+        return ($this->ecLevel->getBits() << 3) | $this->dataMask;
+    }
+
+    /**
+     * Verifies if this instance equals another one.
+     */
+    public function equals(self $other) : bool
+    {
+        return (
+            $this->ecLevel === $other->ecLevel
+            && $this->dataMask === $other->dataMask
+        );
+    }
+}

+ 79 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/Mode.php

@@ -0,0 +1,79 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use DASPRiD\Enum\AbstractEnum;
+
+/**
+ * Enum representing various modes in which data can be encoded to bits.
+ *
+ * @method static self TERMINATOR()
+ * @method static self NUMERIC()
+ * @method static self ALPHANUMERIC()
+ * @method static self STRUCTURED_APPEND()
+ * @method static self BYTE()
+ * @method static self ECI()
+ * @method static self KANJI()
+ * @method static self FNC1_FIRST_POSITION()
+ * @method static self FNC1_SECOND_POSITION()
+ * @method static self HANZI()
+ */
+final class Mode extends AbstractEnum
+{
+    protected const TERMINATOR = [[0, 0, 0], 0x00];
+    protected const NUMERIC = [[10, 12, 14], 0x01];
+    protected const ALPHANUMERIC = [[9, 11, 13], 0x02];
+    protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03];
+    protected const BYTE = [[8, 16, 16], 0x04];
+    protected const ECI = [[0, 0, 0], 0x07];
+    protected const KANJI = [[8, 10, 12], 0x08];
+    protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05];
+    protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09];
+    protected const HANZI = [[8, 10, 12], 0x0d];
+
+    /**
+     * @var int[]
+     */
+    private $characterCountBitsForVersions;
+
+    /**
+     * @var int
+     */
+    private $bits;
+
+    /**
+     * @param int[] $characterCountBitsForVersions
+     */
+    protected function __construct(array $characterCountBitsForVersions, int $bits)
+    {
+        $this->characterCountBitsForVersions = $characterCountBitsForVersions;
+        $this->bits = $bits;
+    }
+
+    /**
+     * Returns the number of bits used in a specific QR code version.
+     */
+    public function getCharacterCountBits(Version $version) : int
+    {
+        $number = $version->getVersionNumber();
+
+        if ($number <= 9) {
+            $offset = 0;
+        } elseif ($number <= 26) {
+            $offset = 1;
+        } else {
+            $offset = 2;
+        }
+
+        return $this->characterCountBitsForVersions[$offset];
+    }
+
+    /**
+     * Returns the four bits used to encode this mode.
+     */
+    public function getBits() : int
+    {
+        return $this->bits;
+    }
+}

+ 468 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/ReedSolomonCodec.php

@@ -0,0 +1,468 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Exception\RuntimeException;
+use SplFixedArray;
+
+/**
+ * Reed-Solomon codec for 8-bit characters.
+ *
+ * Based on libfec by Phil Karn, KA9Q.
+ */
+final class ReedSolomonCodec
+{
+    /**
+     * Symbol size in bits.
+     *
+     * @var int
+     */
+    private $symbolSize;
+
+    /**
+     * Block size in symbols.
+     *
+     * @var int
+     */
+    private $blockSize;
+
+    /**
+     * First root of RS code generator polynomial, index form.
+     *
+     * @var int
+     */
+    private $firstRoot;
+
+    /**
+     * Primitive element to generate polynomial roots, index form.
+     *
+     * @var int
+     */
+    private $primitive;
+
+    /**
+     * Prim-th root of 1, index form.
+     *
+     * @var int
+     */
+    private $iPrimitive;
+
+    /**
+     * RS code generator polynomial degree (number of roots).
+     *
+     * @var int
+     */
+    private $numRoots;
+
+    /**
+     * Padding bytes at front of shortened block.
+     *
+     * @var int
+     */
+    private $padding;
+
+    /**
+     * Log lookup table.
+     *
+     * @var SplFixedArray
+     */
+    private $alphaTo;
+
+    /**
+     * Anti-Log lookup table.
+     *
+     * @var SplFixedArray
+     */
+    private $indexOf;
+
+    /**
+     * Generator polynomial.
+     *
+     * @var SplFixedArray
+     */
+    private $generatorPoly;
+
+    /**
+     * @throws InvalidArgumentException if symbol size ist not between 0 and 8
+     * @throws InvalidArgumentException if first root is invalid
+     * @throws InvalidArgumentException if num roots is invalid
+     * @throws InvalidArgumentException if padding is invalid
+     * @throws RuntimeException if field generator polynomial is not primitive
+     */
+    public function __construct(
+        int $symbolSize,
+        int $gfPoly,
+        int $firstRoot,
+        int $primitive,
+        int $numRoots,
+        int $padding
+    ) {
+        if ($symbolSize < 0 || $symbolSize > 8) {
+            throw new InvalidArgumentException('Symbol size must be between 0 and 8');
+        }
+
+        if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
+            throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
+        }
+
+        if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
+            throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
+        }
+
+        if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
+            throw new InvalidArgumentException(
+                'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)
+            );
+        }
+
+        $this->symbolSize = $symbolSize;
+        $this->blockSize = (1 << $symbolSize) - 1;
+        $this->padding = $padding;
+        $this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
+        $this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
+
+        // Generate galous field lookup table
+        $this->indexOf[0] = $this->blockSize;
+        $this->alphaTo[$this->blockSize] = 0;
+
+        $sr = 1;
+
+        for ($i = 0; $i < $this->blockSize; ++$i) {
+            $this->indexOf[$sr] = $i;
+            $this->alphaTo[$i]  = $sr;
+
+            $sr <<= 1;
+
+            if ($sr & (1 << $symbolSize)) {
+                $sr ^= $gfPoly;
+            }
+
+            $sr &= $this->blockSize;
+        }
+
+        if (1 !== $sr) {
+            throw new RuntimeException('Field generator polynomial is not primitive');
+        }
+
+        // Form RS code generator polynomial from its roots
+        $this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
+        $this->firstRoot = $firstRoot;
+        $this->primitive = $primitive;
+        $this->numRoots = $numRoots;
+
+        // Find prim-th root of 1, used in decoding
+        for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) {
+        }
+
+        $this->iPrimitive = intdiv($iPrimitive, $primitive);
+
+        $this->generatorPoly[0] = 1;
+
+        for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) {
+            $this->generatorPoly[$i + 1] = 1;
+
+            for ($j = $i; $j > 0; $j--) {
+                if ($this->generatorPoly[$j] !== 0) {
+                    $this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[
+                        $this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)
+                    ];
+                } else {
+                    $this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
+                }
+            }
+
+            $this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
+        }
+
+        // Convert generator poly to index form for quicker encoding
+        for ($i = 0; $i <= $numRoots; ++$i) {
+            $this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
+        }
+    }
+
+    /**
+     * Encodes data and writes result back into parity array.
+     */
+    public function encode(SplFixedArray $data, SplFixedArray $parity) : void
+    {
+        for ($i = 0; $i < $this->numRoots; ++$i) {
+            $parity[$i] = 0;
+        }
+
+        $iterations = $this->blockSize - $this->numRoots - $this->padding;
+
+        for ($i = 0; $i < $iterations; ++$i) {
+            $feedback = $this->indexOf[$data[$i] ^ $parity[0]];
+
+            if ($feedback !== $this->blockSize) {
+                // Feedback term is non-zero
+                $feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
+
+                for ($j = 1; $j < $this->numRoots; ++$j) {
+                    $parity[$j] = $parity[$j] ^ $this->alphaTo[
+                        $this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])
+                    ];
+                }
+            }
+
+            for ($j = 0; $j < $this->numRoots - 1; ++$j) {
+                $parity[$j] = $parity[$j + 1];
+            }
+
+            if ($feedback !== $this->blockSize) {
+                $parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
+            } else {
+                $parity[$this->numRoots - 1] = 0;
+            }
+        }
+    }
+
+    /**
+     * Decodes received data.
+     */
+    public function decode(SplFixedArray $data, SplFixedArray $erasures = null) : ?int
+    {
+        // This speeds up the initialization a bit.
+        $numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
+        $numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
+
+        $lambda = clone $numRootsPlusOne;
+        $b = clone $numRootsPlusOne;
+        $t = clone $numRootsPlusOne;
+        $omega = clone $numRootsPlusOne;
+        $root = clone $numRoots;
+        $loc = clone $numRoots;
+
+        $numErasures = (null !== $erasures ? count($erasures) : 0);
+
+        // Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
+        $syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
+
+        for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) {
+            for ($j = 0; $j < $this->numRoots; ++$j) {
+                if ($syndromes[$j] === 0) {
+                    $syndromes[$j] = $data[$i];
+                } else {
+                    $syndromes[$j] = $data[$i] ^ $this->alphaTo[
+                        $this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
+                    ];
+                }
+            }
+        }
+
+        // Convert syndromes to index form, checking for nonzero conditions
+        $syndromeError = 0;
+
+        for ($i = 0; $i < $this->numRoots; ++$i) {
+            $syndromeError |= $syndromes[$i];
+            $syndromes[$i] = $this->indexOf[$syndromes[$i]];
+        }
+
+        if (! $syndromeError) {
+            // If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[]
+            // unmodified.
+            return 0;
+        }
+
+        $lambda[0] = 1;
+
+        if ($numErasures > 0) {
+            // Init lambda to be the erasure locator polynomial
+            $lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
+
+            for ($i = 1; $i < $numErasures; ++$i) {
+                $u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
+
+                for ($j = $i + 1; $j > 0; --$j) {
+                    $tmp = $this->indexOf[$lambda[$j - 1]];
+
+                    if ($tmp !== $this->blockSize) {
+                        $lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
+                    }
+                }
+            }
+        }
+
+        for ($i = 0; $i <= $this->numRoots; ++$i) {
+            $b[$i] = $this->indexOf[$lambda[$i]];
+        }
+
+        // Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial
+        $r  = $numErasures;
+        $el = $numErasures;
+
+        while (++$r <= $this->numRoots) {
+            // Compute discrepancy at the r-th step in poly form
+            $discrepancyR = 0;
+
+            for ($i = 0; $i < $r; ++$i) {
+                if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
+                    $discrepancyR ^= $this->alphaTo[
+                        $this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])
+                    ];
+                }
+            }
+
+            $discrepancyR = $this->indexOf[$discrepancyR];
+
+            if ($discrepancyR === $this->blockSize) {
+                $tmp = $b->toArray();
+                array_unshift($tmp, $this->blockSize);
+                array_pop($tmp);
+                $b = SplFixedArray::fromArray($tmp, false);
+                continue;
+            }
+
+            $t[0] = $lambda[0];
+
+            for ($i = 0; $i < $this->numRoots; ++$i) {
+                if ($b[$i] !== $this->blockSize) {
+                    $t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
+                } else {
+                    $t[$i + 1] = $lambda[$i + 1];
+                }
+            }
+
+            if (2 * $el <= $r + $numErasures - 1) {
+                $el = $r + $numErasures - $el;
+
+                for ($i = 0; $i <= $this->numRoots; ++$i) {
+                    $b[$i] = (
+                        $lambda[$i] === 0
+                        ? $this->blockSize
+                        : $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
+                    );
+                }
+            } else {
+                $tmp = $b->toArray();
+                array_unshift($tmp, $this->blockSize);
+                array_pop($tmp);
+                $b = SplFixedArray::fromArray($tmp, false);
+            }
+
+            $lambda = clone $t;
+        }
+
+        // Convert lambda to index form and compute deg(lambda(x))
+        $degLambda = 0;
+
+        for ($i = 0; $i <= $this->numRoots; ++$i) {
+            $lambda[$i] = $this->indexOf[$lambda[$i]];
+
+            if ($lambda[$i] !== $this->blockSize) {
+                $degLambda = $i;
+            }
+        }
+
+        // Find roots of the error+erasure locator polynomial by Chien search.
+        $reg = clone $lambda;
+        $reg[0] = 0;
+        $count = 0;
+        $i = 1;
+
+        for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) {
+            $q = 1;
+
+            for ($j = $degLambda; $j > 0; $j--) {
+                if ($reg[$j] !== $this->blockSize) {
+                    $reg[$j] = $this->modNn($reg[$j] + $j);
+                    $q ^= $this->alphaTo[$reg[$j]];
+                }
+            }
+
+            if ($q !== 0) {
+                // Not a root
+                continue;
+            }
+
+            // Store root (index-form) and error location number
+            $root[$count] = $i;
+            $loc[$count] = $k;
+
+            if (++$count === $degLambda) {
+                break;
+            }
+        }
+
+        if ($degLambda !== $count) {
+            // deg(lambda) unequal to number of roots: uncorrectable error detected
+            return null;
+        }
+
+        // Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find
+        // deg(omega).
+        $degOmega = $degLambda - 1;
+
+        for ($i = 0; $i <= $degOmega; ++$i) {
+            $tmp = 0;
+
+            for ($j = $i; $j >= 0; --$j) {
+                if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
+                    $tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
+                }
+            }
+
+            $omega[$i] = $this->indexOf[$tmp];
+        }
+
+        // Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and
+        // den = lambda_pr(inv(X(l))) all in poly form.
+        for ($j = $count - 1; $j >= 0; --$j) {
+            $num1 = 0;
+
+            for ($i = $degOmega; $i >= 0; $i--) {
+                if ($omega[$i] !== $this->blockSize) {
+                    $num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
+                }
+            }
+
+            $num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
+            $den  = 0;
+
+            // lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i]
+            for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
+                if ($lambda[$i + 1] !== $this->blockSize) {
+                    $den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
+                }
+            }
+
+            // Apply error to data
+            if ($num1 !== 0 && $loc[$j] >= $this->padding) {
+                $data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
+                    $this->alphaTo[
+                        $this->modNn(
+                            $this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
+                        )
+                    ]
+                );
+            }
+        }
+
+        if (null !== $erasures) {
+            if (count($erasures) < $count) {
+                $erasures->setSize($count);
+            }
+
+            for ($i = 0; $i < $count; $i++) {
+                $erasures[$i] = $loc[$i];
+            }
+        }
+
+        return $count;
+    }
+
+    /**
+     * Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide.
+     */
+    private function modNn(int $x) : int
+    {
+        while ($x >= $this->blockSize) {
+            $x -= $this->blockSize;
+            $x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
+        }
+
+        return $x;
+    }
+}

+ 596 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/Version.php

@@ -0,0 +1,596 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use SplFixedArray;
+
+/**
+ * Version representation.
+ */
+final class Version
+{
+    private const VERSION_DECODE_INFO = [
+        0x07c94,
+        0x085bc,
+        0x09a99,
+        0x0a4d3,
+        0x0bbf6,
+        0x0c762,
+        0x0d847,
+        0x0e60d,
+        0x0f928,
+        0x10b78,
+        0x1145d,
+        0x12a17,
+        0x13532,
+        0x149a6,
+        0x15683,
+        0x168c9,
+        0x177ec,
+        0x18ec4,
+        0x191e1,
+        0x1afab,
+        0x1b08e,
+        0x1cc1a,
+        0x1d33f,
+        0x1ed75,
+        0x1f250,
+        0x209d5,
+        0x216f0,
+        0x228ba,
+        0x2379f,
+        0x24b0b,
+        0x2542e,
+        0x26a64,
+        0x27541,
+        0x28c69,
+    ];
+
+    /**
+     * Version number of this version.
+     *
+     * @var int
+     */
+    private $versionNumber;
+
+    /**
+     * Alignment pattern centers.
+     *
+     * @var SplFixedArray
+     */
+    private $alignmentPatternCenters;
+
+    /**
+     * Error correction blocks.
+     *
+     * @var EcBlocks[]
+     */
+    private $ecBlocks;
+
+    /**
+     * Total number of codewords.
+     *
+     * @var int
+     */
+    private $totalCodewords;
+
+    /**
+     * Cached version instances.
+     *
+     * @var array<int, self>|null
+     */
+    private static $versions;
+
+    /**
+     * @param int[] $alignmentPatternCenters
+     */
+    private function __construct(
+        int $versionNumber,
+        array $alignmentPatternCenters,
+        EcBlocks ...$ecBlocks
+    ) {
+        $this->versionNumber = $versionNumber;
+        $this->alignmentPatternCenters = $alignmentPatternCenters;
+        $this->ecBlocks = $ecBlocks;
+
+        $totalCodewords = 0;
+        $ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock();
+
+        foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) {
+            $totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords);
+        }
+
+        $this->totalCodewords = $totalCodewords;
+    }
+
+    /**
+     * Returns the version number.
+     */
+    public function getVersionNumber() : int
+    {
+        return $this->versionNumber;
+    }
+
+    /**
+     * Returns the alignment pattern centers.
+     *
+     * @return int[]
+     */
+    public function getAlignmentPatternCenters() : array
+    {
+        return $this->alignmentPatternCenters;
+    }
+
+    /**
+     * Returns the total number of codewords.
+     */
+    public function getTotalCodewords() : int
+    {
+        return $this->totalCodewords;
+    }
+
+    /**
+     * Calculates the dimension for the current version.
+     */
+    public function getDimensionForVersion() : int
+    {
+        return 17 + 4 * $this->versionNumber;
+    }
+
+    /**
+     * Returns the number of EC blocks for a specific EC level.
+     */
+    public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks
+    {
+        return $this->ecBlocks[$ecLevel->ordinal()];
+    }
+
+    /**
+     * Gets a provisional version number for a specific dimension.
+     *
+     * @throws InvalidArgumentException if dimension is not 1 mod 4
+     */
+    public static function getProvisionalVersionForDimension(int $dimension) : self
+    {
+        if (1 !== $dimension % 4) {
+            throw new InvalidArgumentException('Dimension is not 1 mod 4');
+        }
+
+        return self::getVersionForNumber(intdiv($dimension - 17, 4));
+    }
+
+    /**
+     * Gets a version instance for a specific version number.
+     *
+     * @throws InvalidArgumentException if version number is out of range
+     */
+    public static function getVersionForNumber(int $versionNumber) : self
+    {
+        if ($versionNumber < 1 || $versionNumber > 40) {
+            throw new InvalidArgumentException('Version number must be between 1 and 40');
+        }
+
+        return self::versions()[$versionNumber - 1];
+    }
+
+    /**
+     * Decodes version information from an integer and returns the version.
+     */
+    public static function decodeVersionInformation(int $versionBits) : ?self
+    {
+        $bestDifference = PHP_INT_MAX;
+        $bestVersion = 0;
+
+        foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) {
+            if ($targetVersion === $versionBits) {
+                return self::getVersionForNumber($i + 7);
+            }
+
+            $bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion);
+
+            if ($bitsDifference < $bestDifference) {
+                $bestVersion = $i + 7;
+                $bestDifference = $bitsDifference;
+            }
+        }
+
+        if ($bestDifference <= 3) {
+            return self::getVersionForNumber($bestVersion);
+        }
+
+        return null;
+    }
+
+    /**
+     * Builds the function pattern for the current version.
+     */
+    public function buildFunctionPattern() : BitMatrix
+    {
+        $dimension = $this->getDimensionForVersion();
+        $bitMatrix = new BitMatrix($dimension);
+
+        // Top left finder pattern + separator + format
+        $bitMatrix->setRegion(0, 0, 9, 9);
+        // Top right finder pattern + separator + format
+        $bitMatrix->setRegion($dimension - 8, 0, 8, 9);
+        // Bottom left finder pattern + separator + format
+        $bitMatrix->setRegion(0, $dimension - 8, 9, 8);
+
+        // Alignment patterns
+        $max = count($this->alignmentPatternCenters);
+
+        for ($x = 0; $x < $max; ++$x) {
+            $i = $this->alignmentPatternCenters[$x] - 2;
+
+            for ($y = 0; $y < $max; ++$y) {
+                if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) {
+                    // No alignment patterns near the three finder paterns
+                    continue;
+                }
+
+                $bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5);
+            }
+        }
+
+        // Vertical timing pattern
+        $bitMatrix->setRegion(6, 9, 1, $dimension - 17);
+        // Horizontal timing pattern
+        $bitMatrix->setRegion(9, 6, $dimension - 17, 1);
+
+        if ($this->versionNumber > 6) {
+            // Version info, top right
+            $bitMatrix->setRegion($dimension - 11, 0, 3, 6);
+            // Version info, bottom left
+            $bitMatrix->setRegion(0, $dimension - 11, 6, 3);
+        }
+
+        return $bitMatrix;
+    }
+
+    /**
+     * Returns a string representation for the version.
+     */
+    public function __toString() : string
+    {
+        return (string) $this->versionNumber;
+    }
+
+    /**
+     * Build and cache a specific version.
+     *
+     * See ISO 18004:2006 6.5.1 Table 9.
+     *
+     * @return array<int, self>
+     */
+    private static function versions() : array
+    {
+        if (null !== self::$versions) {
+            return self::$versions;
+        }
+
+        return self::$versions = [
+            new self(
+                1,
+                [],
+                new EcBlocks(7, new EcBlock(1, 19)),
+                new EcBlocks(10, new EcBlock(1, 16)),
+                new EcBlocks(13, new EcBlock(1, 13)),
+                new EcBlocks(17, new EcBlock(1, 9))
+            ),
+            new self(
+                2,
+                [6, 18],
+                new EcBlocks(10, new EcBlock(1, 34)),
+                new EcBlocks(16, new EcBlock(1, 28)),
+                new EcBlocks(22, new EcBlock(1, 22)),
+                new EcBlocks(28, new EcBlock(1, 16))
+            ),
+            new self(
+                3,
+                [6, 22],
+                new EcBlocks(15, new EcBlock(1, 55)),
+                new EcBlocks(26, new EcBlock(1, 44)),
+                new EcBlocks(18, new EcBlock(2, 17)),
+                new EcBlocks(22, new EcBlock(2, 13))
+            ),
+            new self(
+                4,
+                [6, 26],
+                new EcBlocks(20, new EcBlock(1, 80)),
+                new EcBlocks(18, new EcBlock(2, 32)),
+                new EcBlocks(26, new EcBlock(3, 24)),
+                new EcBlocks(16, new EcBlock(4, 9))
+            ),
+            new self(
+                5,
+                [6, 30],
+                new EcBlocks(26, new EcBlock(1, 108)),
+                new EcBlocks(24, new EcBlock(2, 43)),
+                new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)),
+                new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12))
+            ),
+            new self(
+                6,
+                [6, 34],
+                new EcBlocks(18, new EcBlock(2, 68)),
+                new EcBlocks(16, new EcBlock(4, 27)),
+                new EcBlocks(24, new EcBlock(4, 19)),
+                new EcBlocks(28, new EcBlock(4, 15))
+            ),
+            new self(
+                7,
+                [6, 22, 38],
+                new EcBlocks(20, new EcBlock(2, 78)),
+                new EcBlocks(18, new EcBlock(4, 31)),
+                new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)),
+                new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14))
+            ),
+            new self(
+                8,
+                [6, 24, 42],
+                new EcBlocks(24, new EcBlock(2, 97)),
+                new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)),
+                new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)),
+                new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15))
+            ),
+            new self(
+                9,
+                [6, 26, 46],
+                new EcBlocks(30, new EcBlock(2, 116)),
+                new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)),
+                new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)),
+                new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13))
+            ),
+            new self(
+                10,
+                [6, 28, 50],
+                new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)),
+                new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)),
+                new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)),
+                new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16))
+            ),
+            new self(
+                11,
+                [6, 30, 54],
+                new EcBlocks(20, new EcBlock(4, 81)),
+                new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)),
+                new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)),
+                new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13))
+            ),
+            new self(
+                12,
+                [6, 32, 58],
+                new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)),
+                new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)),
+                new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)),
+                new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15))
+            ),
+            new self(
+                13,
+                [6, 34, 62],
+                new EcBlocks(26, new EcBlock(4, 107)),
+                new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)),
+                new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)),
+                new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12))
+            ),
+            new self(
+                14,
+                [6, 26, 46, 66],
+                new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)),
+                new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)),
+                new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)),
+                new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13))
+            ),
+            new self(
+                15,
+                [6, 26, 48, 70],
+                new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)),
+                new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)),
+                new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)),
+                new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13))
+            ),
+            new self(
+                16,
+                [6, 26, 50, 74],
+                new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)),
+                new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)),
+                new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)),
+                new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16))
+            ),
+            new self(
+                17,
+                [6, 30, 54, 78],
+                new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)),
+                new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)),
+                new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)),
+                new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15))
+            ),
+            new self(
+                18,
+                [6, 30, 56, 82],
+                new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)),
+                new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)),
+                new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)),
+                new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15))
+            ),
+            new self(
+                19,
+                [6, 30, 58, 86],
+                new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)),
+                new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)),
+                new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)),
+                new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14))
+            ),
+            new self(
+                20,
+                [6, 34, 62, 90],
+                new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)),
+                new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)),
+                new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)),
+                new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16))
+            ),
+            new self(
+                21,
+                [6, 28, 50, 72, 94],
+                new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)),
+                new EcBlocks(26, new EcBlock(17, 42)),
+                new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)),
+                new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17))
+            ),
+            new self(
+                22,
+                [6, 26, 50, 74, 98],
+                new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)),
+                new EcBlocks(28, new EcBlock(17, 46)),
+                new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)),
+                new EcBlocks(24, new EcBlock(34, 13))
+            ),
+            new self(
+                23,
+                [6, 30, 54, 78, 102],
+                new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)),
+                new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)),
+                new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)),
+                new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16))
+            ),
+            new self(
+                24,
+                [6, 28, 54, 80, 106],
+                new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)),
+                new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)),
+                new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)),
+                new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17))
+            ),
+            new self(
+                25,
+                [6, 32, 58, 84, 110],
+                new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)),
+                new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)),
+                new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)),
+                new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16))
+            ),
+            new self(
+                26,
+                [6, 30, 58, 86, 114],
+                new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)),
+                new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)),
+                new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)),
+                new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17))
+            ),
+            new self(
+                27,
+                [6, 34, 62, 90, 118],
+                new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)),
+                new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)),
+                new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)),
+                new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16))
+            ),
+            new self(
+                28,
+                [6, 26, 50, 74, 98, 122],
+                new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)),
+                new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)),
+                new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)),
+                new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16))
+            ),
+            new self(
+                29,
+                [6, 30, 54, 78, 102, 126],
+                new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)),
+                new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)),
+                new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)),
+                new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16))
+            ),
+            new self(
+                30,
+                [6, 26, 52, 78, 104, 130],
+                new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)),
+                new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)),
+                new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)),
+                new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16))
+            ),
+            new self(
+                31,
+                [6, 30, 56, 82, 108, 134],
+                new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)),
+                new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)),
+                new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)),
+                new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16))
+            ),
+            new self(
+                32,
+                [6, 34, 60, 86, 112, 138],
+                new EcBlocks(30, new EcBlock(17, 115)),
+                new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)),
+                new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)),
+                new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16))
+            ),
+            new self(
+                33,
+                [6, 30, 58, 86, 114, 142],
+                new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)),
+                new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)),
+                new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)),
+                new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16))
+            ),
+            new self(
+                34,
+                [6, 34, 62, 90, 118, 146],
+                new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)),
+                new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)),
+                new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)),
+                new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17))
+            ),
+            new self(
+                35,
+                [6, 30, 54, 78, 102, 126, 150],
+                new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)),
+                new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)),
+                new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)),
+                new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16))
+            ),
+            new self(
+                36,
+                [6, 24, 50, 76, 102, 128, 154],
+                new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)),
+                new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)),
+                new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)),
+                new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16))
+            ),
+            new self(
+                37,
+                [6, 28, 54, 80, 106, 132, 158],
+                new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)),
+                new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)),
+                new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)),
+                new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16))
+            ),
+            new self(
+                38,
+                [6, 32, 58, 84, 110, 136, 162],
+                new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)),
+                new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)),
+                new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)),
+                new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16))
+            ),
+            new self(
+                39,
+                [6, 26, 54, 82, 110, 138, 166],
+                new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)),
+                new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)),
+                new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)),
+                new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16))
+            ),
+            new self(
+                40,
+                [6, 30, 58, 86, 114, 142, 170],
+                new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)),
+                new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)),
+                new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)),
+                new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16))
+            ),
+        ];
+    }
+}

+ 58 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/BlockPair.php

@@ -0,0 +1,58 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use SplFixedArray;
+
+/**
+ * Block pair.
+ */
+final class BlockPair
+{
+    /**
+     * Data bytes in the block.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $dataBytes;
+
+    /**
+     * Error correction bytes in the block.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $errorCorrectionBytes;
+
+    /**
+     * Creates a new block pair.
+     *
+     * @param SplFixedArray<int> $data
+     * @param SplFixedArray<int> $errorCorrection
+     */
+    public function __construct(SplFixedArray $data, SplFixedArray $errorCorrection)
+    {
+        $this->dataBytes = $data;
+        $this->errorCorrectionBytes = $errorCorrection;
+    }
+
+    /**
+     * Gets the data bytes.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function getDataBytes() : SplFixedArray
+    {
+        return $this->dataBytes;
+    }
+
+    /**
+     * Gets the error correction bytes.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function getErrorCorrectionBytes() : SplFixedArray
+    {
+        return $this->errorCorrectionBytes;
+    }
+}

+ 150 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/ByteMatrix.php

@@ -0,0 +1,150 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use SplFixedArray;
+use Traversable;
+
+/**
+ * Byte matrix.
+ */
+final class ByteMatrix
+{
+    /**
+     * Bytes in the matrix, represented as array.
+     *
+     * @var SplFixedArray<SplFixedArray<int>>
+     */
+    private $bytes;
+
+    /**
+     * Width of the matrix.
+     *
+     * @var int
+     */
+    private $width;
+
+    /**
+     * Height of the matrix.
+     *
+     * @var int
+     */
+    private $height;
+
+    public function __construct(int $width, int $height)
+    {
+        $this->height = $height;
+        $this->width = $width;
+        $this->bytes = new SplFixedArray($height);
+
+        for ($y = 0; $y < $height; ++$y) {
+            $this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0));
+        }
+    }
+
+    /**
+     * Gets the width of the matrix.
+     */
+    public function getWidth() : int
+    {
+        return $this->width;
+    }
+
+    /**
+     * Gets the height of the matrix.
+     */
+    public function getHeight() : int
+    {
+        return $this->height;
+    }
+
+    /**
+     * Gets the internal representation of the matrix.
+     *
+     * @return SplFixedArray<SplFixedArray<int>>
+     */
+    public function getArray() : SplFixedArray
+    {
+        return $this->bytes;
+    }
+
+    /**
+     * @return Traversable<int>
+     */
+    public function getBytes() : Traversable
+    {
+        foreach ($this->bytes as $row) {
+            foreach ($row as $byte) {
+                yield $byte;
+            }
+        }
+    }
+
+    /**
+     * Gets the byte for a specific position.
+     */
+    public function get(int $x, int $y) : int
+    {
+        return $this->bytes[$y][$x];
+    }
+
+    /**
+     * Sets the byte for a specific position.
+     */
+    public function set(int $x, int $y, int $value) : void
+    {
+        $this->bytes[$y][$x] = $value;
+    }
+
+    /**
+     * Clears the matrix with a specific value.
+     */
+    public function clear(int $value) : void
+    {
+        for ($y = 0; $y < $this->height; ++$y) {
+            for ($x = 0; $x < $this->width; ++$x) {
+                $this->bytes[$y][$x] = $value;
+            }
+        }
+    }
+
+    public function __clone()
+    {
+        $this->bytes = clone $this->bytes;
+
+        foreach ($this->bytes as $index => $row) {
+            $this->bytes[$index] = clone $row;
+        }
+    }
+
+    /**
+     * Returns a string representation of the matrix.
+     */
+    public function __toString() : string
+    {
+        $result = '';
+
+        for ($y = 0; $y < $this->height; $y++) {
+            for ($x = 0; $x < $this->width; $x++) {
+                switch ($this->bytes[$y][$x]) {
+                    case 0:
+                        $result .= ' 0';
+                        break;
+
+                    case 1:
+                        $result .= ' 1';
+                        break;
+
+                    default:
+                        $result .= '  ';
+                        break;
+                }
+            }
+
+            $result .= "\n";
+        }
+
+        return $result;
+    }
+}

+ 668 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/Encoder.php

@@ -0,0 +1,668 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\BitArray;
+use BaconQrCode\Common\CharacterSetEci;
+use BaconQrCode\Common\ErrorCorrectionLevel;
+use BaconQrCode\Common\Mode;
+use BaconQrCode\Common\ReedSolomonCodec;
+use BaconQrCode\Common\Version;
+use BaconQrCode\Exception\WriterException;
+use SplFixedArray;
+
+/**
+ * Encoder.
+ */
+final class Encoder
+{
+    /**
+     * Default byte encoding.
+     */
+    public const DEFAULT_BYTE_MODE_ECODING = 'ISO-8859-1';
+
+    /**
+     * The original table is defined in the table 5 of JISX0510:2004 (p.19).
+     */
+    private const ALPHANUMERIC_TABLE = [
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x00-0x0f
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x10-0x1f
+        36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43,  // 0x20-0x2f
+        0,   1,  2,  3,  4,  5,  6,  7,  8,  9, 44, -1, -1, -1, -1, -1,  // 0x30-0x3f
+        -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,  // 0x40-0x4f
+        25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1,  // 0x50-0x5f
+    ];
+
+    /**
+     * Codec cache.
+     *
+     * @var array<string,ReedSolomonCodec>
+     */
+    private static $codecs = [];
+
+    /**
+     * Encodes "content" with the error correction level "ecLevel".
+     */
+    public static function encode(
+        string $content,
+        ErrorCorrectionLevel $ecLevel,
+        string $encoding = self::DEFAULT_BYTE_MODE_ECODING,
+        ?Version $forcedVersion = null
+    ) : QrCode {
+        // Pick an encoding mode appropriate for the content. Note that this
+        // will not attempt to use multiple modes / segments even if that were
+        // more efficient. Would be nice.
+        $mode = self::chooseMode($content, $encoding);
+
+        // This will store the header information, like mode and length, as well
+        // as "header" segments like an ECI segment.
+        $headerBits = new BitArray();
+
+        // Append ECI segment if applicable
+        if (Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ECODING !== $encoding) {
+            $eci = CharacterSetEci::getCharacterSetEciByName($encoding);
+
+            if (null !== $eci) {
+                self::appendEci($eci, $headerBits);
+            }
+        }
+
+        // (With ECI in place,) Write the mode marker
+        self::appendModeInfo($mode, $headerBits);
+
+        // Collect data within the main segment, separately, to count its size
+        // if needed. Don't add it to main payload yet.
+        $dataBits = new BitArray();
+        self::appendBytes($content, $mode, $dataBits, $encoding);
+
+        // Hard part: need to know version to know how many bits length takes.
+        // But need to know how many bits it takes to know version. First we
+        // take a guess at version by assuming version will be the minimum, 1:
+        $provisionalBitsNeeded = $headerBits->getSize()
+            + $mode->getCharacterCountBits(Version::getVersionForNumber(1))
+            + $dataBits->getSize();
+        $provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
+
+        // Use that guess to calculate the right version. I am still not sure
+        // this works in 100% of cases.
+        $bitsNeeded = $headerBits->getSize()
+            + $mode->getCharacterCountBits($provisionalVersion)
+            + $dataBits->getSize();
+        $version = self::chooseVersion($bitsNeeded, $ecLevel);
+
+        if (null !== $forcedVersion) {
+            // Forced version check
+            if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) {
+                // Calculated minimum version is same or equal as forced version
+                $version = $forcedVersion;
+            } else {
+                throw new WriterException(
+                    'Invalid version! Calculated version: '
+                    . $version->getVersionNumber()
+                    . ', requested version: '
+                    . $forcedVersion->getVersionNumber()
+                );
+            }
+        }
+
+        $headerAndDataBits = new BitArray();
+        $headerAndDataBits->appendBitArray($headerBits);
+
+        // Find "length" of main segment and write it.
+        $numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content));
+        self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
+
+        // Put data together into the overall payload.
+        $headerAndDataBits->appendBitArray($dataBits);
+        $ecBlocks = $version->getEcBlocksForLevel($ecLevel);
+        $numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
+
+        // Terminate the bits properly.
+        self::terminateBits($numDataBytes, $headerAndDataBits);
+
+        // Interleave data bits with error correction code.
+        $finalBits = self::interleaveWithEcBytes(
+            $headerAndDataBits,
+            $version->getTotalCodewords(),
+            $numDataBytes,
+            $ecBlocks->getNumBlocks()
+        );
+
+        // Choose the mask pattern.
+        $dimension = $version->getDimensionForVersion();
+        $matrix = new ByteMatrix($dimension, $dimension);
+        $maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
+
+        // Build the matrix.
+        MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
+
+        return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix);
+    }
+
+    /**
+     * Gets the alphanumeric code for a byte.
+     */
+    private static function getAlphanumericCode(int $code) : int
+    {
+        if (isset(self::ALPHANUMERIC_TABLE[$code])) {
+            return self::ALPHANUMERIC_TABLE[$code];
+        }
+
+        return -1;
+    }
+
+    /**
+     * Chooses the best mode for a given content.
+     */
+    private static function chooseMode(string $content, string $encoding = null) : Mode
+    {
+        if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) {
+            return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
+        }
+
+        $hasNumeric = false;
+        $hasAlphanumeric = false;
+        $contentLength = strlen($content);
+
+        for ($i = 0; $i < $contentLength; ++$i) {
+            $char = $content[$i];
+
+            if (ctype_digit($char)) {
+                $hasNumeric = true;
+            } elseif (-1 !== self::getAlphanumericCode(ord($char))) {
+                $hasAlphanumeric = true;
+            } else {
+                return Mode::BYTE();
+            }
+        }
+
+        if ($hasAlphanumeric) {
+            return Mode::ALPHANUMERIC();
+        } elseif ($hasNumeric) {
+            return Mode::NUMERIC();
+        }
+
+        return Mode::BYTE();
+    }
+
+    /**
+     * Calculates the mask penalty for a matrix.
+     */
+    private static function calculateMaskPenalty(ByteMatrix $matrix) : int
+    {
+        return (
+            MaskUtil::applyMaskPenaltyRule1($matrix)
+            + MaskUtil::applyMaskPenaltyRule2($matrix)
+            + MaskUtil::applyMaskPenaltyRule3($matrix)
+            + MaskUtil::applyMaskPenaltyRule4($matrix)
+        );
+    }
+
+    /**
+     * Checks if content only consists of double-byte kanji characters.
+     */
+    private static function isOnlyDoubleByteKanji(string $content) : bool
+    {
+        $bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
+
+        if (false === $bytes) {
+            return false;
+        }
+
+        $length = strlen($bytes);
+
+        if (0 !== $length % 2) {
+            return false;
+        }
+
+        for ($i = 0; $i < $length; $i += 2) {
+            $byte = $bytes[$i] & 0xff;
+
+            if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Chooses the best mask pattern for a matrix.
+     */
+    private static function chooseMaskPattern(
+        BitArray $bits,
+        ErrorCorrectionLevel $ecLevel,
+        Version $version,
+        ByteMatrix $matrix
+    ) : int {
+        $minPenalty = PHP_INT_MAX;
+        $bestMaskPattern = -1;
+
+        for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) {
+            MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
+            $penalty = self::calculateMaskPenalty($matrix);
+
+            if ($penalty < $minPenalty) {
+                $minPenalty = $penalty;
+                $bestMaskPattern = $maskPattern;
+            }
+        }
+
+        return $bestMaskPattern;
+    }
+
+    /**
+     * Chooses the best version for the input.
+     *
+     * @throws WriterException if data is too big
+     */
+    private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version
+    {
+        for ($versionNum = 1; $versionNum <= 40; ++$versionNum) {
+            $version = Version::getVersionForNumber($versionNum);
+            $numBytes = $version->getTotalCodewords();
+
+            $ecBlocks = $version->getEcBlocksForLevel($ecLevel);
+            $numEcBytes = $ecBlocks->getTotalEcCodewords();
+
+            $numDataBytes = $numBytes - $numEcBytes;
+            $totalInputBytes = intdiv($numInputBits + 8, 8);
+
+            if ($numDataBytes >= $totalInputBytes) {
+                return $version;
+            }
+        }
+
+        throw new WriterException('Data too big');
+    }
+
+    /**
+     * Terminates the bits in a bit array.
+     *
+     * @throws WriterException if data bits cannot fit in the QR code
+     * @throws WriterException if bits size does not equal the capacity
+     */
+    private static function terminateBits(int $numDataBytes, BitArray $bits) : void
+    {
+        $capacity = $numDataBytes << 3;
+
+        if ($bits->getSize() > $capacity) {
+            throw new WriterException('Data bits cannot fit in the QR code');
+        }
+
+        for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) {
+            $bits->appendBit(false);
+        }
+
+        $numBitsInLastByte = $bits->getSize() & 0x7;
+
+        if ($numBitsInLastByte > 0) {
+            for ($i = $numBitsInLastByte; $i < 8; ++$i) {
+                $bits->appendBit(false);
+            }
+        }
+
+        $numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
+
+        for ($i = 0; $i < $numPaddingBytes; ++$i) {
+            $bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8);
+        }
+
+        if ($bits->getSize() !== $capacity) {
+            throw new WriterException('Bits size does not equal capacity');
+        }
+    }
+
+    /**
+     * Gets number of data- and EC bytes for a block ID.
+     *
+     * @return int[]
+     * @throws WriterException if block ID is too large
+     * @throws WriterException if EC bytes mismatch
+     * @throws WriterException if RS blocks mismatch
+     * @throws WriterException if total bytes mismatch
+     */
+    private static function getNumDataBytesAndNumEcBytesForBlockId(
+        int $numTotalBytes,
+        int $numDataBytes,
+        int $numRsBlocks,
+        int $blockId
+    ) : array {
+        if ($blockId >= $numRsBlocks) {
+            throw new WriterException('Block ID too large');
+        }
+
+        $numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
+        $numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
+        $numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks);
+        $numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
+        $numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks);
+        $numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
+        $numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
+        $numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
+
+        if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
+            throw new WriterException('EC bytes mismatch');
+        }
+
+        if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
+            throw new WriterException('RS blocks mismatch');
+        }
+
+        if ($numTotalBytes !==
+            (($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1)
+            + (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2)
+        ) {
+            throw new WriterException('Total bytes mismatch');
+        }
+
+        if ($blockId < $numRsBlocksInGroup1) {
+            return [$numDataBytesInGroup1, $numEcBytesInGroup1];
+        } else {
+            return [$numDataBytesInGroup2, $numEcBytesInGroup2];
+        }
+    }
+
+    /**
+     * Interleaves data with EC bytes.
+     *
+     * @throws WriterException if number of bits and data bytes does not match
+     * @throws WriterException if data bytes does not match offset
+     * @throws WriterException if an interleaving error occurs
+     */
+    private static function interleaveWithEcBytes(
+        BitArray $bits,
+        int $numTotalBytes,
+        int $numDataBytes,
+        int $numRsBlocks
+    ) : BitArray {
+        if ($bits->getSizeInBytes() !== $numDataBytes) {
+            throw new WriterException('Number of bits and data bytes does not match');
+        }
+
+        $dataBytesOffset = 0;
+        $maxNumDataBytes = 0;
+        $maxNumEcBytes   = 0;
+
+        $blocks = new SplFixedArray($numRsBlocks);
+
+        for ($i = 0; $i < $numRsBlocks; ++$i) {
+            list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId(
+                $numTotalBytes,
+                $numDataBytes,
+                $numRsBlocks,
+                $i
+            );
+
+            $size = $numDataBytesInBlock;
+            $dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
+            $ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
+            $blocks[$i] = new BlockPair($dataBytes, $ecBytes);
+
+            $maxNumDataBytes = max($maxNumDataBytes, $size);
+            $maxNumEcBytes = max($maxNumEcBytes, count($ecBytes));
+            $dataBytesOffset += $numDataBytesInBlock;
+        }
+
+        if ($numDataBytes !== $dataBytesOffset) {
+            throw new WriterException('Data bytes does not match offset');
+        }
+
+        $result = new BitArray();
+
+        for ($i = 0; $i < $maxNumDataBytes; ++$i) {
+            foreach ($blocks as $block) {
+                $dataBytes = $block->getDataBytes();
+
+                if ($i < count($dataBytes)) {
+                    $result->appendBits($dataBytes[$i], 8);
+                }
+            }
+        }
+
+        for ($i = 0; $i < $maxNumEcBytes; ++$i) {
+            foreach ($blocks as $block) {
+                $ecBytes = $block->getErrorCorrectionBytes();
+
+                if ($i < count($ecBytes)) {
+                    $result->appendBits($ecBytes[$i], 8);
+                }
+            }
+        }
+
+        if ($numTotalBytes !== $result->getSizeInBytes()) {
+            throw new WriterException(
+                'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ'
+            );
+        }
+
+        return $result;
+    }
+
+    /**
+     * Generates EC bytes for given data.
+     *
+     * @param  SplFixedArray<int> $dataBytes
+     * @return SplFixedArray<int>
+     */
+    private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray
+    {
+        $numDataBytes = count($dataBytes);
+        $toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
+
+        for ($i = 0; $i < $numDataBytes; $i++) {
+            $toEncode[$i] = $dataBytes[$i] & 0xff;
+        }
+
+        $ecBytes = new SplFixedArray($numEcBytesInBlock);
+        $codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
+        $codec->encode($toEncode, $ecBytes);
+
+        return $ecBytes;
+    }
+
+    /**
+     * Gets an RS codec and caches it.
+     */
+    private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec
+    {
+        $cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
+
+        if (isset(self::$codecs[$cacheId])) {
+            return self::$codecs[$cacheId];
+        }
+
+        return self::$codecs[$cacheId] = new ReedSolomonCodec(
+            8,
+            0x11d,
+            0,
+            1,
+            $numEcBytesInBlock,
+            255 - $numDataBytes - $numEcBytesInBlock
+        );
+    }
+
+    /**
+     * Appends mode information to a bit array.
+     */
+    private static function appendModeInfo(Mode $mode, BitArray $bits) : void
+    {
+        $bits->appendBits($mode->getBits(), 4);
+    }
+
+    /**
+     * Appends length information to a bit array.
+     *
+     * @throws WriterException if num letters is bigger than expected
+     */
+    private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void
+    {
+        $numBits = $mode->getCharacterCountBits($version);
+
+        if ($numLetters >= (1 << $numBits)) {
+            throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
+        }
+
+        $bits->appendBits($numLetters, $numBits);
+    }
+
+    /**
+     * Appends bytes to a bit array in a specific mode.
+     *
+     * @throws WriterException if an invalid mode was supplied
+     */
+    private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
+    {
+        switch ($mode) {
+            case Mode::NUMERIC():
+                self::appendNumericBytes($content, $bits);
+                break;
+
+            case Mode::ALPHANUMERIC():
+                self::appendAlphanumericBytes($content, $bits);
+                break;
+
+            case Mode::BYTE():
+                self::append8BitBytes($content, $bits, $encoding);
+                break;
+
+            case Mode::KANJI():
+                self::appendKanjiBytes($content, $bits);
+                break;
+
+            default:
+                throw new WriterException('Invalid mode: ' . $mode);
+        }
+    }
+
+    /**
+     * Appends numeric bytes to a bit array.
+     */
+    private static function appendNumericBytes(string $content, BitArray $bits) : void
+    {
+        $length = strlen($content);
+        $i = 0;
+
+        while ($i < $length) {
+            $num1 = (int) $content[$i];
+
+            if ($i + 2 < $length) {
+                // Encode three numeric letters in ten bits.
+                $num2 = (int) $content[$i + 1];
+                $num3 = (int) $content[$i + 2];
+                $bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
+                $i += 3;
+            } elseif ($i + 1 < $length) {
+                // Encode two numeric letters in seven bits.
+                $num2 = (int) $content[$i + 1];
+                $bits->appendBits($num1 * 10 + $num2, 7);
+                $i += 2;
+            } else {
+                // Encode one numeric letter in four bits.
+                $bits->appendBits($num1, 4);
+                ++$i;
+            }
+        }
+    }
+
+    /**
+     * Appends alpha-numeric bytes to a bit array.
+     *
+     * @throws WriterException if an invalid alphanumeric code was found
+     */
+    private static function appendAlphanumericBytes(string $content, BitArray $bits) : void
+    {
+        $length = strlen($content);
+        $i = 0;
+
+        while ($i < $length) {
+            $code1 = self::getAlphanumericCode(ord($content[$i]));
+
+            if (-1 === $code1) {
+                throw new WriterException('Invalid alphanumeric code');
+            }
+
+            if ($i + 1 < $length) {
+                $code2 = self::getAlphanumericCode(ord($content[$i + 1]));
+
+                if (-1 === $code2) {
+                    throw new WriterException('Invalid alphanumeric code');
+                }
+
+                // Encode two alphanumeric letters in 11 bits.
+                $bits->appendBits($code1 * 45 + $code2, 11);
+                $i += 2;
+            } else {
+                // Encode one alphanumeric letter in six bits.
+                $bits->appendBits($code1, 6);
+                ++$i;
+            }
+        }
+    }
+
+    /**
+     * Appends regular 8-bit bytes to a bit array.
+     *
+     * @throws WriterException if content cannot be encoded to target encoding
+     */
+    private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void
+    {
+        $bytes = @iconv('utf-8', $encoding, $content);
+
+        if (false === $bytes) {
+            throw new WriterException('Could not encode content to ' . $encoding);
+        }
+
+        $length = strlen($bytes);
+
+        for ($i = 0; $i < $length; $i++) {
+            $bits->appendBits(ord($bytes[$i]), 8);
+        }
+    }
+
+    /**
+     * Appends KANJI bytes to a bit array.
+     *
+     * @throws WriterException if content does not seem to be encoded in SHIFT-JIS
+     * @throws WriterException if an invalid byte sequence occurs
+     */
+    private static function appendKanjiBytes(string $content, BitArray $bits) : void
+    {
+        if (strlen($content) % 2 > 0) {
+            // We just do a simple length check here. The for loop will check
+            // individual characters.
+            throw new WriterException('Content does not seem to be encoded in SHIFT-JIS');
+        }
+
+        $length = strlen($content);
+
+        for ($i = 0; $i < $length; $i += 2) {
+            $byte1 = ord($content[$i]) & 0xff;
+            $byte2 = ord($content[$i + 1]) & 0xff;
+            $code = ($byte1 << 8) | $byte2;
+
+            if ($code >= 0x8140 && $code <= 0x9ffc) {
+                $subtracted = $code - 0x8140;
+            } elseif ($code >= 0xe040 && $code <= 0xebbf) {
+                $subtracted = $code - 0xc140;
+            } else {
+                throw new WriterException('Invalid byte sequence');
+            }
+
+            $encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff);
+
+            $bits->appendBits($encoded, 13);
+        }
+    }
+
+    /**
+     * Appends ECI information to a bit array.
+     */
+    private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void
+    {
+        $mode = Mode::ECI();
+        $bits->appendBits($mode->getBits(), 4);
+        $bits->appendBits($eci->getValue(), 8);
+    }
+}

+ 271 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/MaskUtil.php

@@ -0,0 +1,271 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\BitUtils;
+use BaconQrCode\Exception\InvalidArgumentException;
+
+/**
+ * Mask utility.
+ */
+final class MaskUtil
+{
+    /**#@+
+     * Penalty weights from section 6.8.2.1
+     */
+    const N1 = 3;
+    const N2 = 3;
+    const N3 = 40;
+    const N4 = 10;
+    /**#@-*/
+
+    private function __construct()
+    {
+    }
+
+    /**
+     * Applies mask penalty rule 1 and returns the penalty.
+     *
+     * Finds repetitive cells with the same color and gives penalty to them.
+     * Example: 00000 or 11111.
+     */
+    public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int
+    {
+        return (
+            self::applyMaskPenaltyRule1Internal($matrix, true)
+            + self::applyMaskPenaltyRule1Internal($matrix, false)
+        );
+    }
+
+    /**
+     * Applies mask penalty rule 2 and returns the penalty.
+     *
+     * Finds 2x2 blocks with the same color and gives penalty to them. This is
+     * actually equivalent to the spec's rule, which is to find MxN blocks and
+     * give a penalty proportional to (M-1)x(N-1), because this is the number of
+     * 2x2 blocks inside such a block.
+     */
+    public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int
+    {
+        $penalty = 0;
+        $array = $matrix->getArray();
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+
+        for ($y = 0; $y < $height - 1; ++$y) {
+            for ($x = 0; $x < $width - 1; ++$x) {
+                $value = $array[$y][$x];
+
+                if ($value === $array[$y][$x + 1]
+                    && $value === $array[$y + 1][$x]
+                    && $value === $array[$y + 1][$x + 1]
+                ) {
+                    ++$penalty;
+                }
+            }
+        }
+
+        return self::N2 * $penalty;
+    }
+
+    /**
+     * Applies mask penalty rule 3 and returns the penalty.
+     *
+     * Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
+     * to them. If we find patterns like 000010111010000, we give penalties
+     * twice (i.e. 40 * 2).
+     */
+    public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int
+    {
+        $penalty = 0;
+        $array = $matrix->getArray();
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+
+        for ($y = 0; $y < $height; ++$y) {
+            for ($x = 0; $x < $width; ++$x) {
+                if ($x + 6 < $width
+                    && 1 === $array[$y][$x]
+                    && 0 === $array[$y][$x + 1]
+                    && 1 === $array[$y][$x + 2]
+                    && 1 === $array[$y][$x + 3]
+                    && 1 === $array[$y][$x + 4]
+                    && 0 === $array[$y][$x + 5]
+                    && 1 === $array[$y][$x + 6]
+                    && (
+                        (
+                            $x + 10 < $width
+                            && 0 === $array[$y][$x + 7]
+                            && 0 === $array[$y][$x + 8]
+                            && 0 === $array[$y][$x + 9]
+                            && 0 === $array[$y][$x + 10]
+                        )
+                        || (
+                            $x - 4 >= 0
+                            && 0 === $array[$y][$x - 1]
+                            && 0 === $array[$y][$x - 2]
+                            && 0 === $array[$y][$x - 3]
+                            && 0 === $array[$y][$x - 4]
+                        )
+                    )
+                ) {
+                    $penalty += self::N3;
+                }
+
+                if ($y + 6 < $height
+                    && 1 === $array[$y][$x]
+                    && 0 === $array[$y + 1][$x]
+                    && 1 === $array[$y + 2][$x]
+                    && 1 === $array[$y + 3][$x]
+                    && 1 === $array[$y + 4][$x]
+                    && 0 === $array[$y + 5][$x]
+                    && 1 === $array[$y + 6][$x]
+                    && (
+                        (
+                            $y + 10 < $height
+                            && 0 === $array[$y + 7][$x]
+                            && 0 === $array[$y + 8][$x]
+                            && 0 === $array[$y + 9][$x]
+                            && 0 === $array[$y + 10][$x]
+                        )
+                        || (
+                            $y - 4 >= 0
+                            && 0 === $array[$y - 1][$x]
+                            && 0 === $array[$y - 2][$x]
+                            && 0 === $array[$y - 3][$x]
+                            && 0 === $array[$y - 4][$x]
+                        )
+                    )
+                ) {
+                    $penalty += self::N3;
+                }
+            }
+        }
+
+        return $penalty;
+    }
+
+    /**
+     * Applies mask penalty rule 4 and returns the penalty.
+     *
+     * Calculates the ratio of dark cells and gives penalty if the ratio is far
+     * from 50%. It gives 10 penalty for 5% distance.
+     */
+    public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int
+    {
+        $numDarkCells = 0;
+
+        $array = $matrix->getArray();
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+
+        for ($y = 0; $y < $height; ++$y) {
+            $arrayY = $array[$y];
+
+            for ($x = 0; $x < $width; ++$x) {
+                if (1 === $arrayY[$x]) {
+                    ++$numDarkCells;
+                }
+            }
+        }
+
+        $numTotalCells = $height * $width;
+        $darkRatio = $numDarkCells / $numTotalCells;
+        $fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20);
+
+        return $fixedPercentVariances * self::N4;
+    }
+
+    /**
+     * Returns the mask bit for "getMaskPattern" at "x" and "y".
+     *
+     * See 8.8 of JISX0510:2004 for mask pattern conditions.
+     *
+     * @throws InvalidArgumentException if an invalid mask pattern was supplied
+     */
+    public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
+    {
+        switch ($maskPattern) {
+            case 0:
+                $intermediate = ($y + $x) & 0x1;
+                break;
+
+            case 1:
+                $intermediate = $y & 0x1;
+                break;
+
+            case 2:
+                $intermediate = $x % 3;
+                break;
+
+            case 3:
+                $intermediate = ($y + $x) % 3;
+                break;
+
+            case 4:
+                $intermediate = (BitUtils::unsignedRightShift($y, 1) + (int) ($x / 3)) & 0x1;
+                break;
+
+            case 5:
+                $temp = $y * $x;
+                $intermediate = ($temp & 0x1) + ($temp % 3);
+                break;
+
+            case 6:
+                $temp = $y * $x;
+                $intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1;
+                break;
+
+            case 7:
+                $temp = $y * $x;
+                $intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1;
+                break;
+
+            default:
+                throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern);
+        }
+
+        return 0 == $intermediate;
+    }
+
+    /**
+     * Helper function for applyMaskPenaltyRule1.
+     *
+     * We need this for doing this calculation in both vertical and horizontal
+     * orders respectively.
+     */
+    private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int
+    {
+        $penalty = 0;
+        $iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
+        $jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
+        $array = $matrix->getArray();
+
+        for ($i = 0; $i < $iLimit; ++$i) {
+            $numSameBitCells = 0;
+            $prevBit = -1;
+
+            for ($j = 0; $j < $jLimit; $j++) {
+                $bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
+
+                if ($bit === $prevBit) {
+                    ++$numSameBitCells;
+                } else {
+                    if ($numSameBitCells >= 5) {
+                        $penalty += self::N1 + ($numSameBitCells - 5);
+                    }
+
+                    $numSameBitCells = 1;
+                    $prevBit = $bit;
+                }
+            }
+
+            if ($numSameBitCells >= 5) {
+                $penalty += self::N1 + ($numSameBitCells - 5);
+            }
+        }
+
+        return $penalty;
+    }
+}

+ 513 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/MatrixUtil.php

@@ -0,0 +1,513 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\BitArray;
+use BaconQrCode\Common\ErrorCorrectionLevel;
+use BaconQrCode\Common\Version;
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Exception\WriterException;
+
+/**
+ * Matrix utility.
+ */
+final class MatrixUtil
+{
+    /**
+     * Position detection pattern.
+     */
+    private const POSITION_DETECTION_PATTERN = [
+        [1, 1, 1, 1, 1, 1, 1],
+        [1, 0, 0, 0, 0, 0, 1],
+        [1, 0, 1, 1, 1, 0, 1],
+        [1, 0, 1, 1, 1, 0, 1],
+        [1, 0, 1, 1, 1, 0, 1],
+        [1, 0, 0, 0, 0, 0, 1],
+        [1, 1, 1, 1, 1, 1, 1],
+    ];
+
+    /**
+     * Position adjustment pattern.
+     */
+    private const POSITION_ADJUSTMENT_PATTERN = [
+        [1, 1, 1, 1, 1],
+        [1, 0, 0, 0, 1],
+        [1, 0, 1, 0, 1],
+        [1, 0, 0, 0, 1],
+        [1, 1, 1, 1, 1],
+    ];
+
+    /**
+     * Coordinates for position adjustment patterns for each version.
+     */
+    private const POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = [
+        [null, null, null, null, null, null, null], // Version 1
+        [   6,   18, null, null, null, null, null], // Version 2
+        [   6,   22, null, null, null, null, null], // Version 3
+        [   6,   26, null, null, null, null, null], // Version 4
+        [   6,   30, null, null, null, null, null], // Version 5
+        [   6,   34, null, null, null, null, null], // Version 6
+        [   6,   22,   38, null, null, null, null], // Version 7
+        [   6,   24,   42, null, null, null, null], // Version 8
+        [   6,   26,   46, null, null, null, null], // Version 9
+        [   6,   28,   50, null, null, null, null], // Version 10
+        [   6,   30,   54, null, null, null, null], // Version 11
+        [   6,   32,   58, null, null, null, null], // Version 12
+        [   6,   34,   62, null, null, null, null], // Version 13
+        [   6,   26,   46,   66, null, null, null], // Version 14
+        [   6,   26,   48,   70, null, null, null], // Version 15
+        [   6,   26,   50,   74, null, null, null], // Version 16
+        [   6,   30,   54,   78, null, null, null], // Version 17
+        [   6,   30,   56,   82, null, null, null], // Version 18
+        [   6,   30,   58,   86, null, null, null], // Version 19
+        [   6,   34,   62,   90, null, null, null], // Version 20
+        [   6,   28,   50,   72,   94, null, null], // Version 21
+        [   6,   26,   50,   74,   98, null, null], // Version 22
+        [   6,   30,   54,   78,  102, null, null], // Version 23
+        [   6,   28,   54,   80,  106, null, null], // Version 24
+        [   6,   32,   58,   84,  110, null, null], // Version 25
+        [   6,   30,   58,   86,  114, null, null], // Version 26
+        [   6,   34,   62,   90,  118, null, null], // Version 27
+        [   6,   26,   50,   74,   98,  122, null], // Version 28
+        [   6,   30,   54,   78,  102,  126, null], // Version 29
+        [   6,   26,   52,   78,  104,  130, null], // Version 30
+        [   6,   30,   56,   82,  108,  134, null], // Version 31
+        [   6,   34,   60,   86,  112,  138, null], // Version 32
+        [   6,   30,   58,   86,  114,  142, null], // Version 33
+        [   6,   34,   62,   90,  118,  146, null], // Version 34
+        [   6,   30,   54,   78,  102,  126,  150], // Version 35
+        [   6,   24,   50,   76,  102,  128,  154], // Version 36
+        [   6,   28,   54,   80,  106,  132,  158], // Version 37
+        [   6,   32,   58,   84,  110,  136,  162], // Version 38
+        [   6,   26,   54,   82,  110,  138,  166], // Version 39
+        [   6,   30,   58,   86,  114,  142,  170], // Version 40
+    ];
+
+    /**
+     * Type information coordinates.
+     */
+    private const TYPE_INFO_COORDINATES = [
+        [8, 0],
+        [8, 1],
+        [8, 2],
+        [8, 3],
+        [8, 4],
+        [8, 5],
+        [8, 7],
+        [8, 8],
+        [7, 8],
+        [5, 8],
+        [4, 8],
+        [3, 8],
+        [2, 8],
+        [1, 8],
+        [0, 8],
+    ];
+
+    /**
+     * Version information polynomial.
+     */
+    private const VERSION_INFO_POLY = 0x1f25;
+
+    /**
+     * Type information polynomial.
+     */
+    private const TYPE_INFO_POLY = 0x537;
+
+    /**
+     * Type information mask pattern.
+     */
+    private const TYPE_INFO_MASK_PATTERN = 0x5412;
+
+    /**
+     * Clears a given matrix.
+     */
+    public static function clearMatrix(ByteMatrix $matrix) : void
+    {
+        $matrix->clear(-1);
+    }
+
+    /**
+     * Builds a complete matrix.
+     */
+    public static function buildMatrix(
+        BitArray $dataBits,
+        ErrorCorrectionLevel $level,
+        Version $version,
+        int $maskPattern,
+        ByteMatrix $matrix
+    ) : void {
+        self::clearMatrix($matrix);
+        self::embedBasicPatterns($version, $matrix);
+        self::embedTypeInfo($level, $maskPattern, $matrix);
+        self::maybeEmbedVersionInfo($version, $matrix);
+        self::embedDataBits($dataBits, $maskPattern, $matrix);
+    }
+
+    /**
+     * Removes the position detection patterns from a matrix.
+     *
+     * This can be useful if you need to render those patterns separately.
+     */
+    public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void
+    {
+        $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
+
+        self::removePositionDetectionPattern(0, 0, $matrix);
+        self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
+        self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
+    }
+
+    /**
+     * Embeds type information into a matrix.
+     */
+    private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void
+    {
+        $typeInfoBits = new BitArray();
+        self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits);
+
+        $typeInfoBitsSize = $typeInfoBits->getSize();
+
+        for ($i = 0; $i < $typeInfoBitsSize; ++$i) {
+            $bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i);
+
+            $x1 = self::TYPE_INFO_COORDINATES[$i][0];
+            $y1 = self::TYPE_INFO_COORDINATES[$i][1];
+
+            $matrix->set($x1, $y1, (int) $bit);
+
+            if ($i < 8) {
+                $x2 = $matrix->getWidth() - $i - 1;
+                $y2 = 8;
+            } else {
+                $x2 = 8;
+                $y2 = $matrix->getHeight() - 7 + ($i - 8);
+            }
+
+            $matrix->set($x2, $y2, (int) $bit);
+        }
+    }
+
+    /**
+     * Generates type information bits and appends them to a bit array.
+     *
+     * @throws RuntimeException if bit array resulted in invalid size
+     */
+    private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void
+    {
+        $typeInfo = ($level->getBits() << 3) | $maskPattern;
+        $bits->appendBits($typeInfo, 5);
+
+        $bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY);
+        $bits->appendBits($bchCode, 10);
+
+        $maskBits = new BitArray();
+        $maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15);
+        $bits->xorBits($maskBits);
+
+        if (15 !== $bits->getSize()) {
+            throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
+        }
+    }
+
+    /**
+     * Embeds version information if required.
+     */
+    private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void
+    {
+        if ($version->getVersionNumber() < 7) {
+            return;
+        }
+
+        $versionInfoBits = new BitArray();
+        self::makeVersionInfoBits($version, $versionInfoBits);
+
+        $bitIndex = 6 * 3 - 1;
+
+        for ($i = 0; $i < 6; ++$i) {
+            for ($j = 0; $j < 3; ++$j) {
+                $bit = $versionInfoBits->get($bitIndex);
+                --$bitIndex;
+
+                $matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit);
+                $matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit);
+            }
+        }
+    }
+
+    /**
+     * Generates version information bits and appends them to a bit array.
+     *
+     * @throws RuntimeException if bit array resulted in invalid size
+     */
+    private static function makeVersionInfoBits(Version $version, BitArray $bits) : void
+    {
+        $bits->appendBits($version->getVersionNumber(), 6);
+
+        $bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY);
+        $bits->appendBits($bchCode, 12);
+
+        if (18 !== $bits->getSize()) {
+            throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
+        }
+    }
+
+    /**
+     * Calculates the BCH code for a value and a polynomial.
+     */
+    private static function calculateBchCode(int $value, int $poly) : int
+    {
+        $msbSetInPoly = self::findMsbSet($poly);
+        $value <<= $msbSetInPoly - 1;
+
+        while (self::findMsbSet($value) >= $msbSetInPoly) {
+            $value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly);
+        }
+
+        return $value;
+    }
+
+    /**
+     * Finds and MSB set.
+     */
+    private static function findMsbSet(int $value) : int
+    {
+        $numDigits = 0;
+
+        while (0 !== $value) {
+            $value >>= 1;
+            ++$numDigits;
+        }
+
+        return $numDigits;
+    }
+
+    /**
+     * Embeds basic patterns into a matrix.
+     */
+    private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void
+    {
+        self::embedPositionDetectionPatternsAndSeparators($matrix);
+        self::embedDarkDotAtLeftBottomCorner($matrix);
+        self::maybeEmbedPositionAdjustmentPatterns($version, $matrix);
+        self::embedTimingPatterns($matrix);
+    }
+
+    /**
+     * Embeds position detection patterns and separators into a byte matrix.
+     */
+    private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void
+    {
+        $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
+
+        self::embedPositionDetectionPattern(0, 0, $matrix);
+        self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
+        self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
+
+        $hspWidth = 8;
+
+        self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix);
+        self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix);
+        self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix);
+
+        $vspSize = 7;
+
+        self::embedVerticalSeparationPattern($vspSize, 0, $matrix);
+        self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix);
+        self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix);
+    }
+
+    /**
+     * Embeds a single position detection pattern into a byte matrix.
+     */
+    private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 7; ++$y) {
+            for ($x = 0; $x < 7; ++$x) {
+                $matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]);
+            }
+        }
+    }
+
+    private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 7; ++$y) {
+            for ($x = 0; $x < 7; ++$x) {
+                $matrix->set($xStart + $x, $yStart + $y, 0);
+            }
+        }
+    }
+
+    /**
+     * Embeds a single horizontal separation pattern.
+     *
+     * @throws RuntimeException if a byte was already set
+     */
+    private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($x = 0; $x < 8; $x++) {
+            if (-1 !== $matrix->get($xStart + $x, $yStart)) {
+                throw new RuntimeException('Byte already set');
+            }
+
+            $matrix->set($xStart + $x, $yStart, 0);
+        }
+    }
+
+    /**
+     * Embeds a single vertical separation pattern.
+     *
+     * @throws RuntimeException if a byte was already set
+     */
+    private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 7; $y++) {
+            if (-1 !== $matrix->get($xStart, $yStart + $y)) {
+                throw new RuntimeException('Byte already set');
+            }
+
+            $matrix->set($xStart, $yStart + $y, 0);
+        }
+    }
+
+    /**
+     * Embeds a dot at the left bottom corner.
+     *
+     * @throws RuntimeException if a byte was already set to 0
+     */
+    private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void
+    {
+        if (0 === $matrix->get(8, $matrix->getHeight() - 8)) {
+            throw new RuntimeException('Byte already set to 0');
+        }
+
+        $matrix->set(8, $matrix->getHeight() - 8, 1);
+    }
+
+    /**
+     * Embeds position adjustment patterns if required.
+     */
+    private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void
+    {
+        if ($version->getVersionNumber() < 2) {
+            return;
+        }
+
+        $index = $version->getVersionNumber() - 1;
+
+        $coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index];
+        $numCoordinates = count($coordinates);
+
+        for ($i = 0; $i < $numCoordinates; ++$i) {
+            for ($j = 0; $j < $numCoordinates; ++$j) {
+                $y = $coordinates[$i];
+                $x = $coordinates[$j];
+
+                if (null === $x || null === $y) {
+                    continue;
+                }
+
+                if (-1 === $matrix->get($x, $y)) {
+                    self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix);
+                }
+            }
+        }
+    }
+
+    /**
+     * Embeds a single position adjustment pattern.
+     */
+    private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 5; $y++) {
+            for ($x = 0; $x < 5; $x++) {
+                $matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]);
+            }
+        }
+    }
+
+    /**
+     * Embeds timing patterns into a matrix.
+     */
+    private static function embedTimingPatterns(ByteMatrix $matrix) : void
+    {
+        $matrixWidth = $matrix->getWidth();
+
+        for ($i = 8; $i < $matrixWidth - 8; ++$i) {
+            $bit = ($i + 1) % 2;
+
+            if (-1 === $matrix->get($i, 6)) {
+                $matrix->set($i, 6, $bit);
+            }
+
+            if (-1 === $matrix->get(6, $i)) {
+                $matrix->set(6, $i, $bit);
+            }
+        }
+    }
+
+    /**
+     * Embeds "dataBits" using "getMaskPattern".
+     *
+     * For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for
+     * how to embed data bits.
+     *
+     * @throws WriterException if not all bits could be consumed
+     */
+    private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void
+    {
+        $bitIndex = 0;
+        $direction = -1;
+
+        // Start from the right bottom cell.
+        $x = $matrix->getWidth() - 1;
+        $y = $matrix->getHeight() - 1;
+
+        while ($x > 0) {
+            // Skip vertical timing pattern.
+            if (6 === $x) {
+                --$x;
+            }
+
+            while ($y >= 0 && $y < $matrix->getHeight()) {
+                for ($i = 0; $i < 2; $i++) {
+                    $xx = $x - $i;
+
+                    // Skip the cell if it's not empty.
+                    if (-1 !== $matrix->get($xx, $y)) {
+                        continue;
+                    }
+
+                    if ($bitIndex < $dataBits->getSize()) {
+                        $bit = $dataBits->get($bitIndex);
+                        ++$bitIndex;
+                    } else {
+                        // Padding bit. If there is no bit left, we'll fill the
+                        // left cells with 0, as described in 8.4.9 of
+                        // JISX0510:2004 (p. 24).
+                        $bit = false;
+                    }
+
+                    // Skip masking if maskPattern is -1.
+                    if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) {
+                        $bit = ! $bit;
+                    }
+
+                    $matrix->set($xx, $y, (int) $bit);
+                }
+
+                $y += $direction;
+            }
+
+            $direction  = -$direction;
+            $y += $direction;
+            $x -= 2;
+        }
+
+        // All bits should be consumed
+        if ($dataBits->getSize() !== $bitIndex) {
+            throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')');
+        }
+    }
+}

+ 141 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/QrCode.php

@@ -0,0 +1,141 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\ErrorCorrectionLevel;
+use BaconQrCode\Common\Mode;
+use BaconQrCode\Common\Version;
+
+/**
+ * QR code.
+ */
+final class QrCode
+{
+    /**
+     * Number of possible mask patterns.
+     */
+    public const NUM_MASK_PATTERNS = 8;
+
+    /**
+     * Mode of the QR code.
+     *
+     * @var Mode
+     */
+    private $mode;
+
+    /**
+     * EC level of the QR code.
+     *
+     * @var ErrorCorrectionLevel
+     */
+    private $errorCorrectionLevel;
+
+    /**
+     * Version of the QR code.
+     *
+     * @var Version
+     */
+    private $version;
+
+    /**
+     * Mask pattern of the QR code.
+     *
+     * @var int
+     */
+    private $maskPattern = -1;
+
+    /**
+     * Matrix of the QR code.
+     *
+     * @var ByteMatrix
+     */
+    private $matrix;
+
+    public function __construct(
+        Mode $mode,
+        ErrorCorrectionLevel $errorCorrectionLevel,
+        Version $version,
+        int $maskPattern,
+        ByteMatrix $matrix
+    ) {
+        $this->mode = $mode;
+        $this->errorCorrectionLevel = $errorCorrectionLevel;
+        $this->version = $version;
+        $this->maskPattern = $maskPattern;
+        $this->matrix = $matrix;
+    }
+
+    /**
+     * Gets the mode.
+     */
+    public function getMode() : Mode
+    {
+        return $this->mode;
+    }
+
+    /**
+     * Gets the EC level.
+     */
+    public function getErrorCorrectionLevel() : ErrorCorrectionLevel
+    {
+        return $this->errorCorrectionLevel;
+    }
+
+    /**
+     * Gets the version.
+     */
+    public function getVersion() : Version
+    {
+        return $this->version;
+    }
+
+    /**
+     * Gets the mask pattern.
+     */
+    public function getMaskPattern() : int
+    {
+        return $this->maskPattern;
+    }
+
+    /**
+     * Gets the matrix.
+     *
+     * @return ByteMatrix
+     */
+    public function getMatrix()
+    {
+        return $this->matrix;
+    }
+
+    /**
+     * Validates whether a mask pattern is valid.
+     */
+    public static function isValidMaskPattern(int $maskPattern) : bool
+    {
+        return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS;
+    }
+
+    /**
+     * Returns a string representation of the QR code.
+     */
+    public function __toString() : string
+    {
+        $result = "<<\n"
+                . ' mode: ' . $this->mode . "\n"
+                . ' ecLevel: ' . $this->errorCorrectionLevel . "\n"
+                . ' version: ' . $this->version . "\n"
+                . ' maskPattern: ' . $this->maskPattern . "\n";
+
+        if ($this->matrix === null) {
+            $result .= " matrix: null\n";
+        } else {
+            $result .= " matrix:\n";
+            $result .= $this->matrix;
+        }
+
+        $result .= ">>\n";
+
+        return $result;
+    }
+}

+ 10 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/ExceptionInterface.php

@@ -0,0 +1,10 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+use Throwable;
+
+interface ExceptionInterface extends Throwable
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/InvalidArgumentException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/OutOfBoundsException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/RuntimeException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/UnexpectedValueException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/WriterException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class WriterException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 57 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Alpha.php

@@ -0,0 +1,57 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Alpha implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $alpha;
+
+    /**
+     * @var ColorInterface
+     */
+    private $baseColor;
+
+    /**
+     * @param int $alpha the alpha value, 0 to 100
+     */
+    public function __construct(int $alpha, ColorInterface $baseColor)
+    {
+        if ($alpha < 0 || $alpha > 100) {
+            throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100');
+        }
+
+        $this->alpha = $alpha;
+        $this->baseColor = $baseColor;
+    }
+
+    public function getAlpha() : int
+    {
+        return $this->alpha;
+    }
+
+    public function getBaseColor() : ColorInterface
+    {
+        return $this->baseColor;
+    }
+
+    public function toRgb() : Rgb
+    {
+        return $this->baseColor->toRgb();
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        return $this->baseColor->toCmyk();
+    }
+
+    public function toGray() : Gray
+    {
+        return $this->baseColor->toGray();
+    }
+}

+ 103 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Cmyk.php

@@ -0,0 +1,103 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Cmyk implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $cyan;
+
+    /**
+     * @var int
+     */
+    private $magenta;
+
+    /**
+     * @var int
+     */
+    private $yellow;
+
+    /**
+     * @var int
+     */
+    private $black;
+
+    /**
+     * @param int $cyan the cyan amount, 0 to 100
+     * @param int $magenta the magenta amount, 0 to 100
+     * @param int $yellow the yellow amount, 0 to 100
+     * @param int $black the black amount, 0 to 100
+     */
+    public function __construct(int $cyan, int $magenta, int $yellow, int $black)
+    {
+        if ($cyan < 0 || $cyan > 100) {
+            throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100');
+        }
+
+        if ($magenta < 0 || $magenta > 100) {
+            throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100');
+        }
+
+        if ($yellow < 0 || $yellow > 100) {
+            throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100');
+        }
+
+        if ($black < 0 || $black > 100) {
+            throw new Exception\InvalidArgumentException('Black must be between 0 and 100');
+        }
+
+        $this->cyan = $cyan;
+        $this->magenta = $magenta;
+        $this->yellow = $yellow;
+        $this->black = $black;
+    }
+
+    public function getCyan() : int
+    {
+        return $this->cyan;
+    }
+
+    public function getMagenta() : int
+    {
+        return $this->magenta;
+    }
+
+    public function getYellow() : int
+    {
+        return $this->yellow;
+    }
+
+    public function getBlack() : int
+    {
+        return $this->black;
+    }
+
+    public function toRgb() : Rgb
+    {
+        $k = $this->black / 100;
+        $c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100;
+        $m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100;
+        $y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100;
+
+        return new Rgb(
+            (int) (-$c * 255 + 255),
+            (int) (-$m * 255 + 255),
+            (int) (-$y * 255 + 255)
+        );
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        return $this;
+    }
+
+    public function toGray() : Gray
+    {
+        return $this->toRgb()->toGray();
+    }
+}

+ 22 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/ColorInterface.php

@@ -0,0 +1,22 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+interface ColorInterface
+{
+    /**
+     * Converts the color to RGB.
+     */
+    public function toRgb() : Rgb;
+
+    /**
+     * Converts the color to CMYK.
+     */
+    public function toCmyk() : Cmyk;
+
+    /**
+     * Converts the color to gray.
+     */
+    public function toGray() : Gray;
+}

+ 46 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Gray.php

@@ -0,0 +1,46 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Gray implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $gray;
+
+    /**
+     * @param int $gray the gray value between 0 (black) and 100 (white)
+     */
+    public function __construct(int $gray)
+    {
+        if ($gray < 0 || $gray > 100) {
+            throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
+        }
+
+        $this->gray = (int) $gray;
+    }
+
+    public function getGray() : int
+    {
+        return $this->gray;
+    }
+
+    public function toRgb() : Rgb
+    {
+        return new Rgb((int) ($this->gray * 2.55), (int) ($this->gray * 2.55), (int) ($this->gray * 2.55));
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        return new Cmyk(0, 0, 0, 100 - $this->gray);
+    }
+
+    public function toGray() : Gray
+    {
+        return $this;
+    }
+}

+ 88 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Rgb.php

@@ -0,0 +1,88 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Rgb implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $red;
+
+    /**
+     * @var int
+     */
+    private $green;
+
+    /**
+     * @var int
+     */
+    private $blue;
+
+    /**
+     * @param int $red the red amount of the color, 0 to 255
+     * @param int $green the green amount of the color, 0 to 255
+     * @param int $blue the blue amount of the color, 0 to 255
+     */
+    public function __construct(int $red, int $green, int $blue)
+    {
+        if ($red < 0 || $red > 255) {
+            throw new Exception\InvalidArgumentException('Red must be between 0 and 255');
+        }
+
+        if ($green < 0 || $green > 255) {
+            throw new Exception\InvalidArgumentException('Green must be between 0 and 255');
+        }
+
+        if ($blue < 0 || $blue > 255) {
+            throw new Exception\InvalidArgumentException('Blue must be between 0 and 255');
+        }
+
+        $this->red = $red;
+        $this->green = $green;
+        $this->blue = $blue;
+    }
+
+    public function getRed() : int
+    {
+        return $this->red;
+    }
+
+    public function getGreen() : int
+    {
+        return $this->green;
+    }
+
+    public function getBlue() : int
+    {
+        return $this->blue;
+    }
+
+    public function toRgb() : Rgb
+    {
+        return $this;
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        $c = 1 - ($this->red / 255);
+        $m = 1 - ($this->green / 255);
+        $y = 1 - ($this->blue / 255);
+        $k = min($c, $m, $y);
+
+        return new Cmyk(
+            (int) (100 * ($c - $k) / (1 - $k)),
+            (int) (100 * ($m - $k) / (1 - $k)),
+            (int) (100 * ($y - $k) / (1 - $k)),
+            (int) (100 * $k)
+        );
+    }
+
+    public function toGray() : Gray
+    {
+        return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55));
+    }
+}

+ 38 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/CompositeEye.php

@@ -0,0 +1,38 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Combines the style of two different eyes.
+ */
+final class CompositeEye implements EyeInterface
+{
+    /**
+     * @var EyeInterface
+     */
+    private $externalEye;
+
+    /**
+     * @var EyeInterface
+     */
+    private $internalEye;
+
+    public function __construct(EyeInterface $externalEye, EyeInterface $internalEye)
+    {
+        $this->externalEye = $externalEye;
+        $this->internalEye = $internalEye;
+    }
+
+    public function getExternalPath() : Path
+    {
+        return $this->externalEye->getExternalPath();
+    }
+
+    public function getInternalPath() : Path
+    {
+        return $this->internalEye->getInternalPath();
+    }
+}

+ 26 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/EyeInterface.php

@@ -0,0 +1,26 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Interface for describing the look of an eye.
+ */
+interface EyeInterface
+{
+    /**
+     * Returns the path of the external eye element.
+     *
+     * The path origin point (0, 0) must be anchored at the middle of the path.
+     */
+    public function getExternalPath() : Path;
+
+    /**
+     * Returns the path of the internal eye element.
+     *
+     * The path origin point (0, 0) must be anchored at the middle of the path.
+     */
+    public function getInternalPath() : Path;
+}

+ 54 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/ModuleEye.php

@@ -0,0 +1,54 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Renderer\Module\ModuleInterface;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders an eye based on a module renderer.
+ */
+final class ModuleEye implements EyeInterface
+{
+    /**
+     * @var ModuleInterface
+     */
+    private $module;
+
+    public function __construct(ModuleInterface $module)
+    {
+        $this->module = $module;
+    }
+
+    public function getExternalPath() : Path
+    {
+        $matrix = new ByteMatrix(7, 7);
+
+        for ($x = 0; $x < 7; ++$x) {
+            $matrix->set($x, 0, 1);
+            $matrix->set($x, 6, 1);
+        }
+
+        for ($y = 1; $y < 6; ++$y) {
+            $matrix->set(0, $y, 1);
+            $matrix->set(6, $y, 1);
+        }
+
+        return $this->module->createPath($matrix)->translate(-3.5, -3.5);
+    }
+
+    public function getInternalPath() : Path
+    {
+        $matrix = new ByteMatrix(3, 3);
+
+        for ($x = 0; $x < 3; ++$x) {
+            for ($y = 0; $y < 3; ++$y) {
+                $matrix->set($x, $y, 1);
+            }
+        }
+
+        return $this->module->createPath($matrix)->translate(-1.5, -1.5);
+    }
+}

+ 54 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SimpleCircleEye.php

@@ -0,0 +1,54 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders the inner eye as a circle.
+ */
+final class SimpleCircleEye implements EyeInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    public function getExternalPath() : Path
+    {
+        return (new Path())
+            ->move(-3.5, -3.5)
+            ->line(3.5, -3.5)
+            ->line(3.5, 3.5)
+            ->line(-3.5, 3.5)
+            ->close()
+            ->move(-2.5, -2.5)
+            ->line(-2.5, 2.5)
+            ->line(2.5, 2.5)
+            ->line(2.5, -2.5)
+            ->close()
+        ;
+    }
+
+    public function getInternalPath() : Path
+    {
+        return (new Path())
+            ->move(1.5, 0)
+            ->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
+            ->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
+            ->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
+            ->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
+            ->close()
+        ;
+    }
+}

+ 53 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SquareEye.php

@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders the eyes in their default square shape.
+ */
+final class SquareEye implements EyeInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    public function getExternalPath() : Path
+    {
+        return (new Path())
+            ->move(-3.5, -3.5)
+            ->line(3.5, -3.5)
+            ->line(3.5, 3.5)
+            ->line(-3.5, 3.5)
+            ->close()
+            ->move(-2.5, -2.5)
+            ->line(-2.5, 2.5)
+            ->line(2.5, 2.5)
+            ->line(2.5, -2.5)
+            ->close()
+        ;
+    }
+
+    public function getInternalPath() : Path
+    {
+        return (new Path())
+            ->move(-1.5, -1.5)
+            ->line(1.5, -1.5)
+            ->line(1.5, 1.5)
+            ->line(-1.5, 1.5)
+            ->close()
+        ;
+    }
+}

+ 376 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/EpsImageBackEnd.php

@@ -0,0 +1,376 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\Alpha;
+use BaconQrCode\Renderer\Color\Cmyk;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Color\Gray;
+use BaconQrCode\Renderer\Color\Rgb;
+use BaconQrCode\Renderer\Path\Close;
+use BaconQrCode\Renderer\Path\Curve;
+use BaconQrCode\Renderer\Path\EllipticArc;
+use BaconQrCode\Renderer\Path\Line;
+use BaconQrCode\Renderer\Path\Move;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+use BaconQrCode\Renderer\RendererStyle\GradientType;
+
+final class EpsImageBackEnd implements ImageBackEndInterface
+{
+    private const PRECISION = 3;
+
+    /**
+     * @var string|null
+     */
+    private $eps;
+
+    public function new(int $size, ColorInterface $backgroundColor) : void
+    {
+        $this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
+            . "%%Creator: BaconQrCode\n"
+            . sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
+            . "%%BeginProlog\n"
+            . "save\n"
+            . "50 dict begin\n"
+            . "/q { gsave } bind def\n"
+            . "/Q { grestore } bind def\n"
+            . "/s { scale } bind def\n"
+            . "/t { translate } bind def\n"
+            . "/r { rotate } bind def\n"
+            . "/n { newpath } bind def\n"
+            . "/m { moveto } bind def\n"
+            . "/l { lineto } bind def\n"
+            . "/c { curveto } bind def\n"
+            . "/z { closepath } bind def\n"
+            . "/f { eofill } bind def\n"
+            . "/rgb { setrgbcolor } bind def\n"
+            . "/cmyk { setcmykcolor } bind def\n"
+            . "/gray { setgray } bind def\n"
+            . "%%EndProlog\n"
+            . "1 -1 s\n"
+            . sprintf("0 -%d t\n", $size);
+
+        if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
+            return;
+        }
+
+        $this->eps .= wordwrap(
+            '0 0 m'
+            . sprintf(' %s 0 l', (string) $size)
+            . sprintf(' %s %s l', (string) $size, (string) $size)
+            . sprintf(' 0 %s l', (string) $size)
+            . ' z'
+            . ' ' .$this->getColorSetString($backgroundColor) . " f\n",
+            75,
+            "\n "
+        );
+    }
+
+    public function scale(float $size) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
+    }
+
+    public function translate(float $x, float $y) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
+    }
+
+    public function rotate(int $degrees) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= sprintf("%d r\n", $degrees);
+    }
+
+    public function push() : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= "q\n";
+    }
+
+    public function pop() : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= "Q\n";
+    }
+
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $fromX = 0;
+        $fromY = 0;
+        $this->eps .= wordwrap(
+            'n '
+            . $this->drawPathOperations($path, $fromX, $fromY)
+            . ' ' . $this->getColorSetString($color) . " f\n",
+            75,
+            "\n "
+        );
+    }
+
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $fromX = 0;
+        $fromY = 0;
+        $this->eps .= wordwrap(
+            'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
+            75,
+            "\n "
+        );
+
+        $this->createGradientFill($gradient, $x, $y, $width, $height);
+    }
+
+    public function done() : string
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= "%%TRAILER\nend restore\n%%EOF";
+        $blob = $this->eps;
+        $this->eps = null;
+
+        return $blob;
+    }
+
+    private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
+    {
+        $pathData = [];
+
+        foreach ($ops as $op) {
+            switch (true) {
+                case $op instanceof Move:
+                    $fromX = $toX = round($op->getX(), self::PRECISION);
+                    $fromY = $toY = round($op->getY(), self::PRECISION);
+                    $pathData[] = sprintf('%s %s m', $toX, $toY);
+                    break;
+
+                case $op instanceof Line:
+                    $fromX = $toX = round($op->getX(), self::PRECISION);
+                    $fromY = $toY = round($op->getY(), self::PRECISION);
+                    $pathData[] = sprintf('%s %s l', $toX, $toY);
+                    break;
+
+                case $op instanceof EllipticArc:
+                    $pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
+                    break;
+
+                case $op instanceof Curve:
+                    $x1 = round($op->getX1(), self::PRECISION);
+                    $y1 = round($op->getY1(), self::PRECISION);
+                    $x2 = round($op->getX2(), self::PRECISION);
+                    $y2 = round($op->getY2(), self::PRECISION);
+                    $fromX = $x3 = round($op->getX3(), self::PRECISION);
+                    $fromY = $y3 = round($op->getY3(), self::PRECISION);
+                    $pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
+                    break;
+
+                case $op instanceof Close:
+                    $pathData[] = 'z';
+                    break;
+
+                default:
+                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
+            }
+        }
+
+        return implode(' ', $pathData);
+    }
+
+    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
+    {
+        $startColor = $gradient->getStartColor();
+        $endColor = $gradient->getEndColor();
+
+        if ($startColor instanceof Alpha) {
+            $startColor = $startColor->getBaseColor();
+        }
+
+        $startColorType = get_class($startColor);
+
+        if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
+            $startColorType = Cmyk::class;
+            $startColor = $startColor->toCmyk();
+        }
+
+        if (get_class($endColor) !== $startColorType) {
+            switch ($startColorType) {
+                case Cmyk::class:
+                    $endColor = $endColor->toCmyk();
+                    break;
+
+                case Rgb::class:
+                    $endColor = $endColor->toRgb();
+                    break;
+
+                case Gray::class:
+                    $endColor = $endColor->toGray();
+                    break;
+            }
+        }
+
+        $this->eps .= "eoclip\n<<\n";
+
+        if ($gradient->getType() === GradientType::RADIAL()) {
+            $this->eps .= " /ShadingType 3\n";
+        } else {
+            $this->eps .= " /ShadingType 2\n";
+        }
+
+        $this->eps .= " /Extend [ true true ]\n"
+            . " /AntiAlias true\n";
+
+        switch ($startColorType) {
+            case Cmyk::class:
+                $this->eps .= " /ColorSpace /DeviceCMYK\n";
+                break;
+
+            case Rgb::class:
+                $this->eps .= " /ColorSpace /DeviceRGB\n";
+                break;
+
+            case Gray::class:
+                $this->eps .= " /ColorSpace /DeviceGray\n";
+                break;
+        }
+
+        switch ($gradient->getType()) {
+            case GradientType::HORIZONTAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y, self::PRECISION),
+                    round($x + $width, self::PRECISION),
+                    round($y, self::PRECISION)
+                );
+                break;
+
+            case GradientType::VERTICAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y, self::PRECISION),
+                    round($x, self::PRECISION),
+                    round($y + $height, self::PRECISION)
+                );
+                break;
+
+            case GradientType::DIAGONAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y, self::PRECISION),
+                    round($x + $width, self::PRECISION),
+                    round($y + $height, self::PRECISION)
+                );
+                break;
+
+            case GradientType::INVERSE_DIAGONAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y + $height, self::PRECISION),
+                    round($x + $width, self::PRECISION),
+                    round($y, self::PRECISION)
+                );
+                break;
+
+            case GradientType::RADIAL():
+                $centerX = ($x + $width) / 2;
+                $centerY = ($y + $height) / 2;
+
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s 0 %s %s %s ]\n",
+                    round($centerX, self::PRECISION),
+                    round($centerY, self::PRECISION),
+                    round($centerX, self::PRECISION),
+                    round($centerY, self::PRECISION),
+                    round(max($width, $height) / 2, self::PRECISION)
+                );
+                break;
+        }
+
+        $this->eps .= " /Function\n"
+            . " <<\n"
+            . "  /FunctionType 2\n"
+            . "  /Domain [ 0 1 ]\n"
+            . sprintf("  /C0 [ %s ]\n", $this->getColorString($startColor))
+            . sprintf("  /C1 [ %s ]\n", $this->getColorString($endColor))
+            . "  /N 1\n"
+            . " >>\n>>\nshfill\nQ\n";
+    }
+
+    private function getColorSetString(ColorInterface $color) : string
+    {
+        if ($color instanceof Rgb) {
+            return $this->getColorString($color) . ' rgb';
+        }
+
+        if ($color instanceof Cmyk) {
+            return $this->getColorString($color) . ' cmyk';
+        }
+
+        if ($color instanceof Gray) {
+            return $this->getColorString($color) . ' gray';
+        }
+
+        return $this->getColorSetString($color->toCmyk());
+    }
+
+    private function getColorString(ColorInterface $color) : string
+    {
+        if ($color instanceof Rgb) {
+            return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
+        }
+
+        if ($color instanceof Cmyk) {
+            return sprintf(
+                '%s %s %s %s',
+                $color->getCyan() / 100,
+                $color->getMagenta() / 100,
+                $color->getYellow() / 100,
+                $color->getBlack() / 100
+            );
+        }
+
+        if ($color instanceof Gray) {
+            return sprintf('%s', $color->getGray() / 100);
+        }
+
+        return $this->getColorString($color->toCmyk());
+    }
+}

+ 87 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImageBackEndInterface.php

@@ -0,0 +1,87 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+
+/**
+ * Interface for back ends able to to produce path based images.
+ */
+interface ImageBackEndInterface
+{
+    /**
+     * Starts a new image.
+     *
+     * If a previous image was already started, previous data get erased.
+     */
+    public function new(int $size, ColorInterface $backgroundColor) : void;
+
+    /**
+     * Transforms all following drawing operation coordinates by scaling them by a given factor.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function scale(float $size) : void;
+
+    /**
+     * Transforms all following drawing operation coordinates by translating them by a given amount.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function translate(float $x, float $y) : void;
+
+    /**
+     * Transforms all following drawing operation coordinates by rotating them by a given amount.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function rotate(int $degrees) : void;
+
+    /**
+     * Pushes the current coordinate transformation onto a stack.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function push() : void;
+
+    /**
+     * Pops the last coordinate transformation from a stack.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function pop() : void;
+
+    /**
+     * Draws a path with a given color.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void;
+
+    /**
+     * Draws a path with a given gradient which spans the box described by the position and size.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void;
+
+    /**
+     * Ends the image drawing operation and returns the resulting blob.
+     *
+     * This should reset the state of the back end and thus this method should only be callable once per image.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function done() : string;
+}

+ 336 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImagickImageBackEnd.php

@@ -0,0 +1,336 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\Alpha;
+use BaconQrCode\Renderer\Color\Cmyk;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Color\Gray;
+use BaconQrCode\Renderer\Color\Rgb;
+use BaconQrCode\Renderer\Path\Close;
+use BaconQrCode\Renderer\Path\Curve;
+use BaconQrCode\Renderer\Path\EllipticArc;
+use BaconQrCode\Renderer\Path\Line;
+use BaconQrCode\Renderer\Path\Move;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+use BaconQrCode\Renderer\RendererStyle\GradientType;
+use Imagick;
+use ImagickDraw;
+use ImagickPixel;
+
+final class ImagickImageBackEnd implements ImageBackEndInterface
+{
+    /**
+     * @var string
+     */
+    private $imageFormat;
+
+    /**
+     * @var int
+     */
+    private $compressionQuality;
+
+    /**
+     * @var Imagick|null
+     */
+    private $image;
+
+    /**
+     * @var ImagickDraw|null
+     */
+    private $draw;
+
+    /**
+     * @var int|null
+     */
+    private $gradientCount;
+
+    /**
+     * @var TransformationMatrix[]|null
+     */
+    private $matrices;
+
+    /**
+     * @var int|null
+     */
+    private $matrixIndex;
+
+    public function __construct(string $imageFormat = 'png', int $compressionQuality = 100)
+    {
+        if (! class_exists(Imagick::class)) {
+            throw new RuntimeException('You need to install the imagick extension to use this back end');
+        }
+
+        $this->imageFormat = $imageFormat;
+        $this->compressionQuality = $compressionQuality;
+    }
+
+    public function new(int $size, ColorInterface $backgroundColor) : void
+    {
+        $this->image = new Imagick();
+        $this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
+        $this->image->setImageFormat($this->imageFormat);
+        $this->image->setCompressionQuality($this->compressionQuality);
+        $this->draw = new ImagickDraw();
+        $this->gradientCount = 0;
+        $this->matrices = [new TransformationMatrix()];
+        $this->matrixIndex = 0;
+    }
+
+    public function scale(float $size) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->scale($size, $size);
+        $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
+            ->multiply(TransformationMatrix::scale($size));
+    }
+
+    public function translate(float $x, float $y) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->translate($x, $y);
+        $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
+            ->multiply(TransformationMatrix::translate($x, $y));
+    }
+
+    public function rotate(int $degrees) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->rotate($degrees);
+        $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
+            ->multiply(TransformationMatrix::rotate($degrees));
+    }
+
+    public function push() : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->push();
+        $this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
+    }
+
+    public function pop() : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->pop();
+        unset($this->matrices[$this->matrixIndex--]);
+    }
+
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->setFillColor($this->getColorPixel($color));
+        $this->drawPath($path);
+    }
+
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
+        $this->drawPath($path);
+    }
+
+    public function done() : string
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->image->drawImage($this->draw);
+        $blob = $this->image->getImageBlob();
+        $this->draw->clear();
+        $this->image->clear();
+        $this->draw = null;
+        $this->image = null;
+        $this->gradientCount = null;
+
+        return $blob;
+    }
+
+    private function drawPath(Path $path) : void
+    {
+        $this->draw->pathStart();
+
+        foreach ($path as $op) {
+            switch (true) {
+                case $op instanceof Move:
+                    $this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
+                    break;
+
+                case $op instanceof Line:
+                    $this->draw->pathLineToAbsolute($op->getX(), $op->getY());
+                    break;
+
+                case $op instanceof EllipticArc:
+                    $this->draw->pathEllipticArcAbsolute(
+                        $op->getXRadius(),
+                        $op->getYRadius(),
+                        $op->getXAxisAngle(),
+                        $op->isLargeArc(),
+                        $op->isSweep(),
+                        $op->getX(),
+                        $op->getY()
+                    );
+                    break;
+
+                case $op instanceof Curve:
+                    $this->draw->pathCurveToAbsolute(
+                        $op->getX1(),
+                        $op->getY1(),
+                        $op->getX2(),
+                        $op->getY2(),
+                        $op->getX3(),
+                        $op->getY3()
+                    );
+                    break;
+
+                case $op instanceof Close:
+                    $this->draw->pathClose();
+                    break;
+
+                default:
+                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
+            }
+        }
+
+        $this->draw->pathFinish();
+    }
+
+    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
+    {
+        list($width, $height) = $this->matrices[$this->matrixIndex]->apply($width, $height);
+
+        $startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
+        $endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
+        $gradientImage = new Imagick();
+
+        switch ($gradient->getType()) {
+            case GradientType::HORIZONTAL():
+                $gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
+                    'gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+                $gradientImage->rotateImage('transparent', -90);
+                break;
+
+            case GradientType::VERTICAL():
+                $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
+                    'gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+                break;
+
+            case GradientType::DIAGONAL():
+            case GradientType::INVERSE_DIAGONAL():
+                $gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
+                    'gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+
+                if (GradientType::DIAGONAL() === $gradient->getType()) {
+                    $gradientImage->rotateImage('transparent', -45);
+                } else {
+                    $gradientImage->rotateImage('transparent', -135);
+                }
+
+                $rotatedWidth = $gradientImage->getImageWidth();
+                $rotatedHeight = $gradientImage->getImageHeight();
+
+                $gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
+                $gradientImage->cropImage(
+                    intdiv($rotatedWidth, 2) - 2,
+                    intdiv($rotatedHeight, 2) - 2,
+                    intdiv($rotatedWidth, 4) + 1,
+                    intdiv($rotatedWidth, 4) + 1
+                );
+                break;
+
+            case GradientType::RADIAL():
+                $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
+                    'radial-gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+                break;
+        }
+
+        $id = sprintf('g%d', ++$this->gradientCount);
+        $this->draw->pushPattern($id, 0, 0, $width, $height);
+        $this->draw->composite(Imagick::COMPOSITE_COPY, 0, 0, $width, $height, $gradientImage);
+        $this->draw->popPattern();
+        return $id;
+    }
+
+    private function getColorPixel(ColorInterface $color) : ImagickPixel
+    {
+        $alpha = 100;
+
+        if ($color instanceof Alpha) {
+            $alpha = $color->getAlpha();
+            $color = $color->getBaseColor();
+        }
+
+        if ($color instanceof Rgb) {
+            return new ImagickPixel(sprintf(
+                'rgba(%d, %d, %d, %F)',
+                $color->getRed(),
+                $color->getGreen(),
+                $color->getBlue(),
+                $alpha / 100
+            ));
+        }
+
+        if ($color instanceof Cmyk) {
+            return new ImagickPixel(sprintf(
+                'cmyka(%d, %d, %d, %d, %F)',
+                $color->getCyan(),
+                $color->getMagenta(),
+                $color->getYellow(),
+                $color->getBlack(),
+                $alpha / 100
+            ));
+        }
+
+        if ($color instanceof Gray) {
+            return new ImagickPixel(sprintf(
+                'graya(%d%%, %F)',
+                $color->getGray(),
+                $alpha / 100
+            ));
+        }
+
+        return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
+    }
+}

+ 369 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/SvgImageBackEnd.php

@@ -0,0 +1,369 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\Alpha;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Path\Close;
+use BaconQrCode\Renderer\Path\Curve;
+use BaconQrCode\Renderer\Path\EllipticArc;
+use BaconQrCode\Renderer\Path\Line;
+use BaconQrCode\Renderer\Path\Move;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+use BaconQrCode\Renderer\RendererStyle\GradientType;
+use XMLWriter;
+
+final class SvgImageBackEnd implements ImageBackEndInterface
+{
+    private const PRECISION = 3;
+
+    /**
+     * @var XMLWriter|null
+     */
+    private $xmlWriter;
+
+    /**
+     * @var int[]|null
+     */
+    private $stack;
+
+    /**
+     * @var int|null
+     */
+    private $currentStack;
+
+    /**
+     * @var int|null
+     */
+    private $gradientCount;
+
+    public function __construct()
+    {
+        if (! class_exists(XMLWriter::class)) {
+            throw new RuntimeException('You need to install the libxml extension to use this back end');
+        }
+    }
+
+    public function new(int $size, ColorInterface $backgroundColor) : void
+    {
+        $this->xmlWriter = new XMLWriter();
+        $this->xmlWriter->openMemory();
+
+        $this->xmlWriter->startDocument('1.0', 'UTF-8');
+        $this->xmlWriter->startElement('svg');
+        $this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
+        $this->xmlWriter->writeAttribute('version', '1.1');
+        $this->xmlWriter->writeAttribute('width', (string) $size);
+        $this->xmlWriter->writeAttribute('height', (string) $size);
+        $this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
+
+        $this->gradientCount = 0;
+        $this->currentStack = 0;
+        $this->stack[0] = 0;
+
+        $alpha = 1;
+
+        if ($backgroundColor instanceof Alpha) {
+            $alpha = $backgroundColor->getAlpha() / 100;
+        }
+
+        if (0 === $alpha) {
+            return;
+        }
+
+        $this->xmlWriter->startElement('rect');
+        $this->xmlWriter->writeAttribute('x', '0');
+        $this->xmlWriter->writeAttribute('y', '0');
+        $this->xmlWriter->writeAttribute('width', (string) $size);
+        $this->xmlWriter->writeAttribute('height', (string) $size);
+        $this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
+
+        if ($alpha < 1) {
+            $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
+        }
+
+        $this->xmlWriter->endElement();
+    }
+
+    public function scale(float $size) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->xmlWriter->writeAttribute(
+            'transform',
+            sprintf('scale(%s)', round($size, self::PRECISION))
+        );
+        ++$this->stack[$this->currentStack];
+    }
+
+    public function translate(float $x, float $y) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->xmlWriter->writeAttribute(
+            'transform',
+            sprintf('translate(%s,%s)', round($x, self::PRECISION), round($y, self::PRECISION))
+        );
+        ++$this->stack[$this->currentStack];
+    }
+
+    public function rotate(int $degrees) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
+        ++$this->stack[$this->currentStack];
+    }
+
+    public function push() : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->stack[] = 1;
+        ++$this->currentStack;
+    }
+
+    public function pop() : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
+            $this->xmlWriter->endElement();
+        }
+
+        array_pop($this->stack);
+        --$this->currentStack;
+    }
+
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $alpha = 1;
+
+        if ($color instanceof Alpha) {
+            $alpha = $color->getAlpha() / 100;
+        }
+
+        $this->startPathElement($path);
+        $this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
+
+        if ($alpha < 1) {
+            $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
+        }
+
+        $this->xmlWriter->endElement();
+    }
+
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
+        $this->startPathElement($path);
+        $this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
+        $this->xmlWriter->endElement();
+    }
+
+    public function done() : string
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        foreach ($this->stack as $openElements) {
+            for ($i = $openElements; $i > 0; --$i) {
+                $this->xmlWriter->endElement();
+            }
+        }
+
+        $this->xmlWriter->endDocument();
+        $blob = $this->xmlWriter->outputMemory(true);
+        $this->xmlWriter = null;
+        $this->stack = null;
+        $this->currentStack = null;
+        $this->gradientCount = null;
+
+        return $blob;
+    }
+
+    private function startPathElement(Path $path) : void
+    {
+        $pathData = [];
+
+        foreach ($path as $op) {
+            switch (true) {
+                case $op instanceof Move:
+                    $pathData[] = sprintf(
+                        'M%s %s',
+                        round($op->getX(), self::PRECISION),
+                        round($op->getY(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof Line:
+                    $pathData[] = sprintf(
+                        'L%s %s',
+                        round($op->getX(), self::PRECISION),
+                        round($op->getY(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof EllipticArc:
+                    $pathData[] = sprintf(
+                        'A%s %s %s %u %u %s %s',
+                        round($op->getXRadius(), self::PRECISION),
+                        round($op->getYRadius(), self::PRECISION),
+                        round($op->getXAxisAngle(), self::PRECISION),
+                        $op->isLargeArc(),
+                        $op->isSweep(),
+                        round($op->getX(), self::PRECISION),
+                        round($op->getY(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof Curve:
+                    $pathData[] = sprintf(
+                        'C%s %s %s %s %s %s',
+                        round($op->getX1(), self::PRECISION),
+                        round($op->getY1(), self::PRECISION),
+                        round($op->getX2(), self::PRECISION),
+                        round($op->getY2(), self::PRECISION),
+                        round($op->getX3(), self::PRECISION),
+                        round($op->getY3(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof Close:
+                    $pathData[] = 'Z';
+                    break;
+
+                default:
+                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
+            }
+        }
+
+        $this->xmlWriter->startElement('path');
+        $this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
+        $this->xmlWriter->writeAttribute('d', implode('', $pathData));
+    }
+
+    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
+    {
+        $this->xmlWriter->startElement('defs');
+
+        $startColor = $gradient->getStartColor();
+        $endColor = $gradient->getEndColor();
+
+        if ($gradient->getType() === GradientType::RADIAL()) {
+            $this->xmlWriter->startElement('radialGradient');
+        } else {
+            $this->xmlWriter->startElement('linearGradient');
+        }
+
+        $this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
+
+        switch ($gradient->getType()) {
+            case GradientType::HORIZONTAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
+                break;
+
+            case GradientType::VERTICAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
+                break;
+
+            case GradientType::DIAGONAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
+                break;
+
+            case GradientType::INVERSE_DIAGONAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
+                break;
+
+            case GradientType::RADIAL():
+                $this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
+                $this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
+                $this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
+                break;
+        }
+
+        $id = sprintf('g%d', ++$this->gradientCount);
+        $this->xmlWriter->writeAttribute('id', $id);
+
+        $this->xmlWriter->startElement('stop');
+        $this->xmlWriter->writeAttribute('offset', '0%');
+        $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
+
+        if ($startColor instanceof Alpha) {
+            $this->xmlWriter->writeAttribute('stop-opacity', (string) $startColor->getAlpha());
+        }
+
+        $this->xmlWriter->endElement();
+
+        $this->xmlWriter->startElement('stop');
+        $this->xmlWriter->writeAttribute('offset', '100%');
+        $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
+
+        if ($endColor instanceof Alpha) {
+            $this->xmlWriter->writeAttribute('stop-opacity', (string) $endColor->getAlpha());
+        }
+
+        $this->xmlWriter->endElement();
+
+        $this->xmlWriter->endElement();
+        $this->xmlWriter->endElement();
+
+        return $id;
+    }
+
+    private function getColorString(ColorInterface $color) : string
+    {
+        $color = $color->toRgb();
+
+        return sprintf(
+            '#%02x%02x%02x',
+            $color->getRed(),
+            $color->getGreen(),
+            $color->getBlue()
+        );
+    }
+}

+ 68 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/TransformationMatrix.php

@@ -0,0 +1,68 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+final class TransformationMatrix
+{
+    /**
+     * @var float[]
+     */
+    private $values;
+
+    public function __construct()
+    {
+        $this->values = [1, 0, 0, 1, 0, 0];
+    }
+
+    public function multiply(self $other) : self
+    {
+        $matrix = new self();
+        $matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1];
+        $matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1];
+        $matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3];
+        $matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3];
+        $matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5]
+            + $this->values[4];
+        $matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5]
+            + $this->values[5];
+
+        return $matrix;
+    }
+
+    public static function scale(float $size) : self
+    {
+        $matrix = new self();
+        $matrix->values = [$size, 0, 0, $size, 0, 0];
+        return $matrix;
+    }
+
+    public static function translate(float $x, float $y) : self
+    {
+        $matrix = new self();
+        $matrix->values = [1, 0, 0, 1, $x, $y];
+        return $matrix;
+    }
+
+    public static function rotate(int $degrees) : self
+    {
+        $matrix = new self();
+        $rad = deg2rad($degrees);
+        $matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0];
+        return $matrix;
+    }
+
+
+    /**
+     * Applies this matrix onto a point and returns the resulting viewport point.
+     *
+     * @return float[]
+     */
+    public function apply(float $x, float $y) : array
+    {
+        return [
+            $x * $this->values[0] + $y * $this->values[2] + $this->values[4],
+            $x * $this->values[1] + $y * $this->values[3] + $this->values[5],
+        ];
+    }
+}

+ 152 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/ImageRenderer.php

@@ -0,0 +1,152 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer;
+
+use BaconQrCode\Encoder\MatrixUtil;
+use BaconQrCode\Encoder\QrCode;
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Renderer\Image\ImageBackEndInterface;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\EyeFill;
+use BaconQrCode\Renderer\RendererStyle\RendererStyle;
+
+final class ImageRenderer implements RendererInterface
+{
+    /**
+     * @var RendererStyle
+     */
+    private $rendererStyle;
+
+    /**
+     * @var ImageBackEndInterface
+     */
+    private $imageBackEnd;
+
+    public function __construct(RendererStyle $rendererStyle, ImageBackEndInterface $imageBackEnd)
+    {
+        $this->rendererStyle = $rendererStyle;
+        $this->imageBackEnd = $imageBackEnd;
+    }
+
+    /**
+     * @throws InvalidArgumentException if matrix width doesn't match height
+     */
+    public function render(QrCode $qrCode) : string
+    {
+        $size = $this->rendererStyle->getSize();
+        $margin = $this->rendererStyle->getMargin();
+        $matrix = $qrCode->getMatrix();
+        $matrixSize = $matrix->getWidth();
+
+        if ($matrixSize !== $matrix->getHeight()) {
+            throw new InvalidArgumentException('Matrix must have the same width and height');
+        }
+
+        $totalSize = $matrixSize + ($margin * 2);
+        $moduleSize = $size / $totalSize;
+        $fill = $this->rendererStyle->getFill();
+
+        $this->imageBackEnd->new($size, $fill->getBackgroundColor());
+        $this->imageBackEnd->scale((float) $moduleSize);
+        $this->imageBackEnd->translate((float) $margin, (float) $margin);
+
+        $module = $this->rendererStyle->getModule();
+        $moduleMatrix = clone $matrix;
+        MatrixUtil::removePositionDetectionPatterns($moduleMatrix);
+        $modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix));
+
+        if ($fill->hasGradientFill()) {
+            $this->imageBackEnd->drawPathWithGradient(
+                $modulePath,
+                $fill->getForegroundGradient(),
+                0,
+                0,
+                $matrixSize,
+                $matrixSize
+            );
+        } else {
+            $this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor());
+        }
+
+        return $this->imageBackEnd->done();
+    }
+
+    private function drawEyes(int $matrixSize, Path $modulePath) : Path
+    {
+        $fill = $this->rendererStyle->getFill();
+
+        $eye = $this->rendererStyle->getEye();
+        $externalPath = $eye->getExternalPath();
+        $internalPath = $eye->getInternalPath();
+
+        $modulePath = $this->drawEye(
+            $externalPath,
+            $internalPath,
+            $fill->getTopLeftEyeFill(),
+            3.5,
+            3.5,
+            0,
+            $modulePath
+        );
+        $modulePath = $this->drawEye(
+            $externalPath,
+            $internalPath,
+            $fill->getTopRightEyeFill(),
+            $matrixSize - 3.5,
+            3.5,
+            90,
+            $modulePath
+        );
+        $modulePath = $this->drawEye(
+            $externalPath,
+            $internalPath,
+            $fill->getBottomLeftEyeFill(),
+            3.5,
+            $matrixSize - 3.5,
+            -90,
+            $modulePath
+        );
+
+        return $modulePath;
+    }
+
+    private function drawEye(
+        Path $externalPath,
+        Path $internalPath,
+        EyeFill $fill,
+        float $xTranslation,
+        float $yTranslation,
+        int $rotation,
+        Path $modulePath
+    ) : Path {
+        if ($fill->inheritsBothColors()) {
+            return $modulePath
+                ->append($externalPath->translate($xTranslation, $yTranslation))
+                ->append($internalPath->translate($xTranslation, $yTranslation));
+        }
+
+        $this->imageBackEnd->push();
+        $this->imageBackEnd->translate($xTranslation, $yTranslation);
+
+        if (0 !== $rotation) {
+            $this->imageBackEnd->rotate($rotation);
+        }
+
+        if ($fill->inheritsExternalColor()) {
+            $modulePath = $modulePath->append($externalPath->translate($xTranslation, $yTranslation));
+        } else {
+            $this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
+        }
+
+        if ($fill->inheritsInternalColor()) {
+            $modulePath = $modulePath->append($internalPath->translate($xTranslation, $yTranslation));
+        } else {
+            $this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
+        }
+
+        $this->imageBackEnd->pop();
+
+        return $modulePath;
+    }
+}

+ 63 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/DotsModule.php

@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders individual modules as dots.
+ */
+final class DotsModule implements ModuleInterface
+{
+    public const LARGE = 1;
+    public const MEDIUM = .8;
+    public const SMALL = .6;
+
+    /**
+     * @var float
+     */
+    private $size;
+
+    public function __construct(float $size)
+    {
+        if ($size <= 0 || $size > 1) {
+            throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)');
+        }
+
+        $this->size = $size;
+    }
+
+    public function createPath(ByteMatrix $matrix) : Path
+    {
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+        $path = new Path();
+        $halfSize = $this->size / 2;
+        $margin = (1 - $this->size) / 2;
+
+        for ($y = 0; $y < $height; ++$y) {
+            for ($x = 0; $x < $width; ++$x) {
+                if (! $matrix->get($x, $y)) {
+                    continue;
+                }
+
+                $pathX = $x + $margin;
+                $pathY = $y + $margin;
+
+                $path = $path
+                    ->move($pathX + $this->size, $pathY + $halfSize)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY + $this->size)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX, $pathY + $halfSize)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $this->size, $pathY + $halfSize)
+                    ->close()
+                ;
+            }
+        }
+
+        return $path;
+    }
+}

+ 100 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/Edge.php

@@ -0,0 +1,100 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module\EdgeIterator;
+
+final class Edge
+{
+    /**
+     * @var bool
+     */
+    private $positive;
+
+    /**
+     * @var array<int[]>
+     */
+    private $points = [];
+
+    /**
+     * @var array<int[]>|null
+     */
+    private $simplifiedPoints;
+
+    /**
+     * @var int
+     */
+    private $minX = PHP_INT_MAX;
+
+    /**
+     * @var int
+     */
+    private $minY = PHP_INT_MAX;
+
+    /**
+     * @var int
+     */
+    private $maxX = -1;
+
+    /**
+     * @var int
+     */
+    private $maxY = -1;
+
+    public function __construct(bool $positive)
+    {
+        $this->positive = $positive;
+    }
+
+    public function addPoint(int $x, int $y) : void
+    {
+        $this->points[] = [$x, $y];
+        $this->minX = min($this->minX, $x);
+        $this->minY = min($this->minY, $y);
+        $this->maxX = max($this->maxX, $x);
+        $this->maxY = max($this->maxY, $y);
+    }
+
+    public function isPositive() : bool
+    {
+        return $this->positive;
+    }
+
+    /**
+     * @return array<int[]>
+     */
+    public function getPoints() : array
+    {
+        return $this->points;
+    }
+
+    public function getMaxX() : int
+    {
+        return $this->maxX;
+    }
+
+    public function getSimplifiedPoints() : array
+    {
+        if (null !== $this->simplifiedPoints) {
+            return $this->simplifiedPoints;
+        }
+
+        $points = [];
+        $length = count($this->points);
+
+        for ($i = 0; $i < $length; ++$i) {
+            $previousPoint = $this->points[(0 === $i ? $length : $i) - 1];
+            $nextPoint = $this->points[($length - 1 === $i ? -1 : $i) + 1];
+            $currentPoint = $this->points[$i];
+
+            if (($previousPoint[0] === $currentPoint[0] && $currentPoint[0] === $nextPoint[0])
+                || ($previousPoint[1] === $currentPoint[1] && $currentPoint[1] === $nextPoint[1])
+            ) {
+                continue;
+            }
+
+            $points[] = $currentPoint;
+        }
+
+        return $this->simplifiedPoints = $points;
+    }
+}

+ 169 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/EdgeIterator.php

@@ -0,0 +1,169 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module\EdgeIterator;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use IteratorAggregate;
+use Traversable;
+
+/**
+ * Edge iterator based on potrace.
+ */
+final class EdgeIterator implements IteratorAggregate
+{
+    /**
+     * @var int[]
+     */
+    private $bytes = [];
+
+    /**
+     * @var int
+     */
+    private $size;
+
+    /**
+     * @var int
+     */
+    private $width;
+
+    /**
+     * @var int
+     */
+    private $height;
+
+    public function __construct(ByteMatrix $matrix)
+    {
+        $this->bytes = iterator_to_array($matrix->getBytes());
+        $this->size = count($this->bytes);
+        $this->width = $matrix->getWidth();
+        $this->height = $matrix->getHeight();
+    }
+
+    /**
+     * @return Traversable<Edge>
+     */
+    public function getIterator() : Traversable
+    {
+        $originalBytes = $this->bytes;
+        $point = $this->findNext(0, 0);
+
+        while (null !== $point) {
+            $edge = $this->findEdge($point[0], $point[1]);
+            $this->xorEdge($edge);
+
+            yield $edge;
+
+            $point = $this->findNext($point[0], $point[1]);
+        }
+
+        $this->bytes = $originalBytes;
+    }
+
+    /**
+     * @return int[]|null
+     */
+    private function findNext(int $x, int $y) : ?array
+    {
+        $i = $this->width * $y + $x;
+
+        while ($i < $this->size && 1 !== $this->bytes[$i]) {
+            ++$i;
+        }
+
+        if ($i < $this->size) {
+            return $this->pointOf($i);
+        }
+
+        return null;
+    }
+
+    private function findEdge(int $x, int $y) : Edge
+    {
+        $edge = new Edge($this->isSet($x, $y));
+        $startX = $x;
+        $startY = $y;
+        $dirX = 0;
+        $dirY = 1;
+
+        while (true) {
+            $edge->addPoint($x, $y);
+            $x += $dirX;
+            $y += $dirY;
+
+            if ($x === $startX && $y === $startY) {
+                break;
+            }
+
+            $left = $this->isSet($x + ($dirX + $dirY - 1 ) / 2, $y + ($dirY - $dirX - 1) / 2);
+            $right = $this->isSet($x + ($dirX - $dirY - 1) / 2, $y + ($dirY + $dirX - 1) / 2);
+
+            if ($right && ! $left) {
+                $tmp = $dirX;
+                $dirX = -$dirY;
+                $dirY = $tmp;
+            } elseif ($right) {
+                $tmp = $dirX;
+                $dirX = -$dirY;
+                $dirY = $tmp;
+            } elseif (! $left) {
+                $tmp = $dirX;
+                $dirX = $dirY;
+                $dirY = -$tmp;
+            }
+        }
+
+        return $edge;
+    }
+
+    private function xorEdge(Edge $path) : void
+    {
+        $points = $path->getPoints();
+        $y1 = $points[0][1];
+        $length = count($points);
+        $maxX = $path->getMaxX();
+
+        for ($i = 1; $i < $length; ++$i) {
+            $y = $points[$i][1];
+
+            if ($y === $y1) {
+                continue;
+            }
+
+            $x = $points[$i][0];
+            $minY = min($y1, $y);
+
+            for ($j = $x; $j < $maxX; ++$j) {
+                $this->flip($j, $minY);
+            }
+
+            $y1 = $y;
+        }
+    }
+
+    private function isSet(int $x, int $y) : bool
+    {
+        return (
+            $x >= 0
+            && $x < $this->width
+            && $y >= 0
+            && $y < $this->height
+        ) && 1 === $this->bytes[$this->width * $y + $x];
+    }
+
+    /**
+     * @return int[]
+     */
+    private function pointOf(int $i) : array
+    {
+        $y = intdiv($i, $this->width);
+        return [$i - $y * $this->width, $y];
+    }
+
+    private function flip(int $x, int $y) : void
+    {
+        $this->bytes[$this->width * $y + $x] = (
+            $this->isSet($x, $y) ? 0 : 1
+        );
+    }
+}

+ 18 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/ModuleInterface.php

@@ -0,0 +1,18 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Interface describing how modules should be rendered.
+ *
+ * A module always receives a byte matrix (with values either being 1 or 0). It returns a path, where the origin
+ * coordinate (0, 0) equals the top left corner of the first matrix value.
+ */
+interface ModuleInterface
+{
+    public function createPath(ByteMatrix $matrix) : Path;
+}

+ 129 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/RoundnessModule.php

@@ -0,0 +1,129 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Rounds the corners of module groups.
+ */
+final class RoundnessModule implements ModuleInterface
+{
+    public const STRONG = 1;
+    public const MEDIUM = .5;
+    public const SOFT = .25;
+
+    /**
+     * @var float
+     */
+    private $intensity;
+
+    public function __construct(float $intensity)
+    {
+        if ($intensity <= 0 || $intensity > 1) {
+            throw new InvalidArgumentException('Intensity must between 0 (exclusive) and 1 (inclusive)');
+        }
+
+        $this->intensity = $intensity / 2;
+    }
+
+    public function createPath(ByteMatrix $matrix) : Path
+    {
+        $path = new Path();
+
+        foreach (new EdgeIterator($matrix) as $edge) {
+            $points = $edge->getSimplifiedPoints();
+            $length = count($points);
+
+            $currentPoint = $points[0];
+            $nextPoint = $points[1];
+            $horizontal = ($currentPoint[1] === $nextPoint[1]);
+
+            if ($horizontal) {
+                $right = $nextPoint[0] > $currentPoint[0];
+                $path = $path->move(
+                    $currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
+                    $currentPoint[1]
+                );
+            } else {
+                $up = $nextPoint[0] < $currentPoint[0];
+                $path = $path->move(
+                    $currentPoint[0],
+                    $currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
+                );
+            }
+
+            for ($i = 1; $i <= $length; ++$i) {
+                if ($i === $length) {
+                    $previousPoint = $points[$length - 1];
+                    $currentPoint = $points[0];
+                    $nextPoint = $points[1];
+                } else {
+                    $previousPoint = $points[(0 === $i ? $length : $i) - 1];
+                    $currentPoint = $points[$i];
+                    $nextPoint = $points[($length - 1 === $i ? -1 : $i) + 1];
+                }
+
+                $horizontal = ($previousPoint[1] === $currentPoint[1]);
+
+                if ($horizontal) {
+                    $right = $previousPoint[0] < $currentPoint[0];
+                    $up = $nextPoint[1] < $currentPoint[1];
+                    $sweep = ($up xor $right);
+
+                    if ($this->intensity < 0.5
+                        || ($right && $previousPoint[0] !== $currentPoint[0] - 1)
+                        || (! $right && $previousPoint[0] - 1 !== $currentPoint[0])
+                    ) {
+                        $path = $path->line(
+                            $currentPoint[0] + ($right ? -$this->intensity : $this->intensity),
+                            $currentPoint[1]
+                        );
+                    }
+
+                    $path = $path->ellipticArc(
+                        $this->intensity,
+                        $this->intensity,
+                        0,
+                        false,
+                        $sweep,
+                        $currentPoint[0],
+                        $currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
+                    );
+                } else {
+                    $up = $previousPoint[1] > $currentPoint[1];
+                    $right = $nextPoint[0] > $currentPoint[0];
+                    $sweep = ! ($up xor $right);
+
+                    if ($this->intensity < 0.5
+                        || ($up && $previousPoint[1] !== $currentPoint[1] + 1)
+                        || (! $up && $previousPoint[0] + 1 !== $currentPoint[0])
+                    ) {
+                        $path = $path->line(
+                            $currentPoint[0],
+                            $currentPoint[1] + ($up ? $this->intensity : -$this->intensity)
+                        );
+                    }
+
+                    $path = $path->ellipticArc(
+                        $this->intensity,
+                        $this->intensity,
+                        0,
+                        false,
+                        $sweep,
+                        $currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
+                        $currentPoint[1]
+                    );
+                }
+            }
+
+            $path = $path->close();
+        }
+
+        return $path;
+    }
+}

+ 47 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/SquareModule.php

@@ -0,0 +1,47 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Groups modules together to a single path.
+ */
+final class SquareModule implements ModuleInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    public function createPath(ByteMatrix $matrix) : Path
+    {
+        $path = new Path();
+
+        foreach (new EdgeIterator($matrix) as $edge) {
+            $points = $edge->getSimplifiedPoints();
+            $length = count($points);
+            $path = $path->move($points[0][0], $points[0][1]);
+
+            for ($i = 1; $i < $length; ++$i) {
+                $path = $path->line($points[$i][0], $points[$i][1]);
+            }
+
+            $path = $path->close();
+        }
+
+        return $path;
+    }
+}

+ 29 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Path/Close.php

@@ -0,0 +1,29 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Path;
+
+final class Close implements OperationInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    /**
+     * @return self
+     */
+    public function translate(float $x, float $y) : OperationInterface
+    {
+        return $this;
+    }
+}

Some files were not shown because too many files changed in this diff