From ef7ec5555cffa9908b6ff79ebf6383b79da0e24e Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:23:58 +0100 Subject: [PATCH 01/63] update with html5 compliant code --- htdocs/adherents/subscription/list.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/adherents/subscription/list.php b/htdocs/adherents/subscription/list.php index 85c92196a73..336122c605e 100644 --- a/htdocs/adherents/subscription/list.php +++ b/htdocs/adherents/subscription/list.php @@ -596,8 +596,8 @@ if (isset($totalarray['pos'])) { if ($i == 1) { - if ($num < $limit) print ''.$langs->trans("Total").''; - else print ''.$langs->trans("Totalforthispage").''; + if ($num < $limit) print ''.$langs->trans("Total").''; + else print ''.$langs->trans("Totalforthispage").''; } else print ''; } From 66e4bc450249cb8e8959ec964455b21eb4933d37 Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:27:26 +0100 Subject: [PATCH 02/63] update with html5 compliant code --- htdocs/comm/propal/stats/index.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/htdocs/comm/propal/stats/index.php b/htdocs/comm/propal/stats/index.php index 7f808135ca1..5c54baf7360 100644 --- a/htdocs/comm/propal/stats/index.php +++ b/htdocs/comm/propal/stats/index.php @@ -251,20 +251,20 @@ print '
'; print ''; print ''; // Company - print ''; // User - print ''; // Status - print ''; // Year - print ''; - print ''; + print ''; print '
'.$langs->trans("Filter").'
'.$langs->trans("ThirdParty").''; + print '
'.$langs->trans("ThirdParty").''; $filter='s.client in (1,2,3)'; print $form->select_company($socid,'socid',$filter,1,0,0,array(),0,'','style="width: 95%"'); print '
'.$langs->trans("CreatedBy").''; + print '
'.$langs->trans("CreatedBy").''; print $form->select_dolusers($userid, 'userid', 1, '', 0, '', '', 0, 0, 0, '', 0, '', 'maxwidth300'); print '
'.$langs->trans("Status").''; + print '
'.$langs->trans("Status").''; $formpropal->selectProposalStatus(($object_status!=''?$object_status:-1),0,0,1,$mode,'object_status'); print '
'.$langs->trans("Year").''; + print '
'.$langs->trans("Year").''; if (! in_array($year,$arrayyears)) $arrayyears[$year]=$year; if (! in_array($nowyear,$arrayyears)) $arrayyears[$nowyear]=$nowyear; arsort($arrayyears); From 336be085f6e9ff432924f01ea5bac2aeb45aafe9 Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:28:18 +0100 Subject: [PATCH 03/63] update with html5 compliant code --- htdocs/commande/stats/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/commande/stats/index.php b/htdocs/commande/stats/index.php index 578f08b7af3..6fc778fb064 100644 --- a/htdocs/commande/stats/index.php +++ b/htdocs/commande/stats/index.php @@ -263,16 +263,16 @@ print ''; print ''; print ''; // Company -print ''; // User -print ''; // Unit - if($conf->global->PRODUCT_USE_UNITS) print ''; + if($conf->global->PRODUCT_USE_UNITS) print ''; // Remise if ($objp->remise_percent > 0) { @@ -1752,7 +1752,7 @@ else print ''; if ($conf->global->PRODUCT_USE_UNITS) { - print ''; } diff --git a/htdocs/contrat/index.php b/htdocs/contrat/index.php index 69e2b0d0ca8..098a1737955 100644 --- a/htdocs/contrat/index.php +++ b/htdocs/contrat/index.php @@ -353,7 +353,7 @@ if ($result) print $staticcompany->getNomUrl(1,'',20); print ''; print ''; - //print ''; + //print ''; print ''; print ''; print ''; diff --git a/htdocs/contrat/services_list.php b/htdocs/contrat/services_list.php index 806bff48605..2e4fa4ce3da 100644 --- a/htdocs/contrat/services_list.php +++ b/htdocs/contrat/services_list.php @@ -732,8 +732,8 @@ if (isset($totalarray['displaytotalline'])) { while ($i < $totalarray['nbfield']) { $i++; if ($i == 1) { - if ($num < $limit && empty($offset)) print ''; - else print ''; + if ($num < $limit && empty($offset)) print ''; + else print ''; } elseif ($totalarray['totalhtfield'] == $i) print ''; elseif ($totalarray['totalvatfield'] == $i) print ''; From 29fdad144dec696ab850ddff5f52511bfe39b7fd Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:40:17 +0100 Subject: [PATCH 05/63] update with html5 compliant code --- htdocs/core/ajax/ajaxdirtree.php | 6 ++--- htdocs/core/class/commonobject.class.php | 4 ++-- htdocs/core/class/html.form.class.php | 24 +++++++++---------- htdocs/core/class/html.formbarcode.class.php | 2 +- htdocs/core/lib/sendings.lib.php | 6 ++--- .../core/tpl/admin_extrafields_view.tpl.php | 2 +- htdocs/core/tpl/objectline_edit.tpl.php | 2 +- htdocs/core/tpl/originproductline.tpl.php | 2 +- htdocs/don/stats/index.php | 6 ++--- htdocs/expedition/card.php | 22 ++++++++--------- htdocs/expedition/stats/index.php | 6 ++--- 11 files changed, 41 insertions(+), 41 deletions(-) diff --git a/htdocs/core/ajax/ajaxdirtree.php b/htdocs/core/ajax/ajaxdirtree.php index b44b8fd835f..d90a3f5f316 100644 --- a/htdocs/core/ajax/ajaxdirtree.php +++ b/htdocs/core/ajax/ajaxdirtree.php @@ -274,7 +274,7 @@ if (empty($conf->use_javascript_ajax) || ! empty($conf->global->MAIN_ECM_DISABLE print '
'.$langs->trans("Filter").'
'.$langs->trans("ThirdParty").''; +print '
'.$langs->trans("ThirdParty").''; if ($mode == 'customer') $filter='s.client in (1,2,3)'; if ($mode == 'supplier') $filter='s.fournisseur = 1'; print $form->select_company($socid,'socid',$filter,1,0,0,array(),0,'','style="width: 95%"'); print '
'.$langs->trans("CreatedBy").''; +print '
'.$langs->trans("CreatedBy").''; print $form->select_dolusers($userid, 'userid', 1, '', 0, '', '', 0, 0, 0, '', 0, '', 'maxwidth300'); // Status -print '
'.$langs->trans("Status").''; +print '
'.$langs->trans("Status").''; if ($mode == 'customer') { $liststatus=array( From 43a2b5b1e2f2f92dbd7d9e1049fc03cb7b03a89a Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:29:41 +0100 Subject: [PATCH 04/63] update with html5 compliant code --- htdocs/contrat/card.php | 4 ++-- htdocs/contrat/index.php | 2 +- htdocs/contrat/services_list.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/htdocs/contrat/card.php b/htdocs/contrat/card.php index 1a911ca4355..491ead83b6e 100644 --- a/htdocs/contrat/card.php +++ b/htdocs/contrat/card.php @@ -1625,7 +1625,7 @@ else // Quantite print ''.$objp->qty.''.$langs->trans($object->lines[$cursorline-1]->getLabelOfUnit()).''.$langs->trans($object->lines[$cursorline-1]->getLabelOfUnit()).''; + print ''; print $form->selectUnits($objp->fk_unit, "unit"); print ''.dol_print_date($db->jdate($obj->tms),'dayhour').''.$staticcontrat->LibStatut($obj->statut,2).''.$staticcontrat->LibStatut($obj->statut,2).''.($obj->nb_initial>0 ? $obj->nb_initial.$staticcontratligne->LibStatut(0,3):'').''.($obj->nb_running>0 ? $obj->nb_running.$staticcontratligne->LibStatut(4,3,0):'').''.($obj->nb_expired>0 ? $obj->nb_expired.$staticcontratligne->LibStatut(4,3,1):'').''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.price($totalarray['totalht']).''.price($totalarray['totalvat']).'
'; - print ''; @@ -420,7 +420,7 @@ function treeOutputForAbsoluteDir($sqltree, $selecteddir, $fullpathselecteddir, print '
'; print $val['cachenbofdoc']; print ''; + print ''; if ($nbofsubdir && $nboffilesinsubdir) print '+'.$nboffilesinsubdir.' '; print '
'; - /*print '';*/ @@ -428,7 +428,7 @@ function treeOutputForAbsoluteDir($sqltree, $selecteddir, $fullpathselecteddir, print ''; - print ''; diff --git a/htdocs/core/class/commonobject.class.php b/htdocs/core/class/commonobject.class.php index fd5deb31c36..627e67a38b7 100644 --- a/htdocs/core/class/commonobject.class.php +++ b/htdocs/core/class/commonobject.class.php @@ -3,7 +3,7 @@ * Copyright (C) 2005-2013 Regis Houssin * Copyright (C) 2010-2015 Juanjo Menent * Copyright (C) 2012-2013 Christophe Battarel - * Copyright (C) 2011-2018 Philippe Grand + * Copyright (C) 2011-2019 Philippe Grand * Copyright (C) 2012-2015 Marcos García * Copyright (C) 2012-2015 Raphaël Doursenaud * Copyright (C) 2012 Cedric Salvador @@ -4126,7 +4126,7 @@ abstract class CommonObject print ''; if($conf->global->PRODUCT_USE_UNITS) { - print ''; + print ''; } print ''; diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php index 035d922e0c1..5cafb86cdc6 100644 --- a/htdocs/core/class/html.form.class.php +++ b/htdocs/core/class/html.form.class.php @@ -10,7 +10,7 @@ * Copyright (C) 2007 Franky Van Liedekerke * Copyright (C) 2007 Patrick Raguin * Copyright (C) 2010 Juanjo Menent - * Copyright (C) 2010-2014 Philippe Grand + * Copyright (C) 2010-2019 Philippe Grand * Copyright (C) 2011 Herve Prot * Copyright (C) 2012-2016 Marcos García * Copyright (C) 2012 Cedric Salvador @@ -244,7 +244,7 @@ class Form } if (empty($notabletag)) $ret.=''; - if (empty($notabletag)) $ret.=''.$input['label'].''."\n"; + $more.=''.$input['label'].''."\n"; } elseif ($input['type'] == 'password') { - $more.=''.$input['label'].''."\n"; + $more.=''.$input['label'].''."\n"; } elseif ($input['type'] == 'select') { @@ -3877,7 +3877,7 @@ class Form elseif ($input['type'] == 'checkbox') { $more.=''; - $more.=''.$input['label'].' '; - $more.=''."\n"; $formquestion[] = array('name'=>$input['name'].'day'); @@ -3916,14 +3916,14 @@ class Form elseif ($input['type'] == 'other') { $more.=''; - if (! empty($input['label'])) $more.=$input['label'].''."\n"; } elseif ($input['type'] == 'onecolumn') { - $more.=''."\n"; } @@ -4286,7 +4286,7 @@ class Form $ret.=''; - $ret.=''; + $ret.=''; $ret.='
'; + /*print ''; print dol_escape_htmltag($file); print ''; print (isset($val['cachenbofdoc']) && $val['cachenbofdoc'] >= 0)?$val['cachenbofdoc']:' '; print ''; + print ''; if ($nbofsubdir > 0 && $nboffilesinsubdir > 0) print '+'.$nboffilesinsubdir.' '; print ''.$langs->trans('Qty').''.$langs->trans('Unit').''.$langs->trans('Unit').''.$langs->trans('ReductionShort').'
'; + if (empty($notabletag)) $ret.=''; //else $ret.='
'; $ret.=''; if (preg_match('/ckeditor|textarea/',$typeofdata) && empty($notabletag)) $ret.='
'."\n"; @@ -3861,11 +3861,11 @@ class Form if ($input['type'] == 'text') { - $more.='
'; + $more.=''.$input['label'].' '; $more.=''.$input['label'].''; + $more.=''; $more.=$this->selectDate($input['value'],$input['name'],0,0,0,'',1,0); $more.='
'; + if (! empty($input['label'])) $more.=$input['label'].''; $more.=$input['value']; $more.='
'; + $more.='
'; $more.=$input['value']; $more.='
'; $ret.=$this->selectDate($selected,$htmlname,$displayhour,$displaymin,1,'form'.$htmlname,1,0); $ret.='
'; } else @@ -4575,7 +4575,7 @@ class Form print ''.$addcontact.''; } print '
'; } else @@ -6543,9 +6543,9 @@ class Form print ''; print ''; print '' . $langs->trans("Ref") . ''; - print '' . $langs->trans("RefCustomer") . ''; + print '' . $langs->trans("RefCustomer") . ''; print '' . $langs->trans("AmountHTShort") . ''; - print '' . $langs->trans("Company") . ''; + print '' . $langs->trans("Company") . ''; print ''; while ($i < $num) { diff --git a/htdocs/core/class/html.formbarcode.class.php b/htdocs/core/class/html.formbarcode.class.php index 5a005e7de04..65a3213bad8 100644 --- a/htdocs/core/class/html.formbarcode.class.php +++ b/htdocs/core/class/html.formbarcode.class.php @@ -219,7 +219,7 @@ class FormBarCode $out .= ''; $out .= $this->selectBarcodeType($selected, $htmlname, 1); $out .= ''; - $out .= ''; + $out .= ''; $out .= ''; } return $out; diff --git a/htdocs/core/lib/sendings.lib.php b/htdocs/core/lib/sendings.lib.php index 1cafb6dc3d4..be8aeb890fc 100644 --- a/htdocs/core/lib/sendings.lib.php +++ b/htdocs/core/lib/sendings.lib.php @@ -217,9 +217,9 @@ function show_list_sending_receive($origin,$origin_id,$filter='') print ''; print ''; - //print ''; - print ''; - print ''; + //print ''; + print ''; + print ''; print ''; print ''; print ''; diff --git a/htdocs/core/tpl/admin_extrafields_view.tpl.php b/htdocs/core/tpl/admin_extrafields_view.tpl.php index 42f62d03ff7..79f5a0bad5c 100644 --- a/htdocs/core/tpl/admin_extrafields_view.tpl.php +++ b/htdocs/core/tpl/admin_extrafields_view.tpl.php @@ -49,7 +49,7 @@ print '
'; print '
'.$langs->trans("QtyOrdered").''.$langs->trans("SendingSheet").''.$langs->trans("Description").''.$langs->trans("QtyOrdered").''.$langs->trans("SendingSheet").''.$langs->trans("Description").''.$langs->trans("DateCreation").''.$langs->trans("DateDeliveryPlanned").''.$langs->trans("QtyPreparedOrShipped").'
'; print ''; -print ''; } diff --git a/htdocs/core/tpl/originproductline.tpl.php b/htdocs/core/tpl/originproductline.tpl.php index 9274359eada..ed16cec17f1 100644 --- a/htdocs/core/tpl/originproductline.tpl.php +++ b/htdocs/core/tpl/originproductline.tpl.php @@ -37,7 +37,7 @@ if (!empty($conf->multicurrency->enabled)) print ''; if($conf->global->PRODUCT_USE_UNITS) - print ''; + print ''; print ''; print ''."\n"; diff --git a/htdocs/don/stats/index.php b/htdocs/don/stats/index.php index 5e7e08c0ee2..56657694f80 100644 --- a/htdocs/don/stats/index.php +++ b/htdocs/don/stats/index.php @@ -237,17 +237,17 @@ print '
'; print '
'.$langs->trans("Position"); +print ''.$langs->trans("Position"); print ''; print img_picto('A-Z', '1downarrow.png'); print ''; diff --git a/htdocs/core/tpl/objectline_edit.tpl.php b/htdocs/core/tpl/objectline_edit.tpl.php index ebb2863dbfa..c26298d65e5 100644 --- a/htdocs/core/tpl/objectline_edit.tpl.php +++ b/htdocs/core/tpl/objectline_edit.tpl.php @@ -178,7 +178,7 @@ $coldisplay=-1; // We remove first td global->PRODUCT_USE_UNITS) { - print ''; + print ''; print $form->selectUnits($line->fk_unit, "units"); print ''.$this->tpl['qty'].''.$langs->trans($this->tpl['unit']).''.$langs->trans($this->tpl['unit']).''.$this->tpl['remise_percent'].'
'; print ''; // Company - print ''; // User - print ''; // Year - print ''; + print ''; } else { - print ''; + print ''; } } print "\n"; @@ -1248,7 +1248,7 @@ if ($action == 'create') // Stock if (! empty($conf->stock->enabled)) { - print ''; print ''; - print ''; - print ''; } @@ -1412,7 +1412,7 @@ if ($action == 'create') // Stock if (! empty($conf->stock->enabled)) { - print ''; - print ''; - print ''; + print ''; } if (! empty($conf->productbatch->enabled)) { - print ''; + print ''; } } print ''; @@ -2313,7 +2313,7 @@ else if ($id || $ref) // Warehouse source if (! empty($conf->stock->enabled)) { - print ''; - else print ''; + if ($num < $limit && empty($offset)) print ''; + else print ''; } elseif ($totalarray['totaldurationfield'] == $i) print ''; else print ''; diff --git a/htdocs/fichinter/stats/index.php b/htdocs/fichinter/stats/index.php index 7ebf701d3d5..cbc7c3424bc 100644 --- a/htdocs/fichinter/stats/index.php +++ b/htdocs/fichinter/stats/index.php @@ -233,22 +233,22 @@ print '
'; print '
'.$langs->trans("Filter").'
'.$langs->trans("ThirdParty").''; + print '
'.$langs->trans("ThirdParty").''; if ($mode == 'customer') $filter='s.client in (1,2,3)'; if ($mode == 'supplier') $filter='s.fournisseur = 1'; print $form->select_company($socid,'socid',$filter,1,0,0,array(),0,'','style="width: 95%"'); print '
'.$langs->trans("CreatedBy").''; + print '
'.$langs->trans("CreatedBy").''; print $form->select_dolusers($userid, 'userid', 1, '', 0, '', '', 0, 0, 0, '', 0, '', 'maxwidth300'); print '
'.$langs->trans("Year").''; + print '
'.$langs->trans("Year").''; if (! in_array($year,$arrayyears)) $arrayyears[$year]=$year; if (! in_array($nowyear,$arrayyears)) $arrayyears[$nowyear]=$nowyear; arsort($arrayyears); diff --git a/htdocs/expedition/card.php b/htdocs/expedition/card.php index ed2eb5239b3..e99f04415b3 100644 --- a/htdocs/expedition/card.php +++ b/htdocs/expedition/card.php @@ -1122,11 +1122,11 @@ if ($action == 'create') { if (empty($conf->productbatch->enabled)) { - print ''.$langs->trans("Warehouse").' ('.$langs->trans("Stock").')'.$langs->trans("Warehouse").' ('.$langs->trans("Stock").')'.$langs->trans("Warehouse").' / '.$langs->trans("Batch").' ('.$langs->trans("Stock").')'.$langs->trans("Warehouse").' / '.$langs->trans("Batch").' ('.$langs->trans("Stock").')
'; + print ''; if ($line->product_type == Product::TYPE_PRODUCT || ! empty($conf->global->STOCK_SUPPORTS_SERVICES)) // Type of product need stock change ? { // Show warehouse combo list @@ -1334,7 +1334,7 @@ if ($action == 'create') print ''; + print ''; print $staticwarehouse->getNomUrl(0).' / '; @@ -1364,7 +1364,7 @@ if ($action == 'create') print ' '; print ''; + print ''; print img_warning().' '.$langs->trans("NoProductToShipFoundIntoStock", $staticwarehouse->libelle); print '
'; + print ''; if ($line->product_type == Product::TYPE_PRODUCT || ! empty($conf->global->STOCK_SUPPORTS_SERVICES)) { print $tmpwarehouseObject->getNomUrl(0).' '; @@ -1495,7 +1495,7 @@ if ($action == 'create') print ''; print ''; + print ''; print $tmpwarehouseObject->getNomUrl(0).' / '; @@ -1539,7 +1539,7 @@ if ($action == 'create') } print ''; + print ''; if ($line->product_type == Product::TYPE_PRODUCT || ! empty($conf->global->STOCK_SUPPORTS_SERVICES)) { $warehouse_selected_id = GETPOST('entrepot_id','int'); @@ -2052,12 +2052,12 @@ else if ($id || $ref) } if (! empty($conf->stock->enabled)) { - print ''.$langs->trans("WarehouseSource").''.$langs->trans("WarehouseSource").''.$langs->trans("Batch").''.$langs->trans("Batch").''.$langs->trans("CalculatedWeight").''; + print ''; if ($lines[$i]->entrepot_id > 0) { $entrepot = new Entrepot($db); diff --git a/htdocs/expedition/stats/index.php b/htdocs/expedition/stats/index.php index e0b750e9797..95f93904c5f 100644 --- a/htdocs/expedition/stats/index.php +++ b/htdocs/expedition/stats/index.php @@ -237,17 +237,17 @@ print '
'; print ''; print ''; // Company - print ''; // User - print ''; // Year - print ''; - else print ''; + if ($num < $limit && empty($offset)) print ''; + else print ''; } elseif ($totalarray['totalhtfield'] == $i) print ''; elseif ($totalarray['totalvatfield'] == $i) print ''; diff --git a/htdocs/expensereport/stats/index.php b/htdocs/expensereport/stats/index.php index c192fb83447..08af170f5e7 100644 --- a/htdocs/expensereport/stats/index.php +++ b/htdocs/expensereport/stats/index.php @@ -2,7 +2,7 @@ /* Copyright (C) 2003-2006 Rodolphe Quiedeville * Copyright (c) 2004-2012 Laurent Destailleur * Copyright (C) 2012 Marcos García - * Copyright (C) 2018 Frédéric France + * Copyright (C) 2018 Frédéric France * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -232,7 +232,7 @@ if (empty($user->rights->expensereport->readall) && empty($user->rights->expense print $form->select_dolusers($userid, 'userid', 1, '', 0, $include, '', 0, 0, 0, '', 0, '', 'maxwidth300'); print ''; // Status -print ''; From 3d695362ece9768d79b0685e7cd44eb19d7eaf0e Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:44:26 +0100 Subject: [PATCH 07/63] update with html5 compliant code --- htdocs/fichinter/card-rec.php | 4 ++-- htdocs/fichinter/list.php | 4 ++-- htdocs/fichinter/stats/index.php | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/htdocs/fichinter/card-rec.php b/htdocs/fichinter/card-rec.php index 187b6fbc3da..e896c9682e4 100644 --- a/htdocs/fichinter/card-rec.php +++ b/htdocs/fichinter/card-rec.php @@ -8,7 +8,7 @@ * Copyright (C) 2012 Cedric Salvador * Copyright (C) 2015 Alexandre Spangaro * Copyright (C) 2016-2018 Charlie Benke - * Copyright (C) 2018 Frédéric France + * Copyright (C) 2018 Frédéric France * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -613,7 +613,7 @@ if ($action == 'create') { ($object->unit_frequency?$object->unit_frequency:'m') ); print ''; - print ''; + print ''; print '
'.$langs->trans("Filter").'
'.$langs->trans("ThirdParty").''; + print '
'.$langs->trans("ThirdParty").''; if ($mode == 'customer') $filter='s.client in (1,2,3)'; if ($mode == 'supplier') $filter='s.fournisseur = 1'; print $form->select_company($socid,'socid',$filter,1,0,0,array(),0,'','style="width: 95%"'); print '
'.$langs->trans("CreatedBy").''; + print '
'.$langs->trans("CreatedBy").''; print $form->select_dolusers($userid, 'userid', 1, '', 0, '', '', 0, 0, 0, '', 0, '', 'maxwidth300'); print '
'.$langs->trans("Year").''; + print '
'.$langs->trans("Year").''; if (! in_array($year,$arrayyears)) $arrayyears[$year]=$year; if (! in_array($nowyear,$arrayyears)) $arrayyears[$nowyear]=$nowyear; arsort($arrayyears); From f613121c2d678b51e62cf092cf3de56d8b28bd2f Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:42:17 +0100 Subject: [PATCH 06/63] update with html5 compliant code --- htdocs/expensereport/list.php | 12 ++++++------ htdocs/expensereport/stats/index.php | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/htdocs/expensereport/list.php b/htdocs/expensereport/list.php index 34a28442e45..7e94a10418a 100644 --- a/htdocs/expensereport/list.php +++ b/htdocs/expensereport/list.php @@ -3,9 +3,9 @@ * Copyright (C) 2004-2017 Laurent Destailleur * Copyright (C) 2004 Eric Seigne * Copyright (C) 2005-2009 Regis Houssin - * Copyright (C) 2015 Alexandre Spangaro - * Copyright (C) 2018 Ferran Marcet - * Copyright (C) 2018 Charlene Benke + * Copyright (C) 2015 Alexandre Spangaro + * Copyright (C) 2018 Ferran Marcet + * Copyright (C) 2018 Charlene Benke * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -682,7 +682,7 @@ if ($resql) } // User if (! empty($arrayfields['user']['checked'])) { - print ''; + print ''; $usertmp->id=$obj->id_user; $usertmp->lastname=$obj->lastname; $usertmp->firstname=$obj->firstname; @@ -805,8 +805,8 @@ if ($resql) $i++; if ($i == 1) { - if ($num < $limit && empty($offset)) print ''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.price($totalarray['totalht']).''.price($totalarray['totalvat']).'
'.$langs->trans("Status").''; +print '
'.$langs->trans("Status").''; $liststatus=$tmpexpensereport->statuts; print $form->selectarray('object_status', $liststatus, GETPOST('object_status'), -4, 0, 0, '', 1); print '
'; } else { if ($object->frequency > 0) diff --git a/htdocs/fichinter/list.php b/htdocs/fichinter/list.php index 85f92f55f85..14622e39397 100644 --- a/htdocs/fichinter/list.php +++ b/htdocs/fichinter/list.php @@ -557,8 +557,8 @@ if ($resql) $i++; if ($i == 1) { - if ($num < $limit && empty($offset)) print '
'.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.convertSecondToTime($totalarray['totalduration'], 'allhourmin').'
'; print ''; // Company - print ''; // User - print ''; // Year - print ''; - else print ''; + if ($num < $limit) print ''; + else print ''; } elseif ($totalarray['totalhtfield'] == $i) print ''; elseif ($totalarray['totalvatfield'] == $i) print ''; diff --git a/htdocs/fourn/facture/list.php b/htdocs/fourn/facture/list.php index 9286c038507..4fab329142c 100644 --- a/htdocs/fourn/facture/list.php +++ b/htdocs/fourn/facture/list.php @@ -1,8 +1,8 @@ +/* Copyright (C) 2002-2006 Rodolphe Quiedeville * Copyright (C) 2004-2016 Laurent Destailleur * Copyright (C) 2005-2013 Regis Houssin - * Copyright (C) 2013-2018 Philippe Grand + * Copyright (C) 2013-2019 Philippe Grand * Copyright (C) 2013 Florian Henry * Copyright (C) 2013 Cédric Salvador * Copyright (C) 2015 Marcos García @@ -1108,8 +1108,8 @@ if ($resql) $i++; if ($i == 1) { - if ($num < $limit && empty($offset)) print ''; - else print ''; + if ($num < $limit && empty($offset)) print ''; + else print ''; } elseif ($totalarray['totalhtfield'] == $i) print ''; elseif ($totalarray['totalvatfield'] == $i) print ''; diff --git a/htdocs/fourn/index.php b/htdocs/fourn/index.php index 5812553db5c..d60519dda04 100644 --- a/htdocs/fourn/index.php +++ b/htdocs/fourn/index.php @@ -203,7 +203,7 @@ if (! empty($conf->fournisseur->enabled) && $user->rights->fournisseur->facture- $i++; } - print ''; + print ''; print ''; print ''; @@ -258,7 +258,7 @@ if ($resql) print ''; print '\n"; - print ''; + print ''; print ''; print "\n"; } From e6d5925b7a457e066de1185727c2690201ba7bc7 Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:52:34 +0100 Subject: [PATCH 09/63] update with html5 compliant code --- htdocs/hrm/admin/admin_establishment.php | 6 +++--- htdocs/loan/payment/payment.php | 2 +- htdocs/margin/admin/margin.php | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/htdocs/hrm/admin/admin_establishment.php b/htdocs/hrm/admin/admin_establishment.php index 5b15c1fb8bc..98221ce6594 100644 --- a/htdocs/hrm/admin/admin_establishment.php +++ b/htdocs/hrm/admin/admin_establishment.php @@ -113,9 +113,9 @@ if ($result) print ''; print ''; - print ''; - print ''; - print ''; + print ''; + print ''; + print ''; print '
'.$langs->trans("Filter").'
'.$langs->trans("ThirdParty").''; + print '
'.$langs->trans("ThirdParty").''; $filter = 's.client in (1,2,3)'; print $form->select_company($socid, 'socid', $filter, 1, 0, 0, array(), 0, '', 'style="width: 95%"'); print '
'.$langs->trans("CreatedBy").''; + print '
'.$langs->trans("CreatedBy").''; print $form->select_dolusers($userid, 'userid', 1, '', 0, '', '', 0, 0, 0, '', 0, '', 'maxwidth300'); // Status - print '
'.$langs->trans("Status").''; + print '
'.$langs->trans("Status").''; $tmp = $objectstatic->LibStatut(0); // To load $this->statuts_short $liststatus=$objectstatic->statuts_short; if (empty($conf->global->FICHINTER_CLASSIFY_BILLED)) unset($liststatus[2]); // Option deprecated. In a future, billed must be managed with a dedicated field to 0 or 1 print $form->selectarray('object_status', $liststatus, $object_status, 1, 0, 0, '', 1); print '
'.$langs->trans("Year").''; + print '
'.$langs->trans("Year").''; if (! in_array($year,$arrayyears)) $arrayyears[$year]=$year; if (! in_array($nowyear,$arrayyears)) $arrayyears[$nowyear]=$nowyear; arsort($arrayyears); From 549bbb06fc20c4f702aca23f1a22dbb6b1c147c1 Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:50:06 +0100 Subject: [PATCH 08/63] update with html5 compliant code --- htdocs/fourn/commande/list.php | 6 +++--- htdocs/fourn/facture/list.php | 8 ++++---- htdocs/fourn/index.php | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/htdocs/fourn/commande/list.php b/htdocs/fourn/commande/list.php index b4892d3d89f..4aa18b6d40e 100644 --- a/htdocs/fourn/commande/list.php +++ b/htdocs/fourn/commande/list.php @@ -1,5 +1,5 @@ +/* Copyright (C) 2001-2006 Rodolphe Quiedeville * Copyright (C) 2004-2016 Laurent Destailleur * Copyright (C) 2005-2012 Regis Houssin * Copyright (C) 2013 Cédric Salvador @@ -1147,8 +1147,8 @@ if ($resql) $i++; if ($i == 1) { - if ($num < $limit) print ''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.price($totalarray['totalht']).''.price($totalarray['totalvat']).''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.price($totalarray['totalht']).''.price($totalarray['totalvat']).'
'.$langs->trans("Total").'
'.$langs->trans("Total").''.price($tot_ttc).'
'.img_object($langs->trans("ShowSupplier"),"company").''; print " socid."\">".$obj->name."'.$obj->code_fournisseur.' '.$obj->code_fournisseur.' '.dol_print_date($db->jdate($obj->tms),'day').'
'.$establishmentstatic->getNomUrl(1).''.$obj->address.''.$obj->zip.''.$obj->town.''.$obj->address.''.$obj->zip.''.$obj->town.''; print $establishmentstatic->getLibStatut(5); diff --git a/htdocs/loan/payment/payment.php b/htdocs/loan/payment/payment.php index 07425ea1942..2ca310a1689 100644 --- a/htdocs/loan/payment/payment.php +++ b/htdocs/loan/payment/payment.php @@ -278,7 +278,7 @@ if ($action == 'create') print ''; print ''; - print ''; + print ''; print ''; print ''; print ''; diff --git a/htdocs/margin/admin/margin.php b/htdocs/margin/admin/margin.php index b2c74506990..3d8d5ec2ab9 100644 --- a/htdocs/margin/admin/margin.php +++ b/htdocs/margin/admin/margin.php @@ -123,7 +123,7 @@ print '
'.$langs->trans("DateDue").''.$langs->trans("DateDue").''.$langs->trans("LoanCapital").''.$langs->trans("AlreadyPaid").''.$langs->trans("RemainderToPay").'
'; print ''; print ''; print ''."\n"; -print ''."\n"; +print ''."\n"; print ''; $form = new Form($db); @@ -241,7 +241,7 @@ print ''; print ""; print ''; print ''; -print ''; print ''; print ''; -print ''; - else print ''; + if ($num < $limit) print ''; + else print ''; } else print ''; } diff --git a/htdocs/opensurvey/list.php b/htdocs/opensurvey/list.php index cf52d83c680..93086de389f 100644 --- a/htdocs/opensurvey/list.php +++ b/htdocs/opensurvey/list.php @@ -442,8 +442,8 @@ if (isset($totalarray['pos'])) { if ($i == 1) { - if ($num < $limit) print ''; - else print ''; + if ($num < $limit) print ''; + else print ''; } else print ''; } From a45ea9239debccab0ad451b004f9762e61944fb6 Mon Sep 17 00:00:00 2001 From: markus Date: Sat, 19 Jan 2019 14:24:20 +0100 Subject: [PATCH 11/63] Rename consts and cleanup --- htdocs/admin/prelevement.php | 15 ++++++++------- .../prelevement/class/bonprelevement.class.php | 4 ++-- htdocs/compta/prelevement/create.php | 3 ++- htdocs/install/mysql/data/llx_const.sql | 3 ++- htdocs/install/mysql/migration/8.0.0-9.0.0.sql | 3 +++ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/htdocs/admin/prelevement.php b/htdocs/admin/prelevement.php index d8671b29557..6cec4a52bb9 100644 --- a/htdocs/admin/prelevement.php +++ b/htdocs/admin/prelevement.php @@ -83,20 +83,20 @@ if ($action == "set") } if (GETPOST("PRELEVEMENT_END_TO_END") || GETPOST("PRELEVEMENT_END_TO_END")=="") { - $res = dolibarr_set_const($db, "END_TO_END", GETPOST("PRELEVEMENT_END_TO_END"),'chaine',0,'',$conf->entity); + $res = dolibarr_set_const($db, "PRELEVEMENT_END_TO_END", GETPOST("PRELEVEMENT_END_TO_END"),'chaine',0,'',$conf->entity); if (! $res > 0) $error++; } if (GETPOST("PRELEVEMENT_USTRD") || GETPOST("PRELEVEMENT_USTRD")=="") { - $res = dolibarr_set_const($db, "USTRD", GETPOST("PRELEVEMENT_USTRD"),'chaine',0,'',$conf->entity); + $res = dolibarr_set_const($db, "PRELEVEMENT_USTRD", GETPOST("PRELEVEMENT_USTRD"),'chaine',0,'',$conf->entity); if (! $res > 0) $error++; } if (GETPOST("PRELEVEMENT_ADDDAYS") || GETPOST("PRELEVEMENT_ADDDAYS")=="") { - $res = dolibarr_set_const($db, "ADDDAYS", GETPOST("PRELEVEMENT_ADDDAYS"),'chaine',0,'',$conf->entity); + $res = dolibarr_set_const($db, "PRELEVEMENT_ADDDAYS", GETPOST("PRELEVEMENT_ADDDAYS"),'chaine',0,'',$conf->entity); if (! $res > 0) $error++; - } + } else if (! $error) { @@ -241,19 +241,20 @@ print ''; //EntToEnd print ''; print ''; +print ''; print ''; //USTRD print ''; print ''; +print ''; print ''; //ADDDAYS print ''; print ''; +if (! $conf->global->PRELEVEMENT_ADDDAYS) $conf->global->PRELEVEMENT_ADDDAYS=0; +print ''; print ''; print '
'.$langs->trans("Description").''.$langs->trans("Value").''.$langs->trans("Description").''.$langs->trans("Description").'
'.$langs->trans("MARGIN_METHODE_FOR_DISCOUNT").''; +print ''; print Form::selectarray('MARGIN_METHODE_FOR_DISCOUNT', $methods, $conf->global->MARGIN_METHODE_FOR_DISCOUNT); print ''; @@ -257,7 +257,7 @@ print ''; print ""; print '
'.$langs->trans("AgentContactType").''; +print ''; $formcompany = new FormCompany($db); $facture = new Facture($db); print $formcompany->selectTypeContact($facture, $conf->global->AGENT_CONTACT_TYPE, "AGENT_CONTACT_TYPE","internal","code",1); From 6fc28af92b71f7e8444f64f2bdcc842b469bd1d2 Mon Sep 17 00:00:00 2001 From: Philippe GRAND Date: Sat, 19 Jan 2019 12:53:47 +0100 Subject: [PATCH 10/63] update with html5 compliant code --- htdocs/modulebuilder/template/myobject_list.php | 4 ++-- htdocs/opensurvey/list.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/htdocs/modulebuilder/template/myobject_list.php b/htdocs/modulebuilder/template/myobject_list.php index a1738474459..eeabbb11b1e 100644 --- a/htdocs/modulebuilder/template/myobject_list.php +++ b/htdocs/modulebuilder/template/myobject_list.php @@ -542,8 +542,8 @@ if (isset($totalarray['pos'])) { if ($i == 1) { - if ($num < $limit) print ''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").''.$langs->trans("Total").''.$langs->trans("Totalforthispage").'
'.$langs->trans("END_TO_END").''; -print '
'.$langs->trans("USTRD").''; -print '
'.$langs->trans("ADDDAYS").''; -print '
'; print '
'; diff --git a/htdocs/compta/prelevement/class/bonprelevement.class.php b/htdocs/compta/prelevement/class/bonprelevement.class.php index d002e4292f1..143b73fbaeb 100644 --- a/htdocs/compta/prelevement/class/bonprelevement.class.php +++ b/htdocs/compta/prelevement/class/bonprelevement.class.php @@ -1605,7 +1605,7 @@ class BonPrelevement extends CommonObject $XML_DEBITOR .=' '.$CrLf; $XML_DEBITOR .=' '.$CrLf; // $XML_DEBITOR .=' '.('AS-'.dol_trunc($row_ref,20).'-'.$Rowing).''.$CrLf; // ISO20022 states that EndToEndId has a MaxLength of 35 characters - $XML_DEBITOR .=' '.(($conf->global->END_TO_END != "" ) ? $conf->global->END_TO_END : ('AS-'.dol_trunc($row_ref,20)).'-'.$Rowing).''.$CrLf; // ISO20022 states that EndToEndId has a MaxLength of 35 characters + $XML_DEBITOR .=' '.(($conf->global->PRELEVEMENT_END_TO_END != "" ) ? $conf->global->PRELEVEMENT_END_TO_END : ('AS-'.dol_trunc($row_ref,20)).'-'.$Rowing).''.$CrLf; // ISO20022 states that EndToEndId has a MaxLength of 35 characters $XML_DEBITOR .=' '.$CrLf; $XML_DEBITOR .=' '.round($row_somme, 2).''.$CrLf; $XML_DEBITOR .=' '.$CrLf; @@ -1638,7 +1638,7 @@ class BonPrelevement extends CommonObject $XML_DEBITOR .=' '.$CrLf; // $XML_DEBITOR .=' '.($row_ref.'/'.$Rowing.'/'.$Rum).''.$CrLf; // $XML_DEBITOR .=' '.dol_trunc($row_ref, 135).''.$CrLf; // 140 max - $XML_DEBITOR .=' '.(($conf->global->USTRD != "" ) ? $conf->global->USTRD : dol_trunc($row_ref, 135) ).''.$CrLf; // 140 max + $XML_DEBITOR .=' '.(($conf->global->PRELEVEMENT_USTRD != "" ) ? $conf->global->PRELEVEMENT_USTRD : dol_trunc($row_ref, 135) ).''.$CrLf; // 140 max $XML_DEBITOR .=' '.$CrLf; $XML_DEBITOR .=' '.$CrLf; return $XML_DEBITOR; diff --git a/htdocs/compta/prelevement/create.php b/htdocs/compta/prelevement/create.php index f50b81073d6..2c542f9bad0 100644 --- a/htdocs/compta/prelevement/create.php +++ b/htdocs/compta/prelevement/create.php @@ -5,6 +5,7 @@ * Copyright (C) 2010-2012 Juanjo Menent * Copyright (C) 2018 Nicolas ZABOURI * Copyright (C) 2018 Frédéric France + * Copyright (C) 2019 Markus Welters * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -67,7 +68,7 @@ if ($action == 'create') { // $conf->global->PRELEVEMENT_CODE_BANQUE and $conf->global->PRELEVEMENT_CODE_GUICHET should be empty $bprev = new BonPrelevement($db); - $executiondate = dol_mktime(0, 0, 0, GETPOST('remonth'), (GETPOST('reday')+$conf->global->ADDDAYS), GETPOST('reyear')); + $executiondate = dol_mktime(0, 0, 0, GETPOST('remonth'), (GETPOST('reday')+$conf->global->PRELEVEMENT_ADDDAYS), GETPOST('reyear')); $result = $bprev->create($conf->global->PRELEVEMENT_CODE_BANQUE, $conf->global->PRELEVEMENT_CODE_GUICHET, $mode, $format,$executiondate); if ($result < 0) diff --git a/htdocs/install/mysql/data/llx_const.sql b/htdocs/install/mysql/data/llx_const.sql index 9eb31db347a..bca58905104 100644 --- a/htdocs/install/mysql/data/llx_const.sql +++ b/htdocs/install/mysql/data/llx_const.sql @@ -5,6 +5,7 @@ -- Copyright (C) 2004 Guillaume Delecourt -- Copyright (C) 2005-2012 Regis Houssin -- Copyright (C) 2007 Patrick Raguin +-- Copyright (C) 2019 Markus Welters -- -- This program is free software; you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by @@ -43,7 +44,7 @@ insert into llx_const (name, value, type, note, visible, entity) values ('SYSLOG insert into llx_const (name, value, type, note, visible, entity) values ('SYSLOG_LEVEL','7','chaine','Level of debug info to show',0,0); insert into llx_const (name, value, type, note, visible, entity) values ('MAIN_UPLOAD_DOC','2048','chaine','Max size for file upload (0 means no upload allowed)',0,0); --- Hidden but specific to one entity +-- Hidden but specific to one entity insert into llx_const (name, value, type, note, visible, entity) values ('MAIN_ENABLE_OVERWRITE_TRANSLATION','1','chaine','Enable translation overwrite',0,1); insert into llx_const (name, value, type, note, visible, entity) values ('MAIN_ENABLE_DEFAULT_VALUES','1','chaine','Enable default value overwrite',0,1); insert into llx_const (name, value, type, note, visible, entity) values ('MAIN_MONNAIE','EUR','chaine','Monnaie',0,1); diff --git a/htdocs/install/mysql/migration/8.0.0-9.0.0.sql b/htdocs/install/mysql/migration/8.0.0-9.0.0.sql index 1a8bb4ee64d..f4b0fc74d20 100644 --- a/htdocs/install/mysql/migration/8.0.0-9.0.0.sql +++ b/htdocs/install/mysql/migration/8.0.0-9.0.0.sql @@ -256,3 +256,6 @@ CREATE TABLE llx_pos_cash_fence( import_key VARCHAR(14) ) ENGINE=innodb; +-- Withdrawals / Prelevements +UPDATE llx_const set name = 'PRELEVEMENT_END_TO_END' where name = 'END_TO_END'); +UPDATE llx_const set name = 'PRELEVEMENT_USTRD' where name = 'USTRD'); \ No newline at end of file From b1e51835f51bd9ca9814c9101aba45e1312e70b1 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 14:49:26 +0100 Subject: [PATCH 12/63] Fix bad link and css --- htdocs/projet/card.php | 2 +- htdocs/ticket/list.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/projet/card.php b/htdocs/projet/card.php index 113ca7322b7..7c5b3ab17e3 100644 --- a/htdocs/projet/card.php +++ b/htdocs/projet/card.php @@ -977,7 +977,7 @@ elseif ($object->id > 0) print '
'; print '
'; - print ''; + print '
'; // Description print '
'.$langs->trans("Description").''; diff --git a/htdocs/ticket/list.php b/htdocs/ticket/list.php index c48b86ed8f9..64ec859cc0c 100644 --- a/htdocs/ticket/list.php +++ b/htdocs/ticket/list.php @@ -478,10 +478,10 @@ if ($sall) print '
'; if ($search_fk_status == 'non_closed') { - print ''; + print ''; $param .= '&search_fk_status=non_closed'; } else { - print ''; + print ''; $param .= '&search_fk_status=-1'; } print '
'; From 5c825310a38db18d2fe541a0a1a12a825e0d7c4d Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 14:49:26 +0100 Subject: [PATCH 13/63] Fix bad link and css --- htdocs/projet/card.php | 2 +- htdocs/ticket/list.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/projet/card.php b/htdocs/projet/card.php index 8bf377a9844..f2b735e6340 100644 --- a/htdocs/projet/card.php +++ b/htdocs/projet/card.php @@ -977,7 +977,7 @@ elseif ($object->id > 0) print '
'; print '
'; - print ''; + print '
'; // Description print ', + $tbodyStart = $rowMin; + $theadStart = $theadEnd = 0; // default: no no + if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) { + $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop(); + + // we can only support repeating rows that start at top row + if ($rowsToRepeatAtTop[0] == 1) { + $theadStart = $rowsToRepeatAtTop[0]; + $theadEnd = $rowsToRepeatAtTop[1]; + $tbodyStart = $rowsToRepeatAtTop[1] + 1; + } + } + + // Loop through cells + $row = $rowMin - 1; + while ($row++ < $rowMax) { + // ? + if ($row == $theadStart) { + $html .= ' ' . PHP_EOL; + $cellType = 'th'; + } + + // ? + if ($row == $tbodyStart) { + $html .= ' ' . PHP_EOL; + $cellType = 'td'; + } + + // Write row if there are HTML table cells in it + if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) { + // Start a new rowData + $rowData = []; + // Loop through columns + $column = $dimension[0][0]; + while ($column <= $dimension[1][0]) { + // Cell exists? + if ($sheet->cellExistsByColumnAndRow($column, $row)) { + $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row; + } else { + $rowData[$column] = ''; + } + ++$column; + } + $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType); + } + + // ? + if ($row == $theadEnd) { + $html .= ' ' . PHP_EOL; + } + } + $html .= $this->extendRowsForChartsAndImages($sheet, $row); + + // Close table body. + $html .= ' ' . PHP_EOL; + + // Write table footer + $html .= $this->generateTableFooter(); + + // Writing PDF? + if ($this->isPdf) { + if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) { + $html .= '
'; + } + } + + // Next sheet + ++$sheetId; + } + + return $html; + } + + /** + * Generate sheet tabs. + * + * @throws WriterException + * + * @return string + */ + public function generateNavigation() + { + // Fetch sheets + $sheets = []; + if ($this->sheetIndex === null) { + $sheets = $this->spreadsheet->getAllSheets(); + } else { + $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); + } + + // Construct HTML + $html = ''; + + // Only if there are more than 1 sheets + if (count($sheets) > 1) { + // Loop all sheets + $sheetId = 0; + + $html .= '' . PHP_EOL; + } + + return $html; + } + + private function extendRowsForChartsAndImages(Worksheet $pSheet, $row) + { + $rowMax = $row; + $colMax = 'A'; + if ($this->includeCharts) { + foreach ($pSheet->getChartCollection() as $chart) { + if ($chart instanceof Chart) { + $chartCoordinates = $chart->getTopLeftPosition(); + $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']); + $chartCol = Coordinate::columnIndexFromString($chartTL[0]); + if ($chartTL[1] > $rowMax) { + $rowMax = $chartTL[1]; + if ($chartCol > Coordinate::columnIndexFromString($colMax)) { + $colMax = $chartTL[0]; + } + } + } + } + } + + foreach ($pSheet->getDrawingCollection() as $drawing) { + if ($drawing instanceof Drawing) { + $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates()); + $imageCol = Coordinate::columnIndexFromString($imageTL[0]); + if ($imageTL[1] > $rowMax) { + $rowMax = $imageTL[1]; + if ($imageCol > Coordinate::columnIndexFromString($colMax)) { + $colMax = $imageTL[0]; + } + } + } + } + + // Don't extend rows if not needed + if ($row === $rowMax) { + return ''; + } + + $html = ''; + ++$colMax; + + while ($row <= $rowMax) { + $html .= '
'; + for ($col = 'A'; $col != $colMax; ++$col) { + $html .= ''; + } + ++$row; + $html .= ''; + } + + return $html; + } + + /** + * Generate image tag in cell. + * + * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + * @param string $coordinates Cell coordinates + * + * @return string + */ + private function writeImageInCell(Worksheet $pSheet, $coordinates) + { + // Construct HTML + $html = ''; + + // Write images + foreach ($pSheet->getDrawingCollection() as $drawing) { + if ($drawing instanceof Drawing) { + if ($drawing->getCoordinates() == $coordinates) { + $filename = $drawing->getPath(); + + // Strip off eventual '.' + if (substr($filename, 0, 1) == '.') { + $filename = substr($filename, 1); + } + + // Prepend images root + $filename = $this->getImagesRoot() . $filename; + + // Strip off eventual '.' + if (substr($filename, 0, 1) == '.' && substr($filename, 0, 2) != './') { + $filename = substr($filename, 1); + } + + // Convert UTF8 data to PCDATA + $filename = htmlspecialchars($filename); + + $html .= PHP_EOL; + if ((!$this->embedImages) || ($this->isPdf)) { + $imageData = $filename; + } else { + $imageDetails = getimagesize($filename); + if ($fp = fopen($filename, 'rb', 0)) { + $picture = fread($fp, filesize($filename)); + fclose($fp); + // base64 encode the binary data, then break it + // into chunks according to RFC 2045 semantics + $base64 = chunk_split(base64_encode($picture)); + $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + } else { + $imageData = $filename; + } + } + + $html .= '
'; + $html .= ''; + $html .= '
'; + } + } elseif ($drawing instanceof MemoryDrawing) { + if ($drawing->getCoordinates() != $coordinates) { + continue; + } + ob_start(); // Let's start output buffering. + imagepng($drawing->getImageResource()); // This will normally output the image, but because of ob_start(), it won't. + $contents = ob_get_contents(); // Instead, output above is saved to $contents + ob_end_clean(); // End the output buffer. + + $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents); + + // Because of the nature of tables, width is more important than height. + // max-width: 100% ensures that image doesnt overflow containing cell + // width: X sets width of supplied image. + // As a result, images bigger than cell will be contained and images smaller will not get stretched + $html .= ''; + } + } + + return $html; + } + + /** + * Generate chart tag in cell. + * + * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + * @param string $coordinates Cell coordinates + * + * @return string + */ + private function writeChartInCell(Worksheet $pSheet, $coordinates) + { + // Construct HTML + $html = ''; + + // Write charts + foreach ($pSheet->getChartCollection() as $chart) { + if ($chart instanceof Chart) { + $chartCoordinates = $chart->getTopLeftPosition(); + if ($chartCoordinates['cell'] == $coordinates) { + $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; + if (!$chart->render($chartFileName)) { + return; + } + + $html .= PHP_EOL; + $imageDetails = getimagesize($chartFileName); + if ($fp = fopen($chartFileName, 'rb', 0)) { + $picture = fread($fp, filesize($chartFileName)); + fclose($fp); + // base64 encode the binary data, then break it + // into chunks according to RFC 2045 semantics + $base64 = chunk_split(base64_encode($picture)); + $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + + $html .= '
'; + $html .= '' . PHP_EOL; + $html .= '
'; + + unlink($chartFileName); + } + } + } + } + + // Return + return $html; + } + + /** + * Generate CSS styles. + * + * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (<style> and </style>) + * + * @throws WriterException + * + * @return string + */ + public function generateStyles($generateSurroundingHTML = true) + { + // Build CSS + $css = $this->buildCSS($generateSurroundingHTML); + + // Construct HTML + $html = ''; + + // Start styles + if ($generateSurroundingHTML) { + $html .= ' ' . PHP_EOL; + } + + // Return + return $html; + } + + /** + * Build CSS styles. + * + * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { }) + * + * @throws WriterException + * + * @return array + */ + public function buildCSS($generateSurroundingHTML = true) + { + // Cached? + if ($this->cssStyles !== null) { + return $this->cssStyles; + } + + // Ensure that spans have been calculated + if (!$this->spansAreCalculated) { + $this->calculateSpans(); + } + + // Construct CSS + $css = []; + + // Start styles + if ($generateSurroundingHTML) { + // html { } + $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif'; + $css['html']['font-size'] = '11pt'; + $css['html']['background-color'] = 'white'; + } + + // CSS for comments as found in LibreOffice + $css['a.comment-indicator:hover + div.comment'] = [ + 'background' => '#ffd', + 'position' => 'absolute', + 'display' => 'block', + 'border' => '1px solid black', + 'padding' => '0.5em', + ]; + + $css['a.comment-indicator'] = [ + 'background' => 'red', + 'display' => 'inline-block', + 'border' => '1px solid black', + 'width' => '0.5em', + 'height' => '0.5em', + ]; + + $css['div.comment']['display'] = 'none'; + + // table { } + $css['table']['border-collapse'] = 'collapse'; + if (!$this->isPdf) { + $css['table']['page-break-after'] = 'always'; + } + + // .gridlines td { } + $css['.gridlines td']['border'] = '1px dotted black'; + $css['.gridlines th']['border'] = '1px dotted black'; + + // .b {} + $css['.b']['text-align'] = 'center'; // BOOL + + // .e {} + $css['.e']['text-align'] = 'center'; // ERROR + + // .f {} + $css['.f']['text-align'] = 'right'; // FORMULA + + // .inlineStr {} + $css['.inlineStr']['text-align'] = 'left'; // INLINE + + // .n {} + $css['.n']['text-align'] = 'right'; // NUMERIC + + // .s {} + $css['.s']['text-align'] = 'left'; // STRING + + // Calculate cell style hashes + foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { + $css['td.style' . $index] = $this->createCSSStyle($style); + $css['th.style' . $index] = $this->createCSSStyle($style); + } + + // Fetch sheets + $sheets = []; + if ($this->sheetIndex === null) { + $sheets = $this->spreadsheet->getAllSheets(); + } else { + $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); + } + + // Build styles per sheet + foreach ($sheets as $sheet) { + // Calculate hash code + $sheetIndex = $sheet->getParent()->getIndex($sheet); + + // Build styles + // Calculate column widths + $sheet->calculateColumnWidths(); + + // col elements, initialize + $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1; + $column = -1; + while ($column++ < $highestColumnIndex) { + $this->columnWidths[$sheetIndex][$column] = 42; // approximation + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt'; + } + + // col elements, loop through columnDimensions and set width + foreach ($sheet->getColumnDimensions() as $columnDimension) { + if (($width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont)) >= 0) { + $width = SharedDrawing::pixelsToPoints($width); + $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1; + $this->columnWidths[$sheetIndex][$column] = $width; + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt'; + + if ($columnDimension->getVisible() === false) { + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse'; + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['*display'] = 'none'; // target IE6+7 + } + } + } + + // Default row height + $rowDimension = $sheet->getDefaultRowDimension(); + + // table.sheetN tr { } + $css['table.sheet' . $sheetIndex . ' tr'] = []; + + if ($rowDimension->getRowHeight() == -1) { + $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont()); + } else { + $pt_height = $rowDimension->getRowHeight(); + } + $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt'; + if ($rowDimension->getVisible() === false) { + $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none'; + $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden'; + } + + // Calculate row heights + foreach ($sheet->getRowDimensions() as $rowDimension) { + $row = $rowDimension->getRowIndex() - 1; + + // table.sheetN tr.rowYYYYYY { } + $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = []; + + if ($rowDimension->getRowHeight() == -1) { + $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont()); + } else { + $pt_height = $rowDimension->getRowHeight(); + } + $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt'; + if ($rowDimension->getVisible() === false) { + $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none'; + $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden'; + } + } + } + + // Cache + if ($this->cssStyles === null) { + $this->cssStyles = $css; + } + + // Return + return $css; + } + + /** + * Create CSS style. + * + * @param Style $pStyle + * + * @return array + */ + private function createCSSStyle(Style $pStyle) + { + // Create CSS + $css = array_merge( + $this->createCSSStyleAlignment($pStyle->getAlignment()), + $this->createCSSStyleBorders($pStyle->getBorders()), + $this->createCSSStyleFont($pStyle->getFont()), + $this->createCSSStyleFill($pStyle->getFill()) + ); + + // Return + return $css; + } + + /** + * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Alignment). + * + * @param Alignment $pStyle \PhpOffice\PhpSpreadsheet\Style\Alignment + * + * @return array + */ + private function createCSSStyleAlignment(Alignment $pStyle) + { + // Construct CSS + $css = []; + + // Create CSS + $css['vertical-align'] = $this->mapVAlign($pStyle->getVertical()); + if ($textAlign = $this->mapHAlign($pStyle->getHorizontal())) { + $css['text-align'] = $textAlign; + if (in_array($textAlign, ['left', 'right'])) { + $css['padding-' . $textAlign] = (string) ((int) $pStyle->getIndent() * 9) . 'px'; + } + } + + return $css; + } + + /** + * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Font). + * + * @param Font $pStyle + * + * @return array + */ + private function createCSSStyleFont(Font $pStyle) + { + // Construct CSS + $css = []; + + // Create CSS + if ($pStyle->getBold()) { + $css['font-weight'] = 'bold'; + } + if ($pStyle->getUnderline() != Font::UNDERLINE_NONE && $pStyle->getStrikethrough()) { + $css['text-decoration'] = 'underline line-through'; + } elseif ($pStyle->getUnderline() != Font::UNDERLINE_NONE) { + $css['text-decoration'] = 'underline'; + } elseif ($pStyle->getStrikethrough()) { + $css['text-decoration'] = 'line-through'; + } + if ($pStyle->getItalic()) { + $css['font-style'] = 'italic'; + } + + $css['color'] = '#' . $pStyle->getColor()->getRGB(); + $css['font-family'] = '\'' . $pStyle->getName() . '\''; + $css['font-size'] = $pStyle->getSize() . 'pt'; + + return $css; + } + + /** + * Create CSS style (Borders). + * + * @param Borders $pStyle Borders + * + * @return array + */ + private function createCSSStyleBorders(Borders $pStyle) + { + // Construct CSS + $css = []; + + // Create CSS + $css['border-bottom'] = $this->createCSSStyleBorder($pStyle->getBottom()); + $css['border-top'] = $this->createCSSStyleBorder($pStyle->getTop()); + $css['border-left'] = $this->createCSSStyleBorder($pStyle->getLeft()); + $css['border-right'] = $this->createCSSStyleBorder($pStyle->getRight()); + + return $css; + } + + /** + * Create CSS style (Border). + * + * @param Border $pStyle Border + * + * @return string + */ + private function createCSSStyleBorder(Border $pStyle) + { + // Create CSS - add !important to non-none border styles for merged cells + $borderStyle = $this->mapBorderStyle($pStyle->getBorderStyle()); + $css = $borderStyle . ' #' . $pStyle->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important'); + + return $css; + } + + /** + * Create CSS style (Fill). + * + * @param Fill $pStyle Fill + * + * @return array + */ + private function createCSSStyleFill(Fill $pStyle) + { + // Construct HTML + $css = []; + + // Create CSS + $value = $pStyle->getFillType() == Fill::FILL_NONE ? + 'white' : '#' . $pStyle->getStartColor()->getRGB(); + $css['background-color'] = $value; + + return $css; + } + + /** + * Generate HTML footer. + */ + public function generateHTMLFooter() + { + // Construct HTML + $html = ''; + $html .= ' ' . PHP_EOL; + $html .= '' . PHP_EOL; + + return $html; + } + + /** + * Generate table header. + * + * @param Worksheet $pSheet The worksheet for the table we are writing + * + * @return string + */ + private function generateTableHeader($pSheet) + { + $sheetIndex = $pSheet->getParent()->getIndex($pSheet); + + // Construct HTML + $html = ''; + $html .= $this->setMargins($pSheet); + + if (!$this->useInlineCss) { + $gridlines = $pSheet->getShowGridlines() ? ' gridlines' : ''; + $html .= '
'.$langs->trans("Description").''; diff --git a/htdocs/ticket/list.php b/htdocs/ticket/list.php index c48b86ed8f9..64ec859cc0c 100644 --- a/htdocs/ticket/list.php +++ b/htdocs/ticket/list.php @@ -478,10 +478,10 @@ if ($sall) print '
'; if ($search_fk_status == 'non_closed') { - print ''; + print ''; $param .= '&search_fk_status=non_closed'; } else { - print ''; + print ''; $param .= '&search_fk_status=-1'; } print '
'; From 691deeb2c38102cd735985c0423b830cf8b0f85f Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 15:33:10 +0100 Subject: [PATCH 14/63] Add php7.3 in phpunit --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index d660f89e79d..02ac5a1c805 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,6 +37,7 @@ php: - '7.0' - '7.1' - '7.2' +- '7.3' - nightly env: @@ -77,6 +78,8 @@ matrix: env: DB=postgresql - php: '7.1' env: DB=postgresql + - php: '7.2' + env: DB=postgresql - php: nightly env: DB=postgresql From f311deb405e6d07be8a537f461ec4085bd136d7b Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 16:14:45 +0100 Subject: [PATCH 15/63] Fix hash for gol_getprefix must not contains special char. --- htdocs/core/lib/functions.lib.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index 5d1a977d049..f27224c645f 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -616,7 +616,7 @@ if (! function_exists('dol_getprefix')) /** * Return a prefix to use for this Dolibarr instance, for session/cookie names or email id. * The prefix for session is unique in a web context only and is unique for instance and avoid conflict - * between multi-instances, even when having two instances with one root dir or two instances in virtual servers. + * between multi-instances, even when having two instances with same root dir or two instances in same virtual server. * The prefix for email is unique if MAIL_PREFIX_FOR_EMAIL_ID is set to a value, otherwise value may be same than other instance. * * @param string $mode '' (prefix for session name) or 'email' (prefix for email id) @@ -634,16 +634,16 @@ if (! function_exists('dol_getprefix')) if ($conf->global->MAIL_PREFIX_FOR_EMAIL_ID != 'SERVER_NAME') return $conf->global->MAIL_PREFIX_FOR_EMAIL_ID; else if (isset($_SERVER["SERVER_NAME"])) return $_SERVER["SERVER_NAME"]; } - return dol_hash(DOL_DOCUMENT_ROOT.DOL_URL_ROOT); + return dol_hash(DOL_DOCUMENT_ROOT.DOL_URL_ROOT, '3'); } if (isset($_SERVER["SERVER_NAME"]) && isset($_SERVER["DOCUMENT_ROOT"])) { - return dol_hash($_SERVER["SERVER_NAME"].$_SERVER["DOCUMENT_ROOT"].DOL_DOCUMENT_ROOT.DOL_URL_ROOT); - // Use this for a "readable" cookie name + return dol_hash($_SERVER["SERVER_NAME"].$_SERVER["DOCUMENT_ROOT"].DOL_DOCUMENT_ROOT.DOL_URL_ROOT, '3'); + // Use this for a "readable" key //return dol_sanitizeFileName($_SERVER["SERVER_NAME"].$_SERVER["DOCUMENT_ROOT"].DOL_DOCUMENT_ROOT.DOL_URL_ROOT); } - return dol_hash(DOL_DOCUMENT_ROOT.DOL_URL_ROOT); + return dol_hash(DOL_DOCUMENT_ROOT.DOL_URL_ROOT, '3'); } } From 4ffd7de9f7aa32662381c2cc9941dfab4683e3aa Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 16:15:05 +0100 Subject: [PATCH 16/63] doc --- htdocs/core/lib/functions.lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index f27224c645f..fffe95cbb0c 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -616,7 +616,7 @@ if (! function_exists('dol_getprefix')) /** * Return a prefix to use for this Dolibarr instance, for session/cookie names or email id. * The prefix for session is unique in a web context only and is unique for instance and avoid conflict - * between multi-instances, even when having two instances with same root dir or two instances in same virtual server. + * between multi-instances, even when having two instances with same root dir or two instances in same virtual servers. * The prefix for email is unique if MAIL_PREFIX_FOR_EMAIL_ID is set to a value, otherwise value may be same than other instance. * * @param string $mode '' (prefix for session name) or 'email' (prefix for email id) From 8569987b6095f44665ae2e1d9668ebb22ca6f99b Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 16:26:16 +0100 Subject: [PATCH 17/63] Fix travis for php 7.3 --- .travis.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 02ac5a1c805..aac89618c42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -133,7 +133,7 @@ install: if [ "$TRAVIS_PHP_VERSION" = '5.6' ] || [ "$TRAVIS_PHP_VERSION" = '7.0' ] || [ "$TRAVIS_PHP_VERSION" = '7.1' ]; then composer -n require phpunit/phpunit ^5 fi - if [ "$TRAVIS_PHP_VERSION" = '7.2' ] || [ "$TRAVIS_PHP_VERSION" = 'nightly' ]; then + if [ "$TRAVIS_PHP_VERSION" = '7.2' ] || [ "$TRAVIS_PHP_VERSION" = '7.3' ] || [ "$TRAVIS_PHP_VERSION" = 'nightly' ]; then composer -n require phpunit/phpunit ^5 fi echo @@ -168,13 +168,9 @@ before_script: echo "Set timezone" echo 'date.timezone = "Europe/Paris"' >> ~/.phpenv/versions/$PHP_VERSION_NAME/etc/php.ini if [ "$TRAVIS_PHP_VERSION" = '5.4' ]; then - #echo - #echo "Enabling APC for PHP <= 5.4" - # Documentation says it should be available for PHP <= 5.6 but it's not for 5.5 and 5.6! - #echo 'extension = apc.so' >> ~/.phpenv/versions/$PHP_VERSION_NAME/etc/php.ini + # Documentation says it should be available for all PHP versions but it's not for 5.5 and 5.6, 7.0, 7.1, 7.2 and nightly! echo echo "Enabling Memcached for PHP <= 5.4" - # Documentation says it should be available for all PHP versions but it's not for 5.5 and 5.6, 7.0, 7.1, 7.2 and nightly! echo 'extension = memcached.so' >> ~/.phpenv/versions/$PHP_VERSION_NAME/etc/php.ini fi phpenv rehash @@ -256,7 +252,7 @@ before_script: # enable php-fpm - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf - | - if [ "$TRAVIS_PHP_VERSION" = '7.0' ] || [ "$TRAVIS_PHP_VERSION" = '7.1' ] || [ "$TRAVIS_PHP_VERSION" = '7.2' ] || [ "$TRAVIS_PHP_VERSION" = 'nightly' ]; then + if [ "$TRAVIS_PHP_VERSION" = '7.0' ] || [ "$TRAVIS_PHP_VERSION" = '7.1' ] || [ "$TRAVIS_PHP_VERSION" = '7.2' ] || [ "$TRAVIS_PHP_VERSION" = '7.3' ] || [ "$TRAVIS_PHP_VERSION" = 'nightly' ]; then # Copy the included pool sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.d/www.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.d/www.conf fi From 213a5c9d5d1bd27518cae70339e291b2c56d8d48 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 16:40:36 +0100 Subject: [PATCH 18/63] Add apache error log --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index aac89618c42..657c1bc228a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -211,6 +211,7 @@ before_script: fi if [ "$DB" = 'postgresql' ]; then #pgloader mysql://root:pass@127.0.0.1/dolibarr_35 postgresql://dolibarrowner:dolibarrownerpass@127.0.0.1/dolibarr_dev + echo pgloader mysql://root@127.0.0.1/travis postgresql:///travis pgloader mysql://root@127.0.0.1/travis postgresql:///travis fi # TODO: SQLite @@ -280,6 +281,7 @@ script: # The wget should return a page with line ' wget -O - http://127.0.0.1 > test.html head test.html + tail /var/log/apache2/error.log set +e echo From 9efe10301494738ecfc2fc92724adf904c34b9f4 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 16:44:35 +0100 Subject: [PATCH 19/63] Fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 657c1bc228a..dd5d97fd8a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -281,7 +281,7 @@ script: # The wget should return a page with line ' wget -O - http://127.0.0.1 > test.html head test.html - tail /var/log/apache2/error.log + sudo cat /var/log/apache2/error.log set +e echo From 88635e309903d90fab4fda29b3e6ad7c64979840 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 16:54:04 +0100 Subject: [PATCH 20/63] Try to get log --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dd5d97fd8a3..ed585e2970f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -276,8 +276,8 @@ before_script: script: - | echo "Checking webserver availability by a wget -O - http://127.0.0.1" - # Ensure we catch errors - set -e + # Ensure we stop on error with set -e + set +e # The wget should return a page with line ' wget -O - http://127.0.0.1 > test.html head test.html From 616aa1f81cf2dc9b7310d97d7ffdb6b9d67a943f Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 17:10:14 +0100 Subject: [PATCH 21/63] Fix var not defined --- htdocs/core/modules/modDeplacement.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/modules/modDeplacement.class.php b/htdocs/core/modules/modDeplacement.class.php index 285d795ea11..e5c7146729f 100644 --- a/htdocs/core/modules/modDeplacement.class.php +++ b/htdocs/core/modules/modDeplacement.class.php @@ -139,7 +139,7 @@ class modDeplacement extends DolibarrModules $childids = $user->getAllChildIds(); $childids[]=$user->id; - if (empty($user->rights->deplacement->readall) && empty($user->rights->deplacement->lire_tous)) $sql.=' AND d.fk_user IN ('.join(',',$childids).')'; + if (empty($user->rights->deplacement->readall) && empty($user->rights->deplacement->lire_tous)) $this->export_sql_end[$r] .=' AND d.fk_user IN ('.join(',',$childids).')'; } } From 9a6517a3bdc60ce8ca2f091b3b8826c044434ba9 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 17:38:05 +0100 Subject: [PATCH 22/63] Try to trap apache log --- .travis.yml | 4 ++-- build/travis-ci/apache.conf | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed585e2970f..48ea16ab886 100644 --- a/.travis.yml +++ b/.travis.yml @@ -281,7 +281,7 @@ script: # The wget should return a page with line ' wget -O - http://127.0.0.1 > test.html head test.html - sudo cat /var/log/apache2/error.log + sudo cat /var/log/apache2/travis_error_log set +e echo @@ -379,7 +379,7 @@ after_failure: done # Apache log file echo "Debugging informations for file apache error.log" - sudo cat /var/log/apache2/error.log + sudo cat /var/log/apache2/travis_error_log if [ "$DEBUG" = true ]; then # Dolibarr log file echo "Debugging informations for file dolibarr.log (latest 50 lines)" diff --git a/build/travis-ci/apache.conf b/build/travis-ci/apache.conf index a4965c40d68..60a4095c34a 100644 --- a/build/travis-ci/apache.conf +++ b/build/travis-ci/apache.conf @@ -1,5 +1,6 @@ DocumentRoot %TRAVIS_BUILD_DIR%/htdocs + ErrorLog /var/log/apache2/travis_error_log Options FollowSymLinks MultiViews ExecCGI From 9712da21cb08f1f0cf273a13711eeb714e2f241f Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sat, 19 Jan 2019 18:00:20 +0100 Subject: [PATCH 23/63] Try to fix pgsql connection --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 48ea16ab886..6b3b8af1edf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -229,9 +229,11 @@ before_script: echo '$'dolibarr_main_db_user=\'travis\'';' >> $CONF_FILE if [ "$DB" = 'mysql' ] || [ "$DB" = 'mariadb' ]; then echo '$'dolibarr_main_db_type=\'mysqli\'';' >> $CONF_FILE + echo '$'dolibarr_main_db_port=\'3306\'';' >> $CONF_FILE fi if [ "$DB" = 'postgresql' ]; then echo '$'dolibarr_main_db_type=\'pgsql\'';' >> $CONF_FILE + echo '$'dolibarr_main_db_port=\'5432\'';' >> $CONF_FILE fi # TODO: SQLite echo '$'dolibarr_main_authentication=\'dolibarr\'';' >> $CONF_FILE @@ -370,7 +372,7 @@ after_success: after_failure: - | echo Failure detected, so we show samples of log to help diagnose - # This part of code is executed only if previous commande that fails are enclosed with set +e + # This part of code is executed only if previous command that fails are enclosed with set +e # Upgrade log files for ficlog in `ls $TRAVIS_BUILD_DIR/*.log` do From 43277cb6309ce4139291269edf335326c948e76d Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Sun, 20 Jan 2019 13:22:04 +0100 Subject: [PATCH 24/63] Removed some warning --- dev/setup/codesniffer/ruleset.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/setup/codesniffer/ruleset.xml b/dev/setup/codesniffer/ruleset.xml index b7b6a4a7099..1ac4f504959 100644 --- a/dev/setup/codesniffer/ruleset.xml +++ b/dev/setup/codesniffer/ruleset.xml @@ -59,7 +59,10 @@ 0 - + + 0 + + "hello""world". + * + * @param string $value UTF-8 encoded string + * + * @return string + */ + private static function UTF8toExcelDoubleQuoted($value) + { + return '"' . str_replace('"', '""', $value) . '"'; + } + + /** + * Reads first 8 bytes of a string and return IEEE 754 float. + * + * @param string $data Binary string that is at least 8 bytes long + * + * @return float + */ + private static function extractNumber($data) + { + $rknumhigh = self::getInt4d($data, 4); + $rknumlow = self::getInt4d($data, 0); + $sign = ($rknumhigh & 0x80000000) >> 31; + $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023; + $mantissa = (0x100000 | ($rknumhigh & 0x000fffff)); + $mantissalow1 = ($rknumlow & 0x80000000) >> 31; + $mantissalow2 = ($rknumlow & 0x7fffffff); + $value = $mantissa / pow(2, (20 - $exp)); + + if ($mantissalow1 != 0) { + $value += 1 / pow(2, (21 - $exp)); + } + + $value += $mantissalow2 / pow(2, (52 - $exp)); + if ($sign) { + $value *= -1; + } + + return $value; + } + + /** + * @param int $rknum + * + * @return float + */ + private static function getIEEE754($rknum) + { + if (($rknum & 0x02) != 0) { + $value = $rknum >> 2; + } else { + // changes by mmp, info on IEEE754 encoding from + // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html + // The RK format calls for using only the most significant 30 bits + // of the 64 bit floating point value. The other 34 bits are assumed + // to be 0 so we use the upper 30 bits of $rknum as follows... + $sign = ($rknum & 0x80000000) >> 31; + $exp = ($rknum & 0x7ff00000) >> 20; + $mantissa = (0x100000 | ($rknum & 0x000ffffc)); + $value = $mantissa / pow(2, (20 - ($exp - 1023))); + if ($sign) { + $value = -1 * $value; + } + //end of changes by mmp + } + if (($rknum & 0x01) != 0) { + $value /= 100; + } + + return $value; + } + + /** + * Get UTF-8 string from (compressed or uncompressed) UTF-16 string. + * + * @param string $string + * @param bool $compressed + * + * @return string + */ + private static function encodeUTF16($string, $compressed = false) + { + if ($compressed) { + $string = self::uncompressByteString($string); + } + + return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE'); + } + + /** + * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8. + * + * @param string $string + * + * @return string + */ + private static function uncompressByteString($string) + { + $uncompressedString = ''; + $strLen = strlen($string); + for ($i = 0; $i < $strLen; ++$i) { + $uncompressedString .= $string[$i] . "\0"; + } + + return $uncompressedString; + } + + /** + * Convert string to UTF-8. Only used for BIFF5. + * + * @param string $string + * + * @return string + */ + private function decodeCodepage($string) + { + return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage); + } + + /** + * Read 16-bit unsigned integer. + * + * @param string $data + * @param int $pos + * + * @return int + */ + public static function getUInt2d($data, $pos) + { + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8); + } + + /** + * Read 16-bit signed integer. + * + * @param string $data + * @param int $pos + * + * @return int + */ + public static function getInt2d($data, $pos) + { + return unpack('s', $data[$pos] . $data[$pos + 1])[1]; + } + + /** + * Read 32-bit signed integer. + * + * @param string $data + * @param int $pos + * + * @return int + */ + public static function getInt4d($data, $pos) + { + // FIX: represent numbers correctly on 64-bit system + // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 + // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems + $_or_24 = ord($data[$pos + 3]); + if ($_or_24 >= 128) { + // negative number + $_ord_24 = -abs((256 - $_or_24) << 24); + } else { + $_ord_24 = ($_or_24 & 127) << 24; + } + + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; + } + + private function parseRichText($is) + { + $value = new RichText(); + $value->createText($is); + + return $value; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color.php new file mode 100644 index 00000000000..c45f88c72b5 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color.php @@ -0,0 +1,36 @@ + 'FF0000'] + */ + public static function map($color, $palette, $version) + { + if ($color <= 0x07 || $color >= 0x40) { + // special built-in color + return Color\BuiltIn::lookup($color); + } elseif (isset($palette, $palette[$color - 8])) { + // palette color, color index 0x08 maps to pallete index 0 + return $palette[$color - 8]; + } + + // default color table + if ($version == Xls::XLS_BIFF8) { + return Color\BIFF8::lookup($color); + } + + // BIFF5 + return Color\BIFF5::lookup($color); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php new file mode 100644 index 00000000000..743d938773e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php @@ -0,0 +1,81 @@ + '000000', + 0x09 => 'FFFFFF', + 0x0A => 'FF0000', + 0x0B => '00FF00', + 0x0C => '0000FF', + 0x0D => 'FFFF00', + 0x0E => 'FF00FF', + 0x0F => '00FFFF', + 0x10 => '800000', + 0x11 => '008000', + 0x12 => '000080', + 0x13 => '808000', + 0x14 => '800080', + 0x15 => '008080', + 0x16 => 'C0C0C0', + 0x17 => '808080', + 0x18 => '8080FF', + 0x19 => '802060', + 0x1A => 'FFFFC0', + 0x1B => 'A0E0F0', + 0x1C => '600080', + 0x1D => 'FF8080', + 0x1E => '0080C0', + 0x1F => 'C0C0FF', + 0x20 => '000080', + 0x21 => 'FF00FF', + 0x22 => 'FFFF00', + 0x23 => '00FFFF', + 0x24 => '800080', + 0x25 => '800000', + 0x26 => '008080', + 0x27 => '0000FF', + 0x28 => '00CFFF', + 0x29 => '69FFFF', + 0x2A => 'E0FFE0', + 0x2B => 'FFFF80', + 0x2C => 'A6CAF0', + 0x2D => 'DD9CB3', + 0x2E => 'B38FEE', + 0x2F => 'E3E3E3', + 0x30 => '2A6FF9', + 0x31 => '3FB8CD', + 0x32 => '488436', + 0x33 => '958C41', + 0x34 => '8E5E42', + 0x35 => 'A0627A', + 0x36 => '624FAC', + 0x37 => '969696', + 0x38 => '1D2FBE', + 0x39 => '286676', + 0x3A => '004500', + 0x3B => '453E01', + 0x3C => '6A2813', + 0x3D => '85396A', + 0x3E => '4A3285', + 0x3F => '424242', + ]; + + /** + * Map color array from BIFF5 built-in color index. + * + * @param int $color + * + * @return array + */ + public static function lookup($color) + { + if (isset(self::$map[$color])) { + return ['rgb' => self::$map[$color]]; + } + + return ['rgb' => '000000']; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php new file mode 100644 index 00000000000..5c109fb071f --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php @@ -0,0 +1,81 @@ + '000000', + 0x09 => 'FFFFFF', + 0x0A => 'FF0000', + 0x0B => '00FF00', + 0x0C => '0000FF', + 0x0D => 'FFFF00', + 0x0E => 'FF00FF', + 0x0F => '00FFFF', + 0x10 => '800000', + 0x11 => '008000', + 0x12 => '000080', + 0x13 => '808000', + 0x14 => '800080', + 0x15 => '008080', + 0x16 => 'C0C0C0', + 0x17 => '808080', + 0x18 => '9999FF', + 0x19 => '993366', + 0x1A => 'FFFFCC', + 0x1B => 'CCFFFF', + 0x1C => '660066', + 0x1D => 'FF8080', + 0x1E => '0066CC', + 0x1F => 'CCCCFF', + 0x20 => '000080', + 0x21 => 'FF00FF', + 0x22 => 'FFFF00', + 0x23 => '00FFFF', + 0x24 => '800080', + 0x25 => '800000', + 0x26 => '008080', + 0x27 => '0000FF', + 0x28 => '00CCFF', + 0x29 => 'CCFFFF', + 0x2A => 'CCFFCC', + 0x2B => 'FFFF99', + 0x2C => '99CCFF', + 0x2D => 'FF99CC', + 0x2E => 'CC99FF', + 0x2F => 'FFCC99', + 0x30 => '3366FF', + 0x31 => '33CCCC', + 0x32 => '99CC00', + 0x33 => 'FFCC00', + 0x34 => 'FF9900', + 0x35 => 'FF6600', + 0x36 => '666699', + 0x37 => '969696', + 0x38 => '003366', + 0x39 => '339966', + 0x3A => '003300', + 0x3B => '333300', + 0x3C => '993300', + 0x3D => '993366', + 0x3E => '333399', + 0x3F => '333333', + ]; + + /** + * Map color array from BIFF8 built-in color index. + * + * @param int $color + * + * @return array + */ + public static function lookup($color) + { + if (isset(self::$map[$color])) { + return ['rgb' => self::$map[$color]]; + } + + return ['rgb' => '000000']; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php new file mode 100644 index 00000000000..90d50e336bb --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php @@ -0,0 +1,35 @@ + '000000', + 0x01 => 'FFFFFF', + 0x02 => 'FF0000', + 0x03 => '00FF00', + 0x04 => '0000FF', + 0x05 => 'FFFF00', + 0x06 => 'FF00FF', + 0x07 => '00FFFF', + 0x40 => '000000', // system window text color + 0x41 => 'FFFFFF', // system window background color + ]; + + /** + * Map built-in color to RGB value. + * + * @param int $color Indexed color + * + * @return array + */ + public static function lookup($color) + { + if (isset(self::$map[$color])) { + return ['rgb' => self::$map[$color]]; + } + + return ['rgb' => '000000']; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/ErrorCode.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/ErrorCode.php new file mode 100644 index 00000000000..7daf7230ff0 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/ErrorCode.php @@ -0,0 +1,32 @@ + '#NULL!', + 0x07 => '#DIV/0!', + 0x0F => '#VALUE!', + 0x17 => '#REF!', + 0x1D => '#NAME?', + 0x24 => '#NUM!', + 0x2A => '#N/A', + ]; + + /** + * Map error code, e.g. '#N/A'. + * + * @param int $code + * + * @return bool|string + */ + public static function lookup($code) + { + if (isset(self::$map[$code])) { + return self::$map[$code]; + } + + return false; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Escher.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Escher.php new file mode 100644 index 00000000000..858d6bbbac4 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Escher.php @@ -0,0 +1,677 @@ +object = $object; + } + + /** + * Load Escher stream data. May be a partial Escher stream. + * + * @param string $data + * + * @return BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer + */ + public function load($data) + { + $this->data = $data; + + // total byte size of Excel data (workbook global substream + sheet substreams) + $this->dataSize = strlen($this->data); + + $this->pos = 0; + + // Parse Escher stream + while ($this->pos < $this->dataSize) { + // offset: 2; size: 2: Record Type + $fbt = Xls::getUInt2d($this->data, $this->pos + 2); + + switch ($fbt) { + case self::DGGCONTAINER: + $this->readDggContainer(); + + break; + case self::DGG: + $this->readDgg(); + + break; + case self::BSTORECONTAINER: + $this->readBstoreContainer(); + + break; + case self::BSE: + $this->readBSE(); + + break; + case self::BLIPJPEG: + $this->readBlipJPEG(); + + break; + case self::BLIPPNG: + $this->readBlipPNG(); + + break; + case self::OPT: + $this->readOPT(); + + break; + case self::TERTIARYOPT: + $this->readTertiaryOPT(); + + break; + case self::SPLITMENUCOLORS: + $this->readSplitMenuColors(); + + break; + case self::DGCONTAINER: + $this->readDgContainer(); + + break; + case self::DG: + $this->readDg(); + + break; + case self::SPGRCONTAINER: + $this->readSpgrContainer(); + + break; + case self::SPCONTAINER: + $this->readSpContainer(); + + break; + case self::SPGR: + $this->readSpgr(); + + break; + case self::SP: + $this->readSp(); + + break; + case self::CLIENTTEXTBOX: + $this->readClientTextbox(); + + break; + case self::CLIENTANCHOR: + $this->readClientAnchor(); + + break; + case self::CLIENTDATA: + $this->readClientData(); + + break; + default: + $this->readDefault(); + + break; + } + } + + return $this->object; + } + + /** + * Read a generic record. + */ + private function readDefault() + { + // offset 0; size: 2; recVer and recInstance + $verInstance = Xls::getUInt2d($this->data, $this->pos); + + // offset: 2; size: 2: Record Type + $fbt = Xls::getUInt2d($this->data, $this->pos + 2); + + // bit: 0-3; mask: 0x000F; recVer + $recVer = (0x000F & $verInstance) >> 0; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read DggContainer record (Drawing Group Container). + */ + private function readDggContainer() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $dggContainer = new DggContainer(); + $this->object->setDggContainer($dggContainer); + $reader = new self($dggContainer); + $reader->load($recordData); + } + + /** + * Read Dgg record (Drawing Group). + */ + private function readDgg() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read BstoreContainer record (Blip Store Container). + */ + private function readBstoreContainer() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $bstoreContainer = new BstoreContainer(); + $this->object->setBstoreContainer($bstoreContainer); + $reader = new self($bstoreContainer); + $reader->load($recordData); + } + + /** + * Read BSE record. + */ + private function readBSE() + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // add BSE to BstoreContainer + $BSE = new BSE(); + $this->object->addBSE($BSE); + + $BSE->setBLIPType($recInstance); + + // offset: 0; size: 1; btWin32 (MSOBLIPTYPE) + $btWin32 = ord($recordData[0]); + + // offset: 1; size: 1; btWin32 (MSOBLIPTYPE) + $btMacOS = ord($recordData[1]); + + // offset: 2; size: 16; MD4 digest + $rgbUid = substr($recordData, 2, 16); + + // offset: 18; size: 2; tag + $tag = Xls::getUInt2d($recordData, 18); + + // offset: 20; size: 4; size of BLIP in bytes + $size = Xls::getInt4d($recordData, 20); + + // offset: 24; size: 4; number of references to this BLIP + $cRef = Xls::getInt4d($recordData, 24); + + // offset: 28; size: 4; MSOFO file offset + $foDelay = Xls::getInt4d($recordData, 28); + + // offset: 32; size: 1; unused1 + $unused1 = ord($recordData[32]); + + // offset: 33; size: 1; size of nameData in bytes (including null terminator) + $cbName = ord($recordData[33]); + + // offset: 34; size: 1; unused2 + $unused2 = ord($recordData[34]); + + // offset: 35; size: 1; unused3 + $unused3 = ord($recordData[35]); + + // offset: 36; size: $cbName; nameData + $nameData = substr($recordData, 36, $cbName); + + // offset: 36 + $cbName, size: var; the BLIP data + $blipData = substr($recordData, 36 + $cbName); + + // record is a container, read contents + $reader = new self($BSE); + $reader->load($blipData); + } + + /** + * Read BlipJPEG record. Holds raw JPEG image data. + */ + private function readBlipJPEG() + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $pos = 0; + + // offset: 0; size: 16; rgbUid1 (MD4 digest of) + $rgbUid1 = substr($recordData, 0, 16); + $pos += 16; + + // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 + if (in_array($recInstance, [0x046B, 0x06E3])) { + $rgbUid2 = substr($recordData, 16, 16); + $pos += 16; + } + + // offset: var; size: 1; tag + $tag = ord($recordData[$pos]); + $pos += 1; + + // offset: var; size: var; the raw image data + $data = substr($recordData, $pos); + + $blip = new Blip(); + $blip->setData($data); + + $this->object->setBlip($blip); + } + + /** + * Read BlipPNG record. Holds raw PNG image data. + */ + private function readBlipPNG() + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $pos = 0; + + // offset: 0; size: 16; rgbUid1 (MD4 digest of) + $rgbUid1 = substr($recordData, 0, 16); + $pos += 16; + + // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 + if ($recInstance == 0x06E1) { + $rgbUid2 = substr($recordData, 16, 16); + $pos += 16; + } + + // offset: var; size: 1; tag + $tag = ord($recordData[$pos]); + $pos += 1; + + // offset: var; size: var; the raw image data + $data = substr($recordData, $pos); + + $blip = new Blip(); + $blip->setData($data); + + $this->object->setBlip($blip); + } + + /** + * Read OPT record. This record may occur within DggContainer record or SpContainer. + */ + private function readOPT() + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $this->readOfficeArtRGFOPTE($recordData, $recInstance); + } + + /** + * Read TertiaryOPT record. + */ + private function readTertiaryOPT() + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read SplitMenuColors record. + */ + private function readSplitMenuColors() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read DgContainer record (Drawing Container). + */ + private function readDgContainer() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $dgContainer = new DgContainer(); + $this->object->setDgContainer($dgContainer); + $reader = new self($dgContainer); + $escher = $reader->load($recordData); + } + + /** + * Read Dg record (Drawing). + */ + private function readDg() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read SpgrContainer record (Shape Group Container). + */ + private function readSpgrContainer() + { + // context is either context DgContainer or SpgrContainer + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $spgrContainer = new SpgrContainer(); + + if ($this->object instanceof DgContainer) { + // DgContainer + $this->object->setSpgrContainer($spgrContainer); + } else { + // SpgrContainer + $this->object->addChild($spgrContainer); + } + + $reader = new self($spgrContainer); + $escher = $reader->load($recordData); + } + + /** + * Read SpContainer record (Shape Container). + */ + private function readSpContainer() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // add spContainer to spgrContainer + $spContainer = new SpContainer(); + $this->object->addChild($spContainer); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $reader = new self($spContainer); + $escher = $reader->load($recordData); + } + + /** + * Read Spgr record (Shape Group). + */ + private function readSpgr() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read Sp record (Shape). + */ + private function readSp() + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read ClientTextbox record. + */ + private function readClientTextbox() + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read ClientAnchor record. This record holds information about where the shape is anchored in worksheet. + */ + private function readClientAnchor() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // offset: 2; size: 2; upper-left corner column index (0-based) + $c1 = Xls::getUInt2d($recordData, 2); + + // offset: 4; size: 2; upper-left corner horizontal offset in 1/1024 of column width + $startOffsetX = Xls::getUInt2d($recordData, 4); + + // offset: 6; size: 2; upper-left corner row index (0-based) + $r1 = Xls::getUInt2d($recordData, 6); + + // offset: 8; size: 2; upper-left corner vertical offset in 1/256 of row height + $startOffsetY = Xls::getUInt2d($recordData, 8); + + // offset: 10; size: 2; bottom-right corner column index (0-based) + $c2 = Xls::getUInt2d($recordData, 10); + + // offset: 12; size: 2; bottom-right corner horizontal offset in 1/1024 of column width + $endOffsetX = Xls::getUInt2d($recordData, 12); + + // offset: 14; size: 2; bottom-right corner row index (0-based) + $r2 = Xls::getUInt2d($recordData, 14); + + // offset: 16; size: 2; bottom-right corner vertical offset in 1/256 of row height + $endOffsetY = Xls::getUInt2d($recordData, 16); + + // set the start coordinates + $this->object->setStartCoordinates(Coordinate::stringFromColumnIndex($c1 + 1) . ($r1 + 1)); + + // set the start offsetX + $this->object->setStartOffsetX($startOffsetX); + + // set the start offsetY + $this->object->setStartOffsetY($startOffsetY); + + // set the end coordinates + $this->object->setEndCoordinates(Coordinate::stringFromColumnIndex($c2 + 1) . ($r2 + 1)); + + // set the end offsetX + $this->object->setEndOffsetX($endOffsetX); + + // set the end offsetY + $this->object->setEndOffsetY($endOffsetY); + } + + /** + * Read ClientData record. + */ + private function readClientData() + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read OfficeArtRGFOPTE table of property-value pairs. + * + * @param string $data Binary data + * @param int $n Number of properties + */ + private function readOfficeArtRGFOPTE($data, $n) + { + $splicedComplexData = substr($data, 6 * $n); + + // loop through property-value pairs + for ($i = 0; $i < $n; ++$i) { + // read 6 bytes at a time + $fopte = substr($data, 6 * $i, 6); + + // offset: 0; size: 2; opid + $opid = Xls::getUInt2d($fopte, 0); + + // bit: 0-13; mask: 0x3FFF; opid.opid + $opidOpid = (0x3FFF & $opid) >> 0; + + // bit: 14; mask 0x4000; 1 = value in op field is BLIP identifier + $opidFBid = (0x4000 & $opid) >> 14; + + // bit: 15; mask 0x8000; 1 = this is a complex property, op field specifies size of complex data + $opidFComplex = (0x8000 & $opid) >> 15; + + // offset: 2; size: 4; the value for this property + $op = Xls::getInt4d($fopte, 2); + + if ($opidFComplex) { + $complexData = substr($splicedComplexData, 0, $op); + $splicedComplexData = substr($splicedComplexData, $op); + + // we store string value with complex data + $value = $complexData; + } else { + // we store integer value + $value = $op; + } + + $this->object->setOPT($opidOpid, $value); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/MD5.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/MD5.php new file mode 100644 index 00000000000..6a10e591ecb --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/MD5.php @@ -0,0 +1,184 @@ +reset(); + } + + /** + * Reset the MD5 stream context. + */ + public function reset() + { + $this->a = 0x67452301; + $this->b = 0xEFCDAB89; + $this->c = 0x98BADCFE; + $this->d = 0x10325476; + } + + /** + * Get MD5 stream context. + * + * @return string + */ + public function getContext() + { + $s = ''; + foreach (['a', 'b', 'c', 'd'] as $i) { + $v = $this->{$i}; + $s .= chr($v & 0xff); + $s .= chr(($v >> 8) & 0xff); + $s .= chr(($v >> 16) & 0xff); + $s .= chr(($v >> 24) & 0xff); + } + + return $s; + } + + /** + * Add data to context. + * + * @param string $data Data to add + */ + public function add($data) + { + $words = array_values(unpack('V16', $data)); + + $A = $this->a; + $B = $this->b; + $C = $this->c; + $D = $this->d; + + $F = ['self', 'f']; + $G = ['self', 'g']; + $H = ['self', 'h']; + $I = ['self', 'i']; + + // ROUND 1 + self::step($F, $A, $B, $C, $D, $words[0], 7, 0xd76aa478); + self::step($F, $D, $A, $B, $C, $words[1], 12, 0xe8c7b756); + self::step($F, $C, $D, $A, $B, $words[2], 17, 0x242070db); + self::step($F, $B, $C, $D, $A, $words[3], 22, 0xc1bdceee); + self::step($F, $A, $B, $C, $D, $words[4], 7, 0xf57c0faf); + self::step($F, $D, $A, $B, $C, $words[5], 12, 0x4787c62a); + self::step($F, $C, $D, $A, $B, $words[6], 17, 0xa8304613); + self::step($F, $B, $C, $D, $A, $words[7], 22, 0xfd469501); + self::step($F, $A, $B, $C, $D, $words[8], 7, 0x698098d8); + self::step($F, $D, $A, $B, $C, $words[9], 12, 0x8b44f7af); + self::step($F, $C, $D, $A, $B, $words[10], 17, 0xffff5bb1); + self::step($F, $B, $C, $D, $A, $words[11], 22, 0x895cd7be); + self::step($F, $A, $B, $C, $D, $words[12], 7, 0x6b901122); + self::step($F, $D, $A, $B, $C, $words[13], 12, 0xfd987193); + self::step($F, $C, $D, $A, $B, $words[14], 17, 0xa679438e); + self::step($F, $B, $C, $D, $A, $words[15], 22, 0x49b40821); + + // ROUND 2 + self::step($G, $A, $B, $C, $D, $words[1], 5, 0xf61e2562); + self::step($G, $D, $A, $B, $C, $words[6], 9, 0xc040b340); + self::step($G, $C, $D, $A, $B, $words[11], 14, 0x265e5a51); + self::step($G, $B, $C, $D, $A, $words[0], 20, 0xe9b6c7aa); + self::step($G, $A, $B, $C, $D, $words[5], 5, 0xd62f105d); + self::step($G, $D, $A, $B, $C, $words[10], 9, 0x02441453); + self::step($G, $C, $D, $A, $B, $words[15], 14, 0xd8a1e681); + self::step($G, $B, $C, $D, $A, $words[4], 20, 0xe7d3fbc8); + self::step($G, $A, $B, $C, $D, $words[9], 5, 0x21e1cde6); + self::step($G, $D, $A, $B, $C, $words[14], 9, 0xc33707d6); + self::step($G, $C, $D, $A, $B, $words[3], 14, 0xf4d50d87); + self::step($G, $B, $C, $D, $A, $words[8], 20, 0x455a14ed); + self::step($G, $A, $B, $C, $D, $words[13], 5, 0xa9e3e905); + self::step($G, $D, $A, $B, $C, $words[2], 9, 0xfcefa3f8); + self::step($G, $C, $D, $A, $B, $words[7], 14, 0x676f02d9); + self::step($G, $B, $C, $D, $A, $words[12], 20, 0x8d2a4c8a); + + // ROUND 3 + self::step($H, $A, $B, $C, $D, $words[5], 4, 0xfffa3942); + self::step($H, $D, $A, $B, $C, $words[8], 11, 0x8771f681); + self::step($H, $C, $D, $A, $B, $words[11], 16, 0x6d9d6122); + self::step($H, $B, $C, $D, $A, $words[14], 23, 0xfde5380c); + self::step($H, $A, $B, $C, $D, $words[1], 4, 0xa4beea44); + self::step($H, $D, $A, $B, $C, $words[4], 11, 0x4bdecfa9); + self::step($H, $C, $D, $A, $B, $words[7], 16, 0xf6bb4b60); + self::step($H, $B, $C, $D, $A, $words[10], 23, 0xbebfbc70); + self::step($H, $A, $B, $C, $D, $words[13], 4, 0x289b7ec6); + self::step($H, $D, $A, $B, $C, $words[0], 11, 0xeaa127fa); + self::step($H, $C, $D, $A, $B, $words[3], 16, 0xd4ef3085); + self::step($H, $B, $C, $D, $A, $words[6], 23, 0x04881d05); + self::step($H, $A, $B, $C, $D, $words[9], 4, 0xd9d4d039); + self::step($H, $D, $A, $B, $C, $words[12], 11, 0xe6db99e5); + self::step($H, $C, $D, $A, $B, $words[15], 16, 0x1fa27cf8); + self::step($H, $B, $C, $D, $A, $words[2], 23, 0xc4ac5665); + + // ROUND 4 + self::step($I, $A, $B, $C, $D, $words[0], 6, 0xf4292244); + self::step($I, $D, $A, $B, $C, $words[7], 10, 0x432aff97); + self::step($I, $C, $D, $A, $B, $words[14], 15, 0xab9423a7); + self::step($I, $B, $C, $D, $A, $words[5], 21, 0xfc93a039); + self::step($I, $A, $B, $C, $D, $words[12], 6, 0x655b59c3); + self::step($I, $D, $A, $B, $C, $words[3], 10, 0x8f0ccc92); + self::step($I, $C, $D, $A, $B, $words[10], 15, 0xffeff47d); + self::step($I, $B, $C, $D, $A, $words[1], 21, 0x85845dd1); + self::step($I, $A, $B, $C, $D, $words[8], 6, 0x6fa87e4f); + self::step($I, $D, $A, $B, $C, $words[15], 10, 0xfe2ce6e0); + self::step($I, $C, $D, $A, $B, $words[6], 15, 0xa3014314); + self::step($I, $B, $C, $D, $A, $words[13], 21, 0x4e0811a1); + self::step($I, $A, $B, $C, $D, $words[4], 6, 0xf7537e82); + self::step($I, $D, $A, $B, $C, $words[11], 10, 0xbd3af235); + self::step($I, $C, $D, $A, $B, $words[2], 15, 0x2ad7d2bb); + self::step($I, $B, $C, $D, $A, $words[9], 21, 0xeb86d391); + + $this->a = ($this->a + $A) & 0xffffffff; + $this->b = ($this->b + $B) & 0xffffffff; + $this->c = ($this->c + $C) & 0xffffffff; + $this->d = ($this->d + $D) & 0xffffffff; + } + + private static function f($X, $Y, $Z) + { + return ($X & $Y) | ((~$X) & $Z); // X AND Y OR NOT X AND Z + } + + private static function g($X, $Y, $Z) + { + return ($X & $Z) | ($Y & (~$Z)); // X AND Z OR Y AND NOT Z + } + + private static function h($X, $Y, $Z) + { + return $X ^ $Y ^ $Z; // X XOR Y XOR Z + } + + private static function i($X, $Y, $Z) + { + return $Y ^ ($X | (~$Z)); // Y XOR (X OR NOT Z) + } + + private static function step($func, &$A, $B, $C, $D, $M, $s, $t) + { + $A = ($A + call_user_func($func, $B, $C, $D) + $M + $t) & 0xffffffff; + $A = self::rotate($A, $s); + $A = ($B + $A) & 0xffffffff; + } + + private static function rotate($decimal, $bits) + { + $binary = str_pad(decbin($decimal), 32, '0', STR_PAD_LEFT); + + return bindec(substr($binary, $bits) . substr($binary, 0, $bits)); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/RC4.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/RC4.php new file mode 100644 index 00000000000..691aca7c398 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/RC4.php @@ -0,0 +1,61 @@ +i = 0; $this->i < 256; ++$this->i) { + $this->s[$this->i] = $this->i; + } + + $this->j = 0; + for ($this->i = 0; $this->i < 256; ++$this->i) { + $this->j = ($this->j + $this->s[$this->i] + ord($key[$this->i % $len])) % 256; + $t = $this->s[$this->i]; + $this->s[$this->i] = $this->s[$this->j]; + $this->s[$this->j] = $t; + } + $this->i = $this->j = 0; + } + + /** + * Symmetric decryption/encryption function. + * + * @param string $data Data to encrypt/decrypt + * + * @return string + */ + public function RC4($data) + { + $len = strlen($data); + for ($c = 0; $c < $len; ++$c) { + $this->i = ($this->i + 1) % 256; + $this->j = ($this->j + $this->s[$this->i]) % 256; + $t = $this->s[$this->i]; + $this->s[$this->i] = $this->s[$this->j]; + $this->s[$this->j] = $t; + + $t = ($this->s[$this->i] + $this->s[$this->j]) % 256; + + $data[$c] = chr(ord($data[$c]) ^ $this->s[$t]); + } + + return $data; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Style/Border.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Style/Border.php new file mode 100644 index 00000000000..91cbe36f136 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Style/Border.php @@ -0,0 +1,42 @@ + StyleBorder::BORDER_NONE, + 0x01 => StyleBorder::BORDER_THIN, + 0x02 => StyleBorder::BORDER_MEDIUM, + 0x03 => StyleBorder::BORDER_DASHED, + 0x04 => StyleBorder::BORDER_DOTTED, + 0x05 => StyleBorder::BORDER_THICK, + 0x06 => StyleBorder::BORDER_DOUBLE, + 0x07 => StyleBorder::BORDER_HAIR, + 0x08 => StyleBorder::BORDER_MEDIUMDASHED, + 0x09 => StyleBorder::BORDER_DASHDOT, + 0x0A => StyleBorder::BORDER_MEDIUMDASHDOT, + 0x0B => StyleBorder::BORDER_DASHDOTDOT, + 0x0C => StyleBorder::BORDER_MEDIUMDASHDOTDOT, + 0x0D => StyleBorder::BORDER_SLANTDASHDOT, + ]; + + /** + * Map border style + * OpenOffice documentation: 2.5.11. + * + * @param int $index + * + * @return string + */ + public static function lookup($index) + { + if (isset(self::$map[$index])) { + return self::$map[$index]; + } + + return StyleBorder::BORDER_NONE; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php new file mode 100644 index 00000000000..7b85c088330 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php @@ -0,0 +1,47 @@ + Fill::FILL_NONE, + 0x01 => Fill::FILL_SOLID, + 0x02 => Fill::FILL_PATTERN_MEDIUMGRAY, + 0x03 => Fill::FILL_PATTERN_DARKGRAY, + 0x04 => Fill::FILL_PATTERN_LIGHTGRAY, + 0x05 => Fill::FILL_PATTERN_DARKHORIZONTAL, + 0x06 => Fill::FILL_PATTERN_DARKVERTICAL, + 0x07 => Fill::FILL_PATTERN_DARKDOWN, + 0x08 => Fill::FILL_PATTERN_DARKUP, + 0x09 => Fill::FILL_PATTERN_DARKGRID, + 0x0A => Fill::FILL_PATTERN_DARKTRELLIS, + 0x0B => Fill::FILL_PATTERN_LIGHTHORIZONTAL, + 0x0C => Fill::FILL_PATTERN_LIGHTVERTICAL, + 0x0D => Fill::FILL_PATTERN_LIGHTDOWN, + 0x0E => Fill::FILL_PATTERN_LIGHTUP, + 0x0F => Fill::FILL_PATTERN_LIGHTGRID, + 0x10 => Fill::FILL_PATTERN_LIGHTTRELLIS, + 0x11 => Fill::FILL_PATTERN_GRAY125, + 0x12 => Fill::FILL_PATTERN_GRAY0625, + ]; + + /** + * Get fill pattern from index + * OpenOffice documentation: 2.5.12. + * + * @param int $index + * + * @return string + */ + public static function lookup($index) + { + if (isset(self::$map[$index])) { + return self::$map[$index]; + } + + return Fill::FILL_NONE; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx.php new file mode 100644 index 00000000000..335f5d7e99c --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx.php @@ -0,0 +1,2604 @@ +readFilter = new DefaultReadFilter(); + $this->referenceHelper = ReferenceHelper::getInstance(); + $this->securityScanner = XmlScanner::getInstance($this); + } + + /** + * Can the current IReader read the file? + * + * @param string $pFilename + * + * @throws Exception + * + * @return bool + */ + public function canRead($pFilename) + { + File::assertFile($pFilename); + + $xl = false; + // Load file + $zip = new ZipArchive(); + if ($zip->open($pFilename) === true) { + // check if it is an OOXML archive + $rels = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, '_rels/.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if ($rels !== false) { + foreach ($rels->Relationship as $rel) { + switch ($rel['Type']) { + case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument': + if (basename($rel['Target']) == 'workbook.xml') { + $xl = true; + } + + break; + } + } + } + $zip->close(); + } + + return $xl; + } + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. + * + * @param string $pFilename + * + * @throws Exception + * + * @return array + */ + public function listWorksheetNames($pFilename) + { + File::assertFile($pFilename); + + $worksheetNames = []; + + $zip = new ZipArchive(); + $zip->open($pFilename); + + // The files we're looking at here are small enough that simpleXML is more efficient than XMLReader + //~ http://schemas.openxmlformats.org/package/2006/relationships"); + $rels = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, '_rels/.rels')) + ); + foreach ($rels->Relationship as $rel) { + switch ($rel['Type']) { + case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument': + //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main" + $xmlWorkbook = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "{$rel['Target']}")) + ); + + if ($xmlWorkbook->sheets) { + foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { + // Check if sheet should be skipped + $worksheetNames[] = (string) $eleSheet['name']; + } + } + } + } + + $zip->close(); + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @param string $pFilename + * + * @throws Exception + * + * @return array + */ + public function listWorksheetInfo($pFilename) + { + File::assertFile($pFilename); + + $worksheetInfo = []; + + $zip = new ZipArchive(); + $zip->open($pFilename); + + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $rels = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, '_rels/.rels')), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + foreach ($rels->Relationship as $rel) { + if ($rel['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument') { + $dir = dirname($rel['Target']); + + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorkbook = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, "$dir/_rels/" . basename($rel['Target']) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $relsWorkbook->registerXPathNamespace('rel', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + $worksheets = []; + foreach ($relsWorkbook->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet') { + $worksheets[(string) $ele['Id']] = $ele['Target']; + } + } + + //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main" + $xmlWorkbook = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, "{$rel['Target']}") + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if ($xmlWorkbook->sheets) { + $dir = dirname($rel['Target']); + /** @var SimpleXMLElement $eleSheet */ + foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { + $tmpInfo = [ + 'worksheetName' => (string) $eleSheet['name'], + 'lastColumnLetter' => 'A', + 'lastColumnIndex' => 0, + 'totalRows' => 0, + 'totalColumns' => 0, + ]; + + $fileWorksheet = $worksheets[(string) self::getArrayItem($eleSheet->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'id')]; + + $xml = new XMLReader(); + $xml->xml( + $this->securityScanner->scanFile( + 'zip://' . File::realpath($pFilename) . '#' . "$dir/$fileWorksheet" + ), + null, + Settings::getLibXmlLoaderOptions() + ); + $xml->setParserProperty(2, true); + + $currCells = 0; + while ($xml->read()) { + if ($xml->name == 'row' && $xml->nodeType == XMLReader::ELEMENT) { + $row = $xml->getAttribute('r'); + $tmpInfo['totalRows'] = $row; + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $currCells = 0; + } elseif ($xml->name == 'c' && $xml->nodeType == XMLReader::ELEMENT) { + ++$currCells; + } + } + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $xml->close(); + + $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + + $worksheetInfo[] = $tmpInfo; + } + } + } + } + + $zip->close(); + + return $worksheetInfo; + } + + private static function castToBoolean($c) + { + $value = isset($c->v) ? (string) $c->v : null; + if ($value == '0') { + return false; + } elseif ($value == '1') { + return true; + } + + return (bool) $c->v; + } + + private static function castToError($c) + { + return isset($c->v) ? (string) $c->v : null; + } + + private static function castToString($c) + { + return isset($c->v) ? (string) $c->v : null; + } + + private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType) + { + $cellDataType = 'f'; + $value = "={$c->f}"; + $calculatedValue = self::$castBaseType($c); + + // Shared formula? + if (isset($c->f['t']) && strtolower((string) $c->f['t']) == 'shared') { + $instance = (string) $c->f['si']; + + if (!isset($sharedFormulas[(string) $c->f['si']])) { + $sharedFormulas[$instance] = ['master' => $r, 'formula' => $value]; + } else { + $master = Coordinate::coordinateFromString($sharedFormulas[$instance]['master']); + $current = Coordinate::coordinateFromString($r); + + $difference = [0, 0]; + $difference[0] = Coordinate::columnIndexFromString($current[0]) - Coordinate::columnIndexFromString($master[0]); + $difference[1] = $current[1] - $master[1]; + + $value = $this->referenceHelper->updateFormulaReferences($sharedFormulas[$instance]['formula'], 'A1', $difference[0], $difference[1]); + } + } + } + + /** + * @param ZipArchive $archive + * @param string $fileName + * + * @return string + */ + private function getFromZipArchive(ZipArchive $archive, $fileName = '') + { + // Root-relative paths + if (strpos($fileName, '//') !== false) { + $fileName = substr($fileName, strpos($fileName, '//') + 1); + } + $fileName = File::realpath($fileName); + + // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming + // so we need to load case-insensitively from the zip file + + // Apache POI fixes + $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE); + if ($contents === false) { + $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE); + } + + return $contents; + } + + /** + * Set Worksheet column attributes by attributes array passed. + * + * @param Worksheet $docSheet + * @param string $column A, B, ... DX, ... + * @param array $columnAttributes array of attributes (indexes are attribute name, values are value) + * 'xfIndex', 'visible', 'collapsed', 'outlineLevel', 'width', ... ? + */ + private function setColumnAttributes(Worksheet $docSheet, $column, array $columnAttributes) + { + if (isset($columnAttributes['xfIndex'])) { + $docSheet->getColumnDimension($column)->setXfIndex($columnAttributes['xfIndex']); + } + if (isset($columnAttributes['visible'])) { + $docSheet->getColumnDimension($column)->setVisible($columnAttributes['visible']); + } + if (isset($columnAttributes['collapsed'])) { + $docSheet->getColumnDimension($column)->setCollapsed($columnAttributes['collapsed']); + } + if (isset($columnAttributes['outlineLevel'])) { + $docSheet->getColumnDimension($column)->setOutlineLevel($columnAttributes['outlineLevel']); + } + if (isset($columnAttributes['width'])) { + $docSheet->getColumnDimension($column)->setWidth($columnAttributes['width']); + } + } + + /** + * Set Worksheet row attributes by attributes array passed. + * + * @param Worksheet $docSheet + * @param int $row 1, 2, 3, ... 99, ... + * @param array $rowAttributes array of attributes (indexes are attribute name, values are value) + * 'xfIndex', 'visible', 'collapsed', 'outlineLevel', 'rowHeight', ... ? + */ + private function setRowAttributes(Worksheet $docSheet, $row, array $rowAttributes) + { + if (isset($rowAttributes['xfIndex'])) { + $docSheet->getRowDimension($row)->setXfIndex($rowAttributes['xfIndex']); + } + if (isset($rowAttributes['visible'])) { + $docSheet->getRowDimension($row)->setVisible($rowAttributes['visible']); + } + if (isset($rowAttributes['collapsed'])) { + $docSheet->getRowDimension($row)->setCollapsed($rowAttributes['collapsed']); + } + if (isset($rowAttributes['outlineLevel'])) { + $docSheet->getRowDimension($row)->setOutlineLevel($rowAttributes['outlineLevel']); + } + if (isset($rowAttributes['rowHeight'])) { + $docSheet->getRowDimension($row)->setRowHeight($rowAttributes['rowHeight']); + } + } + + /** + * Loads Spreadsheet from file. + * + * @param string $pFilename + * + * @throws Exception + * + * @return Spreadsheet + */ + public function load($pFilename) + { + File::assertFile($pFilename); + + // Initialisations + $excel = new Spreadsheet(); + $excel->removeSheetByIndex(0); + if (!$this->readDataOnly) { + $excel->removeCellStyleXfByIndex(0); // remove the default style + $excel->removeCellXfByIndex(0); // remove the default style + } + $unparsedLoadedData = []; + + $zip = new ZipArchive(); + $zip->open($pFilename); + + // Read the theme first, because we need the colour scheme when reading the styles + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $wbRels = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, 'xl/_rels/workbook.xml.rels')), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + foreach ($wbRels->Relationship as $rel) { + switch ($rel['Type']) { + case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme': + $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; + $themeOrderAdditional = count($themeOrderArray); + + $xmlTheme = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "xl/{$rel['Target']}")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if (is_object($xmlTheme)) { + $xmlThemeName = $xmlTheme->attributes(); + $xmlTheme = $xmlTheme->children('http://schemas.openxmlformats.org/drawingml/2006/main'); + $themeName = (string) $xmlThemeName['name']; + + $colourScheme = $xmlTheme->themeElements->clrScheme->attributes(); + $colourSchemeName = (string) $colourScheme['name']; + $colourScheme = $xmlTheme->themeElements->clrScheme->children('http://schemas.openxmlformats.org/drawingml/2006/main'); + + $themeColours = []; + foreach ($colourScheme as $k => $xmlColour) { + $themePos = array_search($k, $themeOrderArray); + if ($themePos === false) { + $themePos = $themeOrderAdditional++; + } + if (isset($xmlColour->sysClr)) { + $xmlColourData = $xmlColour->sysClr->attributes(); + $themeColours[$themePos] = $xmlColourData['lastClr']; + } elseif (isset($xmlColour->srgbClr)) { + $xmlColourData = $xmlColour->srgbClr->attributes(); + $themeColours[$themePos] = $xmlColourData['val']; + } + } + self::$theme = new Xlsx\Theme($themeName, $colourSchemeName, $themeColours); + } + + break; + } + } + + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $rels = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, '_rels/.rels')), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + foreach ($rels->Relationship as $rel) { + switch ($rel['Type']) { + case 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties': + $xmlCore = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "{$rel['Target']}")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if (is_object($xmlCore)) { + $xmlCore->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/'); + $xmlCore->registerXPathNamespace('dcterms', 'http://purl.org/dc/terms/'); + $xmlCore->registerXPathNamespace('cp', 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties'); + $docProps = $excel->getProperties(); + $docProps->setCreator((string) self::getArrayItem($xmlCore->xpath('dc:creator'))); + $docProps->setLastModifiedBy((string) self::getArrayItem($xmlCore->xpath('cp:lastModifiedBy'))); + $docProps->setCreated(strtotime(self::getArrayItem($xmlCore->xpath('dcterms:created')))); //! respect xsi:type + $docProps->setModified(strtotime(self::getArrayItem($xmlCore->xpath('dcterms:modified')))); //! respect xsi:type + $docProps->setTitle((string) self::getArrayItem($xmlCore->xpath('dc:title'))); + $docProps->setDescription((string) self::getArrayItem($xmlCore->xpath('dc:description'))); + $docProps->setSubject((string) self::getArrayItem($xmlCore->xpath('dc:subject'))); + $docProps->setKeywords((string) self::getArrayItem($xmlCore->xpath('cp:keywords'))); + $docProps->setCategory((string) self::getArrayItem($xmlCore->xpath('cp:category'))); + } + + break; + case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties': + $xmlCore = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "{$rel['Target']}")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if (is_object($xmlCore)) { + $docProps = $excel->getProperties(); + if (isset($xmlCore->Company)) { + $docProps->setCompany((string) $xmlCore->Company); + } + if (isset($xmlCore->Manager)) { + $docProps->setManager((string) $xmlCore->Manager); + } + } + + break; + case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties': + $xmlCore = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "{$rel['Target']}")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if (is_object($xmlCore)) { + $docProps = $excel->getProperties(); + /** @var SimpleXMLElement $xmlProperty */ + foreach ($xmlCore as $xmlProperty) { + $cellDataOfficeAttributes = $xmlProperty->attributes(); + if (isset($cellDataOfficeAttributes['name'])) { + $propertyName = (string) $cellDataOfficeAttributes['name']; + $cellDataOfficeChildren = $xmlProperty->children('http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); + $attributeType = $cellDataOfficeChildren->getName(); + $attributeValue = (string) $cellDataOfficeChildren->{$attributeType}; + $attributeValue = Properties::convertProperty($attributeValue, $attributeType); + $attributeType = Properties::convertPropertyType($attributeType); + $docProps->setCustomProperty($propertyName, $attributeValue, $attributeType); + } + } + } + + break; + //Ribbon + case 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility': + $customUI = $rel['Target']; + if ($customUI !== null) { + $this->readRibbon($excel, $customUI, $zip); + } + + break; + case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument': + $dir = dirname($rel['Target']); + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorkbook = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/_rels/" . basename($rel['Target']) . '.rels')), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $relsWorkbook->registerXPathNamespace('rel', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + $sharedStrings = []; + $xpath = self::getArrayItem($relsWorkbook->xpath("rel:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings']")); + //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main" + $xmlStrings = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/$xpath[Target]")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if (isset($xmlStrings, $xmlStrings->si)) { + foreach ($xmlStrings->si as $val) { + if (isset($val->t)) { + $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t); + } elseif (isset($val->r)) { + $sharedStrings[] = $this->parseRichText($val); + } + } + } + + $worksheets = []; + $macros = $customUI = null; + foreach ($relsWorkbook->Relationship as $ele) { + switch ($ele['Type']) { + case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet': + $worksheets[(string) $ele['Id']] = $ele['Target']; + + break; + // a vbaProject ? (: some macros) + case 'http://schemas.microsoft.com/office/2006/relationships/vbaProject': + $macros = $ele['Target']; + + break; + } + } + + if ($macros !== null) { + $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin + if ($macrosCode !== false) { + $excel->setMacrosCode($macrosCode); + $excel->setHasMacros(true); + //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir + $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin'); + if ($Certificate !== false) { + $excel->setMacrosCertificate($Certificate); + } + } + } + $styles = []; + $cellStyles = []; + $xpath = self::getArrayItem($relsWorkbook->xpath("rel:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles']")); + //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main" + $xmlStyles = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/$xpath[Target]")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $numFmts = null; + if ($xmlStyles && $xmlStyles->numFmts[0]) { + $numFmts = $xmlStyles->numFmts[0]; + } + if (isset($numFmts) && ($numFmts !== null)) { + $numFmts->registerXPathNamespace('sml', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + } + if (!$this->readDataOnly && $xmlStyles) { + foreach ($xmlStyles->cellXfs->xf as $xf) { + $numFmt = NumberFormat::FORMAT_GENERAL; + + if ($xf['numFmtId']) { + if (isset($numFmts)) { + $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); + + if (isset($tmpNumFmt['formatCode'])) { + $numFmt = (string) $tmpNumFmt['formatCode']; + } + } + + // We shouldn't override any of the built-in MS Excel values (values below id 164) + // But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used + // So we make allowance for them rather than lose formatting masks + if ((int) $xf['numFmtId'] < 164 && + NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== '') { + $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']); + } + } + $quotePrefix = false; + if (isset($xf['quotePrefix'])) { + $quotePrefix = (bool) $xf['quotePrefix']; + } + + $style = (object) [ + 'numFmt' => $numFmt, + 'font' => $xmlStyles->fonts->font[(int) ($xf['fontId'])], + 'fill' => $xmlStyles->fills->fill[(int) ($xf['fillId'])], + 'border' => $xmlStyles->borders->border[(int) ($xf['borderId'])], + 'alignment' => $xf->alignment, + 'protection' => $xf->protection, + 'quotePrefix' => $quotePrefix, + ]; + $styles[] = $style; + + // add style to cellXf collection + $objStyle = new Style(); + self::readStyle($objStyle, $style); + $excel->addCellXf($objStyle); + } + + foreach (isset($xmlStyles->cellStyleXfs->xf) ? $xmlStyles->cellStyleXfs->xf : [] as $xf) { + $numFmt = NumberFormat::FORMAT_GENERAL; + if ($numFmts && $xf['numFmtId']) { + $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); + if (isset($tmpNumFmt['formatCode'])) { + $numFmt = (string) $tmpNumFmt['formatCode']; + } elseif ((int) $xf['numFmtId'] < 165) { + $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']); + } + } + + $cellStyle = (object) [ + 'numFmt' => $numFmt, + 'font' => $xmlStyles->fonts->font[(int) ($xf['fontId'])], + 'fill' => $xmlStyles->fills->fill[(int) ($xf['fillId'])], + 'border' => $xmlStyles->borders->border[(int) ($xf['borderId'])], + 'alignment' => $xf->alignment, + 'protection' => $xf->protection, + 'quotePrefix' => $quotePrefix, + ]; + $cellStyles[] = $cellStyle; + + // add style to cellStyleXf collection + $objStyle = new Style(); + self::readStyle($objStyle, $cellStyle); + $excel->addCellStyleXf($objStyle); + } + } + + $dxfs = []; + if (!$this->readDataOnly && $xmlStyles) { + // Conditional Styles + if ($xmlStyles->dxfs) { + foreach ($xmlStyles->dxfs->dxf as $dxf) { + $style = new Style(false, true); + self::readStyle($style, $dxf); + $dxfs[] = $style; + } + } + // Cell Styles + if ($xmlStyles->cellStyles) { + foreach ($xmlStyles->cellStyles->cellStyle as $cellStyle) { + if ((int) ($cellStyle['builtinId']) == 0) { + if (isset($cellStyles[(int) ($cellStyle['xfId'])])) { + // Set default style + $style = new Style(); + self::readStyle($style, $cellStyles[(int) ($cellStyle['xfId'])]); + + // normal style, currently not using it for anything + } + } + } + } + } + + //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main" + $xmlWorkbook = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "{$rel['Target']}")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + + // Set base date + if ($xmlWorkbook->workbookPr) { + Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + if (isset($xmlWorkbook->workbookPr['date1904'])) { + if (self::boolean((string) $xmlWorkbook->workbookPr['date1904'])) { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + } + } + } + + // Set protection + $this->readProtection($excel, $xmlWorkbook); + + $sheetId = 0; // keep track of new sheet id in final workbook + $oldSheetId = -1; // keep track of old sheet id in final workbook + $countSkippedSheets = 0; // keep track of number of skipped sheets + $mapSheetId = []; // mapping of sheet ids from old to new + + $charts = $chartDetails = []; + + if ($xmlWorkbook->sheets) { + /** @var SimpleXMLElement $eleSheet */ + foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { + ++$oldSheetId; + + // Check if sheet should be skipped + if (isset($this->loadSheetsOnly) && !in_array((string) $eleSheet['name'], $this->loadSheetsOnly)) { + ++$countSkippedSheets; + $mapSheetId[$oldSheetId] = null; + + continue; + } + + // Map old sheet id in original workbook to new sheet id. + // They will differ if loadSheetsOnly() is being used + $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets; + + // Load sheet + $docSheet = $excel->createSheet(); + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet + // references in formula cells... during the load, all formulae should be correct, + // and we're simply bringing the worksheet name in line with the formula, not the + // reverse + $docSheet->setTitle((string) $eleSheet['name'], false, false); + $fileWorksheet = $worksheets[(string) self::getArrayItem($eleSheet->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'id')]; + //~ http://schemas.openxmlformats.org/spreadsheetml/2006/main" + $xmlSheet = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, "$dir/$fileWorksheet")), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + + $sharedFormulas = []; + + if (isset($eleSheet['state']) && (string) $eleSheet['state'] != '') { + $docSheet->setSheetState((string) $eleSheet['state']); + } + + if (isset($xmlSheet->sheetViews, $xmlSheet->sheetViews->sheetView)) { + if (isset($xmlSheet->sheetViews->sheetView['zoomScale'])) { + $zoomScale = (int) ($xmlSheet->sheetViews->sheetView['zoomScale']); + if ($zoomScale <= 0) { + // setZoomScale will throw an Exception if the scale is less than or equals 0 + // that is OK when manually creating documents, but we should be able to read all documents + $zoomScale = 100; + } + + $docSheet->getSheetView()->setZoomScale($zoomScale); + } + if (isset($xmlSheet->sheetViews->sheetView['zoomScaleNormal'])) { + $zoomScaleNormal = (int) ($xmlSheet->sheetViews->sheetView['zoomScaleNormal']); + if ($zoomScaleNormal <= 0) { + // setZoomScaleNormal will throw an Exception if the scale is less than or equals 0 + // that is OK when manually creating documents, but we should be able to read all documents + $zoomScaleNormal = 100; + } + + $docSheet->getSheetView()->setZoomScaleNormal($zoomScaleNormal); + } + if (isset($xmlSheet->sheetViews->sheetView['view'])) { + $docSheet->getSheetView()->setView((string) $xmlSheet->sheetViews->sheetView['view']); + } + if (isset($xmlSheet->sheetViews->sheetView['showGridLines'])) { + $docSheet->setShowGridLines(self::boolean((string) $xmlSheet->sheetViews->sheetView['showGridLines'])); + } + if (isset($xmlSheet->sheetViews->sheetView['showRowColHeaders'])) { + $docSheet->setShowRowColHeaders(self::boolean((string) $xmlSheet->sheetViews->sheetView['showRowColHeaders'])); + } + if (isset($xmlSheet->sheetViews->sheetView['rightToLeft'])) { + $docSheet->setRightToLeft(self::boolean((string) $xmlSheet->sheetViews->sheetView['rightToLeft'])); + } + if (isset($xmlSheet->sheetViews->sheetView->pane)) { + $xSplit = 0; + $ySplit = 0; + $topLeftCell = null; + + if (isset($xmlSheet->sheetViews->sheetView->pane['xSplit'])) { + $xSplit = (int) ($xmlSheet->sheetViews->sheetView->pane['xSplit']); + } + + if (isset($xmlSheet->sheetViews->sheetView->pane['ySplit'])) { + $ySplit = (int) ($xmlSheet->sheetViews->sheetView->pane['ySplit']); + } + + if (isset($xmlSheet->sheetViews->sheetView->pane['topLeftCell'])) { + $topLeftCell = (string) $xmlSheet->sheetViews->sheetView->pane['topLeftCell']; + } + + $docSheet->freezePane(Coordinate::stringFromColumnIndex($xSplit + 1) . ($ySplit + 1), $topLeftCell); + } + + if (isset($xmlSheet->sheetViews->sheetView->selection)) { + if (isset($xmlSheet->sheetViews->sheetView->selection['sqref'])) { + $sqref = (string) $xmlSheet->sheetViews->sheetView->selection['sqref']; + $sqref = explode(' ', $sqref); + $sqref = $sqref[0]; + $docSheet->setSelectedCells($sqref); + } + } + } + + if (isset($xmlSheet->sheetPr, $xmlSheet->sheetPr->tabColor)) { + if (isset($xmlSheet->sheetPr->tabColor['rgb'])) { + $docSheet->getTabColor()->setARGB((string) $xmlSheet->sheetPr->tabColor['rgb']); + } + } + if (isset($xmlSheet->sheetPr, $xmlSheet->sheetPr['codeName'])) { + $docSheet->setCodeName((string) $xmlSheet->sheetPr['codeName'], false); + } + if (isset($xmlSheet->sheetPr, $xmlSheet->sheetPr->outlinePr)) { + if (isset($xmlSheet->sheetPr->outlinePr['summaryRight']) && + !self::boolean((string) $xmlSheet->sheetPr->outlinePr['summaryRight'])) { + $docSheet->setShowSummaryRight(false); + } else { + $docSheet->setShowSummaryRight(true); + } + + if (isset($xmlSheet->sheetPr->outlinePr['summaryBelow']) && + !self::boolean((string) $xmlSheet->sheetPr->outlinePr['summaryBelow'])) { + $docSheet->setShowSummaryBelow(false); + } else { + $docSheet->setShowSummaryBelow(true); + } + } + + if (isset($xmlSheet->sheetPr, $xmlSheet->sheetPr->pageSetUpPr)) { + if (isset($xmlSheet->sheetPr->pageSetUpPr['fitToPage']) && + !self::boolean((string) $xmlSheet->sheetPr->pageSetUpPr['fitToPage'])) { + $docSheet->getPageSetup()->setFitToPage(false); + } else { + $docSheet->getPageSetup()->setFitToPage(true); + } + } + + if (isset($xmlSheet->sheetFormatPr)) { + if (isset($xmlSheet->sheetFormatPr['customHeight']) && + self::boolean((string) $xmlSheet->sheetFormatPr['customHeight']) && + isset($xmlSheet->sheetFormatPr['defaultRowHeight'])) { + $docSheet->getDefaultRowDimension()->setRowHeight((float) $xmlSheet->sheetFormatPr['defaultRowHeight']); + } + if (isset($xmlSheet->sheetFormatPr['defaultColWidth'])) { + $docSheet->getDefaultColumnDimension()->setWidth((float) $xmlSheet->sheetFormatPr['defaultColWidth']); + } + if (isset($xmlSheet->sheetFormatPr['zeroHeight']) && + ((string) $xmlSheet->sheetFormatPr['zeroHeight'] == '1')) { + $docSheet->getDefaultRowDimension()->setZeroHeight(true); + } + } + + if (isset($xmlSheet->printOptions) && !$this->readDataOnly) { + if (self::boolean((string) $xmlSheet->printOptions['gridLinesSet'])) { + $docSheet->setShowGridlines(true); + } + if (self::boolean((string) $xmlSheet->printOptions['gridLines'])) { + $docSheet->setPrintGridlines(true); + } + if (self::boolean((string) $xmlSheet->printOptions['horizontalCentered'])) { + $docSheet->getPageSetup()->setHorizontalCentered(true); + } + if (self::boolean((string) $xmlSheet->printOptions['verticalCentered'])) { + $docSheet->getPageSetup()->setVerticalCentered(true); + } + } + + $this->readColumnsAndRowsAttributes($xmlSheet, $docSheet); + + if ($xmlSheet && $xmlSheet->sheetData && $xmlSheet->sheetData->row) { + $cIndex = 1; // Cell Start from 1 + foreach ($xmlSheet->sheetData->row as $row) { + $rowIndex = 1; + foreach ($row->c as $c) { + $r = (string) $c['r']; + if ($r == '') { + $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex; + } + $cellDataType = (string) $c['t']; + $value = null; + $calculatedValue = null; + + // Read cell? + if ($this->getReadFilter() !== null) { + $coordinates = Coordinate::coordinateFromString($r); + + if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) { + $rowIndex += 1; + + continue; + } + } + + // Read cell! + switch ($cellDataType) { + case 's': + if ((string) $c->v != '') { + $value = $sharedStrings[(int) ($c->v)]; + + if ($value instanceof RichText) { + $value = clone $value; + } + } else { + $value = ''; + } + + break; + case 'b': + if (!isset($c->f)) { + $value = self::castToBoolean($c); + } else { + // Formula + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToBoolean'); + if (isset($c->f['t'])) { + $att = $c->f; + $docSheet->getCell($r)->setFormulaAttributes($att); + } + } + + break; + case 'inlineStr': + if (isset($c->f)) { + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError'); + } else { + $value = $this->parseRichText($c->is); + } + + break; + case 'e': + if (!isset($c->f)) { + $value = self::castToError($c); + } else { + // Formula + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError'); + } + + break; + default: + if (!isset($c->f)) { + $value = self::castToString($c); + } else { + // Formula + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToString'); + } + + break; + } + + // Check for numeric values + if (is_numeric($value) && $cellDataType != 's') { + if ($value == (int) $value) { + $value = (int) $value; + } elseif ($value == (float) $value) { + $value = (float) $value; + } elseif ($value == (float) $value) { + $value = (float) $value; + } + } + + // Rich text? + if ($value instanceof RichText && $this->readDataOnly) { + $value = $value->getPlainText(); + } + + $cell = $docSheet->getCell($r); + // Assign value + if ($cellDataType != '') { + $cell->setValueExplicit($value, $cellDataType); + } else { + $cell->setValue($value); + } + if ($calculatedValue !== null) { + $cell->setCalculatedValue($calculatedValue); + } + + // Style information? + if ($c['s'] && !$this->readDataOnly) { + // no style index means 0, it seems + $cell->setXfIndex(isset($styles[(int) ($c['s'])]) ? + (int) ($c['s']) : 0); + } + $rowIndex += 1; + } + $cIndex += 1; + } + } + + $conditionals = []; + if (!$this->readDataOnly && $xmlSheet && $xmlSheet->conditionalFormatting) { + foreach ($xmlSheet->conditionalFormatting as $conditional) { + foreach ($conditional->cfRule as $cfRule) { + if (((string) $cfRule['type'] == Conditional::CONDITION_NONE || (string) $cfRule['type'] == Conditional::CONDITION_CELLIS || (string) $cfRule['type'] == Conditional::CONDITION_CONTAINSTEXT || (string) $cfRule['type'] == Conditional::CONDITION_EXPRESSION) && isset($dxfs[(int) ($cfRule['dxfId'])])) { + $conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule; + } + } + } + + foreach ($conditionals as $ref => $cfRules) { + ksort($cfRules); + $conditionalStyles = []; + foreach ($cfRules as $cfRule) { + $objConditional = new Conditional(); + $objConditional->setConditionType((string) $cfRule['type']); + $objConditional->setOperatorType((string) $cfRule['operator']); + + if ((string) $cfRule['text'] != '') { + $objConditional->setText((string) $cfRule['text']); + } + + if (isset($cfRule['stopIfTrue']) && (int) $cfRule['stopIfTrue'] === 1) { + $objConditional->setStopIfTrue(true); + } + + if (count($cfRule->formula) > 1) { + foreach ($cfRule->formula as $formula) { + $objConditional->addCondition((string) $formula); + } + } else { + $objConditional->addCondition((string) $cfRule->formula); + } + $objConditional->setStyle(clone $dxfs[(int) ($cfRule['dxfId'])]); + $conditionalStyles[] = $objConditional; + } + + // Extract all cell references in $ref + $cellBlocks = explode(' ', str_replace('$', '', strtoupper($ref))); + foreach ($cellBlocks as $cellBlock) { + $docSheet->getStyle($cellBlock)->setConditionalStyles($conditionalStyles); + } + } + } + + $aKeys = ['sheet', 'objects', 'scenarios', 'formatCells', 'formatColumns', 'formatRows', 'insertColumns', 'insertRows', 'insertHyperlinks', 'deleteColumns', 'deleteRows', 'selectLockedCells', 'sort', 'autoFilter', 'pivotTables', 'selectUnlockedCells']; + if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) { + foreach ($aKeys as $key) { + $method = 'set' . ucfirst($key); + $docSheet->getProtection()->$method(self::boolean((string) $xmlSheet->sheetProtection[$key])); + } + } + + if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) { + $docSheet->getProtection()->setPassword((string) $xmlSheet->sheetProtection['password'], true); + if ($xmlSheet->protectedRanges->protectedRange) { + foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { + $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true); + } + } + } + + if ($xmlSheet && $xmlSheet->autoFilter && !$this->readDataOnly) { + $autoFilterRange = (string) $xmlSheet->autoFilter['ref']; + if (strpos($autoFilterRange, ':') !== false) { + $autoFilter = $docSheet->getAutoFilter(); + $autoFilter->setRange($autoFilterRange); + + foreach ($xmlSheet->autoFilter->filterColumn as $filterColumn) { + $column = $autoFilter->getColumnByOffset((int) $filterColumn['colId']); + // Check for standard filters + if ($filterColumn->filters) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + $filters = $filterColumn->filters; + if ((isset($filters['blank'])) && ($filters['blank'] == 1)) { + // Operator is undefined, but always treated as EQUAL + $column->createRule()->setRule(null, '')->setRuleType(Column\Rule::AUTOFILTER_RULETYPE_FILTER); + } + // Standard filters are always an OR join, so no join rule needs to be set + // Entries can be either filter elements + foreach ($filters->filter as $filterRule) { + // Operator is undefined, but always treated as EQUAL + $column->createRule()->setRule(null, (string) $filterRule['val'])->setRuleType(Column\Rule::AUTOFILTER_RULETYPE_FILTER); + } + // Or Date Group elements + foreach ($filters->dateGroupItem as $dateGroupItem) { + // Operator is undefined, but always treated as EQUAL + $column->createRule()->setRule( + null, + [ + 'year' => (string) $dateGroupItem['year'], + 'month' => (string) $dateGroupItem['month'], + 'day' => (string) $dateGroupItem['day'], + 'hour' => (string) $dateGroupItem['hour'], + 'minute' => (string) $dateGroupItem['minute'], + 'second' => (string) $dateGroupItem['second'], + ], + (string) $dateGroupItem['dateTimeGrouping'] + ) + ->setRuleType(Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP); + } + } + // Check for custom filters + if ($filterColumn->customFilters) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); + $customFilters = $filterColumn->customFilters; + // Custom filters can an AND or an OR join; + // and there should only ever be one or two entries + if ((isset($customFilters['and'])) && ($customFilters['and'] == 1)) { + $column->setJoin(Column::AUTOFILTER_COLUMN_JOIN_AND); + } + foreach ($customFilters->customFilter as $filterRule) { + $column->createRule()->setRule( + (string) $filterRule['operator'], + (string) $filterRule['val'] + ) + ->setRuleType(Column\Rule::AUTOFILTER_RULETYPE_CUSTOMFILTER); + } + } + // Check for dynamic filters + if ($filterColumn->dynamicFilter) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + // We should only ever have one dynamic filter + foreach ($filterColumn->dynamicFilter as $filterRule) { + // Operator is undefined, but always treated as EQUAL + $column->createRule()->setRule( + null, + (string) $filterRule['val'], + (string) $filterRule['type'] + ) + ->setRuleType(Column\Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + if (isset($filterRule['val'])) { + $column->setAttribute('val', (string) $filterRule['val']); + } + if (isset($filterRule['maxVal'])) { + $column->setAttribute('maxVal', (string) $filterRule['maxVal']); + } + } + } + // Check for dynamic filters + if ($filterColumn->top10) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); + // We should only ever have one top10 filter + foreach ($filterColumn->top10 as $filterRule) { + $column->createRule()->setRule( + (((isset($filterRule['percent'])) && ($filterRule['percent'] == 1)) + ? Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT + : Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE + ), + (string) $filterRule['val'], + (((isset($filterRule['top'])) && ($filterRule['top'] == 1)) + ? Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP + : Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM + ) + ) + ->setRuleType(Column\Rule::AUTOFILTER_RULETYPE_TOPTENFILTER); + } + } + } + } + } + + if ($xmlSheet && $xmlSheet->mergeCells && $xmlSheet->mergeCells->mergeCell && !$this->readDataOnly) { + foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) { + $mergeRef = (string) $mergeCell['ref']; + if (strpos($mergeRef, ':') !== false) { + $docSheet->mergeCells((string) $mergeCell['ref']); + } + } + } + + if ($xmlSheet && $xmlSheet->pageMargins && !$this->readDataOnly) { + $docPageMargins = $docSheet->getPageMargins(); + $docPageMargins->setLeft((float) ($xmlSheet->pageMargins['left'])); + $docPageMargins->setRight((float) ($xmlSheet->pageMargins['right'])); + $docPageMargins->setTop((float) ($xmlSheet->pageMargins['top'])); + $docPageMargins->setBottom((float) ($xmlSheet->pageMargins['bottom'])); + $docPageMargins->setHeader((float) ($xmlSheet->pageMargins['header'])); + $docPageMargins->setFooter((float) ($xmlSheet->pageMargins['footer'])); + } + + if ($xmlSheet && $xmlSheet->pageSetup && !$this->readDataOnly) { + $docPageSetup = $docSheet->getPageSetup(); + + if (isset($xmlSheet->pageSetup['orientation'])) { + $docPageSetup->setOrientation((string) $xmlSheet->pageSetup['orientation']); + } + if (isset($xmlSheet->pageSetup['paperSize'])) { + $docPageSetup->setPaperSize((int) ($xmlSheet->pageSetup['paperSize'])); + } + if (isset($xmlSheet->pageSetup['scale'])) { + $docPageSetup->setScale((int) ($xmlSheet->pageSetup['scale']), false); + } + if (isset($xmlSheet->pageSetup['fitToHeight']) && (int) ($xmlSheet->pageSetup['fitToHeight']) >= 0) { + $docPageSetup->setFitToHeight((int) ($xmlSheet->pageSetup['fitToHeight']), false); + } + if (isset($xmlSheet->pageSetup['fitToWidth']) && (int) ($xmlSheet->pageSetup['fitToWidth']) >= 0) { + $docPageSetup->setFitToWidth((int) ($xmlSheet->pageSetup['fitToWidth']), false); + } + if (isset($xmlSheet->pageSetup['firstPageNumber'], $xmlSheet->pageSetup['useFirstPageNumber']) && + self::boolean((string) $xmlSheet->pageSetup['useFirstPageNumber'])) { + $docPageSetup->setFirstPageNumber((int) ($xmlSheet->pageSetup['firstPageNumber'])); + } + + $relAttributes = $xmlSheet->pageSetup->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + if (isset($relAttributes['id'])) { + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['pageSetupRelId'] = (string) $relAttributes['id']; + } + } + + if ($xmlSheet && $xmlSheet->headerFooter && !$this->readDataOnly) { + $docHeaderFooter = $docSheet->getHeaderFooter(); + + if (isset($xmlSheet->headerFooter['differentOddEven']) && + self::boolean((string) $xmlSheet->headerFooter['differentOddEven'])) { + $docHeaderFooter->setDifferentOddEven(true); + } else { + $docHeaderFooter->setDifferentOddEven(false); + } + if (isset($xmlSheet->headerFooter['differentFirst']) && + self::boolean((string) $xmlSheet->headerFooter['differentFirst'])) { + $docHeaderFooter->setDifferentFirst(true); + } else { + $docHeaderFooter->setDifferentFirst(false); + } + if (isset($xmlSheet->headerFooter['scaleWithDoc']) && + !self::boolean((string) $xmlSheet->headerFooter['scaleWithDoc'])) { + $docHeaderFooter->setScaleWithDocument(false); + } else { + $docHeaderFooter->setScaleWithDocument(true); + } + if (isset($xmlSheet->headerFooter['alignWithMargins']) && + !self::boolean((string) $xmlSheet->headerFooter['alignWithMargins'])) { + $docHeaderFooter->setAlignWithMargins(false); + } else { + $docHeaderFooter->setAlignWithMargins(true); + } + + $docHeaderFooter->setOddHeader((string) $xmlSheet->headerFooter->oddHeader); + $docHeaderFooter->setOddFooter((string) $xmlSheet->headerFooter->oddFooter); + $docHeaderFooter->setEvenHeader((string) $xmlSheet->headerFooter->evenHeader); + $docHeaderFooter->setEvenFooter((string) $xmlSheet->headerFooter->evenFooter); + $docHeaderFooter->setFirstHeader((string) $xmlSheet->headerFooter->firstHeader); + $docHeaderFooter->setFirstFooter((string) $xmlSheet->headerFooter->firstFooter); + } + + if ($xmlSheet && $xmlSheet->rowBreaks && $xmlSheet->rowBreaks->brk && !$this->readDataOnly) { + foreach ($xmlSheet->rowBreaks->brk as $brk) { + if ($brk['man']) { + $docSheet->setBreak("A$brk[id]", Worksheet::BREAK_ROW); + } + } + } + if ($xmlSheet && $xmlSheet->colBreaks && $xmlSheet->colBreaks->brk && !$this->readDataOnly) { + foreach ($xmlSheet->colBreaks->brk as $brk) { + if ($brk['man']) { + $docSheet->setBreak(Coordinate::stringFromColumnIndex((string) $brk['id'] + 1) . '1', Worksheet::BREAK_COLUMN); + } + } + } + + if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) { + foreach ($xmlSheet->dataValidations->dataValidation as $dataValidation) { + // Uppercase coordinate + $range = strtoupper($dataValidation['sqref']); + $rangeSet = explode(' ', $range); + foreach ($rangeSet as $range) { + $stRange = $docSheet->shrinkRangeToFit($range); + + // Extract all cell references in $range + foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $reference) { + // Create validation + $docValidation = $docSheet->getCell($reference)->getDataValidation(); + $docValidation->setType((string) $dataValidation['type']); + $docValidation->setErrorStyle((string) $dataValidation['errorStyle']); + $docValidation->setOperator((string) $dataValidation['operator']); + $docValidation->setAllowBlank($dataValidation['allowBlank'] != 0); + $docValidation->setShowDropDown($dataValidation['showDropDown'] == 0); + $docValidation->setShowInputMessage($dataValidation['showInputMessage'] != 0); + $docValidation->setShowErrorMessage($dataValidation['showErrorMessage'] != 0); + $docValidation->setErrorTitle((string) $dataValidation['errorTitle']); + $docValidation->setError((string) $dataValidation['error']); + $docValidation->setPromptTitle((string) $dataValidation['promptTitle']); + $docValidation->setPrompt((string) $dataValidation['prompt']); + $docValidation->setFormula1((string) $dataValidation->formula1); + $docValidation->setFormula2((string) $dataValidation->formula2); + } + } + } + } + + // unparsed sheet AlternateContent + if ($xmlSheet && !$this->readDataOnly) { + $mc = $xmlSheet->children('http://schemas.openxmlformats.org/markup-compatibility/2006'); + if ($mc->AlternateContent) { + foreach ($mc->AlternateContent as $alternateContent) { + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML(); + } + } + } + + // Add hyperlinks + $hyperlinks = []; + if (!$this->readDataOnly) { + // Locate hyperlink relations + if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorksheet = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + foreach ($relsWorksheet->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink') { + $hyperlinks[(string) $ele['Id']] = (string) $ele['Target']; + } + } + } + + // Loop through hyperlinks + if ($xmlSheet && $xmlSheet->hyperlinks) { + /** @var SimpleXMLElement $hyperlink */ + foreach ($xmlSheet->hyperlinks->hyperlink as $hyperlink) { + // Link url + $linkRel = $hyperlink->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + foreach (Coordinate::extractAllCellReferencesInRange($hyperlink['ref']) as $cellReference) { + $cell = $docSheet->getCell($cellReference); + if (isset($linkRel['id'])) { + $hyperlinkUrl = $hyperlinks[(string) $linkRel['id']]; + if (isset($hyperlink['location'])) { + $hyperlinkUrl .= '#' . (string) $hyperlink['location']; + } + $cell->getHyperlink()->setUrl($hyperlinkUrl); + } elseif (isset($hyperlink['location'])) { + $cell->getHyperlink()->setUrl('sheet://' . (string) $hyperlink['location']); + } + + // Tooltip + if (isset($hyperlink['tooltip'])) { + $cell->getHyperlink()->setTooltip((string) $hyperlink['tooltip']); + } + } + } + } + } + + // Add comments + $comments = []; + $vmlComments = []; + if (!$this->readDataOnly) { + // Locate comment relations + if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorksheet = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + foreach ($relsWorksheet->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments') { + $comments[(string) $ele['Id']] = (string) $ele['Target']; + } + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing') { + $vmlComments[(string) $ele['Id']] = (string) $ele['Target']; + } + } + } + + // Loop through comments + foreach ($comments as $relName => $relPath) { + // Load comments file + $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath); + $commentsFile = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, $relPath)), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + + // Utility variables + $authors = []; + + // Loop through authors + foreach ($commentsFile->authors->author as $author) { + $authors[] = (string) $author; + } + + // Loop through contents + foreach ($commentsFile->commentList->comment as $comment) { + if (!empty($comment['authorId'])) { + $docSheet->getComment((string) $comment['ref'])->setAuthor($authors[(string) $comment['authorId']]); + } + $docSheet->getComment((string) $comment['ref'])->setText($this->parseRichText($comment->text)); + } + } + + // later we will remove from it real vmlComments + $unparsedVmlDrawings = $vmlComments; + + // Loop through VML comments + foreach ($vmlComments as $relName => $relPath) { + // Load VML comments file + $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath); + $vmlCommentsFile = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, $relPath)), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $vmlCommentsFile->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml'); + + $shapes = $vmlCommentsFile->xpath('//v:shape'); + foreach ($shapes as $shape) { + $shape->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml'); + + if (isset($shape['style'])) { + $style = (string) $shape['style']; + $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1)); + $column = null; + $row = null; + + $clientData = $shape->xpath('.//x:ClientData'); + if (is_array($clientData) && !empty($clientData)) { + $clientData = $clientData[0]; + + if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') { + $temp = $clientData->xpath('.//x:Row'); + if (is_array($temp)) { + $row = $temp[0]; + } + + $temp = $clientData->xpath('.//x:Column'); + if (is_array($temp)) { + $column = $temp[0]; + } + } + } + + if (($column !== null) && ($row !== null)) { + // Set comment properties + $comment = $docSheet->getCommentByColumnAndRow($column + 1, $row + 1); + $comment->getFillColor()->setRGB($fillColor); + + // Parse style + $styleArray = explode(';', str_replace(' ', '', $style)); + foreach ($styleArray as $stylePair) { + $stylePair = explode(':', $stylePair); + + if ($stylePair[0] == 'margin-left') { + $comment->setMarginLeft($stylePair[1]); + } + if ($stylePair[0] == 'margin-top') { + $comment->setMarginTop($stylePair[1]); + } + if ($stylePair[0] == 'width') { + $comment->setWidth($stylePair[1]); + } + if ($stylePair[0] == 'height') { + $comment->setHeight($stylePair[1]); + } + if ($stylePair[0] == 'visibility') { + $comment->setVisible($stylePair[1] == 'visible'); + } + } + + unset($unparsedVmlDrawings[$relName]); + } + } + } + } + + // unparsed vmlDrawing + if ($unparsedVmlDrawings) { + foreach ($unparsedVmlDrawings as $rId => $relPath) { + $rId = substr($rId, 3); // rIdXXX + $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings']; + $unparsedVmlDrawing[$rId] = []; + $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath); + $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath; + $unparsedVmlDrawing[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath'])); + unset($unparsedVmlDrawing); + } + } + + // Header/footer images + if ($xmlSheet && $xmlSheet->legacyDrawingHF && !$this->readDataOnly) { + if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorksheet = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $vmlRelationship = ''; + + foreach ($relsWorksheet->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing') { + $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + } + } + + if ($vmlRelationship != '') { + // Fetch linked images + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsVML = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $drawings = []; + foreach ($relsVML->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') { + $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']); + } + } + + // Fetch VML document + $vmlDrawing = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, $vmlRelationship)), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $vmlDrawing->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml'); + + $hfImages = []; + + $shapes = $vmlDrawing->xpath('//v:shape'); + foreach ($shapes as $idx => $shape) { + $shape->registerXPathNamespace('v', 'urn:schemas-microsoft-com:vml'); + $imageData = $shape->xpath('//v:imagedata'); + + if (!$imageData) { + continue; + } + + $imageData = $imageData[$idx]; + + $imageData = $imageData->attributes('urn:schemas-microsoft-com:office:office'); + $style = self::toCSSArray((string) $shape['style']); + + $hfImages[(string) $shape['id']] = new HeaderFooterDrawing(); + if (isset($imageData['title'])) { + $hfImages[(string) $shape['id']]->setName((string) $imageData['title']); + } + + $hfImages[(string) $shape['id']]->setPath('zip://' . File::realpath($pFilename) . '#' . $drawings[(string) $imageData['relid']], false); + $hfImages[(string) $shape['id']]->setResizeProportional(false); + $hfImages[(string) $shape['id']]->setWidth($style['width']); + $hfImages[(string) $shape['id']]->setHeight($style['height']); + if (isset($style['margin-left'])) { + $hfImages[(string) $shape['id']]->setOffsetX($style['margin-left']); + } + $hfImages[(string) $shape['id']]->setOffsetY($style['margin-top']); + $hfImages[(string) $shape['id']]->setResizeProportional(true); + } + + $docSheet->getHeaderFooter()->setImages($hfImages); + } + } + } + } + + // TODO: Autoshapes from twoCellAnchors! + if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorksheet = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $drawings = []; + foreach ($relsWorksheet->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing') { + $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + } + } + if ($xmlSheet->drawing && !$this->readDataOnly) { + foreach ($xmlSheet->drawing as $drawing) { + $fileDrawing = $drawings[(string) self::getArrayItem($drawing->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'id')]; + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsDrawing = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $images = []; + $hyperlinks = []; + if ($relsDrawing && $relsDrawing->Relationship) { + foreach ($relsDrawing->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink') { + $hyperlinks[(string) $ele['Id']] = (string) $ele['Target']; + } + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') { + $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']); + } elseif ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart') { + if ($this->includeCharts) { + $charts[self::dirAdd($fileDrawing, $ele['Target'])] = [ + 'id' => (string) $ele['Id'], + 'sheet' => $docSheet->getTitle(), + ]; + } + } + } + } + $xmlDrawing = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, $fileDrawing)), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + )->children('http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing'); + + if ($xmlDrawing->oneCellAnchor) { + foreach ($xmlDrawing->oneCellAnchor as $oneCellAnchor) { + if ($oneCellAnchor->pic->blipFill) { + /** @var SimpleXMLElement $blip */ + $blip = $oneCellAnchor->pic->blipFill->children('http://schemas.openxmlformats.org/drawingml/2006/main')->blip; + /** @var SimpleXMLElement $xfrm */ + $xfrm = $oneCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->xfrm; + /** @var SimpleXMLElement $outerShdw */ + $outerShdw = $oneCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->effectLst->outerShdw; + /** @var \SimpleXMLElement $hlinkClick */ + $hlinkClick = $oneCellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick; + + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + $objDrawing->setName((string) self::getArrayItem($oneCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name')); + $objDrawing->setDescription((string) self::getArrayItem($oneCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'descr')); + $objDrawing->setPath( + 'zip://' . File::realpath($pFilename) . '#' . + $images[(string) self::getArrayItem( + $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), + 'embed' + )], + false + ); + $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((string) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1)); + $objDrawing->setOffsetX(Drawing::EMUToPixels($oneCellAnchor->from->colOff)); + $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff)); + $objDrawing->setResizeProportional(false); + $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cx'))); + $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cy'))); + if ($xfrm) { + $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem($xfrm->attributes(), 'rot'))); + } + if ($outerShdw) { + $shadow = $objDrawing->getShadow(); + $shadow->setVisible(true); + $shadow->setBlurRadius(Drawing::EMUTopixels(self::getArrayItem($outerShdw->attributes(), 'blurRad'))); + $shadow->setDistance(Drawing::EMUTopixels(self::getArrayItem($outerShdw->attributes(), 'dist'))); + $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem($outerShdw->attributes(), 'dir'))); + $shadow->setAlignment((string) self::getArrayItem($outerShdw->attributes(), 'algn')); + $shadow->getColor()->setRGB(self::getArrayItem($outerShdw->srgbClr->attributes(), 'val')); + $shadow->setAlpha(self::getArrayItem($outerShdw->srgbClr->alpha->attributes(), 'val') / 1000); + } + + $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks); + + $objDrawing->setWorksheet($docSheet); + } else { + // ? Can charts be positioned with a oneCellAnchor ? + $coordinates = Coordinate::stringFromColumnIndex(((string) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1); + $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff); + $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff); + $width = Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cx')); + $height = Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cy')); + } + } + } + if ($xmlDrawing->twoCellAnchor) { + foreach ($xmlDrawing->twoCellAnchor as $twoCellAnchor) { + if ($twoCellAnchor->pic->blipFill) { + $blip = $twoCellAnchor->pic->blipFill->children('http://schemas.openxmlformats.org/drawingml/2006/main')->blip; + $xfrm = $twoCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->xfrm; + $outerShdw = $twoCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->effectLst->outerShdw; + $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick; + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + $objDrawing->setName((string) self::getArrayItem($twoCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name')); + $objDrawing->setDescription((string) self::getArrayItem($twoCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'descr')); + $objDrawing->setPath( + 'zip://' . File::realpath($pFilename) . '#' . + $images[(string) self::getArrayItem( + $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), + 'embed' + )], + false + ); + $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1)); + $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff)); + $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff)); + $objDrawing->setResizeProportional(false); + + if ($xfrm) { + $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem($xfrm->ext->attributes(), 'cx'))); + $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem($xfrm->ext->attributes(), 'cy'))); + $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem($xfrm->attributes(), 'rot'))); + } + if ($outerShdw) { + $shadow = $objDrawing->getShadow(); + $shadow->setVisible(true); + $shadow->setBlurRadius(Drawing::EMUTopixels(self::getArrayItem($outerShdw->attributes(), 'blurRad'))); + $shadow->setDistance(Drawing::EMUTopixels(self::getArrayItem($outerShdw->attributes(), 'dist'))); + $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem($outerShdw->attributes(), 'dir'))); + $shadow->setAlignment((string) self::getArrayItem($outerShdw->attributes(), 'algn')); + $shadow->getColor()->setRGB(self::getArrayItem($outerShdw->srgbClr->attributes(), 'val')); + $shadow->setAlpha(self::getArrayItem($outerShdw->srgbClr->alpha->attributes(), 'val') / 1000); + } + + $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks); + + $objDrawing->setWorksheet($docSheet); + } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) { + $fromCoordinate = Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1); + $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff); + $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff); + $toCoordinate = Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1); + $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff); + $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff); + $graphic = $twoCellAnchor->graphicFrame->children('http://schemas.openxmlformats.org/drawingml/2006/main')->graphic; + /** @var SimpleXMLElement $chartRef */ + $chartRef = $graphic->graphicData->children('http://schemas.openxmlformats.org/drawingml/2006/chart')->chart; + $thisChart = (string) $chartRef->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => $fromCoordinate, + 'fromOffsetX' => $fromOffsetX, + 'fromOffsetY' => $fromOffsetY, + 'toCoordinate' => $toCoordinate, + 'toOffsetX' => $toOffsetX, + 'toOffsetY' => $toOffsetY, + 'worksheetTitle' => $docSheet->getTitle(), + ]; + } + } + } + } + + // store original rId of drawing files + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = []; + foreach ($relsWorksheet->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing') { + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = (string) $ele['Id']; + } + } + + // unparsed drawing AlternateContent + $xmlAltDrawing = simplexml_load_string( + $this->securityScanner->scan($this->getFromZipArchive($zip, $fileDrawing)), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + )->children('http://schemas.openxmlformats.org/markup-compatibility/2006'); + + if ($xmlAltDrawing->AlternateContent) { + foreach ($xmlAltDrawing->AlternateContent as $alternateContent) { + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML(); + } + } + } + } + + $this->readFormControlProperties($excel, $zip, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData); + $this->readPrinterSettings($excel, $zip, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData); + + // Loop through definedNames + if ($xmlWorkbook->definedNames) { + foreach ($xmlWorkbook->definedNames->definedName as $definedName) { + // Extract range + $extractedRange = (string) $definedName; + if (($spos = strpos($extractedRange, '!')) !== false) { + $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos)); + } else { + $extractedRange = str_replace('$', '', $extractedRange); + } + + // Valid range? + if (stripos((string) $definedName, '#REF!') !== false || $extractedRange == '') { + continue; + } + + // Some definedNames are only applicable if we are on the same sheet... + if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) { + // Switch on type + switch ((string) $definedName['name']) { + case '_xlnm._FilterDatabase': + if ((string) $definedName['hidden'] !== '1') { + $extractedRange = explode(',', $extractedRange); + foreach ($extractedRange as $range) { + $autoFilterRange = $range; + if (strpos($autoFilterRange, ':') !== false) { + $docSheet->getAutoFilter()->setRange($autoFilterRange); + } + } + } + + break; + case '_xlnm.Print_Titles': + // Split $extractedRange + $extractedRange = explode(',', $extractedRange); + + // Set print titles + foreach ($extractedRange as $range) { + $matches = []; + $range = str_replace('$', '', $range); + + // check for repeating columns, e g. 'A:A' or 'A:D' + if (preg_match('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) { + $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]); + } elseif (preg_match('/!?(\d+)\:(\d+)$/', $range, $matches)) { + // check for repeating rows, e.g. '1:1' or '1:5' + $docSheet->getPageSetup()->setRowsToRepeatAtTop([$matches[1], $matches[2]]); + } + } + + break; + case '_xlnm.Print_Area': + $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $newRangeSets = []; + foreach ($rangeSets as $rangeSet) { + list($sheetName, $rangeSet) = Worksheet::extractSheetTitle($rangeSet, true); + if (strpos($rangeSet, ':') === false) { + $rangeSet = $rangeSet . ':' . $rangeSet; + } + $newRangeSets[] = str_replace('$', '', $rangeSet); + } + $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + + break; + default: + break; + } + } + } + } + + // Next sheet id + ++$sheetId; + } + + // Loop through definedNames + if ($xmlWorkbook->definedNames) { + foreach ($xmlWorkbook->definedNames->definedName as $definedName) { + // Extract range + $extractedRange = (string) $definedName; + if (($spos = strpos($extractedRange, '!')) !== false) { + $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos)); + } else { + $extractedRange = str_replace('$', '', $extractedRange); + } + + // Valid range? + if (stripos((string) $definedName, '#REF!') !== false || $extractedRange == '') { + continue; + } + + // Some definedNames are only applicable if we are on the same sheet... + if ((string) $definedName['localSheetId'] != '') { + // Local defined name + // Switch on type + switch ((string) $definedName['name']) { + case '_xlnm._FilterDatabase': + case '_xlnm.Print_Titles': + case '_xlnm.Print_Area': + break; + default: + if ($mapSheetId[(int) $definedName['localSheetId']] !== null) { + if (strpos((string) $definedName, '!') !== false) { + $range = Worksheet::extractSheetTitle((string) $definedName, true); + $range[0] = str_replace("''", "'", $range[0]); + $range[0] = str_replace("'", '', $range[0]); + if ($worksheet = $docSheet->getParent()->getSheetByName($range[0])) { + $extractedRange = str_replace('$', '', $range[1]); + $scope = $docSheet->getParent()->getSheet($mapSheetId[(int) $definedName['localSheetId']]); + $excel->addNamedRange(new NamedRange((string) $definedName['name'], $worksheet, $extractedRange, true, $scope)); + } + } + } + + break; + } + } elseif (!isset($definedName['localSheetId'])) { + // "Global" definedNames + $locatedSheet = null; + $extractedSheetName = ''; + if (strpos((string) $definedName, '!') !== false) { + // Extract sheet name + $extractedSheetName = Worksheet::extractSheetTitle((string) $definedName, true); + $extractedSheetName = $extractedSheetName[0]; + + // Locate sheet + $locatedSheet = $excel->getSheetByName($extractedSheetName); + + // Modify range + list($worksheetName, $extractedRange) = Worksheet::extractSheetTitle($extractedRange, true); + } + + if ($locatedSheet !== null) { + $excel->addNamedRange(new NamedRange((string) $definedName['name'], $locatedSheet, $extractedRange, false)); + } + } + } + } + } + + if ((!$this->readDataOnly) || (!empty($this->loadSheetsOnly))) { + $workbookView = $xmlWorkbook->bookViews->workbookView; + + // active sheet index + $activeTab = (int) ($workbookView['activeTab']); // refers to old sheet index + + // keep active sheet index if sheet is still loaded, else first sheet is set as the active + if (isset($mapSheetId[$activeTab]) && $mapSheetId[$activeTab] !== null) { + $excel->setActiveSheetIndex($mapSheetId[$activeTab]); + } else { + if ($excel->getSheetCount() == 0) { + $excel->createSheet(); + } + $excel->setActiveSheetIndex(0); + } + + if (isset($workbookView['showHorizontalScroll'])) { + $showHorizontalScroll = (string) $workbookView['showHorizontalScroll']; + $excel->setShowHorizontalScroll($this->castXsdBooleanToBool($showHorizontalScroll)); + } + + if (isset($workbookView['showVerticalScroll'])) { + $showVerticalScroll = (string) $workbookView['showVerticalScroll']; + $excel->setShowVerticalScroll($this->castXsdBooleanToBool($showVerticalScroll)); + } + + if (isset($workbookView['showSheetTabs'])) { + $showSheetTabs = (string) $workbookView['showSheetTabs']; + $excel->setShowSheetTabs($this->castXsdBooleanToBool($showSheetTabs)); + } + + if (isset($workbookView['minimized'])) { + $minimized = (string) $workbookView['minimized']; + $excel->setMinimized($this->castXsdBooleanToBool($minimized)); + } + + if (isset($workbookView['autoFilterDateGrouping'])) { + $autoFilterDateGrouping = (string) $workbookView['autoFilterDateGrouping']; + $excel->setAutoFilterDateGrouping($this->castXsdBooleanToBool($autoFilterDateGrouping)); + } + + if (isset($workbookView['firstSheet'])) { + $firstSheet = (string) $workbookView['firstSheet']; + $excel->setFirstSheetIndex((int) $firstSheet); + } + + if (isset($workbookView['visibility'])) { + $visibility = (string) $workbookView['visibility']; + $excel->setVisibility($visibility); + } + + if (isset($workbookView['tabRatio'])) { + $tabRatio = (string) $workbookView['tabRatio']; + $excel->setTabRatio((int) $tabRatio); + } + } + + break; + } + } + + if (!$this->readDataOnly) { + $contentTypes = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, '[Content_Types].xml') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + + // Default content types + foreach ($contentTypes->Default as $contentType) { + switch ($contentType['ContentType']) { + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings': + $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType']; + + break; + } + } + + // Override content types + foreach ($contentTypes->Override as $contentType) { + switch ($contentType['ContentType']) { + case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml': + if ($this->includeCharts) { + $chartEntryRef = ltrim($contentType['PartName'], '/'); + $chartElements = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, $chartEntryRef) + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml')); + + if (isset($charts[$chartEntryRef])) { + $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; + if (isset($chartDetails[$chartPositionRef])) { + $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); + $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); + } + } + } + + break; + + // unparsed + case 'application/vnd.ms-excel.controlproperties+xml': + $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; + + break; + } + } + } + + $excel->setUnparsedLoadedData($unparsedLoadedData); + + $zip->close(); + + return $excel; + } + + private static function readColor($color, $background = false) + { + if (isset($color['rgb'])) { + return (string) $color['rgb']; + } elseif (isset($color['indexed'])) { + return Color::indexedColor($color['indexed'] - 7, $background)->getARGB(); + } elseif (isset($color['theme'])) { + if (self::$theme !== null) { + $returnColour = self::$theme->getColourByIndex((int) $color['theme']); + if (isset($color['tint'])) { + $tintAdjust = (float) $color['tint']; + $returnColour = Color::changeBrightness($returnColour, $tintAdjust); + } + + return 'FF' . $returnColour; + } + } + + if ($background) { + return 'FFFFFFFF'; + } + + return 'FF000000'; + } + + /** + * @param Style $docStyle + * @param SimpleXMLElement|\stdClass $style + */ + private static function readStyle(Style $docStyle, $style) + { + $docStyle->getNumberFormat()->setFormatCode($style->numFmt); + + // font + if (isset($style->font)) { + $docStyle->getFont()->setName((string) $style->font->name['val']); + $docStyle->getFont()->setSize((string) $style->font->sz['val']); + if (isset($style->font->b)) { + $docStyle->getFont()->setBold(!isset($style->font->b['val']) || self::boolean((string) $style->font->b['val'])); + } + if (isset($style->font->i)) { + $docStyle->getFont()->setItalic(!isset($style->font->i['val']) || self::boolean((string) $style->font->i['val'])); + } + if (isset($style->font->strike)) { + $docStyle->getFont()->setStrikethrough(!isset($style->font->strike['val']) || self::boolean((string) $style->font->strike['val'])); + } + $docStyle->getFont()->getColor()->setARGB(self::readColor($style->font->color)); + + if (isset($style->font->u) && !isset($style->font->u['val'])) { + $docStyle->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); + } elseif (isset($style->font->u, $style->font->u['val'])) { + $docStyle->getFont()->setUnderline((string) $style->font->u['val']); + } + + if (isset($style->font->vertAlign, $style->font->vertAlign['val'])) { + $vertAlign = strtolower((string) $style->font->vertAlign['val']); + if ($vertAlign == 'superscript') { + $docStyle->getFont()->setSuperscript(true); + } + if ($vertAlign == 'subscript') { + $docStyle->getFont()->setSubscript(true); + } + } + } + + // fill + if (isset($style->fill)) { + if ($style->fill->gradientFill) { + /** @var SimpleXMLElement $gradientFill */ + $gradientFill = $style->fill->gradientFill[0]; + if (!empty($gradientFill['type'])) { + $docStyle->getFill()->setFillType((string) $gradientFill['type']); + } + $docStyle->getFill()->setRotation((float) ($gradientFill['degree'])); + $gradientFill->registerXPathNamespace('sml', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + $docStyle->getFill()->getStartColor()->setARGB(self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color)); + $docStyle->getFill()->getEndColor()->setARGB(self::readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color)); + } elseif ($style->fill->patternFill) { + $patternType = (string) $style->fill->patternFill['patternType'] != '' ? (string) $style->fill->patternFill['patternType'] : 'solid'; + $docStyle->getFill()->setFillType($patternType); + if ($style->fill->patternFill->fgColor) { + $docStyle->getFill()->getStartColor()->setARGB(self::readColor($style->fill->patternFill->fgColor, true)); + } else { + $docStyle->getFill()->getStartColor()->setARGB('FF000000'); + } + if ($style->fill->patternFill->bgColor) { + $docStyle->getFill()->getEndColor()->setARGB(self::readColor($style->fill->patternFill->bgColor, true)); + } + } + } + + // border + if (isset($style->border)) { + $diagonalUp = self::boolean((string) $style->border['diagonalUp']); + $diagonalDown = self::boolean((string) $style->border['diagonalDown']); + if (!$diagonalUp && !$diagonalDown) { + $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE); + } elseif ($diagonalUp && !$diagonalDown) { + $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP); + } elseif (!$diagonalUp && $diagonalDown) { + $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN); + } else { + $docStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH); + } + self::readBorder($docStyle->getBorders()->getLeft(), $style->border->left); + self::readBorder($docStyle->getBorders()->getRight(), $style->border->right); + self::readBorder($docStyle->getBorders()->getTop(), $style->border->top); + self::readBorder($docStyle->getBorders()->getBottom(), $style->border->bottom); + self::readBorder($docStyle->getBorders()->getDiagonal(), $style->border->diagonal); + } + + // alignment + if (isset($style->alignment)) { + $docStyle->getAlignment()->setHorizontal((string) $style->alignment['horizontal']); + $docStyle->getAlignment()->setVertical((string) $style->alignment['vertical']); + + $textRotation = 0; + if ((int) $style->alignment['textRotation'] <= 90) { + $textRotation = (int) $style->alignment['textRotation']; + } elseif ((int) $style->alignment['textRotation'] > 90) { + $textRotation = 90 - (int) $style->alignment['textRotation']; + } + + $docStyle->getAlignment()->setTextRotation((int) $textRotation); + $docStyle->getAlignment()->setWrapText(self::boolean((string) $style->alignment['wrapText'])); + $docStyle->getAlignment()->setShrinkToFit(self::boolean((string) $style->alignment['shrinkToFit'])); + $docStyle->getAlignment()->setIndent((int) ((string) $style->alignment['indent']) > 0 ? (int) ((string) $style->alignment['indent']) : 0); + $docStyle->getAlignment()->setReadOrder((int) ((string) $style->alignment['readingOrder']) > 0 ? (int) ((string) $style->alignment['readingOrder']) : 0); + } + + // protection + if (isset($style->protection)) { + if (isset($style->protection['locked'])) { + if (self::boolean((string) $style->protection['locked'])) { + $docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED); + } else { + $docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED); + } + } + + if (isset($style->protection['hidden'])) { + if (self::boolean((string) $style->protection['hidden'])) { + $docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED); + } else { + $docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED); + } + } + } + + // top-level style settings + if (isset($style->quotePrefix)) { + $docStyle->setQuotePrefix($style->quotePrefix); + } + } + + /** + * @param Border $docBorder + * @param SimpleXMLElement $eleBorder + */ + private static function readBorder(Border $docBorder, $eleBorder) + { + if (isset($eleBorder['style'])) { + $docBorder->setBorderStyle((string) $eleBorder['style']); + } + if (isset($eleBorder->color)) { + $docBorder->getColor()->setARGB(self::readColor($eleBorder->color)); + } + } + + /** + * @param SimpleXMLElement | null $is + * + * @return RichText + */ + private function parseRichText($is) + { + $value = new RichText(); + + if (isset($is->t)) { + $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); + } else { + if (is_object($is->r)) { + foreach ($is->r as $run) { + if (!isset($run->rPr)) { + $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + } else { + $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + + if (isset($run->rPr->rFont['val'])) { + $objText->getFont()->setName((string) $run->rPr->rFont['val']); + } + if (isset($run->rPr->sz['val'])) { + $objText->getFont()->setSize((float) $run->rPr->sz['val']); + } + if (isset($run->rPr->color)) { + $objText->getFont()->setColor(new Color(self::readColor($run->rPr->color))); + } + if ((isset($run->rPr->b['val']) && self::boolean((string) $run->rPr->b['val'])) || + (isset($run->rPr->b) && !isset($run->rPr->b['val']))) { + $objText->getFont()->setBold(true); + } + if ((isset($run->rPr->i['val']) && self::boolean((string) $run->rPr->i['val'])) || + (isset($run->rPr->i) && !isset($run->rPr->i['val']))) { + $objText->getFont()->setItalic(true); + } + if (isset($run->rPr->vertAlign, $run->rPr->vertAlign['val'])) { + $vertAlign = strtolower((string) $run->rPr->vertAlign['val']); + if ($vertAlign == 'superscript') { + $objText->getFont()->setSuperscript(true); + } + if ($vertAlign == 'subscript') { + $objText->getFont()->setSubscript(true); + } + } + if (isset($run->rPr->u) && !isset($run->rPr->u['val'])) { + $objText->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); + } elseif (isset($run->rPr->u, $run->rPr->u['val'])) { + $objText->getFont()->setUnderline((string) $run->rPr->u['val']); + } + if ((isset($run->rPr->strike['val']) && self::boolean((string) $run->rPr->strike['val'])) || + (isset($run->rPr->strike) && !isset($run->rPr->strike['val']))) { + $objText->getFont()->setStrikethrough(true); + } + } + } + } + } + + return $value; + } + + /** + * @param Spreadsheet $excel + * @param mixed $customUITarget + * @param mixed $zip + */ + private function readRibbon(Spreadsheet $excel, $customUITarget, $zip) + { + $baseDir = dirname($customUITarget); + $nameCustomUI = basename($customUITarget); + // get the xml file (ribbon) + $localRibbon = $this->getFromZipArchive($zip, $customUITarget); + $customUIImagesNames = []; + $customUIImagesBinaries = []; + // something like customUI/_rels/customUI.xml.rels + $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels'; + $dataRels = $this->getFromZipArchive($zip, $pathRels); + if ($dataRels) { + // exists and not empty if the ribbon have some pictures (other than internal MSO) + $UIRels = simplexml_load_string( + $this->securityScanner->scan($dataRels), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if (false !== $UIRels) { + // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image + foreach ($UIRels->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') { + // an image ? + $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target']; + $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']); + } + } + } + } + if ($localRibbon) { + $excel->setRibbonXMLData($customUITarget, $localRibbon); + if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) { + $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries); + } else { + $excel->setRibbonBinObjects(null, null); + } + } else { + $excel->setRibbonXMLData(null, null); + $excel->setRibbonBinObjects(null, null); + } + } + + private static function getArrayItem($array, $key = 0) + { + return isset($array[$key]) ? $array[$key] : null; + } + + private static function dirAdd($base, $add) + { + return preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); + } + + private static function toCSSArray($style) + { + $style = trim(str_replace(["\r", "\n"], '', $style), ';'); + + $temp = explode(';', $style); + $style = []; + foreach ($temp as $item) { + $item = explode(':', $item); + + if (strpos($item[1], 'px') !== false) { + $item[1] = str_replace('px', '', $item[1]); + } + if (strpos($item[1], 'pt') !== false) { + $item[1] = str_replace('pt', '', $item[1]); + $item[1] = Font::fontSizeToPixels($item[1]); + } + if (strpos($item[1], 'in') !== false) { + $item[1] = str_replace('in', '', $item[1]); + $item[1] = Font::inchSizeToPixels($item[1]); + } + if (strpos($item[1], 'cm') !== false) { + $item[1] = str_replace('cm', '', $item[1]); + $item[1] = Font::centimeterSizeToPixels($item[1]); + } + + $style[$item[0]] = $item[1]; + } + + return $style; + } + + private static function boolean($value) + { + if (is_object($value)) { + $value = (string) $value; + } + if (is_numeric($value)) { + return (bool) $value; + } + + return $value === 'true' || $value === 'TRUE'; + } + + /** + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing + * @param \SimpleXMLElement $cellAnchor + * @param array $hyperlinks + */ + private function readHyperLinkDrawing($objDrawing, $cellAnchor, $hyperlinks) + { + $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick; + + if ($hlinkClick->count() === 0) { + return; + } + + $hlinkId = (string) $hlinkClick->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships')['id']; + $hyperlink = new Hyperlink( + $hyperlinks[$hlinkId], + (string) self::getArrayItem($cellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name') + ); + $objDrawing->setHyperlink($hyperlink); + } + + private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook) + { + if (!$xmlWorkbook->workbookProtection) { + return; + } + + if ($xmlWorkbook->workbookProtection['lockRevision']) { + $excel->getSecurity()->setLockRevision((bool) $xmlWorkbook->workbookProtection['lockRevision']); + } + + if ($xmlWorkbook->workbookProtection['lockStructure']) { + $excel->getSecurity()->setLockStructure((bool) $xmlWorkbook->workbookProtection['lockStructure']); + } + + if ($xmlWorkbook->workbookProtection['lockWindows']) { + $excel->getSecurity()->setLockWindows((bool) $xmlWorkbook->workbookProtection['lockWindows']); + } + + if ($xmlWorkbook->workbookProtection['revisionsPassword']) { + $excel->getSecurity()->setRevisionsPassword((string) $xmlWorkbook->workbookProtection['revisionsPassword'], true); + } + + if ($xmlWorkbook->workbookProtection['workbookPassword']) { + $excel->getSecurity()->setWorkbookPassword((string) $xmlWorkbook->workbookProtection['workbookPassword'], true); + } + } + + private function readFormControlProperties(Spreadsheet $excel, ZipArchive $zip, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData) + { + if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { + return; + } + + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorksheet = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $ctrlProps = []; + foreach ($relsWorksheet->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp') { + $ctrlProps[(string) $ele['Id']] = $ele; + } + } + + $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps']; + foreach ($ctrlProps as $rId => $ctrlProp) { + $rId = substr($rId, 3); // rIdXXX + $unparsedCtrlProps[$rId] = []; + $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']); + $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target']; + $unparsedCtrlProps[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath'])); + } + unset($unparsedCtrlProps); + } + + private function readPrinterSettings(Spreadsheet $excel, ZipArchive $zip, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData) + { + if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { + return; + } + + //~ http://schemas.openxmlformats.org/package/2006/relationships" + $relsWorksheet = simplexml_load_string( + $this->securityScanner->scan( + $this->getFromZipArchive($zip, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') + ), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $sheetPrinterSettings = []; + foreach ($relsWorksheet->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings') { + $sheetPrinterSettings[(string) $ele['Id']] = $ele; + } + } + + $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings']; + foreach ($sheetPrinterSettings as $rId => $printerSettings) { + $rId = substr($rId, 3); // rIdXXX + $unparsedPrinterSettings[$rId] = []; + $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $printerSettings['Target']); + $unparsedPrinterSettings[$rId]['relFilePath'] = (string) $printerSettings['Target']; + $unparsedPrinterSettings[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath'])); + } + unset($unparsedPrinterSettings); + } + + /** + * Convert an 'xsd:boolean' XML value to a PHP boolean value. + * A valid 'xsd:boolean' XML value can be one of the following + * four values: 'true', 'false', '1', '0'. It is case sensitive. + * + * Note that just doing '(bool) $xsdBoolean' is not safe, + * since '(bool) "false"' returns true. + * + * @see https://www.w3.org/TR/xmlschema11-2/#boolean + * + * @param string $xsdBoolean An XML string value of type 'xsd:boolean' + * + * @return bool Boolean value + */ + private function castXsdBooleanToBool($xsdBoolean) + { + if ($xsdBoolean === 'false') { + return false; + } + + return (bool) $xsdBoolean; + } + + /** + * Read columns and rows attributes from XML and set them on the worksheet. + * + * @param SimpleXMLElement $xmlSheet + * @param Worksheet $docSheet + */ + private function readColumnsAndRowsAttributes(SimpleXMLElement $xmlSheet, Worksheet $docSheet) + { + $columnsAttributes = []; + $rowsAttributes = []; + if (isset($xmlSheet->cols) && !$this->readDataOnly) { + foreach ($xmlSheet->cols->col as $col) { + for ($i = (int) ($col['min']); $i <= (int) ($col['max']); ++$i) { + if ($col['style'] && !$this->readDataOnly) { + $columnsAttributes[Coordinate::stringFromColumnIndex($i)]['xfIndex'] = (int) $col['style']; + } + if (self::boolean($col['hidden'])) { + $columnsAttributes[Coordinate::stringFromColumnIndex($i)]['visible'] = false; + } + if (self::boolean($col['collapsed'])) { + $columnsAttributes[Coordinate::stringFromColumnIndex($i)]['collapsed'] = true; + } + if ($col['outlineLevel'] > 0) { + $columnsAttributes[Coordinate::stringFromColumnIndex($i)]['outlineLevel'] = (int) $col['outlineLevel']; + } + $columnsAttributes[Coordinate::stringFromColumnIndex($i)]['width'] = (float) $col['width']; + + if ((int) ($col['max']) == 16384) { + break; + } + } + } + } + + if ($xmlSheet && $xmlSheet->sheetData && $xmlSheet->sheetData->row) { + foreach ($xmlSheet->sheetData->row as $row) { + if ($row['ht'] && !$this->readDataOnly) { + $rowsAttributes[(int) $row['r']]['rowHeight'] = (float) $row['ht']; + } + if (self::boolean($row['hidden']) && !$this->readDataOnly) { + $rowsAttributes[(int) $row['r']]['visible'] = false; + } + if (self::boolean($row['collapsed'])) { + $rowsAttributes[(int) $row['r']]['collapsed'] = true; + } + if ($row['outlineLevel'] > 0) { + $rowsAttributes[(int) $row['r']]['outlineLevel'] = (int) $row['outlineLevel']; + } + if ($row['s'] && !$this->readDataOnly) { + $rowsAttributes[(int) $row['r']]['xfIndex'] = (int) $row['s']; + } + } + } + + $readFilter = (\get_class($this->getReadFilter()) !== DefaultReadFilter::class ? $this->getReadFilter() : null); + + // set columns/rows attributes + $columnsAttributesSet = []; + $rowsAttributesSet = []; + foreach ($columnsAttributes as $coordColumn => $columnAttributes) { + if ($readFilter !== null) { + foreach ($rowsAttributes as $coordRow => $rowAttributes) { + if (!$readFilter->readCell($coordColumn, $coordRow, $docSheet->getTitle())) { + continue 2; + } + } + } + + if (!isset($columnsAttributesSet[$coordColumn])) { + $this->setColumnAttributes($docSheet, $coordColumn, $columnAttributes); + $columnsAttributesSet[$coordColumn] = true; + } + } + + foreach ($rowsAttributes as $coordRow => $rowAttributes) { + if ($readFilter !== null) { + foreach ($columnsAttributes as $coordColumn => $columnAttributes) { + if (!$readFilter->readCell($coordColumn, $coordRow, $docSheet->getTitle())) { + continue 2; + } + } + } + + if (!isset($rowsAttributesSet[$coordRow])) { + $this->setRowAttributes($docSheet, $coordRow, $rowAttributes); + $rowsAttributesSet[$coordRow] = true; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx/Chart.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx/Chart.php new file mode 100644 index 00000000000..2b920d705c3 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -0,0 +1,570 @@ +attributes(); + if (isset($attributes[$name])) { + if ($format == 'string') { + return (string) $attributes[$name]; + } elseif ($format == 'integer') { + return (int) $attributes[$name]; + } elseif ($format == 'boolean') { + return (bool) ($attributes[$name] === '0' || $attributes[$name] !== 'true') ? false : true; + } + + return (float) $attributes[$name]; + } + + return null; + } + + private static function readColor($color, $background = false) + { + if (isset($color['rgb'])) { + return (string) $color['rgb']; + } elseif (isset($color['indexed'])) { + return Color::indexedColor($color['indexed'] - 7, $background)->getARGB(); + } + } + + /** + * @param SimpleXMLElement $chartElements + * @param string $chartName + * + * @return \PhpOffice\PhpSpreadsheet\Chart\Chart + */ + public static function readChart(SimpleXMLElement $chartElements, $chartName) + { + $namespacesChartMeta = $chartElements->getNamespaces(true); + $chartElementsC = $chartElements->children($namespacesChartMeta['c']); + + $XaxisLabel = $YaxisLabel = $legend = $title = null; + $dispBlanksAs = $plotVisOnly = null; + + foreach ($chartElementsC as $chartElementKey => $chartElement) { + switch ($chartElementKey) { + case 'chart': + foreach ($chartElement as $chartDetailsKey => $chartDetails) { + $chartDetailsC = $chartDetails->children($namespacesChartMeta['c']); + switch ($chartDetailsKey) { + case 'plotArea': + $plotAreaLayout = $XaxisLable = $YaxisLable = null; + $plotSeries = $plotAttributes = []; + foreach ($chartDetails as $chartDetailKey => $chartDetail) { + switch ($chartDetailKey) { + case 'layout': + $plotAreaLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + + break; + case 'catAx': + if (isset($chartDetail->title)) { + $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + } + + break; + case 'dateAx': + if (isset($chartDetail->title)) { + $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + } + + break; + case 'valAx': + if (isset($chartDetail->title)) { + $YaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + } + + break; + case 'barChart': + case 'bar3DChart': + $barDirection = self::getAttribute($chartDetail->barDir, 'val', 'string'); + $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer->setPlotDirection($barDirection); + $plotSeries[] = $plotSer; + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'lineChart': + case 'line3DChart': + $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'areaChart': + case 'area3DChart': + $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'doughnutChart': + case 'pieChart': + case 'pie3DChart': + $explosion = isset($chartDetail->ser->explosion); + $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer->setPlotStyle($explosion); + $plotSeries[] = $plotSer; + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'scatterChart': + $scatterStyle = self::getAttribute($chartDetail->scatterStyle, 'val', 'string'); + $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer->setPlotStyle($scatterStyle); + $plotSeries[] = $plotSer; + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'bubbleChart': + $bubbleScale = self::getAttribute($chartDetail->bubbleScale, 'val', 'integer'); + $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer->setPlotStyle($bubbleScale); + $plotSeries[] = $plotSer; + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'radarChart': + $radarStyle = self::getAttribute($chartDetail->radarStyle, 'val', 'string'); + $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer->setPlotStyle($radarStyle); + $plotSeries[] = $plotSer; + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'surfaceChart': + case 'surface3DChart': + $wireFrame = self::getAttribute($chartDetail->wireframe, 'val', 'boolean'); + $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer->setPlotStyle($wireFrame); + $plotSeries[] = $plotSer; + $plotAttributes = self::readChartAttributes($chartDetail); + + break; + case 'stockChart': + $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotAttributes = self::readChartAttributes($plotAreaLayout); + + break; + } + } + if ($plotAreaLayout == null) { + $plotAreaLayout = new Layout(); + } + $plotArea = new PlotArea($plotAreaLayout, $plotSeries); + self::setChartAttributes($plotAreaLayout, $plotAttributes); + + break; + case 'plotVisOnly': + $plotVisOnly = self::getAttribute($chartDetails, 'val', 'string'); + + break; + case 'dispBlanksAs': + $dispBlanksAs = self::getAttribute($chartDetails, 'val', 'string'); + + break; + case 'title': + $title = self::chartTitle($chartDetails, $namespacesChartMeta); + + break; + case 'legend': + $legendPos = 'r'; + $legendLayout = null; + $legendOverlay = false; + foreach ($chartDetails as $chartDetailKey => $chartDetail) { + switch ($chartDetailKey) { + case 'legendPos': + $legendPos = self::getAttribute($chartDetail, 'val', 'string'); + + break; + case 'overlay': + $legendOverlay = self::getAttribute($chartDetail, 'val', 'boolean'); + + break; + case 'layout': + $legendLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + + break; + } + } + $legend = new Legend($legendPos, $legendLayout, $legendOverlay); + + break; + } + } + } + } + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, $dispBlanksAs, $XaxisLabel, $YaxisLabel); + + return $chart; + } + + private static function chartTitle(SimpleXMLElement $titleDetails, array $namespacesChartMeta) + { + $caption = []; + $titleLayout = null; + foreach ($titleDetails as $titleDetailKey => $chartDetail) { + switch ($titleDetailKey) { + case 'tx': + $titleDetails = $chartDetail->rich->children($namespacesChartMeta['a']); + foreach ($titleDetails as $titleKey => $titleDetail) { + switch ($titleKey) { + case 'p': + $titleDetailPart = $titleDetail->children($namespacesChartMeta['a']); + $caption[] = self::parseRichText($titleDetailPart); + } + } + + break; + case 'layout': + $titleLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + + break; + } + } + + return new Title($caption, $titleLayout); + } + + private static function chartLayoutDetails($chartDetail, $namespacesChartMeta) + { + if (!isset($chartDetail->manualLayout)) { + return null; + } + $details = $chartDetail->manualLayout->children($namespacesChartMeta['c']); + if ($details === null) { + return null; + } + $layout = []; + foreach ($details as $detailKey => $detail) { + $layout[$detailKey] = self::getAttribute($detail, 'val', 'string'); + } + + return new Layout($layout); + } + + private static function chartDataSeries($chartDetail, $namespacesChartMeta, $plotType) + { + $multiSeriesType = null; + $smoothLine = false; + $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = []; + + $seriesDetailSet = $chartDetail->children($namespacesChartMeta['c']); + foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) { + switch ($seriesDetailKey) { + case 'grouping': + $multiSeriesType = self::getAttribute($chartDetail->grouping, 'val', 'string'); + + break; + case 'ser': + $marker = null; + $seriesIndex = ''; + foreach ($seriesDetails as $seriesKey => $seriesDetail) { + switch ($seriesKey) { + case 'idx': + $seriesIndex = self::getAttribute($seriesDetail, 'val', 'integer'); + + break; + case 'order': + $seriesOrder = self::getAttribute($seriesDetail, 'val', 'integer'); + $plotOrder[$seriesIndex] = $seriesOrder; + + break; + case 'tx': + $seriesLabel[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + + break; + case 'marker': + $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); + + break; + case 'smooth': + $smoothLine = self::getAttribute($seriesDetail, 'val', 'boolean'); + + break; + case 'cat': + $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + + break; + case 'val': + $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + + break; + case 'xVal': + $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + + break; + case 'yVal': + $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + + break; + } + } + } + } + + return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); + } + + private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker = null) + { + if (isset($seriesDetail->strRef)) { + $seriesSource = (string) $seriesDetail->strRef->f; + $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); + + return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + } elseif (isset($seriesDetail->numRef)) { + $seriesSource = (string) $seriesDetail->numRef->f; + $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); + + return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + } elseif (isset($seriesDetail->multiLvlStrRef)) { + $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; + $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); + $seriesData['pointCount'] = count($seriesData['dataValues']); + + return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + } elseif (isset($seriesDetail->multiLvlNumRef)) { + $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; + $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); + $seriesData['pointCount'] = count($seriesData['dataValues']); + + return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + } + + return null; + } + + private static function chartDataSeriesValues($seriesValueSet, $dataType = 'n') + { + $seriesVal = []; + $formatCode = ''; + $pointCount = 0; + + foreach ($seriesValueSet as $seriesValueIdx => $seriesValue) { + switch ($seriesValueIdx) { + case 'ptCount': + $pointCount = self::getAttribute($seriesValue, 'val', 'integer'); + + break; + case 'formatCode': + $formatCode = (string) $seriesValue; + + break; + case 'pt': + $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); + if ($dataType == 's') { + $seriesVal[$pointVal] = (string) $seriesValue->v; + } elseif ($seriesValue->v === Functions::NA()) { + $seriesVal[$pointVal] = null; + } else { + $seriesVal[$pointVal] = (float) $seriesValue->v; + } + + break; + } + } + + return [ + 'formatCode' => $formatCode, + 'pointCount' => $pointCount, + 'dataValues' => $seriesVal, + ]; + } + + private static function chartDataSeriesValuesMultiLevel($seriesValueSet, $dataType = 'n') + { + $seriesVal = []; + $formatCode = ''; + $pointCount = 0; + + foreach ($seriesValueSet->lvl as $seriesLevelIdx => $seriesLevel) { + foreach ($seriesLevel as $seriesValueIdx => $seriesValue) { + switch ($seriesValueIdx) { + case 'ptCount': + $pointCount = self::getAttribute($seriesValue, 'val', 'integer'); + + break; + case 'formatCode': + $formatCode = (string) $seriesValue; + + break; + case 'pt': + $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); + if ($dataType == 's') { + $seriesVal[$pointVal][] = (string) $seriesValue->v; + } elseif ($seriesValue->v === Functions::NA()) { + $seriesVal[$pointVal] = null; + } else { + $seriesVal[$pointVal][] = (float) $seriesValue->v; + } + + break; + } + } + } + + return [ + 'formatCode' => $formatCode, + 'pointCount' => $pointCount, + 'dataValues' => $seriesVal, + ]; + } + + private static function parseRichText(SimpleXMLElement $titleDetailPart) + { + $value = new RichText(); + $objText = null; + foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { + if (isset($titleDetailElement->t)) { + $objText = $value->createTextRun((string) $titleDetailElement->t); + } + if (isset($titleDetailElement->rPr)) { + if (isset($titleDetailElement->rPr->rFont['val'])) { + $objText->getFont()->setName((string) $titleDetailElement->rPr->rFont['val']); + } + + $fontSize = (self::getAttribute($titleDetailElement->rPr, 'sz', 'integer')); + if ($fontSize !== null) { + $objText->getFont()->setSize(floor($fontSize / 100)); + } + + $fontColor = (self::getAttribute($titleDetailElement->rPr, 'color', 'string')); + if ($fontColor !== null) { + $objText->getFont()->setColor(new Color(self::readColor($fontColor))); + } + + $bold = self::getAttribute($titleDetailElement->rPr, 'b', 'boolean'); + if ($bold !== null) { + $objText->getFont()->setBold($bold); + } + + $italic = self::getAttribute($titleDetailElement->rPr, 'i', 'boolean'); + if ($italic !== null) { + $objText->getFont()->setItalic($italic); + } + + $baseline = self::getAttribute($titleDetailElement->rPr, 'baseline', 'integer'); + if ($baseline !== null) { + if ($baseline > 0) { + $objText->getFont()->setSuperscript(true); + } elseif ($baseline < 0) { + $objText->getFont()->setSubscript(true); + } + } + + $underscore = (self::getAttribute($titleDetailElement->rPr, 'u', 'string')); + if ($underscore !== null) { + if ($underscore == 'sng') { + $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + } elseif ($underscore == 'dbl') { + $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } else { + $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); + } + } + + $strikethrough = (self::getAttribute($titleDetailElement->rPr, 's', 'string')); + if ($strikethrough !== null) { + if ($strikethrough == 'noStrike') { + $objText->getFont()->setStrikethrough(false); + } else { + $objText->getFont()->setStrikethrough(true); + } + } + } + } + + return $value; + } + + private static function readChartAttributes($chartDetail) + { + $plotAttributes = []; + if (isset($chartDetail->dLbls)) { + if (isset($chartDetail->dLbls->howLegendKey)) { + $plotAttributes['showLegendKey'] = self::getAttribute($chartDetail->dLbls->showLegendKey, 'val', 'string'); + } + if (isset($chartDetail->dLbls->showVal)) { + $plotAttributes['showVal'] = self::getAttribute($chartDetail->dLbls->showVal, 'val', 'string'); + } + if (isset($chartDetail->dLbls->showCatName)) { + $plotAttributes['showCatName'] = self::getAttribute($chartDetail->dLbls->showCatName, 'val', 'string'); + } + if (isset($chartDetail->dLbls->showSerName)) { + $plotAttributes['showSerName'] = self::getAttribute($chartDetail->dLbls->showSerName, 'val', 'string'); + } + if (isset($chartDetail->dLbls->showPercent)) { + $plotAttributes['showPercent'] = self::getAttribute($chartDetail->dLbls->showPercent, 'val', 'string'); + } + if (isset($chartDetail->dLbls->showBubbleSize)) { + $plotAttributes['showBubbleSize'] = self::getAttribute($chartDetail->dLbls->showBubbleSize, 'val', 'string'); + } + if (isset($chartDetail->dLbls->showLeaderLines)) { + $plotAttributes['showLeaderLines'] = self::getAttribute($chartDetail->dLbls->showLeaderLines, 'val', 'string'); + } + } + + return $plotAttributes; + } + + /** + * @param Layout $plotArea + * @param mixed $plotAttributes + */ + private static function setChartAttributes(Layout $plotArea, $plotAttributes) + { + foreach ($plotAttributes as $plotAttributeKey => $plotAttributeValue) { + switch ($plotAttributeKey) { + case 'showLegendKey': + $plotArea->setShowLegendKey($plotAttributeValue); + + break; + case 'showVal': + $plotArea->setShowVal($plotAttributeValue); + + break; + case 'showCatName': + $plotArea->setShowCatName($plotAttributeValue); + + break; + case 'showSerName': + $plotArea->setShowSerName($plotAttributeValue); + + break; + case 'showPercent': + $plotArea->setShowPercent($plotAttributeValue); + + break; + case 'showBubbleSize': + $plotArea->setShowBubbleSize($plotAttributeValue); + + break; + case 'showLeaderLines': + $plotArea->setShowLeaderLines($plotAttributeValue); + + break; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx/Theme.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx/Theme.php new file mode 100644 index 00000000000..c105f3c1a66 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xlsx/Theme.php @@ -0,0 +1,93 @@ +themeName = $themeName; + $this->colourSchemeName = $colourSchemeName; + $this->colourMap = $colourMap; + } + + /** + * Get Theme Name. + * + * @return string + */ + public function getThemeName() + { + return $this->themeName; + } + + /** + * Get colour Scheme Name. + * + * @return string + */ + public function getColourSchemeName() + { + return $this->colourSchemeName; + } + + /** + * Get colour Map Value by Position. + * + * @param mixed $index + * + * @return string + */ + public function getColourByIndex($index) + { + if (isset($this->colourMap[$index])) { + return $this->colourMap[$index]; + } + + return null; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if ((is_object($value)) && ($key != '_parent')) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xml.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xml.php new file mode 100644 index 00000000000..2b7959f47c1 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Reader/Xml.php @@ -0,0 +1,882 @@ +readFilter = new DefaultReadFilter(); + $this->securityScanner = XmlScanner::getInstance($this); + } + + /** + * Can the current IReader read the file? + * + * @param string $pFilename + * + * @throws Exception + * + * @return bool + */ + public function canRead($pFilename) + { + // Office xmlns:o="urn:schemas-microsoft-com:office:office" + // Excel xmlns:x="urn:schemas-microsoft-com:office:excel" + // XML Spreadsheet xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" + // Spreadsheet component xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet" + // XML schema xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" + // XML data type xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" + // MS-persist recordset xmlns:rs="urn:schemas-microsoft-com:rowset" + // Rowset xmlns:z="#RowsetSchema" + // + + $signature = [ + '', + ]; + + // Open file + $this->openFile($pFilename); + $fileHandle = $this->fileHandle; + + // Read sample data (first 2 KB will do) + $data = fread($fileHandle, 2048); + fclose($fileHandle); + $data = str_replace("'", '"', $data); // fix headers with single quote + + $valid = true; + foreach ($signature as $match) { + // every part of the signature must be present + if (strpos($data, $match) === false) { + $valid = false; + + break; + } + } + + // Retrieve charset encoding + if (preg_match('//um', $data, $matches)) { + $this->charSet = strtoupper($matches[1]); + } + + return $valid; + } + + /** + * Check if the file is a valid SimpleXML. + * + * @param string $pFilename + * + * @throws Exception + * + * @return false|\SimpleXMLElement + */ + public function trySimpleXMLLoadString($pFilename) + { + try { + $xml = simplexml_load_string( + $this->securityScanner->scan(file_get_contents($pFilename)), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + } catch (\Exception $e) { + throw new Exception('Cannot load invalid XML file: ' . $pFilename, 0, $e); + } + + return $xml; + } + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. + * + * @param string $pFilename + * + * @throws Exception + * + * @return array + */ + public function listWorksheetNames($pFilename) + { + File::assertFile($pFilename); + if (!$this->canRead($pFilename)) { + throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); + } + + $worksheetNames = []; + + $xml = $this->trySimpleXMLLoadString($pFilename); + + $namespaces = $xml->getNamespaces(true); + + $xml_ss = $xml->children($namespaces['ss']); + foreach ($xml_ss->Worksheet as $worksheet) { + $worksheet_ss = $worksheet->attributes($namespaces['ss']); + $worksheetNames[] = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet); + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @param string $pFilename + * + * @throws Exception + * + * @return array + */ + public function listWorksheetInfo($pFilename) + { + File::assertFile($pFilename); + + $worksheetInfo = []; + + $xml = $this->trySimpleXMLLoadString($pFilename); + + $namespaces = $xml->getNamespaces(true); + + $worksheetID = 1; + $xml_ss = $xml->children($namespaces['ss']); + foreach ($xml_ss->Worksheet as $worksheet) { + $worksheet_ss = $worksheet->attributes($namespaces['ss']); + + $tmpInfo = []; + $tmpInfo['worksheetName'] = ''; + $tmpInfo['lastColumnLetter'] = 'A'; + $tmpInfo['lastColumnIndex'] = 0; + $tmpInfo['totalRows'] = 0; + $tmpInfo['totalColumns'] = 0; + + if (isset($worksheet_ss['Name'])) { + $tmpInfo['worksheetName'] = (string) $worksheet_ss['Name']; + } else { + $tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}"; + } + + if (isset($worksheet->Table->Row)) { + $rowIndex = 0; + + foreach ($worksheet->Table->Row as $rowData) { + $columnIndex = 0; + $rowHasData = false; + + foreach ($rowData->Cell as $cell) { + if (isset($cell->Data)) { + $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); + $rowHasData = true; + } + + ++$columnIndex; + } + + ++$rowIndex; + + if ($rowHasData) { + $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); + } + } + } + + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; + + $worksheetInfo[] = $tmpInfo; + ++$worksheetID; + } + + return $worksheetInfo; + } + + /** + * Loads Spreadsheet from file. + * + * @param string $pFilename + * + * @throws Exception + * + * @return Spreadsheet + */ + public function load($pFilename) + { + // Create new Spreadsheet + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + + // Load into this instance + return $this->loadIntoExisting($pFilename, $spreadsheet); + } + + private static function identifyFixedStyleValue($styleList, &$styleAttributeValue) + { + $styleAttributeValue = strtolower($styleAttributeValue); + foreach ($styleList as $style) { + if ($styleAttributeValue == strtolower($style)) { + $styleAttributeValue = $style; + + return true; + } + } + + return false; + } + + /** + * pixel units to excel width units(units of 1/256th of a character width). + * + * @param float $pxs + * + * @return float + */ + protected static function pixel2WidthUnits($pxs) + { + $UNIT_OFFSET_MAP = [0, 36, 73, 109, 146, 182, 219]; + + $widthUnits = 256 * ($pxs / 7); + $widthUnits += $UNIT_OFFSET_MAP[($pxs % 7)]; + + return $widthUnits; + } + + /** + * excel width units(units of 1/256th of a character width) to pixel units. + * + * @param float $widthUnits + * + * @return float + */ + protected static function widthUnits2Pixel($widthUnits) + { + $pixels = ($widthUnits / 256) * 7; + $offsetWidthUnits = $widthUnits % 256; + $pixels += round($offsetWidthUnits / (256 / 7)); + + return $pixels; + } + + protected static function hex2str($hex) + { + return chr(hexdec($hex[1])); + } + + /** + * Loads from file into Spreadsheet instance. + * + * @param string $pFilename + * @param Spreadsheet $spreadsheet + * + * @throws Exception + * + * @return Spreadsheet + */ + public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) + { + File::assertFile($pFilename); + if (!$this->canRead($pFilename)) { + throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); + } + + $xml = $this->trySimpleXMLLoadString($pFilename); + + $namespaces = $xml->getNamespaces(true); + + $docProps = $spreadsheet->getProperties(); + if (isset($xml->DocumentProperties[0])) { + foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) { + switch ($propertyName) { + case 'Title': + $docProps->setTitle(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'Subject': + $docProps->setSubject(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'Author': + $docProps->setCreator(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'Created': + $creationDate = strtotime($propertyValue); + $docProps->setCreated($creationDate); + + break; + case 'LastAuthor': + $docProps->setLastModifiedBy(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'LastSaved': + $lastSaveDate = strtotime($propertyValue); + $docProps->setModified($lastSaveDate); + + break; + case 'Company': + $docProps->setCompany(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'Category': + $docProps->setCategory(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'Manager': + $docProps->setManager(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'Keywords': + $docProps->setKeywords(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + case 'Description': + $docProps->setDescription(self::convertStringEncoding($propertyValue, $this->charSet)); + + break; + } + } + } + if (isset($xml->CustomDocumentProperties)) { + foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { + $propertyAttributes = $propertyValue->attributes($namespaces['dt']); + $propertyName = preg_replace_callback('/_x([0-9a-z]{4})_/', ['self', 'hex2str'], $propertyName); + $propertyType = Properties::PROPERTY_TYPE_UNKNOWN; + switch ((string) $propertyAttributes) { + case 'string': + $propertyType = Properties::PROPERTY_TYPE_STRING; + $propertyValue = trim($propertyValue); + + break; + case 'boolean': + $propertyType = Properties::PROPERTY_TYPE_BOOLEAN; + $propertyValue = (bool) $propertyValue; + + break; + case 'integer': + $propertyType = Properties::PROPERTY_TYPE_INTEGER; + $propertyValue = (int) $propertyValue; + + break; + case 'float': + $propertyType = Properties::PROPERTY_TYPE_FLOAT; + $propertyValue = (float) $propertyValue; + + break; + case 'dateTime.tz': + $propertyType = Properties::PROPERTY_TYPE_DATE; + $propertyValue = strtotime(trim($propertyValue)); + + break; + } + $docProps->setCustomProperty($propertyName, $propertyValue, $propertyType); + } + } + + $this->parseStyles($xml, $namespaces); + + $worksheetID = 0; + $xml_ss = $xml->children($namespaces['ss']); + + foreach ($xml_ss->Worksheet as $worksheet) { + $worksheet_ss = $worksheet->attributes($namespaces['ss']); + + if ((isset($this->loadSheetsOnly)) && (isset($worksheet_ss['Name'])) && + (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))) { + continue; + } + + // Create new Worksheet + $spreadsheet->createSheet(); + $spreadsheet->setActiveSheetIndex($worksheetID); + if (isset($worksheet_ss['Name'])) { + $worksheetName = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet); + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in + // formula cells... during the load, all formulae should be correct, and we're simply bringing + // the worksheet name in line with the formula, not the reverse + $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + } + + $columnID = 'A'; + if (isset($worksheet->Table->Column)) { + foreach ($worksheet->Table->Column as $columnData) { + $columnData_ss = $columnData->attributes($namespaces['ss']); + if (isset($columnData_ss['Index'])) { + $columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']); + } + if (isset($columnData_ss['Width'])) { + $columnWidth = $columnData_ss['Width']; + $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4); + } + ++$columnID; + } + } + + $rowID = 1; + if (isset($worksheet->Table->Row)) { + $additionalMergedCells = 0; + foreach ($worksheet->Table->Row as $rowData) { + $rowHasData = false; + $row_ss = $rowData->attributes($namespaces['ss']); + if (isset($row_ss['Index'])) { + $rowID = (int) $row_ss['Index']; + } + + $columnID = 'A'; + foreach ($rowData->Cell as $cell) { + $cell_ss = $cell->attributes($namespaces['ss']); + if (isset($cell_ss['Index'])) { + $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']); + } + $cellRange = $columnID . $rowID; + + if ($this->getReadFilter() !== null) { + if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + ++$columnID; + + continue; + } + } + + if (isset($cell_ss['HRef'])) { + $spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl($cell_ss['HRef']); + } + + if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) { + $columnTo = $columnID; + if (isset($cell_ss['MergeAcross'])) { + $additionalMergedCells += (int) $cell_ss['MergeAcross']; + $columnTo = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross']); + } + $rowTo = $rowID; + if (isset($cell_ss['MergeDown'])) { + $rowTo = $rowTo + $cell_ss['MergeDown']; + } + $cellRange .= ':' . $columnTo . $rowTo; + $spreadsheet->getActiveSheet()->mergeCells($cellRange); + } + + $cellIsSet = $hasCalculatedValue = false; + $cellDataFormula = ''; + if (isset($cell_ss['Formula'])) { + $cellDataFormula = $cell_ss['Formula']; + $hasCalculatedValue = true; + } + if (isset($cell->Data)) { + $cellValue = $cellData = $cell->Data; + $type = DataType::TYPE_NULL; + $cellData_ss = $cellData->attributes($namespaces['ss']); + if (isset($cellData_ss['Type'])) { + $cellDataType = $cellData_ss['Type']; + switch ($cellDataType) { + /* + const TYPE_STRING = 's'; + const TYPE_FORMULA = 'f'; + const TYPE_NUMERIC = 'n'; + const TYPE_BOOL = 'b'; + const TYPE_NULL = 'null'; + const TYPE_INLINE = 'inlineStr'; + const TYPE_ERROR = 'e'; + */ + case 'String': + $cellValue = self::convertStringEncoding($cellValue, $this->charSet); + $type = DataType::TYPE_STRING; + + break; + case 'Number': + $type = DataType::TYPE_NUMERIC; + $cellValue = (float) $cellValue; + if (floor($cellValue) == $cellValue) { + $cellValue = (int) $cellValue; + } + + break; + case 'Boolean': + $type = DataType::TYPE_BOOL; + $cellValue = ($cellValue != 0); + + break; + case 'DateTime': + $type = DataType::TYPE_NUMERIC; + $cellValue = Date::PHPToExcel(strtotime($cellValue)); + + break; + case 'Error': + $type = DataType::TYPE_ERROR; + + break; + } + } + + if ($hasCalculatedValue) { + $type = DataType::TYPE_FORMULA; + $columnNumber = Coordinate::columnIndexFromString($columnID); + if (substr($cellDataFormula, 0, 3) == 'of:') { + $cellDataFormula = substr($cellDataFormula, 3); + $temp = explode('"', $cellDataFormula); + $key = false; + foreach ($temp as &$value) { + // Only replace in alternate array entries (i.e. non-quoted blocks) + if ($key = !$key) { + $value = str_replace(['[.', '.', ']'], '', $value); + } + } + } else { + // Convert R1C1 style references to A1 style references (but only when not quoted) + $temp = explode('"', $cellDataFormula); + $key = false; + foreach ($temp as &$value) { + // Only replace in alternate array entries (i.e. non-quoted blocks) + if ($key = !$key) { + preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); + // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way + // through the formula from left to right. Reversing means that we work right to left.through + // the formula + $cellReferences = array_reverse($cellReferences); + // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, + // then modify the formula to use that new reference + foreach ($cellReferences as $cellReference) { + $rowReference = $cellReference[2][0]; + // Empty R reference is the current row + if ($rowReference == '') { + $rowReference = $rowID; + } + // Bracketed R references are relative to the current row + if ($rowReference[0] == '[') { + $rowReference = $rowID + trim($rowReference, '[]'); + } + $columnReference = $cellReference[4][0]; + // Empty C reference is the current column + if ($columnReference == '') { + $columnReference = $columnNumber; + } + // Bracketed C references are relative to the current column + if ($columnReference[0] == '[') { + $columnReference = $columnNumber + trim($columnReference, '[]'); + } + $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference; + $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); + } + } + } + } + unset($value); + // Then rebuild the formula string + $cellDataFormula = implode('"', $temp); + } + + $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type); + if ($hasCalculatedValue) { + $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue); + } + $cellIsSet = $rowHasData = true; + } + + if (isset($cell->Comment)) { + $commentAttributes = $cell->Comment->attributes($namespaces['ss']); + $author = 'unknown'; + if (isset($commentAttributes->Author)) { + $author = (string) $commentAttributes->Author; + } + $node = $cell->Comment->Data->asXML(); + $annotation = strip_tags($node); + $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)->setAuthor(self::convertStringEncoding($author, $this->charSet))->setText($this->parseRichText($annotation)); + } + + if (($cellIsSet) && (isset($cell_ss['StyleID']))) { + $style = (string) $cell_ss['StyleID']; + if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) { + if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) { + $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null); + } + $spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($this->styles[$style]); + } + } + ++$columnID; + while ($additionalMergedCells > 0) { + ++$columnID; + --$additionalMergedCells; + } + } + + if ($rowHasData) { + if (isset($row_ss['Height'])) { + $rowHeight = $row_ss['Height']; + $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight($rowHeight); + } + } + + ++$rowID; + } + } + ++$worksheetID; + } + + // Return + return $spreadsheet; + } + + protected static function convertStringEncoding($string, $charset) + { + if ($charset != 'UTF-8') { + return StringHelper::convertEncoding($string, 'UTF-8', $charset); + } + + return $string; + } + + protected function parseRichText($is) + { + $value = new RichText(); + + $value->createText(self::convertStringEncoding($is, $this->charSet)); + + return $value; + } + + /** + * @param SimpleXMLElement $xml + * @param array $namespaces + */ + private function parseStyles(SimpleXMLElement $xml, array $namespaces) + { + if (!isset($xml->Styles)) { + return; + } + + foreach ($xml->Styles[0] as $style) { + $style_ss = $style->attributes($namespaces['ss']); + $styleID = (string) $style_ss['ID']; + $this->styles[$styleID] = (isset($this->styles['Default'])) ? $this->styles['Default'] : []; + foreach ($style as $styleType => $styleData) { + $styleAttributes = $styleData->attributes($namespaces['ss']); + switch ($styleType) { + case 'Alignment': + $this->parseStyleAlignment($styleID, $styleAttributes); + + break; + case 'Borders': + $this->parseStyleBorders($styleID, $styleData, $namespaces); + + break; + case 'Font': + $this->parseStyleFont($styleID, $styleAttributes); + + break; + case 'Interior': + $this->parseStyleInterior($styleID, $styleAttributes); + + break; + case 'NumberFormat': + $this->parseStyleNumberFormat($styleID, $styleAttributes); + + break; + } + } + } + } + + /** + * @param string $styleID + * @param SimpleXMLElement $styleAttributes + */ + private function parseStyleAlignment($styleID, SimpleXMLElement $styleAttributes) + { + $verticalAlignmentStyles = [ + Alignment::VERTICAL_BOTTOM, + Alignment::VERTICAL_TOP, + Alignment::VERTICAL_CENTER, + Alignment::VERTICAL_JUSTIFY, + ]; + $horizontalAlignmentStyles = [ + Alignment::HORIZONTAL_GENERAL, + Alignment::HORIZONTAL_LEFT, + Alignment::HORIZONTAL_RIGHT, + Alignment::HORIZONTAL_CENTER, + Alignment::HORIZONTAL_CENTER_CONTINUOUS, + Alignment::HORIZONTAL_JUSTIFY, + ]; + + foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { + $styleAttributeValue = (string) $styleAttributeValue; + switch ($styleAttributeKey) { + case 'Vertical': + if (self::identifyFixedStyleValue($verticalAlignmentStyles, $styleAttributeValue)) { + $this->styles[$styleID]['alignment']['vertical'] = $styleAttributeValue; + } + + break; + case 'Horizontal': + if (self::identifyFixedStyleValue($horizontalAlignmentStyles, $styleAttributeValue)) { + $this->styles[$styleID]['alignment']['horizontal'] = $styleAttributeValue; + } + + break; + case 'WrapText': + $this->styles[$styleID]['alignment']['wrapText'] = true; + + break; + } + } + } + + /** + * @param $styleID + * @param SimpleXMLElement $styleData + * @param array $namespaces + */ + private function parseStyleBorders($styleID, SimpleXMLElement $styleData, array $namespaces) + { + foreach ($styleData->Border as $borderStyle) { + $borderAttributes = $borderStyle->attributes($namespaces['ss']); + $thisBorder = []; + foreach ($borderAttributes as $borderStyleKey => $borderStyleValue) { + switch ($borderStyleKey) { + case 'LineStyle': + $thisBorder['borderStyle'] = Border::BORDER_MEDIUM; + + break; + case 'Weight': + break; + case 'Position': + $borderPosition = strtolower($borderStyleValue); + + break; + case 'Color': + $borderColour = substr($borderStyleValue, 1); + $thisBorder['color']['rgb'] = $borderColour; + + break; + } + } + if (!empty($thisBorder)) { + if (($borderPosition == 'left') || ($borderPosition == 'right') || ($borderPosition == 'top') || ($borderPosition == 'bottom')) { + $this->styles[$styleID]['borders'][$borderPosition] = $thisBorder; + } + } + } + } + + /** + * @param $styleID + * @param SimpleXMLElement $styleAttributes + */ + private function parseStyleFont($styleID, SimpleXMLElement $styleAttributes) + { + $underlineStyles = [ + Font::UNDERLINE_NONE, + Font::UNDERLINE_DOUBLE, + Font::UNDERLINE_DOUBLEACCOUNTING, + Font::UNDERLINE_SINGLE, + Font::UNDERLINE_SINGLEACCOUNTING, + ]; + + foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { + $styleAttributeValue = (string) $styleAttributeValue; + switch ($styleAttributeKey) { + case 'FontName': + $this->styles[$styleID]['font']['name'] = $styleAttributeValue; + + break; + case 'Size': + $this->styles[$styleID]['font']['size'] = $styleAttributeValue; + + break; + case 'Color': + $this->styles[$styleID]['font']['color']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'Bold': + $this->styles[$styleID]['font']['bold'] = true; + + break; + case 'Italic': + $this->styles[$styleID]['font']['italic'] = true; + + break; + case 'Underline': + if (self::identifyFixedStyleValue($underlineStyles, $styleAttributeValue)) { + $this->styles[$styleID]['font']['underline'] = $styleAttributeValue; + } + + break; + } + } + } + + /** + * @param $styleID + * @param SimpleXMLElement $styleAttributes + */ + private function parseStyleInterior($styleID, SimpleXMLElement $styleAttributes) + { + foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { + switch ($styleAttributeKey) { + case 'Color': + $this->styles[$styleID]['fill']['color']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'Pattern': + $this->styles[$styleID]['fill']['fillType'] = strtolower($styleAttributeValue); + + break; + } + } + } + + /** + * @param $styleID + * @param SimpleXMLElement $styleAttributes + */ + private function parseStyleNumberFormat($styleID, SimpleXMLElement $styleAttributes) + { + $fromFormats = ['\-', '\ ']; + $toFormats = ['-', ' ']; + + foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { + $styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue); + switch ($styleAttributeValue) { + case 'Short Date': + $styleAttributeValue = 'dd/mm/yyyy'; + + break; + } + + if ($styleAttributeValue > '') { + $this->styles[$styleID]['numberFormat']['formatCode'] = $styleAttributeValue; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/ReferenceHelper.php b/htdocs/includes/phpoffice/PhpSpreadsheet/ReferenceHelper.php new file mode 100644 index 00000000000..54bc182a8eb --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/ReferenceHelper.php @@ -0,0 +1,916 @@ += ($beforeRow + $pNumRows)) && + ($cellRow < $beforeRow)) { + return true; + } elseif ($pNumCols < 0 && + ($cellColumnIndex >= ($beforeColumnIndex + $pNumCols)) && + ($cellColumnIndex < $beforeColumnIndex)) { + return true; + } + + return false; + } + + /** + * Update page breaks when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustPageBreaks(Worksheet $pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aBreaks = $pSheet->getBreaks(); + ($pNumCols > 0 || $pNumRows > 0) ? + uksort($aBreaks, ['self', 'cellReverseSort']) : uksort($aBreaks, ['self', 'cellSort']); + + foreach ($aBreaks as $key => $value) { + if (self::cellAddressInDeleteRange($key, $beforeRow, $pNumRows, $beforeColumnIndex, $pNumCols)) { + // If we're deleting, then clear any defined breaks that are within the range + // of rows/columns that we're deleting + $pSheet->setBreak($key, Worksheet::BREAK_NONE); + } else { + // Otherwise update any affected breaks by inserting a new break at the appropriate point + // and removing the old affected break + $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); + if ($key != $newReference) { + $pSheet->setBreak($newReference, $value) + ->setBreak($key, Worksheet::BREAK_NONE); + } + } + } + } + + /** + * Update cell comments when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aComments = $pSheet->getComments(); + $aNewComments = []; // the new array of all comments + + foreach ($aComments as $key => &$value) { + // Any comments inside a deleted range will be ignored + if (!self::cellAddressInDeleteRange($key, $beforeRow, $pNumRows, $beforeColumnIndex, $pNumCols)) { + // Otherwise build a new array of comments indexed by the adjusted cell reference + $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); + $aNewComments[$newReference] = $value; + } + } + // Replace the comments array with the new set of comments + $pSheet->setComments($aNewComments); + } + + /** + * Update hyperlinks when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aHyperlinkCollection = $pSheet->getHyperlinkCollection(); + ($pNumCols > 0 || $pNumRows > 0) ? + uksort($aHyperlinkCollection, ['self', 'cellReverseSort']) : uksort($aHyperlinkCollection, ['self', 'cellSort']); + + foreach ($aHyperlinkCollection as $key => $value) { + $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); + if ($key != $newReference) { + $pSheet->setHyperlink($newReference, $value); + $pSheet->setHyperlink($key, null); + } + } + } + + /** + * Update data validations when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aDataValidationCollection = $pSheet->getDataValidationCollection(); + ($pNumCols > 0 || $pNumRows > 0) ? + uksort($aDataValidationCollection, ['self', 'cellReverseSort']) : uksort($aDataValidationCollection, ['self', 'cellSort']); + + foreach ($aDataValidationCollection as $key => $value) { + $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); + if ($key != $newReference) { + $pSheet->setDataValidation($newReference, $value); + $pSheet->setDataValidation($key, null); + } + } + } + + /** + * Update merged cells when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aMergeCells = $pSheet->getMergeCells(); + $aNewMergeCells = []; // the new array of all merge cells + foreach ($aMergeCells as $key => &$value) { + $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); + $aNewMergeCells[$newReference] = $newReference; + } + $pSheet->setMergeCells($aNewMergeCells); // replace the merge cells array + } + + /** + * Update protected cells when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aProtectedCells = $pSheet->getProtectedCells(); + ($pNumCols > 0 || $pNumRows > 0) ? + uksort($aProtectedCells, ['self', 'cellReverseSort']) : uksort($aProtectedCells, ['self', 'cellSort']); + foreach ($aProtectedCells as $key => $value) { + $newReference = $this->updateCellReference($key, $pBefore, $pNumCols, $pNumRows); + if ($key != $newReference) { + $pSheet->protectCells($newReference, $value, true); + $pSheet->unprotectCells($key); + } + } + } + + /** + * Update column dimensions when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aColumnDimensions = array_reverse($pSheet->getColumnDimensions(), true); + if (!empty($aColumnDimensions)) { + foreach ($aColumnDimensions as $objColumnDimension) { + $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1', $pBefore, $pNumCols, $pNumRows); + list($newReference) = Coordinate::coordinateFromString($newReference); + if ($objColumnDimension->getColumnIndex() != $newReference) { + $objColumnDimension->setColumnIndex($newReference); + } + } + $pSheet->refreshColumnDimensions(); + } + } + + /** + * Update row dimensions when inserting/deleting rows/columns. + * + * @param Worksheet $pSheet The worksheet that we're editing + * @param string $pBefore Insert/Delete before this cell address (e.g. 'A1') + * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows) + { + $aRowDimensions = array_reverse($pSheet->getRowDimensions(), true); + if (!empty($aRowDimensions)) { + foreach ($aRowDimensions as $objRowDimension) { + $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex(), $pBefore, $pNumCols, $pNumRows); + list(, $newReference) = Coordinate::coordinateFromString($newReference); + if ($objRowDimension->getRowIndex() != $newReference) { + $objRowDimension->setRowIndex($newReference); + } + } + $pSheet->refreshRowDimensions(); + + $copyDimension = $pSheet->getRowDimension($beforeRow - 1); + for ($i = $beforeRow; $i <= $beforeRow - 1 + $pNumRows; ++$i) { + $newDimension = $pSheet->getRowDimension($i); + $newDimension->setRowHeight($copyDimension->getRowHeight()); + $newDimension->setVisible($copyDimension->getVisible()); + $newDimension->setOutlineLevel($copyDimension->getOutlineLevel()); + $newDimension->setCollapsed($copyDimension->getCollapsed()); + } + } + } + + /** + * Insert a new column or row, updating all possible related data. + * + * @param string $pBefore Insert before this cell address (e.g. 'A1') + * @param int $pNumCols Number of columns to insert/delete (negative values indicate deletion) + * @param int $pNumRows Number of rows to insert/delete (negative values indicate deletion) + * @param Worksheet $pSheet The worksheet that we're editing + * + * @throws Exception + */ + public function insertNewBefore($pBefore, $pNumCols, $pNumRows, Worksheet $pSheet) + { + $remove = ($pNumCols < 0 || $pNumRows < 0); + $allCoordinates = $pSheet->getCoordinates(); + + // Get coordinate of $pBefore + list($beforeColumn, $beforeRow) = Coordinate::coordinateFromString($pBefore); + $beforeColumnIndex = Coordinate::columnIndexFromString($beforeColumn); + + // Clear cells if we are removing columns or rows + $highestColumn = $pSheet->getHighestColumn(); + $highestRow = $pSheet->getHighestRow(); + + // 1. Clear column strips if we are removing columns + if ($pNumCols < 0 && $beforeColumnIndex - 2 + $pNumCols > 0) { + for ($i = 1; $i <= $highestRow - 1; ++$i) { + for ($j = $beforeColumnIndex - 1 + $pNumCols; $j <= $beforeColumnIndex - 2; ++$j) { + $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; + $pSheet->removeConditionalStyles($coordinate); + if ($pSheet->cellExists($coordinate)) { + $pSheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $pSheet->getCell($coordinate)->setXfIndex(0); + } + } + } + } + + // 2. Clear row strips if we are removing rows + if ($pNumRows < 0 && $beforeRow - 1 + $pNumRows > 0) { + for ($i = $beforeColumnIndex - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$i) { + for ($j = $beforeRow + $pNumRows; $j <= $beforeRow - 1; ++$j) { + $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; + $pSheet->removeConditionalStyles($coordinate); + if ($pSheet->cellExists($coordinate)) { + $pSheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $pSheet->getCell($coordinate)->setXfIndex(0); + } + } + } + } + + // Loop through cells, bottom-up, and change cell coordinate + if ($remove) { + // It's faster to reverse and pop than to use unshift, especially with large cell collections + $allCoordinates = array_reverse($allCoordinates); + } + while ($coordinate = array_pop($allCoordinates)) { + $cell = $pSheet->getCell($coordinate); + $cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); + + if ($cellIndex - 1 + $pNumCols < 0) { + continue; + } + + // New coordinate + $newCoordinate = Coordinate::stringFromColumnIndex($cellIndex + $pNumCols) . ($cell->getRow() + $pNumRows); + + // Should the cell be updated? Move value and cellXf index from one cell to another. + if (($cellIndex >= $beforeColumnIndex) && ($cell->getRow() >= $beforeRow)) { + // Update cell styles + $pSheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); + + // Insert this cell at its new location + if ($cell->getDataType() == DataType::TYPE_FORMULA) { + // Formula should be adjusted + $pSheet->getCell($newCoordinate) + ->setValue($this->updateFormulaReferences($cell->getValue(), $pBefore, $pNumCols, $pNumRows, $pSheet->getTitle())); + } else { + // Formula should not be adjusted + $pSheet->getCell($newCoordinate)->setValue($cell->getValue()); + } + + // Clear the original cell + $pSheet->getCellCollection()->delete($coordinate); + } else { + /* We don't need to update styles for rows/columns before our insertion position, + but we do still need to adjust any formulae in those cells */ + if ($cell->getDataType() == DataType::TYPE_FORMULA) { + // Formula should be adjusted + $cell->setValue($this->updateFormulaReferences($cell->getValue(), $pBefore, $pNumCols, $pNumRows, $pSheet->getTitle())); + } + } + } + + // Duplicate styles for the newly inserted cells + $highestColumn = $pSheet->getHighestColumn(); + $highestRow = $pSheet->getHighestRow(); + + if ($pNumCols > 0 && $beforeColumnIndex - 2 > 0) { + for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { + // Style + $coordinate = Coordinate::stringFromColumnIndex($beforeColumnIndex - 1) . $i; + if ($pSheet->cellExists($coordinate)) { + $xfIndex = $pSheet->getCell($coordinate)->getXfIndex(); + $conditionalStyles = $pSheet->conditionalStylesExists($coordinate) ? + $pSheet->getConditionalStyles($coordinate) : false; + for ($j = $beforeColumnIndex; $j <= $beforeColumnIndex - 1 + $pNumCols; ++$j) { + $pSheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); + if ($conditionalStyles) { + $cloned = []; + foreach ($conditionalStyles as $conditionalStyle) { + $cloned[] = clone $conditionalStyle; + } + $pSheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned); + } + } + } + } + } + + if ($pNumRows > 0 && $beforeRow - 1 > 0) { + for ($i = $beforeColumnIndex; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) { + // Style + $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); + if ($pSheet->cellExists($coordinate)) { + $xfIndex = $pSheet->getCell($coordinate)->getXfIndex(); + $conditionalStyles = $pSheet->conditionalStylesExists($coordinate) ? + $pSheet->getConditionalStyles($coordinate) : false; + for ($j = $beforeRow; $j <= $beforeRow - 1 + $pNumRows; ++$j) { + $pSheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); + if ($conditionalStyles) { + $cloned = []; + foreach ($conditionalStyles as $conditionalStyle) { + $cloned[] = clone $conditionalStyle; + } + $pSheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned); + } + } + } + } + } + + // Update worksheet: column dimensions + $this->adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: row dimensions + $this->adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: page breaks + $this->adjustPageBreaks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: comments + $this->adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: hyperlinks + $this->adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: data validations + $this->adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: merge cells + $this->adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: protected cells + $this->adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + + // Update worksheet: autofilter + $autoFilter = $pSheet->getAutoFilter(); + $autoFilterRange = $autoFilter->getRange(); + if (!empty($autoFilterRange)) { + if ($pNumCols != 0) { + $autoFilterColumns = $autoFilter->getColumns(); + if (count($autoFilterColumns) > 0) { + $column = ''; + $row = 0; + sscanf($pBefore, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString($column); + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($autoFilterRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($pNumCols < 0) { + // If we're actually deleting any columns that fall within the autofilter range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $pNumCols - 1; + $deleteCount = abs($pNumCols); + for ($i = 1; $i <= $deleteCount; ++$i) { + if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) { + $autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1)); + } + ++$deleteColumn; + } + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in autofilter range + if ($pNumCols > 0) { + $startColRef = $startCol; + $endColRef = $rangeEnd[0]; + $toColRef = $rangeEnd[0] + $pNumCols; + + do { + $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } else { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $pNumCols); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1); + do { + $autoFilter->shiftColumn($startColID, $toColID); + ++$startColID; + ++$toColID; + } while ($startColID != $endColID); + } + } + } + } + $pSheet->setAutoFilter($this->updateCellReference($autoFilterRange, $pBefore, $pNumCols, $pNumRows)); + } + + // Update worksheet: freeze pane + if ($pSheet->getFreezePane()) { + $splitCell = $pSheet->getFreezePane(); + $topLeftCell = $pSheet->getTopLeftCell(); + + $splitCell = $this->updateCellReference($splitCell, $pBefore, $pNumCols, $pNumRows); + $topLeftCell = $this->updateCellReference($topLeftCell, $pBefore, $pNumCols, $pNumRows); + + $pSheet->freezePane($splitCell, $topLeftCell); + } + + // Page setup + if ($pSheet->getPageSetup()->isPrintAreaSet()) { + $pSheet->getPageSetup()->setPrintArea($this->updateCellReference($pSheet->getPageSetup()->getPrintArea(), $pBefore, $pNumCols, $pNumRows)); + } + + // Update worksheet: drawings + $aDrawings = $pSheet->getDrawingCollection(); + foreach ($aDrawings as $objDrawing) { + $newReference = $this->updateCellReference($objDrawing->getCoordinates(), $pBefore, $pNumCols, $pNumRows); + if ($objDrawing->getCoordinates() != $newReference) { + $objDrawing->setCoordinates($newReference); + } + } + + // Update workbook: named ranges + if (count($pSheet->getParent()->getNamedRanges()) > 0) { + foreach ($pSheet->getParent()->getNamedRanges() as $namedRange) { + if ($namedRange->getWorksheet()->getHashCode() == $pSheet->getHashCode()) { + $namedRange->setRange($this->updateCellReference($namedRange->getRange(), $pBefore, $pNumCols, $pNumRows)); + } + } + } + + // Garbage collect + $pSheet->garbageCollect(); + } + + /** + * Update references within formulas. + * + * @param string $pFormula Formula to update + * @param int $pBefore Insert before this one + * @param int $pNumCols Number of columns to insert + * @param int $pNumRows Number of rows to insert + * @param string $sheetName Worksheet name/title + * + * @throws Exception + * + * @return string Updated formula + */ + public function updateFormulaReferences($pFormula = '', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0, $sheetName = '') + { + // Update cell references in the formula + $formulaBlocks = explode('"', $pFormula); + $i = false; + foreach ($formulaBlocks as &$formulaBlock) { + // Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode) + if ($i = !$i) { + $adjustCount = 0; + $newCellTokens = $cellTokens = []; + // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5) + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + if ($matchCount > 0) { + foreach ($matches as $match) { + $fromString = ($match[2] > '') ? $match[2] . '!' : ''; + $fromString .= $match[3] . ':' . $match[4]; + $modified3 = substr($this->updateCellReference('$A' . $match[3], $pBefore, $pNumCols, $pNumRows), 2); + $modified4 = substr($this->updateCellReference('$A' . $match[4], $pBefore, $pNumCols, $pNumRows), 2); + + if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { + if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { + $toString = ($match[2] > '') ? $match[2] . '!' : ''; + $toString .= $modified3 . ':' . $modified4; + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = 100000; + $row = 10000000 + trim($match[3], '$'); + $cellIndex = $column . $row; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + foreach ($matches as $match) { + $fromString = ($match[2] > '') ? $match[2] . '!' : ''; + $fromString .= $match[3] . ':' . $match[4]; + $modified3 = substr($this->updateCellReference($match[3] . '$1', $pBefore, $pNumCols, $pNumRows), 0, -2); + $modified4 = substr($this->updateCellReference($match[4] . '$1', $pBefore, $pNumCols, $pNumRows), 0, -2); + + if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { + if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { + $toString = ($match[2] > '') ? $match[2] . '!' : ''; + $toString .= $modified3 . ':' . $modified4; + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000; + $row = 10000000; + $cellIndex = $column . $row; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + foreach ($matches as $match) { + $fromString = ($match[2] > '') ? $match[2] . '!' : ''; + $fromString .= $match[3] . ':' . $match[4]; + $modified3 = $this->updateCellReference($match[3], $pBefore, $pNumCols, $pNumRows); + $modified4 = $this->updateCellReference($match[4], $pBefore, $pNumCols, $pNumRows); + + if ($match[3] . $match[4] !== $modified3 . $modified4) { + if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { + $toString = ($match[2] > '') ? $match[2] . '!' : ''; + $toString .= $modified3 . ':' . $modified4; + list($column, $row) = Coordinate::coordinateFromString($match[3]); + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; + $row = trim($row, '$') + 10000000; + $cellIndex = $column . $row; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + foreach ($matches as $match) { + $fromString = ($match[2] > '') ? $match[2] . '!' : ''; + $fromString .= $match[3]; + + $modified3 = $this->updateCellReference($match[3], $pBefore, $pNumCols, $pNumRows); + if ($match[3] !== $modified3) { + if (($match[2] == '') || (trim($match[2], "'") == $sheetName)) { + $toString = ($match[2] > '') ? $match[2] . '!' : ''; + $toString .= $modified3; + list($column, $row) = Coordinate::coordinateFromString($match[3]); + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; + $row = trim($row, '$') + 10000000; + $cellIndex = $row . $column; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + if ($pNumCols > 0 || $pNumRows > 0) { + krsort($cellTokens); + krsort($newCellTokens); + } else { + ksort($cellTokens); + ksort($newCellTokens); + } // Update cell references in the formula + $formulaBlock = str_replace('\\', '', preg_replace($cellTokens, $newCellTokens, $formulaBlock)); + } + } + } + unset($formulaBlock); + + // Then rebuild the formula string + return implode('"', $formulaBlocks); + } + + /** + * Update cell reference. + * + * @param string $pCellRange Cell range + * @param string $pBefore Insert before this one + * @param int $pNumCols Number of columns to increment + * @param int $pNumRows Number of rows to increment + * + * @throws Exception + * + * @return string Updated cell range + */ + public function updateCellReference($pCellRange = 'A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0) + { + // Is it in another worksheet? Will not have to update anything. + if (strpos($pCellRange, '!') !== false) { + return $pCellRange; + // Is it a range or a single cell? + } elseif (!Coordinate::coordinateIsRange($pCellRange)) { + // Single cell + return $this->updateSingleCellReference($pCellRange, $pBefore, $pNumCols, $pNumRows); + } elseif (Coordinate::coordinateIsRange($pCellRange)) { + // Range + return $this->updateCellRange($pCellRange, $pBefore, $pNumCols, $pNumRows); + } + + // Return original + return $pCellRange; + } + + /** + * Update named formulas (i.e. containing worksheet references / named ranges). + * + * @param Spreadsheet $spreadsheet Object to update + * @param string $oldName Old name (name to replace) + * @param string $newName New name + */ + public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $newName = '') + { + if ($oldName == '') { + return; + } + + foreach ($spreadsheet->getWorksheetIterator() as $sheet) { + foreach ($sheet->getCoordinates(false) as $coordinate) { + $cell = $sheet->getCell($coordinate); + if (($cell !== null) && ($cell->getDataType() == DataType::TYPE_FORMULA)) { + $formula = $cell->getValue(); + if (strpos($formula, $oldName) !== false) { + $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); + $formula = str_replace($oldName . '!', $newName . '!', $formula); + $cell->setValueExplicit($formula, DataType::TYPE_FORMULA); + } + } + } + } + } + + /** + * Update cell range. + * + * @param string $pCellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3') + * @param string $pBefore Insert before this one + * @param int $pNumCols Number of columns to increment + * @param int $pNumRows Number of rows to increment + * + * @throws Exception + * + * @return string Updated cell range + */ + private function updateCellRange($pCellRange = 'A1:A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0) + { + if (!Coordinate::coordinateIsRange($pCellRange)) { + throw new Exception('Only cell ranges may be passed to this method.'); + } + + // Update range + $range = Coordinate::splitRange($pCellRange); + $ic = count($range); + for ($i = 0; $i < $ic; ++$i) { + $jc = count($range[$i]); + for ($j = 0; $j < $jc; ++$j) { + if (ctype_alpha($range[$i][$j])) { + $r = Coordinate::coordinateFromString($this->updateSingleCellReference($range[$i][$j] . '1', $pBefore, $pNumCols, $pNumRows)); + $range[$i][$j] = $r[0]; + } elseif (ctype_digit($range[$i][$j])) { + $r = Coordinate::coordinateFromString($this->updateSingleCellReference('A' . $range[$i][$j], $pBefore, $pNumCols, $pNumRows)); + $range[$i][$j] = $r[1]; + } else { + $range[$i][$j] = $this->updateSingleCellReference($range[$i][$j], $pBefore, $pNumCols, $pNumRows); + } + } + } + + // Recreate range string + return Coordinate::buildRange($range); + } + + /** + * Update single cell reference. + * + * @param string $pCellReference Single cell reference + * @param string $pBefore Insert before this one + * @param int $pNumCols Number of columns to increment + * @param int $pNumRows Number of rows to increment + * + * @throws Exception + * + * @return string Updated cell reference + */ + private function updateSingleCellReference($pCellReference = 'A1', $pBefore = 'A1', $pNumCols = 0, $pNumRows = 0) + { + if (Coordinate::coordinateIsRange($pCellReference)) { + throw new Exception('Only single cell references may be passed to this method.'); + } + + // Get coordinate of $pBefore + list($beforeColumn, $beforeRow) = Coordinate::coordinateFromString($pBefore); + + // Get coordinate of $pCellReference + list($newColumn, $newRow) = Coordinate::coordinateFromString($pCellReference); + + // Verify which parts should be updated + $updateColumn = (($newColumn[0] != '$') && ($beforeColumn[0] != '$') && (Coordinate::columnIndexFromString($newColumn) >= Coordinate::columnIndexFromString($beforeColumn))); + $updateRow = (($newRow[0] != '$') && ($beforeRow[0] != '$') && $newRow >= $beforeRow); + + // Create new column reference + if ($updateColumn) { + $newColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($newColumn) + $pNumCols); + } + + // Create new row reference + if ($updateRow) { + $newRow = $newRow + $pNumRows; + } + + // Return new reference + return $newColumn . $newRow; + } + + /** + * __clone implementation. Cloning should not be allowed in a Singleton! + * + * @throws Exception + */ + final public function __clone() + { + throw new Exception('Cloning a Singleton is not allowed!'); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/ITextElement.php b/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/ITextElement.php new file mode 100644 index 00000000000..69954676028 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/ITextElement.php @@ -0,0 +1,36 @@ +richTextElements = []; + + // Rich-Text string attached to cell? + if ($pCell !== null) { + // Add cell text and style + if ($pCell->getValue() != '') { + $objRun = new Run($pCell->getValue()); + $objRun->setFont(clone $pCell->getWorksheet()->getStyle($pCell->getCoordinate())->getFont()); + $this->addText($objRun); + } + + // Set parent value + $pCell->setValueExplicit($this, DataType::TYPE_STRING); + } + } + + /** + * Add text. + * + * @param ITextElement $pText Rich text element + * + * @return RichText + */ + public function addText(ITextElement $pText) + { + $this->richTextElements[] = $pText; + + return $this; + } + + /** + * Create text. + * + * @param string $pText Text + * + * @throws Exception + * + * @return TextElement + */ + public function createText($pText) + { + $objText = new TextElement($pText); + $this->addText($objText); + + return $objText; + } + + /** + * Create text run. + * + * @param string $pText Text + * + * @throws Exception + * + * @return Run + */ + public function createTextRun($pText) + { + $objText = new Run($pText); + $this->addText($objText); + + return $objText; + } + + /** + * Get plain text. + * + * @return string + */ + public function getPlainText() + { + // Return value + $returnValue = ''; + + // Loop through all ITextElements + foreach ($this->richTextElements as $text) { + $returnValue .= $text->getText(); + } + + return $returnValue; + } + + /** + * Convert to string. + * + * @return string + */ + public function __toString() + { + return $this->getPlainText(); + } + + /** + * Get Rich Text elements. + * + * @return ITextElement[] + */ + public function getRichTextElements() + { + return $this->richTextElements; + } + + /** + * Set Rich Text elements. + * + * @param ITextElement[] $textElements Array of elements + * + * @return RichText + */ + public function setRichTextElements(array $textElements) + { + $this->richTextElements = $textElements; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + $hashElements = ''; + foreach ($this->richTextElements as $element) { + $hashElements .= $element->getHashCode(); + } + + return md5( + $hashElements . + __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/Run.php b/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/Run.php new file mode 100644 index 00000000000..b4996235f33 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/Run.php @@ -0,0 +1,65 @@ +font = new Font(); + } + + /** + * Get font. + * + * @return null|\PhpOffice\PhpSpreadsheet\Style\Font + */ + public function getFont() + { + return $this->font; + } + + /** + * Set font. + * + * @param Font $pFont Font + * + * @return ITextElement + */ + public function setFont(Font $pFont = null) + { + $this->font = $pFont; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->getText() . + $this->font->getHashCode() . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/TextElement.php b/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/TextElement.php new file mode 100644 index 00000000000..d9ad0d7f5ed --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/RichText/TextElement.php @@ -0,0 +1,86 @@ +text = $pText; + } + + /** + * Get text. + * + * @return string Text + */ + public function getText() + { + return $this->text; + } + + /** + * Set text. + * + * @param $text string Text + * + * @return ITextElement + */ + public function setText($text) + { + $this->text = $text; + + return $this; + } + + /** + * Get font. + * + * @return null|\PhpOffice\PhpSpreadsheet\Style\Font + */ + public function getFont() + { + return null; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->text . + __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Settings.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Settings.php new file mode 100644 index 00000000000..22196b7e0e2 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Settings.php @@ -0,0 +1,127 @@ +setLocale($locale); + } + + /** + * Identify to PhpSpreadsheet the external library to use for rendering charts. + * + * @param string $rendererClass Class name of the chart renderer + * eg: PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph + * + * @throws Exception + */ + public static function setChartRenderer($rendererClass) + { + if (!is_a($rendererClass, IRenderer::class, true)) { + throw new Exception('Chart renderer must implement ' . IRenderer::class); + } + + self::$chartRenderer = $rendererClass; + } + + /** + * Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use. + * + * @return null|string Class name of the chart renderer + * eg: PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph + */ + public static function getChartRenderer() + { + return self::$chartRenderer; + } + + /** + * Set default options for libxml loader. + * + * @param int $options Default options for libxml loader + */ + public static function setLibXmlLoaderOptions($options) + { + if ($options === null && defined('LIBXML_DTDLOAD')) { + $options = LIBXML_DTDLOAD | LIBXML_DTDATTR; + } + self::$libXmlLoaderOptions = $options; + } + + /** + * Get default options for libxml loader. + * Defaults to LIBXML_DTDLOAD | LIBXML_DTDATTR when not set explicitly. + * + * @return int Default options for libxml loader + */ + public static function getLibXmlLoaderOptions() + { + if (self::$libXmlLoaderOptions === null && defined('LIBXML_DTDLOAD')) { + self::setLibXmlLoaderOptions(LIBXML_DTDLOAD | LIBXML_DTDATTR); + } elseif (self::$libXmlLoaderOptions === null) { + self::$libXmlLoaderOptions = true; + } + + return self::$libXmlLoaderOptions; + } + + /** + * Sets the implementation of cache that should be used for cell collection. + * + * @param CacheInterface $cache + */ + public static function setCache(CacheInterface $cache) + { + self::$cache = $cache; + } + + /** + * Gets the implementation of cache that should be used for cell collection. + * + * @return CacheInterface + */ + public static function getCache() + { + if (!self::$cache) { + self::$cache = new Memory(); + } + + return self::$cache; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/CodePage.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/CodePage.php new file mode 100644 index 00000000000..4b57824203e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/CodePage.php @@ -0,0 +1,138 @@ + 'January', + 'Feb' => 'February', + 'Mar' => 'March', + 'Apr' => 'April', + 'May' => 'May', + 'Jun' => 'June', + 'Jul' => 'July', + 'Aug' => 'August', + 'Sep' => 'September', + 'Oct' => 'October', + 'Nov' => 'November', + 'Dec' => 'December', + ]; + + /** + * @var string[] + */ + public static $numberSuffixes = [ + 'st', + 'nd', + 'rd', + 'th', + ]; + + /** + * Base calendar year to use for calculations + * Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904). + * + * @var int + */ + protected static $excelCalendar = self::CALENDAR_WINDOWS_1900; + + /** + * Default timezone to use for DateTime objects. + * + * @var null|\DateTimeZone + */ + protected static $defaultTimeZone; + + /** + * Set the Excel calendar (Windows 1900 or Mac 1904). + * + * @param int $baseDate Excel base date (1900 or 1904) + * + * @return bool Success or failure + */ + public static function setExcelCalendar($baseDate) + { + if (($baseDate == self::CALENDAR_WINDOWS_1900) || + ($baseDate == self::CALENDAR_MAC_1904)) { + self::$excelCalendar = $baseDate; + + return true; + } + + return false; + } + + /** + * Return the Excel calendar (Windows 1900 or Mac 1904). + * + * @return int Excel base date (1900 or 1904) + */ + public static function getExcelCalendar() + { + return self::$excelCalendar; + } + + /** + * Set the Default timezone to use for dates. + * + * @param DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions + * + * @throws \Exception + * + * @return bool Success or failure + * @return bool Success or failure + */ + public static function setDefaultTimezone($timeZone) + { + if ($timeZone = self::validateTimeZone($timeZone)) { + self::$defaultTimeZone = $timeZone; + + return true; + } + + return false; + } + + /** + * Return the Default timezone being used for dates. + * + * @return DateTimeZone The timezone being used as default for Excel timestamp to PHP DateTime object + */ + public static function getDefaultTimezone() + { + if (self::$defaultTimeZone === null) { + self::$defaultTimeZone = new DateTimeZone('UTC'); + } + + return self::$defaultTimeZone; + } + + /** + * Validate a timezone. + * + * @param DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object + * + * @throws \Exception + * + * @return DateTimeZone The timezone as a timezone object + * @return DateTimeZone The timezone as a timezone object + */ + protected static function validateTimeZone($timeZone) + { + if (is_object($timeZone) && $timeZone instanceof DateTimeZone) { + return $timeZone; + } elseif (is_string($timeZone)) { + return new DateTimeZone($timeZone); + } + + throw new \Exception('Invalid timezone'); + } + + /** + * Convert a MS serialized datetime value from Excel to a PHP Date/Time object. + * + * @param float|int $excelTimestamp MS Excel serialized date/time value + * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp, + * if you don't want to treat it as a UTC value + * Use the default (UST) unless you absolutely need a conversion + * + * @throws \Exception + * + * @return \DateTime PHP date/time object + */ + public static function excelToDateTimeObject($excelTimestamp, $timeZone = null) + { + $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone); + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { + if ($excelTimestamp < 1.0) { + // Unix timestamp base date + $baseDate = new \DateTime('1970-01-01', $timeZone); + } else { + // MS Excel calendar base dates + if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) { + // Allow adjustment for 1900 Leap Year in MS Excel + $baseDate = ($excelTimestamp < 60) ? new \DateTime('1899-12-31', $timeZone) : new \DateTime('1899-12-30', $timeZone); + } else { + $baseDate = new \DateTime('1904-01-01', $timeZone); + } + } + } else { + $baseDate = new \DateTime('1899-12-30', $timeZone); + } + + $days = floor($excelTimestamp); + $partDay = $excelTimestamp - $days; + $hours = floor($partDay * 24); + $partDay = $partDay * 24 - $hours; + $minutes = floor($partDay * 60); + $partDay = $partDay * 60 - $minutes; + $seconds = round($partDay * 60); + + if ($days >= 0) { + $days = '+' . $days; + } + $interval = $days . ' days'; + + return $baseDate->modify($interval) + ->setTime($hours, $minutes, $seconds); + } + + /** + * Convert a MS serialized datetime value from Excel to a unix timestamp. + * + * @param float|int $excelTimestamp MS Excel serialized date/time value + * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp, + * if you don't want to treat it as a UTC value + * Use the default (UST) unless you absolutely need a conversion + * + * @throws \Exception + * + * @return int Unix timetamp for this date/time + */ + public static function excelToTimestamp($excelTimestamp, $timeZone = null) + { + return (int) self::excelToDateTimeObject($excelTimestamp, $timeZone) + ->format('U'); + } + + /** + * Convert a date from PHP to an MS Excel serialized date/time value. + * + * @param mixed $dateValue Unix Timestamp or PHP DateTime object or a string + * + * @return bool|float Excel date/time value + * or boolean FALSE on failure + */ + public static function PHPToExcel($dateValue) + { + if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) { + return self::dateTimeToExcel($dateValue); + } elseif (is_numeric($dateValue)) { + return self::timestampToExcel($dateValue); + } elseif (is_string($dateValue)) { + return self::stringToExcel($dateValue); + } + + return false; + } + + /** + * Convert a PHP DateTime object to an MS Excel serialized date/time value. + * + * @param DateTimeInterface $dateValue PHP DateTime object + * + * @return float MS Excel serialized date/time value + */ + public static function dateTimeToExcel(DateTimeInterface $dateValue) + { + return self::formattedPHPToExcel( + $dateValue->format('Y'), + $dateValue->format('m'), + $dateValue->format('d'), + $dateValue->format('H'), + $dateValue->format('i'), + $dateValue->format('s') + ); + } + + /** + * Convert a Unix timestamp to an MS Excel serialized date/time value. + * + * @param int $dateValue Unix Timestamp + * + * @return float MS Excel serialized date/time value + */ + public static function timestampToExcel($dateValue) + { + if (!is_numeric($dateValue)) { + return false; + } + + return self::dateTimeToExcel(new \DateTime('@' . $dateValue)); + } + + /** + * formattedPHPToExcel. + * + * @param int $year + * @param int $month + * @param int $day + * @param int $hours + * @param int $minutes + * @param int $seconds + * + * @return float Excel date/time value + */ + public static function formattedPHPToExcel($year, $month, $day, $hours = 0, $minutes = 0, $seconds = 0) + { + if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) { + // + // Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel + // This affects every date following 28th February 1900 + // + $excel1900isLeapYear = true; + if (($year == 1900) && ($month <= 2)) { + $excel1900isLeapYear = false; + } + $myexcelBaseDate = 2415020; + } else { + $myexcelBaseDate = 2416481; + $excel1900isLeapYear = false; + } + + // Julian base date Adjustment + if ($month > 2) { + $month -= 3; + } else { + $month += 9; + --$year; + } + + // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) + $century = substr($year, 0, 2); + $decade = substr($year, 2, 2); + $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear; + + $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; + + return (float) $excelDate + $excelTime; + } + + /** + * Is a given cell a date/time? + * + * @param Cell $pCell + * + * @return bool + */ + public static function isDateTime(Cell $pCell) + { + return self::isDateTimeFormat( + $pCell->getWorksheet()->getStyle( + $pCell->getCoordinate() + )->getNumberFormat() + ); + } + + /** + * Is a given number format a date/time? + * + * @param NumberFormat $pFormat + * + * @return bool + */ + public static function isDateTimeFormat(NumberFormat $pFormat) + { + return self::isDateTimeFormatCode($pFormat->getFormatCode()); + } + + private static $possibleDateFormatCharacters = 'eymdHs'; + + /** + * Is a given number format code a date/time? + * + * @param string $pFormatCode + * + * @return bool + */ + public static function isDateTimeFormatCode($pFormatCode) + { + if (strtolower($pFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) { + // "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check) + return false; + } + if (preg_match('/[0#]E[+-]0/i', $pFormatCode)) { + // Scientific format + return false; + } + + // Switch on formatcode + switch ($pFormatCode) { + // Explicitly defined date formats + case NumberFormat::FORMAT_DATE_YYYYMMDD: + case NumberFormat::FORMAT_DATE_YYYYMMDD2: + case NumberFormat::FORMAT_DATE_DDMMYYYY: + case NumberFormat::FORMAT_DATE_DMYSLASH: + case NumberFormat::FORMAT_DATE_DMYMINUS: + case NumberFormat::FORMAT_DATE_DMMINUS: + case NumberFormat::FORMAT_DATE_MYMINUS: + case NumberFormat::FORMAT_DATE_DATETIME: + case NumberFormat::FORMAT_DATE_TIME1: + case NumberFormat::FORMAT_DATE_TIME2: + case NumberFormat::FORMAT_DATE_TIME3: + case NumberFormat::FORMAT_DATE_TIME4: + case NumberFormat::FORMAT_DATE_TIME5: + case NumberFormat::FORMAT_DATE_TIME6: + case NumberFormat::FORMAT_DATE_TIME7: + case NumberFormat::FORMAT_DATE_TIME8: + case NumberFormat::FORMAT_DATE_YYYYMMDDSLASH: + case NumberFormat::FORMAT_DATE_XLSX14: + case NumberFormat::FORMAT_DATE_XLSX15: + case NumberFormat::FORMAT_DATE_XLSX16: + case NumberFormat::FORMAT_DATE_XLSX17: + case NumberFormat::FORMAT_DATE_XLSX22: + return true; + } + + // Typically number, currency or accounting (or occasionally fraction) formats + if ((substr($pFormatCode, 0, 1) == '_') || (substr($pFormatCode, 0, 2) == '0 ')) { + return false; + } + // Try checking for any of the date formatting characters that don't appear within square braces + if (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $pFormatCode)) { + // We might also have a format mask containing quoted strings... + // we don't want to test for any of our characters within the quoted blocks + if (strpos($pFormatCode, '"') !== false) { + $segMatcher = false; + foreach (explode('"', $pFormatCode) as $subVal) { + // Only test in alternate array entries (the non-quoted blocks) + if (($segMatcher = !$segMatcher) && + (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $subVal))) { + return true; + } + } + + return false; + } + + return true; + } + + // No date... + return false; + } + + /** + * Convert a date/time string to Excel time. + * + * @param string $dateValue Examples: '2009-12-31', '2009-12-31 15:59', '2009-12-31 15:59:10' + * + * @return false|float Excel date/time serial value + */ + public static function stringToExcel($dateValue) + { + if (strlen($dateValue) < 2) { + return false; + } + if (!preg_match('/^(\d{1,4}[ \.\/\-][A-Z]{3,9}([ \.\/\-]\d{1,4})?|[A-Z]{3,9}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?|\d{1,4}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?)( \d{1,2}:\d{1,2}(:\d{1,2})?)?$/iu', $dateValue)) { + return false; + } + + $dateValueNew = DateTime::DATEVALUE($dateValue); + + if ($dateValueNew === Functions::VALUE()) { + return false; + } + + if (strpos($dateValue, ':') !== false) { + $timeValue = DateTime::TIMEVALUE($dateValue); + if ($timeValue === Functions::VALUE()) { + return false; + } + $dateValueNew += $timeValue; + } + + return $dateValueNew; + } + + /** + * Converts a month name (either a long or a short name) to a month number. + * + * @param string $month Month name or abbreviation + * + * @return int|string Month number (1 - 12), or the original string argument if it isn't a valid month name + */ + public static function monthStringToNumber($month) + { + $monthIndex = 1; + foreach (self::$monthNames as $shortMonthName => $longMonthName) { + if (($month === $longMonthName) || ($month === $shortMonthName)) { + return $monthIndex; + } + ++$monthIndex; + } + + return $month; + } + + /** + * Strips an ordinal from a numeric value. + * + * @param string $day Day number with an ordinal + * + * @return int|string The integer value with any ordinal stripped, or the original string argument if it isn't a valid numeric + */ + public static function dayStringToNumber($day) + { + $strippedDayValue = (str_replace(self::$numberSuffixes, '', $day)); + if (is_numeric($strippedDayValue)) { + return (int) $strippedDayValue; + } + + return $day; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Drawing.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Drawing.php new file mode 100644 index 00000000000..25d6910d34e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Drawing.php @@ -0,0 +1,249 @@ +getName(); + $size = $pDefaultFont->getSize(); + + if (isset(Font::$defaultColumnWidths[$name][$size])) { + // Exact width can be determined + $colWidth = $pValue * Font::$defaultColumnWidths[$name][$size]['width'] / Font::$defaultColumnWidths[$name][$size]['px']; + } else { + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + $colWidth = $pValue * 11 * Font::$defaultColumnWidths['Calibri'][11]['width'] / Font::$defaultColumnWidths['Calibri'][11]['px'] / $size; + } + + return $colWidth; + } + + /** + * Convert column width from (intrinsic) Excel units to pixels. + * + * @param float $pValue Value in cell dimension + * @param \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont Default font of the workbook + * + * @return int Value in pixels + */ + public static function cellDimensionToPixels($pValue, \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont) + { + // Font name and size + $name = $pDefaultFont->getName(); + $size = $pDefaultFont->getSize(); + + if (isset(Font::$defaultColumnWidths[$name][$size])) { + // Exact width can be determined + $colWidth = $pValue * Font::$defaultColumnWidths[$name][$size]['px'] / Font::$defaultColumnWidths[$name][$size]['width']; + } else { + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + $colWidth = $pValue * $size * Font::$defaultColumnWidths['Calibri'][11]['px'] / Font::$defaultColumnWidths['Calibri'][11]['width'] / 11; + } + + // Round pixels to closest integer + $colWidth = (int) round($colWidth); + + return $colWidth; + } + + /** + * Convert pixels to points. + * + * @param int $pValue Value in pixels + * + * @return float Value in points + */ + public static function pixelsToPoints($pValue) + { + return $pValue * 0.67777777; + } + + /** + * Convert points to pixels. + * + * @param int $pValue Value in points + * + * @return int Value in pixels + */ + public static function pointsToPixels($pValue) + { + if ($pValue != 0) { + return (int) ceil($pValue * 1.333333333); + } + + return 0; + } + + /** + * Convert degrees to angle. + * + * @param int $pValue Degrees + * + * @return int Angle + */ + public static function degreesToAngle($pValue) + { + return (int) round($pValue * 60000); + } + + /** + * Convert angle to degrees. + * + * @param int $pValue Angle + * + * @return int Degrees + */ + public static function angleToDegrees($pValue) + { + if ($pValue != 0) { + return round($pValue / 60000); + } + + return 0; + } + + /** + * Create a new image from file. By alexander at alexauto dot nl. + * + * @see http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214 + * + * @param string $p_sFile Path to Windows DIB (BMP) image + * + * @return resource + */ + public static function imagecreatefrombmp($p_sFile) + { + // Load the image into a string + $file = fopen($p_sFile, 'rb'); + $read = fread($file, 10); + while (!feof($file) && ($read != '')) { + $read .= fread($file, 1024); + } + + $temp = unpack('H*', $read); + $hex = $temp[1]; + $header = substr($hex, 0, 108); + + // Process the header + // Structure: http://www.fastgraph.com/help/bmp_header_format.html + if (substr($header, 0, 4) == '424d') { + // Cut it in parts of 2 bytes + $header_parts = str_split($header, 2); + + // Get the width 4 bytes + $width = hexdec($header_parts[19] . $header_parts[18]); + + // Get the height 4 bytes + $height = hexdec($header_parts[23] . $header_parts[22]); + + // Unset the header params + unset($header_parts); + } + + // Define starting X and Y + $x = 0; + $y = 1; + + // Create newimage + $image = imagecreatetruecolor($width, $height); + + // Grab the body from the image + $body = substr($hex, 108); + + // Calculate if padding at the end-line is needed + // Divided by two to keep overview. + // 1 byte = 2 HEX-chars + $body_size = (strlen($body) / 2); + $header_size = ($width * $height); + + // Use end-line padding? Only when needed + $usePadding = ($body_size > ($header_size * 3) + 4); + + // Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption + // Calculate the next DWORD-position in the body + for ($i = 0; $i < $body_size; $i += 3) { + // Calculate line-ending and padding + if ($x >= $width) { + // If padding needed, ignore image-padding + // Shift i to the ending of the current 32-bit-block + if ($usePadding) { + $i += $width % 4; + } + + // Reset horizontal position + $x = 0; + + // Raise the height-position (bottom-up) + ++$y; + + // Reached the image-height? Break the for-loop + if ($y > $height) { + break; + } + } + + // Calculation of the RGB-pixel (defined as BGR in image-data) + // Define $i_pos as absolute position in the body + $i_pos = $i * 2; + $r = hexdec($body[$i_pos + 4] . $body[$i_pos + 5]); + $g = hexdec($body[$i_pos + 2] . $body[$i_pos + 3]); + $b = hexdec($body[$i_pos] . $body[$i_pos + 1]); + + // Calculate and draw the pixel + $color = imagecolorallocate($image, $r, $g, $b); + imagesetpixel($image, $x, $height - $y, $color); + + // Raise the horizontal position + ++$x; + } + + // Unset the body / free the memory + unset($body); + + // Return image-object + return $image; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher.php new file mode 100644 index 00000000000..c6d0a6f8118 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher.php @@ -0,0 +1,64 @@ +dggContainer; + } + + /** + * Set Drawing Group Container. + * + * @param Escher\DggContainer $dggContainer + * + * @return Escher\DggContainer + */ + public function setDggContainer($dggContainer) + { + return $this->dggContainer = $dggContainer; + } + + /** + * Get Drawing Container. + * + * @return Escher\DgContainer + */ + public function getDgContainer() + { + return $this->dgContainer; + } + + /** + * Set Drawing Container. + * + * @param Escher\DgContainer $dgContainer + * + * @return Escher\DgContainer + */ + public function setDgContainer($dgContainer) + { + return $this->dgContainer = $dgContainer; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer.php new file mode 100644 index 00000000000..e9d387daa4b --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer.php @@ -0,0 +1,52 @@ +dgId; + } + + public function setDgId($value) + { + $this->dgId = $value; + } + + public function getLastSpId() + { + return $this->lastSpId; + } + + public function setLastSpId($value) + { + $this->lastSpId = $value; + } + + public function getSpgrContainer() + { + return $this->spgrContainer; + } + + public function setSpgrContainer($spgrContainer) + { + return $this->spgrContainer = $spgrContainer; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php new file mode 100644 index 00000000000..7e2c34608a2 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php @@ -0,0 +1,79 @@ +parent = $parent; + } + + /** + * Get the parent Shape Group Container if any. + * + * @return null|\PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer + */ + public function getParent() + { + return $this->parent; + } + + /** + * Add a child. This will be either spgrContainer or spContainer. + * + * @param mixed $child + */ + public function addChild($child) + { + $this->children[] = $child; + $child->setParent($this); + } + + /** + * Get collection of Shape Containers. + */ + public function getChildren() + { + return $this->children; + } + + /** + * Recursively get all spContainers within this spgrContainer. + * + * @return SpgrContainer\SpContainer[] + */ + public function getAllSpContainers() + { + $allSpContainers = []; + + foreach ($this->children as $child) { + if ($child instanceof self) { + $allSpContainers = array_merge($allSpContainers, $child->getAllSpContainers()); + } else { + $allSpContainers[] = $child; + } + } + + return $allSpContainers; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php new file mode 100644 index 00000000000..bbf51df186e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php @@ -0,0 +1,369 @@ +parent = $parent; + } + + /** + * Get the parent Shape Group Container. + * + * @return SpgrContainer + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set whether this is a group shape. + * + * @param bool $value + */ + public function setSpgr($value) + { + $this->spgr = $value; + } + + /** + * Get whether this is a group shape. + * + * @return bool + */ + public function getSpgr() + { + return $this->spgr; + } + + /** + * Set the shape type. + * + * @param int $value + */ + public function setSpType($value) + { + $this->spType = $value; + } + + /** + * Get the shape type. + * + * @return int + */ + public function getSpType() + { + return $this->spType; + } + + /** + * Set the shape flag. + * + * @param int $value + */ + public function setSpFlag($value) + { + $this->spFlag = $value; + } + + /** + * Get the shape flag. + * + * @return int + */ + public function getSpFlag() + { + return $this->spFlag; + } + + /** + * Set the shape index. + * + * @param int $value + */ + public function setSpId($value) + { + $this->spId = $value; + } + + /** + * Get the shape index. + * + * @return int + */ + public function getSpId() + { + return $this->spId; + } + + /** + * Set an option for the Shape Group Container. + * + * @param int $property The number specifies the option + * @param mixed $value + */ + public function setOPT($property, $value) + { + $this->OPT[$property] = $value; + } + + /** + * Get an option for the Shape Group Container. + * + * @param int $property The number specifies the option + * + * @return mixed + */ + public function getOPT($property) + { + if (isset($this->OPT[$property])) { + return $this->OPT[$property]; + } + + return null; + } + + /** + * Get the collection of options. + * + * @return array + */ + public function getOPTCollection() + { + return $this->OPT; + } + + /** + * Set cell coordinates of upper-left corner of shape. + * + * @param string $value eg: 'A1' + */ + public function setStartCoordinates($value) + { + $this->startCoordinates = $value; + } + + /** + * Get cell coordinates of upper-left corner of shape. + * + * @return string + */ + public function getStartCoordinates() + { + return $this->startCoordinates; + } + + /** + * Set offset in x-direction of upper-left corner of shape measured in 1/1024 of column width. + * + * @param int $startOffsetX + */ + public function setStartOffsetX($startOffsetX) + { + $this->startOffsetX = $startOffsetX; + } + + /** + * Get offset in x-direction of upper-left corner of shape measured in 1/1024 of column width. + * + * @return int + */ + public function getStartOffsetX() + { + return $this->startOffsetX; + } + + /** + * Set offset in y-direction of upper-left corner of shape measured in 1/256 of row height. + * + * @param int $startOffsetY + */ + public function setStartOffsetY($startOffsetY) + { + $this->startOffsetY = $startOffsetY; + } + + /** + * Get offset in y-direction of upper-left corner of shape measured in 1/256 of row height. + * + * @return int + */ + public function getStartOffsetY() + { + return $this->startOffsetY; + } + + /** + * Set cell coordinates of bottom-right corner of shape. + * + * @param string $value eg: 'A1' + */ + public function setEndCoordinates($value) + { + $this->endCoordinates = $value; + } + + /** + * Get cell coordinates of bottom-right corner of shape. + * + * @return string + */ + public function getEndCoordinates() + { + return $this->endCoordinates; + } + + /** + * Set offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width. + * + * @param int $endOffsetX + */ + public function setEndOffsetX($endOffsetX) + { + $this->endOffsetX = $endOffsetX; + } + + /** + * Get offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width. + * + * @return int + */ + public function getEndOffsetX() + { + return $this->endOffsetX; + } + + /** + * Set offset in y-direction of bottom-right corner of shape measured in 1/256 of row height. + * + * @param int $endOffsetY + */ + public function setEndOffsetY($endOffsetY) + { + $this->endOffsetY = $endOffsetY; + } + + /** + * Get offset in y-direction of bottom-right corner of shape measured in 1/256 of row height. + * + * @return int + */ + public function getEndOffsetY() + { + return $this->endOffsetY; + } + + /** + * Get the nesting level of this spContainer. This is the number of spgrContainers between this spContainer and + * the dgContainer. A value of 1 = immediately within first spgrContainer + * Higher nesting level occurs if and only if spContainer is part of a shape group. + * + * @return int Nesting level + */ + public function getNestingLevel() + { + $nestingLevel = 0; + + $parent = $this->getParent(); + while ($parent instanceof SpgrContainer) { + ++$nestingLevel; + $parent = $parent->getParent(); + } + + return $nestingLevel; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer.php new file mode 100644 index 00000000000..96da32131e3 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer.php @@ -0,0 +1,175 @@ +spIdMax; + } + + /** + * Set maximum shape index of all shapes in all drawings (plus one). + * + * @param int $value + */ + public function setSpIdMax($value) + { + $this->spIdMax = $value; + } + + /** + * Get total number of drawings saved. + * + * @return int + */ + public function getCDgSaved() + { + return $this->cDgSaved; + } + + /** + * Set total number of drawings saved. + * + * @param int $value + */ + public function setCDgSaved($value) + { + $this->cDgSaved = $value; + } + + /** + * Get total number of shapes saved (including group shapes). + * + * @return int + */ + public function getCSpSaved() + { + return $this->cSpSaved; + } + + /** + * Set total number of shapes saved (including group shapes). + * + * @param int $value + */ + public function setCSpSaved($value) + { + $this->cSpSaved = $value; + } + + /** + * Get BLIP Store Container. + * + * @return DggContainer\BstoreContainer + */ + public function getBstoreContainer() + { + return $this->bstoreContainer; + } + + /** + * Set BLIP Store Container. + * + * @param DggContainer\BstoreContainer $bstoreContainer + */ + public function setBstoreContainer($bstoreContainer) + { + $this->bstoreContainer = $bstoreContainer; + } + + /** + * Set an option for the drawing group. + * + * @param int $property The number specifies the option + * @param mixed $value + */ + public function setOPT($property, $value) + { + $this->OPT[$property] = $value; + } + + /** + * Get an option for the drawing group. + * + * @param int $property The number specifies the option + * + * @return mixed + */ + public function getOPT($property) + { + if (isset($this->OPT[$property])) { + return $this->OPT[$property]; + } + + return null; + } + + /** + * Get identifier clusters. + * + * @return array + */ + public function getIDCLs() + { + return $this->IDCLs; + } + + /** + * Set identifier clusters. [ => , ...]. + * + * @param array $pValue + */ + public function setIDCLs($pValue) + { + $this->IDCLs = $pValue; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php new file mode 100644 index 00000000000..9d1e68ec771 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php @@ -0,0 +1,34 @@ +BSECollection[] = $BSE; + $BSE->setParent($this); + } + + /** + * Get the collection of BLIP Store Entries. + * + * @return BstoreContainer\BSE[] + */ + public function getBSECollection() + { + return $this->BSECollection; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php new file mode 100644 index 00000000000..f83bdc7ee3f --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php @@ -0,0 +1,89 @@ +parent = $parent; + } + + /** + * Get the BLIP. + * + * @return BSE\Blip + */ + public function getBlip() + { + return $this->blip; + } + + /** + * Set the BLIP. + * + * @param BSE\Blip $blip + */ + public function setBlip($blip) + { + $this->blip = $blip; + $blip->setParent($this); + } + + /** + * Get the BLIP type. + * + * @return int + */ + public function getBlipType() + { + return $this->blipType; + } + + /** + * Set the BLIP type. + * + * @param int $blipType + */ + public function setBlipType($blipType) + { + $this->blipType = $blipType; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php new file mode 100644 index 00000000000..88bc117a166 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php @@ -0,0 +1,60 @@ +data; + } + + /** + * Set the raw image data. + * + * @param string $data + */ + public function setData($data) + { + $this->data = $data; + } + + /** + * Set parent BSE. + * + * @param \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE $parent + */ + public function setParent($parent) + { + $this->parent = $parent; + } + + /** + * Get parent BSE. + * + * @return \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE $parent + */ + public function getParent() + { + return $this->parent; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/File.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/File.php new file mode 100644 index 00000000000..239c8375a1e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/File.php @@ -0,0 +1,144 @@ +open($zipFile) === true) { + $returnValue = ($zip->getFromName($archiveFile) !== false); + $zip->close(); + + return $returnValue; + } + + return false; + } + + return file_exists($pFilename); + } + + /** + * Returns canonicalized absolute pathname, also for ZIP archives. + * + * @param string $pFilename + * + * @return string + */ + public static function realpath($pFilename) + { + // Returnvalue + $returnValue = ''; + + // Try using realpath() + if (file_exists($pFilename)) { + $returnValue = realpath($pFilename); + } + + // Found something? + if ($returnValue == '' || ($returnValue === null)) { + $pathArray = explode('/', $pFilename); + while (in_array('..', $pathArray) && $pathArray[0] != '..') { + $iMax = count($pathArray); + for ($i = 0; $i < $iMax; ++$i) { + if ($pathArray[$i] == '..' && $i > 0) { + unset($pathArray[$i], $pathArray[$i - 1]); + + break; + } + } + } + $returnValue = implode('/', $pathArray); + } + + // Return + return $returnValue; + } + + /** + * Get the systems temporary directory. + * + * @return string + */ + public static function sysGetTempDir() + { + if (self::$useUploadTempDirectory) { + // use upload-directory when defined to allow running on environments having very restricted + // open_basedir configs + if (ini_get('upload_tmp_dir') !== false) { + if ($temp = ini_get('upload_tmp_dir')) { + if (file_exists($temp)) { + return realpath($temp); + } + } + } + } + + return realpath(sys_get_temp_dir()); + } + + /** + * Assert that given path is an existing file and is readable, otherwise throw exception. + * + * @param string $filename + * + * @throws InvalidArgumentException + */ + public static function assertFile($filename) + { + if (!is_file($filename)) { + throw new InvalidArgumentException('File "' . $filename . '" does not exist.'); + } + + if (!is_readable($filename)) { + throw new InvalidArgumentException('Could not open "' . $filename . '" for reading.'); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Font.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Font.php new file mode 100644 index 00000000000..8abcef2e192 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Font.php @@ -0,0 +1,764 @@ + [ + 1 => ['px' => 24, 'width' => 12.00000000], + 2 => ['px' => 24, 'width' => 12.00000000], + 3 => ['px' => 32, 'width' => 10.66406250], + 4 => ['px' => 32, 'width' => 10.66406250], + 5 => ['px' => 40, 'width' => 10.00000000], + 6 => ['px' => 48, 'width' => 9.59765625], + 7 => ['px' => 48, 'width' => 9.59765625], + 8 => ['px' => 56, 'width' => 9.33203125], + 9 => ['px' => 64, 'width' => 9.14062500], + 10 => ['px' => 64, 'width' => 9.14062500], + ], + 'Calibri' => [ + 1 => ['px' => 24, 'width' => 12.00000000], + 2 => ['px' => 24, 'width' => 12.00000000], + 3 => ['px' => 32, 'width' => 10.66406250], + 4 => ['px' => 32, 'width' => 10.66406250], + 5 => ['px' => 40, 'width' => 10.00000000], + 6 => ['px' => 48, 'width' => 9.59765625], + 7 => ['px' => 48, 'width' => 9.59765625], + 8 => ['px' => 56, 'width' => 9.33203125], + 9 => ['px' => 56, 'width' => 9.33203125], + 10 => ['px' => 64, 'width' => 9.14062500], + 11 => ['px' => 64, 'width' => 9.14062500], + ], + 'Verdana' => [ + 1 => ['px' => 24, 'width' => 12.00000000], + 2 => ['px' => 24, 'width' => 12.00000000], + 3 => ['px' => 32, 'width' => 10.66406250], + 4 => ['px' => 32, 'width' => 10.66406250], + 5 => ['px' => 40, 'width' => 10.00000000], + 6 => ['px' => 48, 'width' => 9.59765625], + 7 => ['px' => 48, 'width' => 9.59765625], + 8 => ['px' => 64, 'width' => 9.14062500], + 9 => ['px' => 72, 'width' => 9.00000000], + 10 => ['px' => 72, 'width' => 9.00000000], + ], + ]; + + /** + * Set autoSize method. + * + * @param string $pValue see self::AUTOSIZE_METHOD_* + * + * @return bool Success or failure + */ + public static function setAutoSizeMethod($pValue) + { + if (!in_array($pValue, self::$autoSizeMethods)) { + return false; + } + self::$autoSizeMethod = $pValue; + + return true; + } + + /** + * Get autoSize method. + * + * @return string + */ + public static function getAutoSizeMethod() + { + return self::$autoSizeMethod; + } + + /** + * Set the path to the folder containing .ttf files. There should be a trailing slash. + * Typical locations on variout some platforms: + *
    + *
  • C:/Windows/Fonts/
  • + *
  • /usr/share/fonts/truetype/
  • + *
  • ~/.fonts/
  • + *
. + * + * @param string $pValue + */ + public static function setTrueTypeFontPath($pValue) + { + self::$trueTypeFontPath = $pValue; + } + + /** + * Get the path to the folder containing .ttf files. + * + * @return string + */ + public static function getTrueTypeFontPath() + { + return self::$trueTypeFontPath; + } + + /** + * Calculate an (approximate) OpenXML column width, based on font size and text contained. + * + * @param \PhpOffice\PhpSpreadsheet\Style\Font $font Font object + * @param RichText|string $cellText Text to calculate width + * @param int $rotation Rotation angle + * @param null|\PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Font object + * + * @return int Column width + */ + public static function calculateColumnWidth(\PhpOffice\PhpSpreadsheet\Style\Font $font, $cellText = '', $rotation = 0, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont = null) + { + // If it is rich text, use plain text + if ($cellText instanceof RichText) { + $cellText = $cellText->getPlainText(); + } + + // Special case if there are one or more newline characters ("\n") + if (strpos($cellText, "\n") !== false) { + $lineTexts = explode("\n", $cellText); + $lineWidths = []; + foreach ($lineTexts as $lineText) { + $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont); + } + + return max($lineWidths); // width of longest line in cell + } + + // Try to get the exact text width in pixels + $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX; + if (!$approximate) { + $columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07); + + try { + // Width of text in pixels excl. padding + // and addition because Excel adds some padding, just use approx width of 'n' glyph + $columnWidth = self::getTextWidthPixelsExact($cellText, $font, $rotation) + $columnWidthAdjust; + } catch (PhpSpreadsheetException $e) { + $approximate = true; + } + } + + if ($approximate) { + $columnWidthAdjust = self::getTextWidthPixelsApprox('n', $font, 0); + // Width of text in pixels excl. padding, approximation + // and addition because Excel adds some padding, just use approx width of 'n' glyph + $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust; + } + + // Convert from pixel width to column width + $columnWidth = Drawing::pixelsToCellDimension($columnWidth, $defaultFont); + + // Return + return round($columnWidth, 6); + } + + /** + * Get GD text width in pixels for a string of text in a certain font at a certain rotation angle. + * + * @param string $text + * @param \PhpOffice\PhpSpreadsheet\Style\Font + * @param int $rotation + * + * @throws PhpSpreadsheetException + * + * @return int + */ + public static function getTextWidthPixelsExact($text, \PhpOffice\PhpSpreadsheet\Style\Font $font, $rotation = 0) + { + if (!function_exists('imagettfbbox')) { + throw new PhpSpreadsheetException('GD library needs to be enabled'); + } + + // font size should really be supplied in pixels in GD2, + // but since GD2 seems to assume 72dpi, pixels and points are the same + $fontFile = self::getTrueTypeFontFileFromFont($font); + $textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text); + + // Get corners positions + $lowerLeftCornerX = $textBox[0]; + $lowerRightCornerX = $textBox[2]; + $upperRightCornerX = $textBox[4]; + $upperLeftCornerX = $textBox[6]; + + // Consider the rotation when calculating the width + $textWidth = max($lowerRightCornerX - $upperLeftCornerX, $upperRightCornerX - $lowerLeftCornerX); + + return $textWidth; + } + + /** + * Get approximate width in pixels for a string of text in a certain font at a certain rotation angle. + * + * @param string $columnText + * @param \PhpOffice\PhpSpreadsheet\Style\Font $font + * @param int $rotation + * + * @return int Text width in pixels (no padding added) + */ + public static function getTextWidthPixelsApprox($columnText, \PhpOffice\PhpSpreadsheet\Style\Font $font, $rotation = 0) + { + $fontName = $font->getName(); + $fontSize = $font->getSize(); + + // Calculate column width in pixels. We assume fixed glyph width. Result varies with font name and size. + switch ($fontName) { + case 'Calibri': + // value 8.26 was found via interpolation by inspecting real Excel files with Calibri 11 font. + $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size + break; + case 'Arial': + // value 8 was set because of experience in different exports at Arial 10 font. + $columnWidth = (int) (8 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + break; + case 'Verdana': + // value 8 was found via interpolation by inspecting real Excel files with Verdana 10 font. + $columnWidth = (int) (8 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + break; + default: + // just assume Calibri + $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size + break; + } + + // Calculate approximate rotated column width + if ($rotation !== 0) { + if ($rotation == -165) { + // stacked text + $columnWidth = 4; // approximation + } else { + // rotated text + $columnWidth = $columnWidth * cos(deg2rad($rotation)) + + $fontSize * abs(sin(deg2rad($rotation))) / 5; // approximation + } + } + + // pixel width is an integer + return (int) $columnWidth; + } + + /** + * Calculate an (approximate) pixel size, based on a font points size. + * + * @param int $fontSizeInPoints Font size (in points) + * + * @return int Font size (in pixels) + */ + public static function fontSizeToPixels($fontSizeInPoints) + { + return (int) ((4 / 3) * $fontSizeInPoints); + } + + /** + * Calculate an (approximate) pixel size, based on inch size. + * + * @param int $sizeInInch Font size (in inch) + * + * @return int Size (in pixels) + */ + public static function inchSizeToPixels($sizeInInch) + { + return $sizeInInch * 96; + } + + /** + * Calculate an (approximate) pixel size, based on centimeter size. + * + * @param int $sizeInCm Font size (in centimeters) + * + * @return float Size (in pixels) + */ + public static function centimeterSizeToPixels($sizeInCm) + { + return $sizeInCm * 37.795275591; + } + + /** + * Returns the font path given the font. + * + * @param \PhpOffice\PhpSpreadsheet\Style\Font $font + * + * @return string Path to TrueType font file + */ + public static function getTrueTypeFontFileFromFont($font) + { + if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) { + throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified'); + } + + $name = $font->getName(); + $bold = $font->getBold(); + $italic = $font->getItalic(); + + // Check if we can map font to true type font file + switch ($name) { + case 'Arial': + $fontFile = ( + $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD) + : ($italic ? self::ARIAL_ITALIC : self::ARIAL) + ); + + break; + case 'Calibri': + $fontFile = ( + $bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD) + : ($italic ? self::CALIBRI_ITALIC : self::CALIBRI) + ); + + break; + case 'Courier New': + $fontFile = ( + $bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD) + : ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW) + ); + + break; + case 'Comic Sans MS': + $fontFile = ( + $bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS + ); + + break; + case 'Georgia': + $fontFile = ( + $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD) + : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA) + ); + + break; + case 'Impact': + $fontFile = self::IMPACT; + + break; + case 'Liberation Sans': + $fontFile = ( + $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) + : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) + ); + + break; + case 'Lucida Console': + $fontFile = self::LUCIDA_CONSOLE; + + break; + case 'Lucida Sans Unicode': + $fontFile = self::LUCIDA_SANS_UNICODE; + + break; + case 'Microsoft Sans Serif': + $fontFile = self::MICROSOFT_SANS_SERIF; + + break; + case 'Palatino Linotype': + $fontFile = ( + $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) + : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) + ); + + break; + case 'Symbol': + $fontFile = self::SYMBOL; + + break; + case 'Tahoma': + $fontFile = ( + $bold ? self::TAHOMA_BOLD : self::TAHOMA + ); + + break; + case 'Times New Roman': + $fontFile = ( + $bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD) + : ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN) + ); + + break; + case 'Trebuchet MS': + $fontFile = ( + $bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD) + : ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS) + ); + + break; + case 'Verdana': + $fontFile = ( + $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD) + : ($italic ? self::VERDANA_ITALIC : self::VERDANA) + ); + + break; + default: + throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); + + break; + } + + $fontFile = self::$trueTypeFontPath . $fontFile; + + // Check if file actually exists + if (!file_exists($fontFile)) { + throw new PhpSpreadsheetException('TrueType Font file not found'); + } + + return $fontFile; + } + + /** + * Returns the associated charset for the font name. + * + * @param string $name Font name + * + * @return int Character set code + */ + public static function getCharsetFromFontName($name) + { + switch ($name) { + // Add more cases. Check FONT records in real Excel files. + case 'EucrosiaUPC': + return self::CHARSET_ANSI_THAI; + case 'Wingdings': + return self::CHARSET_SYMBOL; + case 'Wingdings 2': + return self::CHARSET_SYMBOL; + case 'Wingdings 3': + return self::CHARSET_SYMBOL; + default: + return self::CHARSET_ANSI_LATIN; + } + } + + /** + * Get the effective column width for columns without a column dimension or column with width -1 + * For example, for Calibri 11 this is 9.140625 (64 px). + * + * @param \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font + * @param bool $pPixels true = return column width in pixels, false = return in OOXML units + * + * @return mixed Column width + */ + public static function getDefaultColumnWidthByFont(\PhpOffice\PhpSpreadsheet\Style\Font $font, $pPixels = false) + { + if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) { + // Exact width can be determined + $columnWidth = $pPixels ? + self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px'] + : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width']; + } else { + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + $columnWidth = $pPixels ? + self::$defaultColumnWidths['Calibri'][11]['px'] + : self::$defaultColumnWidths['Calibri'][11]['width']; + $columnWidth = $columnWidth * $font->getSize() / 11; + + // Round pixels to closest integer + if ($pPixels) { + $columnWidth = (int) round($columnWidth); + } + } + + return $columnWidth; + } + + /** + * Get the effective row height for rows without a row dimension or rows with height -1 + * For example, for Calibri 11 this is 15 points. + * + * @param \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font + * + * @return float Row height in points + */ + public static function getDefaultRowHeightByFont(\PhpOffice\PhpSpreadsheet\Style\Font $font) + { + switch ($font->getName()) { + case 'Arial': + switch ($font->getSize()) { + case 10: + // inspection of Arial 10 workbook says 12.75pt ~17px + $rowHeight = 12.75; + + break; + case 9: + // inspection of Arial 9 workbook says 12.00pt ~16px + $rowHeight = 12; + + break; + case 8: + // inspection of Arial 8 workbook says 11.25pt ~15px + $rowHeight = 11.25; + + break; + case 7: + // inspection of Arial 7 workbook says 9.00pt ~12px + $rowHeight = 9; + + break; + case 6: + case 5: + // inspection of Arial 5,6 workbook says 8.25pt ~11px + $rowHeight = 8.25; + + break; + case 4: + // inspection of Arial 4 workbook says 6.75pt ~9px + $rowHeight = 6.75; + + break; + case 3: + // inspection of Arial 3 workbook says 6.00pt ~8px + $rowHeight = 6; + + break; + case 2: + case 1: + // inspection of Arial 1,2 workbook says 5.25pt ~7px + $rowHeight = 5.25; + + break; + default: + // use Arial 10 workbook as an approximation, extrapolation + $rowHeight = 12.75 * $font->getSize() / 10; + + break; + } + + break; + case 'Calibri': + switch ($font->getSize()) { + case 11: + // inspection of Calibri 11 workbook says 15.00pt ~20px + $rowHeight = 15; + + break; + case 10: + // inspection of Calibri 10 workbook says 12.75pt ~17px + $rowHeight = 12.75; + + break; + case 9: + // inspection of Calibri 9 workbook says 12.00pt ~16px + $rowHeight = 12; + + break; + case 8: + // inspection of Calibri 8 workbook says 11.25pt ~15px + $rowHeight = 11.25; + + break; + case 7: + // inspection of Calibri 7 workbook says 9.00pt ~12px + $rowHeight = 9; + + break; + case 6: + case 5: + // inspection of Calibri 5,6 workbook says 8.25pt ~11px + $rowHeight = 8.25; + + break; + case 4: + // inspection of Calibri 4 workbook says 6.75pt ~9px + $rowHeight = 6.75; + + break; + case 3: + // inspection of Calibri 3 workbook says 6.00pt ~8px + $rowHeight = 6.00; + + break; + case 2: + case 1: + // inspection of Calibri 1,2 workbook says 5.25pt ~7px + $rowHeight = 5.25; + + break; + default: + // use Calibri 11 workbook as an approximation, extrapolation + $rowHeight = 15 * $font->getSize() / 11; + + break; + } + + break; + case 'Verdana': + switch ($font->getSize()) { + case 10: + // inspection of Verdana 10 workbook says 12.75pt ~17px + $rowHeight = 12.75; + + break; + case 9: + // inspection of Verdana 9 workbook says 11.25pt ~15px + $rowHeight = 11.25; + + break; + case 8: + // inspection of Verdana 8 workbook says 10.50pt ~14px + $rowHeight = 10.50; + + break; + case 7: + // inspection of Verdana 7 workbook says 9.00pt ~12px + $rowHeight = 9.00; + + break; + case 6: + case 5: + // inspection of Verdana 5,6 workbook says 8.25pt ~11px + $rowHeight = 8.25; + + break; + case 4: + // inspection of Verdana 4 workbook says 6.75pt ~9px + $rowHeight = 6.75; + + break; + case 3: + // inspection of Verdana 3 workbook says 6.00pt ~8px + $rowHeight = 6; + + break; + case 2: + case 1: + // inspection of Verdana 1,2 workbook says 5.25pt ~7px + $rowHeight = 5.25; + + break; + default: + // use Verdana 10 workbook as an approximation, extrapolation + $rowHeight = 12.75 * $font->getSize() / 10; + + break; + } + + break; + default: + // just use Calibri as an approximation + $rowHeight = 15 * $font->getSize() / 11; + + break; + } + + return $rowHeight; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/CHANGELOG.TXT b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/CHANGELOG.TXT new file mode 100644 index 00000000000..1c18a5da35d --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/CHANGELOG.TXT @@ -0,0 +1,16 @@ +Mar 1, 2005 11:15 AST by PM + ++ For consistency, renamed Math.php to Maths.java, utils to util, + tests to test, docs to doc - + ++ Removed conditional logic from top of Matrix class. + ++ Switched to using hypo function in Maths.php for all php-hypot calls. + NOTE TO SELF: Need to make sure that all decompositions have been + switched over to using the bundled hypo. + +Feb 25, 2005 at 10:00 AST by PM + ++ Recommend using simpler Error.php instead of JAMA_Error.php but + can be persuaded otherwise. + diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/CholeskyDecomposition.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/CholeskyDecomposition.php new file mode 100644 index 00000000000..2b241d555bb --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/CholeskyDecomposition.php @@ -0,0 +1,147 @@ +L = $A->getArray(); + $this->m = $A->getRowDimension(); + + for ($i = 0; $i < $this->m; ++$i) { + for ($j = $i; $j < $this->m; ++$j) { + for ($sum = $this->L[$i][$j], $k = $i - 1; $k >= 0; --$k) { + $sum -= $this->L[$i][$k] * $this->L[$j][$k]; + } + if ($i == $j) { + if ($sum >= 0) { + $this->L[$i][$i] = sqrt($sum); + } else { + $this->isspd = false; + } + } else { + if ($this->L[$i][$i] != 0) { + $this->L[$j][$i] = $sum / $this->L[$i][$i]; + } + } + } + + for ($k = $i + 1; $k < $this->m; ++$k) { + $this->L[$i][$k] = 0.0; + } + } + } + + /** + * Is the matrix symmetric and positive definite? + * + * @return bool + */ + public function isSPD() + { + return $this->isspd; + } + + /** + * getL. + * + * Return triangular factor. + * + * @return Matrix Lower triangular matrix + */ + public function getL() + { + return new Matrix($this->L); + } + + /** + * Solve A*X = B. + * + * @param $B Row-equal matrix + * + * @return Matrix L * L' * X = B + */ + public function solve(Matrix $B) + { + if ($B->getRowDimension() == $this->m) { + if ($this->isspd) { + $X = $B->getArrayCopy(); + $nx = $B->getColumnDimension(); + + for ($k = 0; $k < $this->m; ++$k) { + for ($i = $k + 1; $i < $this->m; ++$i) { + for ($j = 0; $j < $nx; ++$j) { + $X[$i][$j] -= $X[$k][$j] * $this->L[$i][$k]; + } + } + for ($j = 0; $j < $nx; ++$j) { + $X[$k][$j] /= $this->L[$k][$k]; + } + } + + for ($k = $this->m - 1; $k >= 0; --$k) { + for ($j = 0; $j < $nx; ++$j) { + $X[$k][$j] /= $this->L[$k][$k]; + } + for ($i = 0; $i < $k; ++$i) { + for ($j = 0; $j < $nx; ++$j) { + $X[$i][$j] -= $X[$k][$j] * $this->L[$k][$i]; + } + } + } + + return new Matrix($X, $this->m, $nx); + } + + throw new CalculationException(Matrix::MATRIX_SPD_EXCEPTION); + } + + throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php new file mode 100644 index 00000000000..ba59e0e5ade --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php @@ -0,0 +1,861 @@ +d = $this->V[$this->n - 1]; + // Householder reduction to tridiagonal form. + for ($i = $this->n - 1; $i > 0; --$i) { + $i_ = $i - 1; + // Scale to avoid under/overflow. + $h = $scale = 0.0; + $scale += array_sum(array_map('abs', $this->d)); + if ($scale == 0.0) { + $this->e[$i] = $this->d[$i_]; + $this->d = array_slice($this->V[$i_], 0, $i_); + for ($j = 0; $j < $i; ++$j) { + $this->V[$j][$i] = $this->V[$i][$j] = 0.0; + } + } else { + // Generate Householder vector. + for ($k = 0; $k < $i; ++$k) { + $this->d[$k] /= $scale; + $h += pow($this->d[$k], 2); + } + $f = $this->d[$i_]; + $g = sqrt($h); + if ($f > 0) { + $g = -$g; + } + $this->e[$i] = $scale * $g; + $h = $h - $f * $g; + $this->d[$i_] = $f - $g; + for ($j = 0; $j < $i; ++$j) { + $this->e[$j] = 0.0; + } + // Apply similarity transformation to remaining columns. + for ($j = 0; $j < $i; ++$j) { + $f = $this->d[$j]; + $this->V[$j][$i] = $f; + $g = $this->e[$j] + $this->V[$j][$j] * $f; + for ($k = $j + 1; $k <= $i_; ++$k) { + $g += $this->V[$k][$j] * $this->d[$k]; + $this->e[$k] += $this->V[$k][$j] * $f; + } + $this->e[$j] = $g; + } + $f = 0.0; + for ($j = 0; $j < $i; ++$j) { + $this->e[$j] /= $h; + $f += $this->e[$j] * $this->d[$j]; + } + $hh = $f / (2 * $h); + for ($j = 0; $j < $i; ++$j) { + $this->e[$j] -= $hh * $this->d[$j]; + } + for ($j = 0; $j < $i; ++$j) { + $f = $this->d[$j]; + $g = $this->e[$j]; + for ($k = $j; $k <= $i_; ++$k) { + $this->V[$k][$j] -= ($f * $this->e[$k] + $g * $this->d[$k]); + } + $this->d[$j] = $this->V[$i - 1][$j]; + $this->V[$i][$j] = 0.0; + } + } + $this->d[$i] = $h; + } + + // Accumulate transformations. + for ($i = 0; $i < $this->n - 1; ++$i) { + $this->V[$this->n - 1][$i] = $this->V[$i][$i]; + $this->V[$i][$i] = 1.0; + $h = $this->d[$i + 1]; + if ($h != 0.0) { + for ($k = 0; $k <= $i; ++$k) { + $this->d[$k] = $this->V[$k][$i + 1] / $h; + } + for ($j = 0; $j <= $i; ++$j) { + $g = 0.0; + for ($k = 0; $k <= $i; ++$k) { + $g += $this->V[$k][$i + 1] * $this->V[$k][$j]; + } + for ($k = 0; $k <= $i; ++$k) { + $this->V[$k][$j] -= $g * $this->d[$k]; + } + } + } + for ($k = 0; $k <= $i; ++$k) { + $this->V[$k][$i + 1] = 0.0; + } + } + + $this->d = $this->V[$this->n - 1]; + $this->V[$this->n - 1] = array_fill(0, $j, 0.0); + $this->V[$this->n - 1][$this->n - 1] = 1.0; + $this->e[0] = 0.0; + } + + /** + * Symmetric tridiagonal QL algorithm. + * + * This is derived from the Algol procedures tql2, by + * Bowdler, Martin, Reinsch, and Wilkinson, Handbook for + * Auto. Comp., Vol.ii-Linear Algebra, and the corresponding + * Fortran subroutine in EISPACK. + */ + private function tql2() + { + for ($i = 1; $i < $this->n; ++$i) { + $this->e[$i - 1] = $this->e[$i]; + } + $this->e[$this->n - 1] = 0.0; + $f = 0.0; + $tst1 = 0.0; + $eps = pow(2.0, -52.0); + + for ($l = 0; $l < $this->n; ++$l) { + // Find small subdiagonal element + $tst1 = max($tst1, abs($this->d[$l]) + abs($this->e[$l])); + $m = $l; + while ($m < $this->n) { + if (abs($this->e[$m]) <= $eps * $tst1) { + break; + } + ++$m; + } + // If m == l, $this->d[l] is an eigenvalue, + // otherwise, iterate. + if ($m > $l) { + $iter = 0; + do { + // Could check iteration count here. + $iter += 1; + // Compute implicit shift + $g = $this->d[$l]; + $p = ($this->d[$l + 1] - $g) / (2.0 * $this->e[$l]); + $r = hypo($p, 1.0); + if ($p < 0) { + $r *= -1; + } + $this->d[$l] = $this->e[$l] / ($p + $r); + $this->d[$l + 1] = $this->e[$l] * ($p + $r); + $dl1 = $this->d[$l + 1]; + $h = $g - $this->d[$l]; + for ($i = $l + 2; $i < $this->n; ++$i) { + $this->d[$i] -= $h; + } + $f += $h; + // Implicit QL transformation. + $p = $this->d[$m]; + $c = 1.0; + $c2 = $c3 = $c; + $el1 = $this->e[$l + 1]; + $s = $s2 = 0.0; + for ($i = $m - 1; $i >= $l; --$i) { + $c3 = $c2; + $c2 = $c; + $s2 = $s; + $g = $c * $this->e[$i]; + $h = $c * $p; + $r = hypo($p, $this->e[$i]); + $this->e[$i + 1] = $s * $r; + $s = $this->e[$i] / $r; + $c = $p / $r; + $p = $c * $this->d[$i] - $s * $g; + $this->d[$i + 1] = $h + $s * ($c * $g + $s * $this->d[$i]); + // Accumulate transformation. + for ($k = 0; $k < $this->n; ++$k) { + $h = $this->V[$k][$i + 1]; + $this->V[$k][$i + 1] = $s * $this->V[$k][$i] + $c * $h; + $this->V[$k][$i] = $c * $this->V[$k][$i] - $s * $h; + } + } + $p = -$s * $s2 * $c3 * $el1 * $this->e[$l] / $dl1; + $this->e[$l] = $s * $p; + $this->d[$l] = $c * $p; + // Check for convergence. + } while (abs($this->e[$l]) > $eps * $tst1); + } + $this->d[$l] = $this->d[$l] + $f; + $this->e[$l] = 0.0; + } + + // Sort eigenvalues and corresponding vectors. + for ($i = 0; $i < $this->n - 1; ++$i) { + $k = $i; + $p = $this->d[$i]; + for ($j = $i + 1; $j < $this->n; ++$j) { + if ($this->d[$j] < $p) { + $k = $j; + $p = $this->d[$j]; + } + } + if ($k != $i) { + $this->d[$k] = $this->d[$i]; + $this->d[$i] = $p; + for ($j = 0; $j < $this->n; ++$j) { + $p = $this->V[$j][$i]; + $this->V[$j][$i] = $this->V[$j][$k]; + $this->V[$j][$k] = $p; + } + } + } + } + + /** + * Nonsymmetric reduction to Hessenberg form. + * + * This is derived from the Algol procedures orthes and ortran, + * by Martin and Wilkinson, Handbook for Auto. Comp., + * Vol.ii-Linear Algebra, and the corresponding + * Fortran subroutines in EISPACK. + */ + private function orthes() + { + $low = 0; + $high = $this->n - 1; + + for ($m = $low + 1; $m <= $high - 1; ++$m) { + // Scale column. + $scale = 0.0; + for ($i = $m; $i <= $high; ++$i) { + $scale = $scale + abs($this->H[$i][$m - 1]); + } + if ($scale != 0.0) { + // Compute Householder transformation. + $h = 0.0; + for ($i = $high; $i >= $m; --$i) { + $this->ort[$i] = $this->H[$i][$m - 1] / $scale; + $h += $this->ort[$i] * $this->ort[$i]; + } + $g = sqrt($h); + if ($this->ort[$m] > 0) { + $g *= -1; + } + $h -= $this->ort[$m] * $g; + $this->ort[$m] -= $g; + // Apply Householder similarity transformation + // H = (I -u * u' / h) * H * (I -u * u') / h) + for ($j = $m; $j < $this->n; ++$j) { + $f = 0.0; + for ($i = $high; $i >= $m; --$i) { + $f += $this->ort[$i] * $this->H[$i][$j]; + } + $f /= $h; + for ($i = $m; $i <= $high; ++$i) { + $this->H[$i][$j] -= $f * $this->ort[$i]; + } + } + for ($i = 0; $i <= $high; ++$i) { + $f = 0.0; + for ($j = $high; $j >= $m; --$j) { + $f += $this->ort[$j] * $this->H[$i][$j]; + } + $f = $f / $h; + for ($j = $m; $j <= $high; ++$j) { + $this->H[$i][$j] -= $f * $this->ort[$j]; + } + } + $this->ort[$m] = $scale * $this->ort[$m]; + $this->H[$m][$m - 1] = $scale * $g; + } + } + + // Accumulate transformations (Algol's ortran). + for ($i = 0; $i < $this->n; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->V[$i][$j] = ($i == $j ? 1.0 : 0.0); + } + } + for ($m = $high - 1; $m >= $low + 1; --$m) { + if ($this->H[$m][$m - 1] != 0.0) { + for ($i = $m + 1; $i <= $high; ++$i) { + $this->ort[$i] = $this->H[$i][$m - 1]; + } + for ($j = $m; $j <= $high; ++$j) { + $g = 0.0; + for ($i = $m; $i <= $high; ++$i) { + $g += $this->ort[$i] * $this->V[$i][$j]; + } + // Double division avoids possible underflow + $g = ($g / $this->ort[$m]) / $this->H[$m][$m - 1]; + for ($i = $m; $i <= $high; ++$i) { + $this->V[$i][$j] += $g * $this->ort[$i]; + } + } + } + } + } + + /** + * Performs complex division. + * + * @param mixed $xr + * @param mixed $xi + * @param mixed $yr + * @param mixed $yi + */ + private function cdiv($xr, $xi, $yr, $yi) + { + if (abs($yr) > abs($yi)) { + $r = $yi / $yr; + $d = $yr + $r * $yi; + $this->cdivr = ($xr + $r * $xi) / $d; + $this->cdivi = ($xi - $r * $xr) / $d; + } else { + $r = $yr / $yi; + $d = $yi + $r * $yr; + $this->cdivr = ($r * $xr + $xi) / $d; + $this->cdivi = ($r * $xi - $xr) / $d; + } + } + + /** + * Nonsymmetric reduction from Hessenberg to real Schur form. + * + * Code is derived from the Algol procedure hqr2, + * by Martin and Wilkinson, Handbook for Auto. Comp., + * Vol.ii-Linear Algebra, and the corresponding + * Fortran subroutine in EISPACK. + */ + private function hqr2() + { + // Initialize + $nn = $this->n; + $n = $nn - 1; + $low = 0; + $high = $nn - 1; + $eps = pow(2.0, -52.0); + $exshift = 0.0; + $p = $q = $r = $s = $z = 0; + // Store roots isolated by balanc and compute matrix norm + $norm = 0.0; + + for ($i = 0; $i < $nn; ++$i) { + if (($i < $low) or ($i > $high)) { + $this->d[$i] = $this->H[$i][$i]; + $this->e[$i] = 0.0; + } + for ($j = max($i - 1, 0); $j < $nn; ++$j) { + $norm = $norm + abs($this->H[$i][$j]); + } + } + + // Outer loop over eigenvalue index + $iter = 0; + while ($n >= $low) { + // Look for single small sub-diagonal element + $l = $n; + while ($l > $low) { + $s = abs($this->H[$l - 1][$l - 1]) + abs($this->H[$l][$l]); + if ($s == 0.0) { + $s = $norm; + } + if (abs($this->H[$l][$l - 1]) < $eps * $s) { + break; + } + --$l; + } + // Check for convergence + // One root found + if ($l == $n) { + $this->H[$n][$n] = $this->H[$n][$n] + $exshift; + $this->d[$n] = $this->H[$n][$n]; + $this->e[$n] = 0.0; + --$n; + $iter = 0; + // Two roots found + } elseif ($l == $n - 1) { + $w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n]; + $p = ($this->H[$n - 1][$n - 1] - $this->H[$n][$n]) / 2.0; + $q = $p * $p + $w; + $z = sqrt(abs($q)); + $this->H[$n][$n] = $this->H[$n][$n] + $exshift; + $this->H[$n - 1][$n - 1] = $this->H[$n - 1][$n - 1] + $exshift; + $x = $this->H[$n][$n]; + // Real pair + if ($q >= 0) { + if ($p >= 0) { + $z = $p + $z; + } else { + $z = $p - $z; + } + $this->d[$n - 1] = $x + $z; + $this->d[$n] = $this->d[$n - 1]; + if ($z != 0.0) { + $this->d[$n] = $x - $w / $z; + } + $this->e[$n - 1] = 0.0; + $this->e[$n] = 0.0; + $x = $this->H[$n][$n - 1]; + $s = abs($x) + abs($z); + $p = $x / $s; + $q = $z / $s; + $r = sqrt($p * $p + $q * $q); + $p = $p / $r; + $q = $q / $r; + // Row modification + for ($j = $n - 1; $j < $nn; ++$j) { + $z = $this->H[$n - 1][$j]; + $this->H[$n - 1][$j] = $q * $z + $p * $this->H[$n][$j]; + $this->H[$n][$j] = $q * $this->H[$n][$j] - $p * $z; + } + // Column modification + for ($i = 0; $i <= $n; ++$i) { + $z = $this->H[$i][$n - 1]; + $this->H[$i][$n - 1] = $q * $z + $p * $this->H[$i][$n]; + $this->H[$i][$n] = $q * $this->H[$i][$n] - $p * $z; + } + // Accumulate transformations + for ($i = $low; $i <= $high; ++$i) { + $z = $this->V[$i][$n - 1]; + $this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n]; + $this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z; + } + // Complex pair + } else { + $this->d[$n - 1] = $x + $p; + $this->d[$n] = $x + $p; + $this->e[$n - 1] = $z; + $this->e[$n] = -$z; + } + $n = $n - 2; + $iter = 0; + // No convergence yet + } else { + // Form shift + $x = $this->H[$n][$n]; + $y = 0.0; + $w = 0.0; + if ($l < $n) { + $y = $this->H[$n - 1][$n - 1]; + $w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n]; + } + // Wilkinson's original ad hoc shift + if ($iter == 10) { + $exshift += $x; + for ($i = $low; $i <= $n; ++$i) { + $this->H[$i][$i] -= $x; + } + $s = abs($this->H[$n][$n - 1]) + abs($this->H[$n - 1][$n - 2]); + $x = $y = 0.75 * $s; + $w = -0.4375 * $s * $s; + } + // MATLAB's new ad hoc shift + if ($iter == 30) { + $s = ($y - $x) / 2.0; + $s = $s * $s + $w; + if ($s > 0) { + $s = sqrt($s); + if ($y < $x) { + $s = -$s; + } + $s = $x - $w / (($y - $x) / 2.0 + $s); + for ($i = $low; $i <= $n; ++$i) { + $this->H[$i][$i] -= $s; + } + $exshift += $s; + $x = $y = $w = 0.964; + } + } + // Could check iteration count here. + $iter = $iter + 1; + // Look for two consecutive small sub-diagonal elements + $m = $n - 2; + while ($m >= $l) { + $z = $this->H[$m][$m]; + $r = $x - $z; + $s = $y - $z; + $p = ($r * $s - $w) / $this->H[$m + 1][$m] + $this->H[$m][$m + 1]; + $q = $this->H[$m + 1][$m + 1] - $z - $r - $s; + $r = $this->H[$m + 2][$m + 1]; + $s = abs($p) + abs($q) + abs($r); + $p = $p / $s; + $q = $q / $s; + $r = $r / $s; + if ($m == $l) { + break; + } + if (abs($this->H[$m][$m - 1]) * (abs($q) + abs($r)) < + $eps * (abs($p) * (abs($this->H[$m - 1][$m - 1]) + abs($z) + abs($this->H[$m + 1][$m + 1])))) { + break; + } + --$m; + } + for ($i = $m + 2; $i <= $n; ++$i) { + $this->H[$i][$i - 2] = 0.0; + if ($i > $m + 2) { + $this->H[$i][$i - 3] = 0.0; + } + } + // Double QR step involving rows l:n and columns m:n + for ($k = $m; $k <= $n - 1; ++$k) { + $notlast = ($k != $n - 1); + if ($k != $m) { + $p = $this->H[$k][$k - 1]; + $q = $this->H[$k + 1][$k - 1]; + $r = ($notlast ? $this->H[$k + 2][$k - 1] : 0.0); + $x = abs($p) + abs($q) + abs($r); + if ($x != 0.0) { + $p = $p / $x; + $q = $q / $x; + $r = $r / $x; + } + } + if ($x == 0.0) { + break; + } + $s = sqrt($p * $p + $q * $q + $r * $r); + if ($p < 0) { + $s = -$s; + } + if ($s != 0) { + if ($k != $m) { + $this->H[$k][$k - 1] = -$s * $x; + } elseif ($l != $m) { + $this->H[$k][$k - 1] = -$this->H[$k][$k - 1]; + } + $p = $p + $s; + $x = $p / $s; + $y = $q / $s; + $z = $r / $s; + $q = $q / $p; + $r = $r / $p; + // Row modification + for ($j = $k; $j < $nn; ++$j) { + $p = $this->H[$k][$j] + $q * $this->H[$k + 1][$j]; + if ($notlast) { + $p = $p + $r * $this->H[$k + 2][$j]; + $this->H[$k + 2][$j] = $this->H[$k + 2][$j] - $p * $z; + } + $this->H[$k][$j] = $this->H[$k][$j] - $p * $x; + $this->H[$k + 1][$j] = $this->H[$k + 1][$j] - $p * $y; + } + // Column modification + $iMax = min($n, $k + 3); + for ($i = 0; $i <= $iMax; ++$i) { + $p = $x * $this->H[$i][$k] + $y * $this->H[$i][$k + 1]; + if ($notlast) { + $p = $p + $z * $this->H[$i][$k + 2]; + $this->H[$i][$k + 2] = $this->H[$i][$k + 2] - $p * $r; + } + $this->H[$i][$k] = $this->H[$i][$k] - $p; + $this->H[$i][$k + 1] = $this->H[$i][$k + 1] - $p * $q; + } + // Accumulate transformations + for ($i = $low; $i <= $high; ++$i) { + $p = $x * $this->V[$i][$k] + $y * $this->V[$i][$k + 1]; + if ($notlast) { + $p = $p + $z * $this->V[$i][$k + 2]; + $this->V[$i][$k + 2] = $this->V[$i][$k + 2] - $p * $r; + } + $this->V[$i][$k] = $this->V[$i][$k] - $p; + $this->V[$i][$k + 1] = $this->V[$i][$k + 1] - $p * $q; + } + } // ($s != 0) + } // k loop + } // check convergence + } // while ($n >= $low) + + // Backsubstitute to find vectors of upper triangular form + if ($norm == 0.0) { + return; + } + + for ($n = $nn - 1; $n >= 0; --$n) { + $p = $this->d[$n]; + $q = $this->e[$n]; + // Real vector + if ($q == 0) { + $l = $n; + $this->H[$n][$n] = 1.0; + for ($i = $n - 1; $i >= 0; --$i) { + $w = $this->H[$i][$i] - $p; + $r = 0.0; + for ($j = $l; $j <= $n; ++$j) { + $r = $r + $this->H[$i][$j] * $this->H[$j][$n]; + } + if ($this->e[$i] < 0.0) { + $z = $w; + $s = $r; + } else { + $l = $i; + if ($this->e[$i] == 0.0) { + if ($w != 0.0) { + $this->H[$i][$n] = -$r / $w; + } else { + $this->H[$i][$n] = -$r / ($eps * $norm); + } + // Solve real equations + } else { + $x = $this->H[$i][$i + 1]; + $y = $this->H[$i + 1][$i]; + $q = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i]; + $t = ($x * $s - $z * $r) / $q; + $this->H[$i][$n] = $t; + if (abs($x) > abs($z)) { + $this->H[$i + 1][$n] = (-$r - $w * $t) / $x; + } else { + $this->H[$i + 1][$n] = (-$s - $y * $t) / $z; + } + } + // Overflow control + $t = abs($this->H[$i][$n]); + if (($eps * $t) * $t > 1) { + for ($j = $i; $j <= $n; ++$j) { + $this->H[$j][$n] = $this->H[$j][$n] / $t; + } + } + } + } + // Complex vector + } elseif ($q < 0) { + $l = $n - 1; + // Last vector component imaginary so matrix is triangular + if (abs($this->H[$n][$n - 1]) > abs($this->H[$n - 1][$n])) { + $this->H[$n - 1][$n - 1] = $q / $this->H[$n][$n - 1]; + $this->H[$n - 1][$n] = -($this->H[$n][$n] - $p) / $this->H[$n][$n - 1]; + } else { + $this->cdiv(0.0, -$this->H[$n - 1][$n], $this->H[$n - 1][$n - 1] - $p, $q); + $this->H[$n - 1][$n - 1] = $this->cdivr; + $this->H[$n - 1][$n] = $this->cdivi; + } + $this->H[$n][$n - 1] = 0.0; + $this->H[$n][$n] = 1.0; + for ($i = $n - 2; $i >= 0; --$i) { + // double ra,sa,vr,vi; + $ra = 0.0; + $sa = 0.0; + for ($j = $l; $j <= $n; ++$j) { + $ra = $ra + $this->H[$i][$j] * $this->H[$j][$n - 1]; + $sa = $sa + $this->H[$i][$j] * $this->H[$j][$n]; + } + $w = $this->H[$i][$i] - $p; + if ($this->e[$i] < 0.0) { + $z = $w; + $r = $ra; + $s = $sa; + } else { + $l = $i; + if ($this->e[$i] == 0) { + $this->cdiv(-$ra, -$sa, $w, $q); + $this->H[$i][$n - 1] = $this->cdivr; + $this->H[$i][$n] = $this->cdivi; + } else { + // Solve complex equations + $x = $this->H[$i][$i + 1]; + $y = $this->H[$i + 1][$i]; + $vr = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i] - $q * $q; + $vi = ($this->d[$i] - $p) * 2.0 * $q; + if ($vr == 0.0 & $vi == 0.0) { + $vr = $eps * $norm * (abs($w) + abs($q) + abs($x) + abs($y) + abs($z)); + } + $this->cdiv($x * $r - $z * $ra + $q * $sa, $x * $s - $z * $sa - $q * $ra, $vr, $vi); + $this->H[$i][$n - 1] = $this->cdivr; + $this->H[$i][$n] = $this->cdivi; + if (abs($x) > (abs($z) + abs($q))) { + $this->H[$i + 1][$n - 1] = (-$ra - $w * $this->H[$i][$n - 1] + $q * $this->H[$i][$n]) / $x; + $this->H[$i + 1][$n] = (-$sa - $w * $this->H[$i][$n] - $q * $this->H[$i][$n - 1]) / $x; + } else { + $this->cdiv(-$r - $y * $this->H[$i][$n - 1], -$s - $y * $this->H[$i][$n], $z, $q); + $this->H[$i + 1][$n - 1] = $this->cdivr; + $this->H[$i + 1][$n] = $this->cdivi; + } + } + // Overflow control + $t = max(abs($this->H[$i][$n - 1]), abs($this->H[$i][$n])); + if (($eps * $t) * $t > 1) { + for ($j = $i; $j <= $n; ++$j) { + $this->H[$j][$n - 1] = $this->H[$j][$n - 1] / $t; + $this->H[$j][$n] = $this->H[$j][$n] / $t; + } + } + } // end else + } // end for + } // end else for complex case + } // end for + + // Vectors of isolated roots + for ($i = 0; $i < $nn; ++$i) { + if ($i < $low | $i > $high) { + for ($j = $i; $j < $nn; ++$j) { + $this->V[$i][$j] = $this->H[$i][$j]; + } + } + } + + // Back transformation to get eigenvectors of original matrix + for ($j = $nn - 1; $j >= $low; --$j) { + for ($i = $low; $i <= $high; ++$i) { + $z = 0.0; + $kMax = min($j, $high); + for ($k = $low; $k <= $kMax; ++$k) { + $z = $z + $this->V[$i][$k] * $this->H[$k][$j]; + } + $this->V[$i][$j] = $z; + } + } + } + + // end hqr2 + + /** + * Constructor: Check for symmetry, then construct the eigenvalue decomposition. + * + * @param mixed $Arg A Square matrix + */ + public function __construct($Arg) + { + $this->A = $Arg->getArray(); + $this->n = $Arg->getColumnDimension(); + + $issymmetric = true; + for ($j = 0; ($j < $this->n) & $issymmetric; ++$j) { + for ($i = 0; ($i < $this->n) & $issymmetric; ++$i) { + $issymmetric = ($this->A[$i][$j] == $this->A[$j][$i]); + } + } + + if ($issymmetric) { + $this->V = $this->A; + // Tridiagonalize. + $this->tred2(); + // Diagonalize. + $this->tql2(); + } else { + $this->H = $this->A; + $this->ort = []; + // Reduce to Hessenberg form. + $this->orthes(); + // Reduce Hessenberg to real Schur form. + $this->hqr2(); + } + } + + /** + * Return the eigenvector matrix. + * + * @return Matrix V + */ + public function getV() + { + return new Matrix($this->V, $this->n, $this->n); + } + + /** + * Return the real parts of the eigenvalues. + * + * @return array real(diag(D)) + */ + public function getRealEigenvalues() + { + return $this->d; + } + + /** + * Return the imaginary parts of the eigenvalues. + * + * @return array imag(diag(D)) + */ + public function getImagEigenvalues() + { + return $this->e; + } + + /** + * Return the block diagonal eigenvalue matrix. + * + * @return Matrix D + */ + public function getD() + { + for ($i = 0; $i < $this->n; ++$i) { + $D[$i] = array_fill(0, $this->n, 0.0); + $D[$i][$i] = $this->d[$i]; + if ($this->e[$i] == 0) { + continue; + } + $o = ($this->e[$i] > 0) ? $i + 1 : $i - 1; + $D[$i][$o] = $this->e[$i]; + } + + return new Matrix($D); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php new file mode 100644 index 00000000000..bb2b4b040a9 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php @@ -0,0 +1,285 @@ += n, the LU decomposition is an m-by-n + * unit lower triangular matrix L, an n-by-n upper triangular matrix U, + * and a permutation vector piv of length m so that A(piv,:) = L*U. + * If m < n, then L is m-by-m and U is m-by-n. + * + * The LU decompostion with pivoting always exists, even if the matrix is + * singular, so the constructor will never fail. The primary use of the + * LU decomposition is in the solution of square systems of simultaneous + * linear equations. This will fail if isNonsingular() returns false. + * + * @author Paul Meagher + * @author Bartosz Matosiuk + * @author Michael Bommarito + * + * @version 1.1 + */ +class LUDecomposition +{ + const MATRIX_SINGULAR_EXCEPTION = 'Can only perform operation on singular matrix.'; + const MATRIX_SQUARE_EXCEPTION = 'Mismatched Row dimension'; + + /** + * Decomposition storage. + * + * @var array + */ + private $LU = []; + + /** + * Row dimension. + * + * @var int + */ + private $m; + + /** + * Column dimension. + * + * @var int + */ + private $n; + + /** + * Pivot sign. + * + * @var int + */ + private $pivsign; + + /** + * Internal storage of pivot vector. + * + * @var array + */ + private $piv = []; + + /** + * LU Decomposition constructor. + * + * @param Matrix $A Rectangular matrix + */ + public function __construct($A) + { + if ($A instanceof Matrix) { + // Use a "left-looking", dot-product, Crout/Doolittle algorithm. + $this->LU = $A->getArray(); + $this->m = $A->getRowDimension(); + $this->n = $A->getColumnDimension(); + for ($i = 0; $i < $this->m; ++$i) { + $this->piv[$i] = $i; + } + $this->pivsign = 1; + $LUrowi = $LUcolj = []; + + // Outer loop. + for ($j = 0; $j < $this->n; ++$j) { + // Make a copy of the j-th column to localize references. + for ($i = 0; $i < $this->m; ++$i) { + $LUcolj[$i] = &$this->LU[$i][$j]; + } + // Apply previous transformations. + for ($i = 0; $i < $this->m; ++$i) { + $LUrowi = $this->LU[$i]; + // Most of the time is spent in the following dot product. + $kmax = min($i, $j); + $s = 0.0; + for ($k = 0; $k < $kmax; ++$k) { + $s += $LUrowi[$k] * $LUcolj[$k]; + } + $LUrowi[$j] = $LUcolj[$i] -= $s; + } + // Find pivot and exchange if necessary. + $p = $j; + for ($i = $j + 1; $i < $this->m; ++$i) { + if (abs($LUcolj[$i]) > abs($LUcolj[$p])) { + $p = $i; + } + } + if ($p != $j) { + for ($k = 0; $k < $this->n; ++$k) { + $t = $this->LU[$p][$k]; + $this->LU[$p][$k] = $this->LU[$j][$k]; + $this->LU[$j][$k] = $t; + } + $k = $this->piv[$p]; + $this->piv[$p] = $this->piv[$j]; + $this->piv[$j] = $k; + $this->pivsign = $this->pivsign * -1; + } + // Compute multipliers. + if (($j < $this->m) && ($this->LU[$j][$j] != 0.0)) { + for ($i = $j + 1; $i < $this->m; ++$i) { + $this->LU[$i][$j] /= $this->LU[$j][$j]; + } + } + } + } else { + throw new CalculationException(Matrix::ARGUMENT_TYPE_EXCEPTION); + } + } + + // function __construct() + + /** + * Get lower triangular factor. + * + * @return Matrix Lower triangular factor + */ + public function getL() + { + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + if ($i > $j) { + $L[$i][$j] = $this->LU[$i][$j]; + } elseif ($i == $j) { + $L[$i][$j] = 1.0; + } else { + $L[$i][$j] = 0.0; + } + } + } + + return new Matrix($L); + } + + // function getL() + + /** + * Get upper triangular factor. + * + * @return Matrix Upper triangular factor + */ + public function getU() + { + for ($i = 0; $i < $this->n; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + if ($i <= $j) { + $U[$i][$j] = $this->LU[$i][$j]; + } else { + $U[$i][$j] = 0.0; + } + } + } + + return new Matrix($U); + } + + // function getU() + + /** + * Return pivot permutation vector. + * + * @return array Pivot vector + */ + public function getPivot() + { + return $this->piv; + } + + // function getPivot() + + /** + * Alias for getPivot. + * + * @see getPivot + */ + public function getDoublePivot() + { + return $this->getPivot(); + } + + // function getDoublePivot() + + /** + * Is the matrix nonsingular? + * + * @return bool true if U, and hence A, is nonsingular + */ + public function isNonsingular() + { + for ($j = 0; $j < $this->n; ++$j) { + if ($this->LU[$j][$j] == 0) { + return false; + } + } + + return true; + } + + // function isNonsingular() + + /** + * Count determinants. + * + * @return array d matrix deterninat + */ + public function det() + { + if ($this->m == $this->n) { + $d = $this->pivsign; + for ($j = 0; $j < $this->n; ++$j) { + $d *= $this->LU[$j][$j]; + } + + return $d; + } + + throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION); + } + + // function det() + + /** + * Solve A*X = B. + * + * @param mixed $B a Matrix with as many rows as A and any number of columns + * + * @throws CalculationException illegalArgumentException Matrix row dimensions must agree + * @throws CalculationException runtimeException Matrix is singular + * + * @return Matrix X so that L*U*X = B(piv,:) + */ + public function solve($B) + { + if ($B->getRowDimension() == $this->m) { + if ($this->isNonsingular()) { + // Copy right hand side with pivoting + $nx = $B->getColumnDimension(); + $X = $B->getMatrix($this->piv, 0, $nx - 1); + // Solve L*Y = B(piv,:) + for ($k = 0; $k < $this->n; ++$k) { + for ($i = $k + 1; $i < $this->n; ++$i) { + for ($j = 0; $j < $nx; ++$j) { + $X->A[$i][$j] -= $X->A[$k][$j] * $this->LU[$i][$k]; + } + } + } + // Solve U*X = Y; + for ($k = $this->n - 1; $k >= 0; --$k) { + for ($j = 0; $j < $nx; ++$j) { + $X->A[$k][$j] /= $this->LU[$k][$k]; + } + for ($i = 0; $i < $k; ++$i) { + for ($j = 0; $j < $nx; ++$j) { + $X->A[$i][$j] -= $X->A[$k][$j] * $this->LU[$i][$k]; + } + } + } + + return $X; + } + + throw new CalculationException(self::MATRIX_SINGULAR_EXCEPTION); + } + + throw new CalculationException(self::MATRIX_SQUARE_EXCEPTION); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/Matrix.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/Matrix.php new file mode 100644 index 00000000000..b3d82d466cc --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -0,0 +1,1233 @@ + 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + //Rectangular matrix - m x n initialized from 2D array + case 'array': + $this->m = count($args[0]); + $this->n = count($args[0][0]); + $this->A = $args[0]; + + break; + //Square matrix - n x n + case 'integer': + $this->m = $args[0]; + $this->n = $args[0]; + $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); + + break; + //Rectangular matrix - m x n + case 'integer,integer': + $this->m = $args[0]; + $this->n = $args[1]; + $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); + + break; + //Rectangular matrix - m x n initialized from packed array + case 'array,integer': + $this->m = $args[1]; + if ($this->m != 0) { + $this->n = count($args[0]) / $this->m; + } else { + $this->n = 0; + } + if (($this->m * $this->n) == count($args[0])) { + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = $args[0][$i + $j * $this->m]; + } + } + } else { + throw new CalculationException(self::ARRAY_LENGTH_EXCEPTION); + } + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + } else { + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } + + /** + * getArray. + * + * @return array Matrix array + */ + public function getArray() + { + return $this->A; + } + + /** + * getRowDimension. + * + * @return int Row dimension + */ + public function getRowDimension() + { + return $this->m; + } + + /** + * getColumnDimension. + * + * @return int Column dimension + */ + public function getColumnDimension() + { + return $this->n; + } + + /** + * get. + * + * Get the i,j-th element of the matrix. + * + * @param int $i Row position + * @param int $j Column position + * + * @return mixed Element (int/float/double) + */ + public function get($i = null, $j = null) + { + return $this->A[$i][$j]; + } + + /** + * getMatrix. + * + * Get a submatrix + * + * @param int $i0 Initial row index + * @param int $iF Final row index + * @param int $j0 Initial column index + * @param int $jF Final column index + * + * @return Matrix Submatrix + */ + public function getMatrix(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + //A($i0...; $j0...) + case 'integer,integer': + list($i0, $j0) = $args; + if ($i0 >= 0) { + $m = $this->m - $i0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if ($j0 >= 0) { + $n = $this->n - $j0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n); + for ($i = $i0; $i < $this->m; ++$i) { + for ($j = $j0; $j < $this->n; ++$j) { + $R->set($i, $j, $this->A[$i][$j]); + } + } + + return $R; + + break; + //A($i0...$iF; $j0...$jF) + case 'integer,integer,integer,integer': + list($i0, $iF, $j0, $jF) = $args; + if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { + $m = $iF - $i0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (($jF > $j0) && ($this->n >= $jF) && ($j0 >= 0)) { + $n = $jF - $j0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m + 1, $n + 1); + for ($i = $i0; $i <= $iF; ++$i) { + for ($j = $j0; $j <= $jF; ++$j) { + $R->set($i - $i0, $j - $j0, $this->A[$i][$j]); + } + } + + return $R; + + break; + //$R = array of row indices; $C = array of column indices + case 'array,array': + list($RL, $CL) = $args; + if (count($RL) > 0) { + $m = count($RL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (count($CL) > 0) { + $n = count($CL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n); + for ($i = 0; $i < $m; ++$i) { + for ($j = 0; $j < $n; ++$j) { + $R->set($i, $j, $this->A[$RL[$i]][$CL[$j]]); + } + } + + return $R; + + break; + //A($i0...$iF); $CL = array of column indices + case 'integer,integer,array': + list($i0, $iF, $CL) = $args; + if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { + $m = $iF - $i0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (count($CL) > 0) { + $n = count($CL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n); + for ($i = $i0; $i < $iF; ++$i) { + for ($j = 0; $j < $n; ++$j) { + $R->set($i - $i0, $j, $this->A[$i][$CL[$j]]); + } + } + + return $R; + + break; + //$RL = array of row indices + case 'array,integer,integer': + list($RL, $j0, $jF) = $args; + if (count($RL) > 0) { + $m = count($RL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (($jF >= $j0) && ($this->n >= $jF) && ($j0 >= 0)) { + $n = $jF - $j0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n + 1); + for ($i = 0; $i < $m; ++$i) { + for ($j = $j0; $j <= $jF; ++$j) { + $R->set($i, $j - $j0, $this->A[$RL[$i]][$j]); + } + } + + return $R; + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + } else { + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } + + /** + * checkMatrixDimensions. + * + * Is matrix B the same size? + * + * @param Matrix $B Matrix B + * + * @return bool + */ + public function checkMatrixDimensions($B = null) + { + if ($B instanceof self) { + if (($this->m == $B->getRowDimension()) && ($this->n == $B->getColumnDimension())) { + return true; + } + + throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); + } + + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + // function checkMatrixDimensions() + + /** + * set. + * + * Set the i,j-th element of the matrix. + * + * @param int $i Row position + * @param int $j Column position + * @param mixed $c Int/float/double value + * + * @return mixed Element (int/float/double) + */ + public function set($i = null, $j = null, $c = null) + { + // Optimized set version just has this + $this->A[$i][$j] = $c; + } + + // function set() + + /** + * identity. + * + * Generate an identity matrix. + * + * @param int $m Row dimension + * @param int $n Column dimension + * + * @return Matrix Identity matrix + */ + public function identity($m = null, $n = null) + { + return $this->diagonal($m, $n, 1); + } + + /** + * diagonal. + * + * Generate a diagonal matrix + * + * @param int $m Row dimension + * @param int $n Column dimension + * @param mixed $c Diagonal value + * + * @return Matrix Diagonal matrix + */ + public function diagonal($m = null, $n = null, $c = 1) + { + $R = new self($m, $n); + for ($i = 0; $i < $m; ++$i) { + $R->set($i, $i, $c); + } + + return $R; + } + + /** + * getMatrixByRow. + * + * Get a submatrix by row index/range + * + * @param int $i0 Initial row index + * @param int $iF Final row index + * + * @return Matrix Submatrix + */ + public function getMatrixByRow($i0 = null, $iF = null) + { + if (is_int($i0)) { + if (is_int($iF)) { + return $this->getMatrix($i0, 0, $iF + 1, $this->n); + } + + return $this->getMatrix($i0, 0, $i0 + 1, $this->n); + } + + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + /** + * getMatrixByCol. + * + * Get a submatrix by column index/range + * + * @param int $j0 Initial column index + * @param int $jF Final column index + * + * @return Matrix Submatrix + */ + public function getMatrixByCol($j0 = null, $jF = null) + { + if (is_int($j0)) { + if (is_int($jF)) { + return $this->getMatrix(0, $j0, $this->m, $jF + 1); + } + + return $this->getMatrix(0, $j0, $this->m, $j0 + 1); + } + + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + /** + * transpose. + * + * Tranpose matrix + * + * @return Matrix Transposed matrix + */ + public function transpose() + { + $R = new self($this->n, $this->m); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $R->set($j, $i, $this->A[$i][$j]); + } + } + + return $R; + } + + // function transpose() + + /** + * trace. + * + * Sum of diagonal elements + * + * @return float Sum of diagonal elements + */ + public function trace() + { + $s = 0; + $n = min($this->m, $this->n); + for ($i = 0; $i < $n; ++$i) { + $s += $this->A[$i][$i]; + } + + return $s; + } + + /** + * uminus. + * + * Unary minus matrix -A + * + * @return Matrix Unary minus matrix + */ + public function uminus() + { + } + + /** + * plus. + * + * A + B + * + * @param mixed $B Matrix/Array + * + * @return Matrix Sum + */ + public function plus(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) + $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * plusEquals. + * + * A = A + B + * + * @param mixed $B Matrix/Array + * + * @return Matrix Sum + */ + public function plusEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] += $value; + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * minus. + * + * A - B + * + * @param mixed $B Matrix/Array + * + * @return Matrix Sum + */ + public function minus(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) - $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * minusEquals. + * + * A = A - B + * + * @param mixed $B Matrix/Array + * + * @return Matrix Sum + */ + public function minusEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] -= $value; + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayTimes. + * + * Element-by-element multiplication + * Cij = Aij * Bij + * + * @param mixed $B Matrix/Array + * + * @return Matrix Matrix Cij + */ + public function arrayTimes(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) * $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayTimesEquals. + * + * Element-by-element multiplication + * Aij = Aij * Bij + * + * @param mixed $B Matrix/Array + * + * @return Matrix Matrix Aij + */ + public function arrayTimesEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] *= $value; + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayRightDivide. + * + * Element-by-element right division + * A / B + * + * @param Matrix $B Matrix B + * + * @return Matrix Division result + */ + public function arrayRightDivide(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + if ($value == 0) { + // Trap for Divide by Zero error + $M->set($i, $j, '#DIV/0!'); + } else { + $M->set($i, $j, $this->A[$i][$j] / $value); + } + } else { + $M->set($i, $j, Functions::NAN()); + } + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayRightDivideEquals. + * + * Element-by-element right division + * Aij = Aij / Bij + * + * @param mixed $B Matrix/Array + * + * @return Matrix Matrix Aij + */ + public function arrayRightDivideEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = $this->A[$i][$j] / $M->get($i, $j); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayLeftDivide. + * + * Element-by-element Left division + * A / B + * + * @param Matrix $B Matrix B + * + * @return Matrix Division result + */ + public function arrayLeftDivide(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) / $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayLeftDivideEquals. + * + * Element-by-element Left division + * Aij = Aij / Bij + * + * @param mixed $B Matrix/Array + * + * @return Matrix Matrix Aij + */ + public function arrayLeftDivideEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = $M->get($i, $j) / $this->A[$i][$j]; + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * times. + * + * Matrix multiplication + * + * @param mixed $n Matrix/Array/Scalar + * + * @return Matrix Product + */ + public function times(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $B = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + if ($this->n == $B->m) { + $C = new self($this->m, $B->n); + for ($j = 0; $j < $B->n; ++$j) { + $Bcolj = []; + for ($k = 0; $k < $this->n; ++$k) { + $Bcolj[$k] = $B->A[$k][$j]; + } + for ($i = 0; $i < $this->m; ++$i) { + $Arowi = $this->A[$i]; + $s = 0; + for ($k = 0; $k < $this->n; ++$k) { + $s += $Arowi[$k] * $Bcolj[$k]; + } + $C->A[$i][$j] = $s; + } + } + + return $C; + } + + throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); + case 'array': + $B = new self($args[0]); + if ($this->n == $B->m) { + $C = new self($this->m, $B->n); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $s = '0'; + for ($k = 0; $k < $C->n; ++$k) { + $s += $this->A[$i][$k] * $B->A[$k][$j]; + } + $C->A[$i][$j] = $s; + } + } + + return $C; + } + + throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); + case 'integer': + $C = new self($this->A); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $C->A[$i][$j] *= $args[0]; + } + } + + return $C; + case 'double': + $C = new self($this->m, $this->n); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $C->A[$i][$j] = $args[0] * $this->A[$i][$j]; + } + } + + return $C; + case 'float': + $C = new self($this->A); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $C->A[$i][$j] *= $args[0]; + } + } + + return $C; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } else { + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } + + /** + * power. + * + * A = A ^ B + * + * @param mixed $B Matrix/Array + * + * @return Matrix Sum + */ + public function power(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] = pow($this->A[$i][$j], $value); + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * concat. + * + * A = A & B + * + * @param mixed $B Matrix/Array + * + * @return Matrix Sum + */ + public function concat(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"') . trim($M->get($i, $j), '"'); + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * Solve A*X = B. + * + * @param Matrix $B Right hand side + * + * @return Matrix ... Solution if A is square, least squares solution otherwise + */ + public function solve($B) + { + if ($this->m == $this->n) { + $LU = new LUDecomposition($this); + + return $LU->solve($B); + } + $QR = new QRDecomposition($this); + + return $QR->solve($B); + } + + /** + * Matrix inverse or pseudoinverse. + * + * @return Matrix ... Inverse(A) if A is square, pseudoinverse otherwise. + */ + public function inverse() + { + return $this->solve($this->identity($this->m, $this->m)); + } + + /** + * det. + * + * Calculate determinant + * + * @return float Determinant + */ + public function det() + { + $L = new LUDecomposition($this); + + return $L->det(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php new file mode 100644 index 00000000000..e666d74b6a3 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php @@ -0,0 +1,249 @@ += n, the QR decomposition is an m-by-n + * orthogonal matrix Q and an n-by-n upper triangular matrix R so that + * A = Q*R. + * + * The QR decompostion always exists, even if the matrix does not have + * full rank, so the constructor will never fail. The primary use of the + * QR decomposition is in the least squares solution of nonsquare systems + * of simultaneous linear equations. This will fail if isFullRank() + * returns false. + * + * @author Paul Meagher + * + * @version 1.1 + */ +class QRDecomposition +{ + const MATRIX_RANK_EXCEPTION = 'Can only perform operation on full-rank matrix.'; + + /** + * Array for internal storage of decomposition. + * + * @var array + */ + private $QR = []; + + /** + * Row dimension. + * + * @var int + */ + private $m; + + /** + * Column dimension. + * + * @var int + */ + private $n; + + /** + * Array for internal storage of diagonal of R. + * + * @var array + */ + private $Rdiag = []; + + /** + * QR Decomposition computed by Householder reflections. + * + * @param matrix $A Rectangular matrix + */ + public function __construct($A) + { + if ($A instanceof Matrix) { + // Initialize. + $this->QR = $A->getArrayCopy(); + $this->m = $A->getRowDimension(); + $this->n = $A->getColumnDimension(); + // Main loop. + for ($k = 0; $k < $this->n; ++$k) { + // Compute 2-norm of k-th column without under/overflow. + $nrm = 0.0; + for ($i = $k; $i < $this->m; ++$i) { + $nrm = hypo($nrm, $this->QR[$i][$k]); + } + if ($nrm != 0.0) { + // Form k-th Householder vector. + if ($this->QR[$k][$k] < 0) { + $nrm = -$nrm; + } + for ($i = $k; $i < $this->m; ++$i) { + $this->QR[$i][$k] /= $nrm; + } + $this->QR[$k][$k] += 1.0; + // Apply transformation to remaining columns. + for ($j = $k + 1; $j < $this->n; ++$j) { + $s = 0.0; + for ($i = $k; $i < $this->m; ++$i) { + $s += $this->QR[$i][$k] * $this->QR[$i][$j]; + } + $s = -$s / $this->QR[$k][$k]; + for ($i = $k; $i < $this->m; ++$i) { + $this->QR[$i][$j] += $s * $this->QR[$i][$k]; + } + } + } + $this->Rdiag[$k] = -$nrm; + } + } else { + throw new CalculationException(Matrix::ARGUMENT_TYPE_EXCEPTION); + } + } + + // function __construct() + + /** + * Is the matrix full rank? + * + * @return bool true if R, and hence A, has full rank, else false + */ + public function isFullRank() + { + for ($j = 0; $j < $this->n; ++$j) { + if ($this->Rdiag[$j] == 0) { + return false; + } + } + + return true; + } + + // function isFullRank() + + /** + * Return the Householder vectors. + * + * @return Matrix Lower trapezoidal matrix whose columns define the reflections + */ + public function getH() + { + $H = []; + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + if ($i >= $j) { + $H[$i][$j] = $this->QR[$i][$j]; + } else { + $H[$i][$j] = 0.0; + } + } + } + + return new Matrix($H); + } + + // function getH() + + /** + * Return the upper triangular factor. + * + * @return Matrix upper triangular factor + */ + public function getR() + { + $R = []; + for ($i = 0; $i < $this->n; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + if ($i < $j) { + $R[$i][$j] = $this->QR[$i][$j]; + } elseif ($i == $j) { + $R[$i][$j] = $this->Rdiag[$i]; + } else { + $R[$i][$j] = 0.0; + } + } + } + + return new Matrix($R); + } + + // function getR() + + /** + * Generate and return the (economy-sized) orthogonal factor. + * + * @return Matrix orthogonal factor + */ + public function getQ() + { + $Q = []; + for ($k = $this->n - 1; $k >= 0; --$k) { + for ($i = 0; $i < $this->m; ++$i) { + $Q[$i][$k] = 0.0; + } + $Q[$k][$k] = 1.0; + for ($j = $k; $j < $this->n; ++$j) { + if ($this->QR[$k][$k] != 0) { + $s = 0.0; + for ($i = $k; $i < $this->m; ++$i) { + $s += $this->QR[$i][$k] * $Q[$i][$j]; + } + $s = -$s / $this->QR[$k][$k]; + for ($i = $k; $i < $this->m; ++$i) { + $Q[$i][$j] += $s * $this->QR[$i][$k]; + } + } + } + } + + return new Matrix($Q); + } + + // function getQ() + + /** + * Least squares solution of A*X = B. + * + * @param Matrix $B a Matrix with as many rows as A and any number of columns + * + * @return Matrix matrix that minimizes the two norm of Q*R*X-B + */ + public function solve($B) + { + if ($B->getRowDimension() == $this->m) { + if ($this->isFullRank()) { + // Copy right hand side + $nx = $B->getColumnDimension(); + $X = $B->getArrayCopy(); + // Compute Y = transpose(Q)*B + for ($k = 0; $k < $this->n; ++$k) { + for ($j = 0; $j < $nx; ++$j) { + $s = 0.0; + for ($i = $k; $i < $this->m; ++$i) { + $s += $this->QR[$i][$k] * $X[$i][$j]; + } + $s = -$s / $this->QR[$k][$k]; + for ($i = $k; $i < $this->m; ++$i) { + $X[$i][$j] += $s * $this->QR[$i][$k]; + } + } + } + // Solve R*X = Y; + for ($k = $this->n - 1; $k >= 0; --$k) { + for ($j = 0; $j < $nx; ++$j) { + $X[$k][$j] /= $this->Rdiag[$k]; + } + for ($i = 0; $i < $k; ++$i) { + for ($j = 0; $j < $nx; ++$j) { + $X[$i][$j] -= $X[$k][$j] * $this->QR[$i][$k]; + } + } + } + $X = new Matrix($X); + + return $X->getMatrix(0, $this->n - 1, 0, $nx); + } + + throw new CalculationException(self::MATRIX_RANK_EXCEPTION); + } + + throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php new file mode 100644 index 00000000000..3ca95619c86 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php @@ -0,0 +1,528 @@ += n, the singular value decomposition is + * an m-by-n orthogonal matrix U, an n-by-n diagonal matrix S, and + * an n-by-n orthogonal matrix V so that A = U*S*V'. + * + * The singular values, sigma[$k] = S[$k][$k], are ordered so that + * sigma[0] >= sigma[1] >= ... >= sigma[n-1]. + * + * The singular value decompostion always exists, so the constructor will + * never fail. The matrix condition number and the effective numerical + * rank can be computed from this decomposition. + * + * @author Paul Meagher + * + * @version 1.1 + */ +class SingularValueDecomposition +{ + /** + * Internal storage of U. + * + * @var array + */ + private $U = []; + + /** + * Internal storage of V. + * + * @var array + */ + private $V = []; + + /** + * Internal storage of singular values. + * + * @var array + */ + private $s = []; + + /** + * Row dimension. + * + * @var int + */ + private $m; + + /** + * Column dimension. + * + * @var int + */ + private $n; + + /** + * Construct the singular value decomposition. + * + * Derived from LINPACK code. + * + * @param mixed $Arg Rectangular matrix + */ + public function __construct($Arg) + { + // Initialize. + $A = $Arg->getArrayCopy(); + $this->m = $Arg->getRowDimension(); + $this->n = $Arg->getColumnDimension(); + $nu = min($this->m, $this->n); + $e = []; + $work = []; + $wantu = true; + $wantv = true; + $nct = min($this->m - 1, $this->n); + $nrt = max(0, min($this->n - 2, $this->m)); + + // Reduce A to bidiagonal form, storing the diagonal elements + // in s and the super-diagonal elements in e. + $kMax = max($nct, $nrt); + for ($k = 0; $k < $kMax; ++$k) { + if ($k < $nct) { + // Compute the transformation for the k-th column and + // place the k-th diagonal in s[$k]. + // Compute 2-norm of k-th column without under/overflow. + $this->s[$k] = 0; + for ($i = $k; $i < $this->m; ++$i) { + $this->s[$k] = hypo($this->s[$k], $A[$i][$k]); + } + if ($this->s[$k] != 0.0) { + if ($A[$k][$k] < 0.0) { + $this->s[$k] = -$this->s[$k]; + } + for ($i = $k; $i < $this->m; ++$i) { + $A[$i][$k] /= $this->s[$k]; + } + $A[$k][$k] += 1.0; + } + $this->s[$k] = -$this->s[$k]; + } + + for ($j = $k + 1; $j < $this->n; ++$j) { + if (($k < $nct) & ($this->s[$k] != 0.0)) { + // Apply the transformation. + $t = 0; + for ($i = $k; $i < $this->m; ++$i) { + $t += $A[$i][$k] * $A[$i][$j]; + } + $t = -$t / $A[$k][$k]; + for ($i = $k; $i < $this->m; ++$i) { + $A[$i][$j] += $t * $A[$i][$k]; + } + // Place the k-th row of A into e for the + // subsequent calculation of the row transformation. + $e[$j] = $A[$k][$j]; + } + } + + if ($wantu and ($k < $nct)) { + // Place the transformation in U for subsequent back + // multiplication. + for ($i = $k; $i < $this->m; ++$i) { + $this->U[$i][$k] = $A[$i][$k]; + } + } + + if ($k < $nrt) { + // Compute the k-th row transformation and place the + // k-th super-diagonal in e[$k]. + // Compute 2-norm without under/overflow. + $e[$k] = 0; + for ($i = $k + 1; $i < $this->n; ++$i) { + $e[$k] = hypo($e[$k], $e[$i]); + } + if ($e[$k] != 0.0) { + if ($e[$k + 1] < 0.0) { + $e[$k] = -$e[$k]; + } + for ($i = $k + 1; $i < $this->n; ++$i) { + $e[$i] /= $e[$k]; + } + $e[$k + 1] += 1.0; + } + $e[$k] = -$e[$k]; + if (($k + 1 < $this->m) and ($e[$k] != 0.0)) { + // Apply the transformation. + for ($i = $k + 1; $i < $this->m; ++$i) { + $work[$i] = 0.0; + } + for ($j = $k + 1; $j < $this->n; ++$j) { + for ($i = $k + 1; $i < $this->m; ++$i) { + $work[$i] += $e[$j] * $A[$i][$j]; + } + } + for ($j = $k + 1; $j < $this->n; ++$j) { + $t = -$e[$j] / $e[$k + 1]; + for ($i = $k + 1; $i < $this->m; ++$i) { + $A[$i][$j] += $t * $work[$i]; + } + } + } + if ($wantv) { + // Place the transformation in V for subsequent + // back multiplication. + for ($i = $k + 1; $i < $this->n; ++$i) { + $this->V[$i][$k] = $e[$i]; + } + } + } + } + + // Set up the final bidiagonal matrix or order p. + $p = min($this->n, $this->m + 1); + if ($nct < $this->n) { + $this->s[$nct] = $A[$nct][$nct]; + } + if ($this->m < $p) { + $this->s[$p - 1] = 0.0; + } + if ($nrt + 1 < $p) { + $e[$nrt] = $A[$nrt][$p - 1]; + } + $e[$p - 1] = 0.0; + // If required, generate U. + if ($wantu) { + for ($j = $nct; $j < $nu; ++$j) { + for ($i = 0; $i < $this->m; ++$i) { + $this->U[$i][$j] = 0.0; + } + $this->U[$j][$j] = 1.0; + } + for ($k = $nct - 1; $k >= 0; --$k) { + if ($this->s[$k] != 0.0) { + for ($j = $k + 1; $j < $nu; ++$j) { + $t = 0; + for ($i = $k; $i < $this->m; ++$i) { + $t += $this->U[$i][$k] * $this->U[$i][$j]; + } + $t = -$t / $this->U[$k][$k]; + for ($i = $k; $i < $this->m; ++$i) { + $this->U[$i][$j] += $t * $this->U[$i][$k]; + } + } + for ($i = $k; $i < $this->m; ++$i) { + $this->U[$i][$k] = -$this->U[$i][$k]; + } + $this->U[$k][$k] = 1.0 + $this->U[$k][$k]; + for ($i = 0; $i < $k - 1; ++$i) { + $this->U[$i][$k] = 0.0; + } + } else { + for ($i = 0; $i < $this->m; ++$i) { + $this->U[$i][$k] = 0.0; + } + $this->U[$k][$k] = 1.0; + } + } + } + + // If required, generate V. + if ($wantv) { + for ($k = $this->n - 1; $k >= 0; --$k) { + if (($k < $nrt) and ($e[$k] != 0.0)) { + for ($j = $k + 1; $j < $nu; ++$j) { + $t = 0; + for ($i = $k + 1; $i < $this->n; ++$i) { + $t += $this->V[$i][$k] * $this->V[$i][$j]; + } + $t = -$t / $this->V[$k + 1][$k]; + for ($i = $k + 1; $i < $this->n; ++$i) { + $this->V[$i][$j] += $t * $this->V[$i][$k]; + } + } + } + for ($i = 0; $i < $this->n; ++$i) { + $this->V[$i][$k] = 0.0; + } + $this->V[$k][$k] = 1.0; + } + } + + // Main iteration loop for the singular values. + $pp = $p - 1; + $iter = 0; + $eps = pow(2.0, -52.0); + + while ($p > 0) { + // Here is where a test for too many iterations would go. + // This section of the program inspects for negligible + // elements in the s and e arrays. On completion the + // variables kase and k are set as follows: + // kase = 1 if s(p) and e[k-1] are negligible and k

= -1; --$k) { + if ($k == -1) { + break; + } + if (abs($e[$k]) <= $eps * (abs($this->s[$k]) + abs($this->s[$k + 1]))) { + $e[$k] = 0.0; + + break; + } + } + if ($k == $p - 2) { + $kase = 4; + } else { + for ($ks = $p - 1; $ks >= $k; --$ks) { + if ($ks == $k) { + break; + } + $t = ($ks != $p ? abs($e[$ks]) : 0.) + ($ks != $k + 1 ? abs($e[$ks - 1]) : 0.); + if (abs($this->s[$ks]) <= $eps * $t) { + $this->s[$ks] = 0.0; + + break; + } + } + if ($ks == $k) { + $kase = 3; + } elseif ($ks == $p - 1) { + $kase = 1; + } else { + $kase = 2; + $k = $ks; + } + } + ++$k; + + // Perform the task indicated by kase. + switch ($kase) { + // Deflate negligible s(p). + case 1: + $f = $e[$p - 2]; + $e[$p - 2] = 0.0; + for ($j = $p - 2; $j >= $k; --$j) { + $t = hypo($this->s[$j], $f); + $cs = $this->s[$j] / $t; + $sn = $f / $t; + $this->s[$j] = $t; + if ($j != $k) { + $f = -$sn * $e[$j - 1]; + $e[$j - 1] = $cs * $e[$j - 1]; + } + if ($wantv) { + for ($i = 0; $i < $this->n; ++$i) { + $t = $cs * $this->V[$i][$j] + $sn * $this->V[$i][$p - 1]; + $this->V[$i][$p - 1] = -$sn * $this->V[$i][$j] + $cs * $this->V[$i][$p - 1]; + $this->V[$i][$j] = $t; + } + } + } + + break; + // Split at negligible s(k). + case 2: + $f = $e[$k - 1]; + $e[$k - 1] = 0.0; + for ($j = $k; $j < $p; ++$j) { + $t = hypo($this->s[$j], $f); + $cs = $this->s[$j] / $t; + $sn = $f / $t; + $this->s[$j] = $t; + $f = -$sn * $e[$j]; + $e[$j] = $cs * $e[$j]; + if ($wantu) { + for ($i = 0; $i < $this->m; ++$i) { + $t = $cs * $this->U[$i][$j] + $sn * $this->U[$i][$k - 1]; + $this->U[$i][$k - 1] = -$sn * $this->U[$i][$j] + $cs * $this->U[$i][$k - 1]; + $this->U[$i][$j] = $t; + } + } + } + + break; + // Perform one qr step. + case 3: + // Calculate the shift. + $scale = max(max(max(max(abs($this->s[$p - 1]), abs($this->s[$p - 2])), abs($e[$p - 2])), abs($this->s[$k])), abs($e[$k])); + $sp = $this->s[$p - 1] / $scale; + $spm1 = $this->s[$p - 2] / $scale; + $epm1 = $e[$p - 2] / $scale; + $sk = $this->s[$k] / $scale; + $ek = $e[$k] / $scale; + $b = (($spm1 + $sp) * ($spm1 - $sp) + $epm1 * $epm1) / 2.0; + $c = ($sp * $epm1) * ($sp * $epm1); + $shift = 0.0; + if (($b != 0.0) || ($c != 0.0)) { + $shift = sqrt($b * $b + $c); + if ($b < 0.0) { + $shift = -$shift; + } + $shift = $c / ($b + $shift); + } + $f = ($sk + $sp) * ($sk - $sp) + $shift; + $g = $sk * $ek; + // Chase zeros. + for ($j = $k; $j < $p - 1; ++$j) { + $t = hypo($f, $g); + $cs = $f / $t; + $sn = $g / $t; + if ($j != $k) { + $e[$j - 1] = $t; + } + $f = $cs * $this->s[$j] + $sn * $e[$j]; + $e[$j] = $cs * $e[$j] - $sn * $this->s[$j]; + $g = $sn * $this->s[$j + 1]; + $this->s[$j + 1] = $cs * $this->s[$j + 1]; + if ($wantv) { + for ($i = 0; $i < $this->n; ++$i) { + $t = $cs * $this->V[$i][$j] + $sn * $this->V[$i][$j + 1]; + $this->V[$i][$j + 1] = -$sn * $this->V[$i][$j] + $cs * $this->V[$i][$j + 1]; + $this->V[$i][$j] = $t; + } + } + $t = hypo($f, $g); + $cs = $f / $t; + $sn = $g / $t; + $this->s[$j] = $t; + $f = $cs * $e[$j] + $sn * $this->s[$j + 1]; + $this->s[$j + 1] = -$sn * $e[$j] + $cs * $this->s[$j + 1]; + $g = $sn * $e[$j + 1]; + $e[$j + 1] = $cs * $e[$j + 1]; + if ($wantu && ($j < $this->m - 1)) { + for ($i = 0; $i < $this->m; ++$i) { + $t = $cs * $this->U[$i][$j] + $sn * $this->U[$i][$j + 1]; + $this->U[$i][$j + 1] = -$sn * $this->U[$i][$j] + $cs * $this->U[$i][$j + 1]; + $this->U[$i][$j] = $t; + } + } + } + $e[$p - 2] = $f; + $iter = $iter + 1; + + break; + // Convergence. + case 4: + // Make the singular values positive. + if ($this->s[$k] <= 0.0) { + $this->s[$k] = ($this->s[$k] < 0.0 ? -$this->s[$k] : 0.0); + if ($wantv) { + for ($i = 0; $i <= $pp; ++$i) { + $this->V[$i][$k] = -$this->V[$i][$k]; + } + } + } + // Order the singular values. + while ($k < $pp) { + if ($this->s[$k] >= $this->s[$k + 1]) { + break; + } + $t = $this->s[$k]; + $this->s[$k] = $this->s[$k + 1]; + $this->s[$k + 1] = $t; + if ($wantv and ($k < $this->n - 1)) { + for ($i = 0; $i < $this->n; ++$i) { + $t = $this->V[$i][$k + 1]; + $this->V[$i][$k + 1] = $this->V[$i][$k]; + $this->V[$i][$k] = $t; + } + } + if ($wantu and ($k < $this->m - 1)) { + for ($i = 0; $i < $this->m; ++$i) { + $t = $this->U[$i][$k + 1]; + $this->U[$i][$k + 1] = $this->U[$i][$k]; + $this->U[$i][$k] = $t; + } + } + ++$k; + } + $iter = 0; + --$p; + + break; + } // end switch + } // end while + } + + /** + * Return the left singular vectors. + * + * @return Matrix U + */ + public function getU() + { + return new Matrix($this->U, $this->m, min($this->m + 1, $this->n)); + } + + /** + * Return the right singular vectors. + * + * @return Matrix V + */ + public function getV() + { + return new Matrix($this->V); + } + + /** + * Return the one-dimensional array of singular values. + * + * @return array diagonal of S + */ + public function getSingularValues() + { + return $this->s; + } + + /** + * Return the diagonal matrix of singular values. + * + * @return Matrix S + */ + public function getS() + { + for ($i = 0; $i < $this->n; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $S[$i][$j] = 0.0; + } + $S[$i][$i] = $this->s[$i]; + } + + return new Matrix($S); + } + + /** + * Two norm. + * + * @return float max(S) + */ + public function norm2() + { + return $this->s[0]; + } + + /** + * Two norm condition number. + * + * @return float max(S)/min(S) + */ + public function cond() + { + return $this->s[0] / $this->s[min($this->m, $this->n) - 1]; + } + + /** + * Effective numerical matrix rank. + * + * @return int Number of nonnegligible singular values + */ + public function rank() + { + $eps = pow(2.0, -52.0); + $tol = max($this->m, $this->n) * $this->s[0] * $eps; + $r = 0; + $iMax = count($this->s); + for ($i = 0; $i < $iMax; ++$i) { + if ($this->s[$i] > $tol) { + ++$r; + } + } + + return $r; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/utils/Maths.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/utils/Maths.php new file mode 100644 index 00000000000..68c3864932c --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/JAMA/utils/Maths.php @@ -0,0 +1,30 @@ + abs($b)) { + $r = $b / $a; + $r = abs($a) * sqrt(1 + $r * $r); + } elseif ($b != 0) { + $r = $a / $b; + $r = abs($b) * sqrt(1 + $r * $r); + } else { + $r = 0.0; + } + + return $r; +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE.php new file mode 100644 index 00000000000..2e9ec256734 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE.php @@ -0,0 +1,567 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// + +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; +use PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root; + +/* + * Array for storing OLE instances that are accessed from + * OLE_ChainedBlockStream::stream_open(). + * + * @var array + */ +$GLOBALS['_OLE_INSTANCES'] = []; + +/** + * OLE package base class. + * + * @author Xavier Noguer + * @author Christian Schmidt + * + * @category PhpSpreadsheet + */ +class OLE +{ + const OLE_PPS_TYPE_ROOT = 5; + const OLE_PPS_TYPE_DIR = 1; + const OLE_PPS_TYPE_FILE = 2; + const OLE_DATA_SIZE_SMALL = 0x1000; + const OLE_LONG_INT_SIZE = 4; + const OLE_PPS_SIZE = 0x80; + + /** + * The file handle for reading an OLE container. + * + * @var resource + */ + public $_file_handle; + + /** + * Array of PPS's found on the OLE container. + * + * @var array + */ + public $_list = []; + + /** + * Root directory of OLE container. + * + * @var Root + */ + public $root; + + /** + * Big Block Allocation Table. + * + * @var array (blockId => nextBlockId) + */ + public $bbat; + + /** + * Short Block Allocation Table. + * + * @var array (blockId => nextBlockId) + */ + public $sbat; + + /** + * Size of big blocks. This is usually 512. + * + * @var int number of octets per block + */ + public $bigBlockSize; + + /** + * Size of small blocks. This is usually 64. + * + * @var int number of octets per block + */ + public $smallBlockSize; + + /** + * Threshold for big blocks. + * + * @var int + */ + public $bigBlockThreshold; + + /** + * Reads an OLE container from the contents of the file given. + * + * @acces public + * + * @param string $file + * + * @throws ReaderException + * + * @return bool true on success, PEAR_Error on failure + */ + public function read($file) + { + $fh = fopen($file, 'r'); + if (!$fh) { + throw new ReaderException("Can't open file $file"); + } + $this->_file_handle = $fh; + + $signature = fread($fh, 8); + if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) { + throw new ReaderException("File doesn't seem to be an OLE container."); + } + fseek($fh, 28); + if (fread($fh, 2) != "\xFE\xFF") { + // This shouldn't be a problem in practice + throw new ReaderException('Only Little-Endian encoding is supported.'); + } + // Size of blocks and short blocks in bytes + $this->bigBlockSize = pow(2, self::_readInt2($fh)); + $this->smallBlockSize = pow(2, self::_readInt2($fh)); + + // Skip UID, revision number and version number + fseek($fh, 44); + // Number of blocks in Big Block Allocation Table + $bbatBlockCount = self::_readInt4($fh); + + // Root chain 1st block + $directoryFirstBlockId = self::_readInt4($fh); + + // Skip unused bytes + fseek($fh, 56); + // Streams shorter than this are stored using small blocks + $this->bigBlockThreshold = self::_readInt4($fh); + // Block id of first sector in Short Block Allocation Table + $sbatFirstBlockId = self::_readInt4($fh); + // Number of blocks in Short Block Allocation Table + $sbbatBlockCount = self::_readInt4($fh); + // Block id of first sector in Master Block Allocation Table + $mbatFirstBlockId = self::_readInt4($fh); + // Number of blocks in Master Block Allocation Table + $mbbatBlockCount = self::_readInt4($fh); + $this->bbat = []; + + // Remaining 4 * 109 bytes of current block is beginning of Master + // Block Allocation Table + $mbatBlocks = []; + for ($i = 0; $i < 109; ++$i) { + $mbatBlocks[] = self::_readInt4($fh); + } + + // Read rest of Master Block Allocation Table (if any is left) + $pos = $this->_getBlockOffset($mbatFirstBlockId); + for ($i = 0; $i < $mbbatBlockCount; ++$i) { + fseek($fh, $pos); + for ($j = 0; $j < $this->bigBlockSize / 4 - 1; ++$j) { + $mbatBlocks[] = self::_readInt4($fh); + } + // Last block id in each block points to next block + $pos = $this->_getBlockOffset(self::_readInt4($fh)); + } + + // Read Big Block Allocation Table according to chain specified by $mbatBlocks + for ($i = 0; $i < $bbatBlockCount; ++$i) { + $pos = $this->_getBlockOffset($mbatBlocks[$i]); + fseek($fh, $pos); + for ($j = 0; $j < $this->bigBlockSize / 4; ++$j) { + $this->bbat[] = self::_readInt4($fh); + } + } + + // Read short block allocation table (SBAT) + $this->sbat = []; + $shortBlockCount = $sbbatBlockCount * $this->bigBlockSize / 4; + $sbatFh = $this->getStream($sbatFirstBlockId); + for ($blockId = 0; $blockId < $shortBlockCount; ++$blockId) { + $this->sbat[$blockId] = self::_readInt4($sbatFh); + } + fclose($sbatFh); + + $this->_readPpsWks($directoryFirstBlockId); + + return true; + } + + /** + * @param int $blockId byte offset from beginning of file + * + * @return int + */ + public function _getBlockOffset($blockId) + { + return 512 + $blockId * $this->bigBlockSize; + } + + /** + * Returns a stream for use with fread() etc. External callers should + * use \PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File::getStream(). + * + * @param int|OLE\PPS $blockIdOrPps block id or PPS + * + * @return resource read-only stream + */ + public function getStream($blockIdOrPps) + { + static $isRegistered = false; + if (!$isRegistered) { + stream_wrapper_register('ole-chainedblockstream', ChainedBlockStream::class); + $isRegistered = true; + } + + // Store current instance in global array, so that it can be accessed + // in OLE_ChainedBlockStream::stream_open(). + // Object is removed from self::$instances in OLE_Stream::close(). + $GLOBALS['_OLE_INSTANCES'][] = $this; + $instanceId = end(array_keys($GLOBALS['_OLE_INSTANCES'])); + + $path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId; + if ($blockIdOrPps instanceof OLE\PPS) { + $path .= '&blockId=' . $blockIdOrPps->startBlock; + $path .= '&size=' . $blockIdOrPps->Size; + } else { + $path .= '&blockId=' . $blockIdOrPps; + } + + return fopen($path, 'r'); + } + + /** + * Reads a signed char. + * + * @param resource $fh file handle + * + * @return int + */ + private static function _readInt1($fh) + { + list(, $tmp) = unpack('c', fread($fh, 1)); + + return $tmp; + } + + /** + * Reads an unsigned short (2 octets). + * + * @param resource $fh file handle + * + * @return int + */ + private static function _readInt2($fh) + { + list(, $tmp) = unpack('v', fread($fh, 2)); + + return $tmp; + } + + /** + * Reads an unsigned long (4 octets). + * + * @param resource $fh file handle + * + * @return int + */ + private static function _readInt4($fh) + { + list(, $tmp) = unpack('V', fread($fh, 4)); + + return $tmp; + } + + /** + * Gets information about all PPS's on the OLE container from the PPS WK's + * creates an OLE_PPS object for each one. + * + * @param int $blockId the block id of the first block + * + * @return bool true on success, PEAR_Error on failure + */ + public function _readPpsWks($blockId) + { + $fh = $this->getStream($blockId); + for ($pos = 0; true; $pos += 128) { + fseek($fh, $pos, SEEK_SET); + $nameUtf16 = fread($fh, 64); + $nameLength = self::_readInt2($fh); + $nameUtf16 = substr($nameUtf16, 0, $nameLength - 2); + // Simple conversion from UTF-16LE to ISO-8859-1 + $name = str_replace("\x00", '', $nameUtf16); + $type = self::_readInt1($fh); + switch ($type) { + case self::OLE_PPS_TYPE_ROOT: + $pps = new OLE\PPS\Root(null, null, []); + $this->root = $pps; + + break; + case self::OLE_PPS_TYPE_DIR: + $pps = new OLE\PPS(null, null, null, null, null, null, null, null, null, []); + + break; + case self::OLE_PPS_TYPE_FILE: + $pps = new OLE\PPS\File($name); + + break; + default: + break; + } + fseek($fh, 1, SEEK_CUR); + $pps->Type = $type; + $pps->Name = $name; + $pps->PrevPps = self::_readInt4($fh); + $pps->NextPps = self::_readInt4($fh); + $pps->DirPps = self::_readInt4($fh); + fseek($fh, 20, SEEK_CUR); + $pps->Time1st = self::OLE2LocalDate(fread($fh, 8)); + $pps->Time2nd = self::OLE2LocalDate(fread($fh, 8)); + $pps->startBlock = self::_readInt4($fh); + $pps->Size = self::_readInt4($fh); + $pps->No = count($this->_list); + $this->_list[] = $pps; + + // check if the PPS tree (starting from root) is complete + if (isset($this->root) && $this->_ppsTreeComplete($this->root->No)) { + break; + } + } + fclose($fh); + + // Initialize $pps->children on directories + foreach ($this->_list as $pps) { + if ($pps->Type == self::OLE_PPS_TYPE_DIR || $pps->Type == self::OLE_PPS_TYPE_ROOT) { + $nos = [$pps->DirPps]; + $pps->children = []; + while ($nos) { + $no = array_pop($nos); + if ($no != -1) { + $childPps = $this->_list[$no]; + $nos[] = $childPps->PrevPps; + $nos[] = $childPps->NextPps; + $pps->children[] = $childPps; + } + } + } + } + + return true; + } + + /** + * It checks whether the PPS tree is complete (all PPS's read) + * starting with the given PPS (not necessarily root). + * + * @param int $index The index of the PPS from which we are checking + * + * @return bool Whether the PPS tree for the given PPS is complete + */ + public function _ppsTreeComplete($index) + { + return isset($this->_list[$index]) && + ($pps = $this->_list[$index]) && + ($pps->PrevPps == -1 || + $this->_ppsTreeComplete($pps->PrevPps)) && + ($pps->NextPps == -1 || + $this->_ppsTreeComplete($pps->NextPps)) && + ($pps->DirPps == -1 || + $this->_ppsTreeComplete($pps->DirPps)); + } + + /** + * Checks whether a PPS is a File PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @param int $index The index for the PPS + * + * @return bool true if it's a File PPS, false otherwise + */ + public function isFile($index) + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Type == self::OLE_PPS_TYPE_FILE; + } + + return false; + } + + /** + * Checks whether a PPS is a Root PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @param int $index the index for the PPS + * + * @return bool true if it's a Root PPS, false otherwise + */ + public function isRoot($index) + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Type == self::OLE_PPS_TYPE_ROOT; + } + + return false; + } + + /** + * Gives the total number of PPS's found in the OLE container. + * + * @return int The total number of PPS's found in the OLE container + */ + public function ppsTotal() + { + return count($this->_list); + } + + /** + * Gets data from a PPS + * If there is no PPS for the index given, it will return an empty string. + * + * @param int $index The index for the PPS + * @param int $position The position from which to start reading + * (relative to the PPS) + * @param int $length The amount of bytes to read (at most) + * + * @return string The binary string containing the data requested + * + * @see OLE_PPS_File::getStream() + */ + public function getData($index, $position, $length) + { + // if position is not valid return empty string + if (!isset($this->_list[$index]) || ($position >= $this->_list[$index]->Size) || ($position < 0)) { + return ''; + } + $fh = $this->getStream($this->_list[$index]); + $data = stream_get_contents($fh, $length, $position); + fclose($fh); + + return $data; + } + + /** + * Gets the data length from a PPS + * If there is no PPS for the index given, it will return 0. + * + * @param int $index The index for the PPS + * + * @return int The amount of bytes in data the PPS has + */ + public function getDataLength($index) + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Size; + } + + return 0; + } + + /** + * Utility function to transform ASCII text to Unicode. + * + * @param string $ascii The ASCII string to transform + * + * @return string The string in Unicode + */ + public static function ascToUcs($ascii) + { + $rawname = ''; + $iMax = strlen($ascii); + for ($i = 0; $i < $iMax; ++$i) { + $rawname .= $ascii[$i] + . "\x00"; + } + + return $rawname; + } + + /** + * Utility function + * Returns a string for the OLE container with the date given. + * + * @param int $date A timestamp + * + * @return string The string for the OLE container + */ + public static function localDateToOLE($date) + { + if (!isset($date)) { + return "\x00\x00\x00\x00\x00\x00\x00\x00"; + } + + // factor used for separating numbers into 4 bytes parts + $factor = pow(2, 32); + + // days from 1-1-1601 until the beggining of UNIX era + $days = 134774; + // calculate seconds + $big_date = $days * 24 * 3600 + gmmktime(date('H', $date), date('i', $date), date('s', $date), date('m', $date), date('d', $date), date('Y', $date)); + // multiply just to make MS happy + $big_date *= 10000000; + + $high_part = floor($big_date / $factor); + // lower 4 bytes + $low_part = floor((($big_date / $factor) - $high_part) * $factor); + + // Make HEX string + $res = ''; + + for ($i = 0; $i < 4; ++$i) { + $hex = $low_part % 0x100; + $res .= pack('c', $hex); + $low_part /= 0x100; + } + for ($i = 0; $i < 4; ++$i) { + $hex = $high_part % 0x100; + $res .= pack('c', $hex); + $high_part /= 0x100; + } + + return $res; + } + + /** + * Returns a timestamp from an OLE container's date. + * + * @param int $string A binary string with the encoded date + * + * @return string The timestamp corresponding to the string + */ + public static function OLE2LocalDate($string) + { + if (strlen($string) != 8) { + throw new ReaderException('Expecting 8 byte string'); + } + + // factor used for separating numbers into 4 bytes parts + $factor = pow(2, 32); + list(, $high_part) = unpack('V', substr($string, 4, 4)); + list(, $low_part) = unpack('V', substr($string, 0, 4)); + + $big_date = ($high_part * $factor) + $low_part; + // translate to seconds + $big_date /= 10000000; + + // days from 1-1-1601 until the beggining of UNIX era + $days = 134774; + + // translate to seconds from beggining of UNIX era + $big_date -= $days * 24 * 3600; + + return floor($big_date); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php new file mode 100644 index 00000000000..e6ba72428fc --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php @@ -0,0 +1,196 @@ +params); + if (!isset($this->params['oleInstanceId'], $this->params['blockId'], $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']])) { + if ($options & STREAM_REPORT_ERRORS) { + trigger_error('OLE stream not found', E_USER_WARNING); + } + + return false; + } + $this->ole = $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']]; + + $blockId = $this->params['blockId']; + $this->data = ''; + if (isset($this->params['size']) && $this->params['size'] < $this->ole->bigBlockThreshold && $blockId != $this->ole->root->startBlock) { + // Block id refers to small blocks + $rootPos = $this->ole->_getBlockOffset($this->ole->root->startBlock); + while ($blockId != -2) { + $pos = $rootPos + $blockId * $this->ole->bigBlockSize; + $blockId = $this->ole->sbat[$blockId]; + fseek($this->ole->_file_handle, $pos); + $this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize); + } + } else { + // Block id refers to big blocks + while ($blockId != -2) { + $pos = $this->ole->_getBlockOffset($blockId); + fseek($this->ole->_file_handle, $pos); + $this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize); + $blockId = $this->ole->bbat[$blockId]; + } + } + if (isset($this->params['size'])) { + $this->data = substr($this->data, 0, $this->params['size']); + } + + if ($options & STREAM_USE_PATH) { + $openedPath = $path; + } + + return true; + } + + /** + * Implements support for fclose(). + */ + public function stream_close() // @codingStandardsIgnoreLine + { + $this->ole = null; + unset($GLOBALS['_OLE_INSTANCES']); + } + + /** + * Implements support for fread(), fgets() etc. + * + * @param int $count maximum number of bytes to read + * + * @return string + */ + public function stream_read($count) // @codingStandardsIgnoreLine + { + if ($this->stream_eof()) { + return false; + } + $s = substr($this->data, $this->pos, $count); + $this->pos += $count; + + return $s; + } + + /** + * Implements support for feof(). + * + * @return bool TRUE if the file pointer is at EOF; otherwise FALSE + */ + public function stream_eof() // @codingStandardsIgnoreLine + { + return $this->pos >= strlen($this->data); + } + + /** + * Returns the position of the file pointer, i.e. its offset into the file + * stream. Implements support for ftell(). + * + * @return int + */ + public function stream_tell() // @codingStandardsIgnoreLine + { + return $this->pos; + } + + /** + * Implements support for fseek(). + * + * @param int $offset byte offset + * @param int $whence SEEK_SET, SEEK_CUR or SEEK_END + * + * @return bool + */ + public function stream_seek($offset, $whence) // @codingStandardsIgnoreLine + { + if ($whence == SEEK_SET && $offset >= 0) { + $this->pos = $offset; + } elseif ($whence == SEEK_CUR && -$offset <= $this->pos) { + $this->pos += $offset; + } elseif ($whence == SEEK_END && -$offset <= count($this->data)) { + $this->pos = strlen($this->data) + $offset; + } else { + return false; + } + + return true; + } + + /** + * Implements support for fstat(). Currently the only supported field is + * "size". + * + * @return array + */ + public function stream_stat() // @codingStandardsIgnoreLine + { + return [ + 'size' => strlen($this->data), + ]; + } + + // Methods used by stream_wrapper_register() that are not implemented: + // bool stream_flush ( void ) + // int stream_write ( string data ) + // bool rename ( string path_from, string path_to ) + // bool mkdir ( string path, int mode, int options ) + // bool rmdir ( string path, int options ) + // bool dir_opendir ( string path, int options ) + // array url_stat ( string path, int flags ) + // string dir_readdir ( void ) + // bool dir_rewinddir ( void ) + // bool dir_closedir ( void ) +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS.php new file mode 100644 index 00000000000..e53f2575fca --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS.php @@ -0,0 +1,238 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +use PhpOffice\PhpSpreadsheet\Shared\OLE; + +/** + * Class for creating PPS's for OLE containers. + * + * @author Xavier Noguer + * + * @category PhpSpreadsheet + */ +class PPS +{ + /** + * The PPS index. + * + * @var int + */ + public $No; + + /** + * The PPS name (in Unicode). + * + * @var string + */ + public $Name; + + /** + * The PPS type. Dir, Root or File. + * + * @var int + */ + public $Type; + + /** + * The index of the previous PPS. + * + * @var int + */ + public $PrevPps; + + /** + * The index of the next PPS. + * + * @var int + */ + public $NextPps; + + /** + * The index of it's first child if this is a Dir or Root PPS. + * + * @var int + */ + public $DirPps; + + /** + * A timestamp. + * + * @var int + */ + public $Time1st; + + /** + * A timestamp. + * + * @var int + */ + public $Time2nd; + + /** + * Starting block (small or big) for this PPS's data inside the container. + * + * @var int + */ + public $startBlock; + + /** + * The size of the PPS's data (in bytes). + * + * @var int + */ + public $Size; + + /** + * The PPS's data (only used if it's not using a temporary file). + * + * @var string + */ + public $_data; + + /** + * Array of child PPS's (only used by Root and Dir PPS's). + * + * @var array + */ + public $children = []; + + /** + * Pointer to OLE container. + * + * @var OLE + */ + public $ole; + + /** + * The constructor. + * + * @param int $No The PPS index + * @param string $name The PPS name + * @param int $type The PPS type. Dir, Root or File + * @param int $prev The index of the previous PPS + * @param int $next The index of the next PPS + * @param int $dir The index of it's first child if this is a Dir or Root PPS + * @param int $time_1st A timestamp + * @param int $time_2nd A timestamp + * @param string $data The (usually binary) source data of the PPS + * @param array $children Array containing children PPS for this PPS + */ + public function __construct($No, $name, $type, $prev, $next, $dir, $time_1st, $time_2nd, $data, $children) + { + $this->No = $No; + $this->Name = $name; + $this->Type = $type; + $this->PrevPps = $prev; + $this->NextPps = $next; + $this->DirPps = $dir; + $this->Time1st = $time_1st; + $this->Time2nd = $time_2nd; + $this->_data = $data; + $this->children = $children; + if ($data != '') { + $this->Size = strlen($data); + } else { + $this->Size = 0; + } + } + + /** + * Returns the amount of data saved for this PPS. + * + * @return int The amount of data (in bytes) + */ + public function getDataLen() + { + if (!isset($this->_data)) { + return 0; + } + + return strlen($this->_data); + } + + /** + * Returns a string with the PPS's WK (What is a WK?). + * + * @return string The binary string + */ + public function _getPpsWk() + { + $ret = str_pad($this->Name, 64, "\x00"); + + $ret .= pack('v', strlen($this->Name) + 2) // 66 + . pack('c', $this->Type) // 67 + . pack('c', 0x00) //UK // 68 + . pack('V', $this->PrevPps) //Prev // 72 + . pack('V', $this->NextPps) //Next // 76 + . pack('V', $this->DirPps) //Dir // 80 + . "\x00\x09\x02\x00" // 84 + . "\x00\x00\x00\x00" // 88 + . "\xc0\x00\x00\x00" // 92 + . "\x00\x00\x00\x46" // 96 // Seems to be ok only for Root + . "\x00\x00\x00\x00" // 100 + . OLE::localDateToOLE($this->Time1st) // 108 + . OLE::localDateToOLE($this->Time2nd) // 116 + . pack('V', isset($this->startBlock) ? $this->startBlock : 0) // 120 + . pack('V', $this->Size) // 124 + . pack('V', 0); // 128 + return $ret; + } + + /** + * Updates index and pointers to previous, next and children PPS's for this + * PPS. I don't think it'll work with Dir PPS's. + * + * @param array &$raList Reference to the array of PPS's for the whole OLE + * container + * @param mixed $to_save + * @param mixed $depth + * + * @return int The index for this PPS + */ + public static function _savePpsSetPnt(&$raList, $to_save, $depth = 0) + { + if (!is_array($to_save) || (empty($to_save))) { + return 0xFFFFFFFF; + } elseif (count($to_save) == 1) { + $cnt = count($raList); + // If the first entry, it's the root... Don't clone it! + $raList[$cnt] = ($depth == 0) ? $to_save[0] : clone $to_save[0]; + $raList[$cnt]->No = $cnt; + $raList[$cnt]->PrevPps = 0xFFFFFFFF; + $raList[$cnt]->NextPps = 0xFFFFFFFF; + $raList[$cnt]->DirPps = self::_savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++); + } else { + $iPos = floor(count($to_save) / 2); + $aPrev = array_slice($to_save, 0, $iPos); + $aNext = array_slice($to_save, $iPos + 1); + $cnt = count($raList); + // If the first entry, it's the root... Don't clone it! + $raList[$cnt] = ($depth == 0) ? $to_save[$iPos] : clone $to_save[$iPos]; + $raList[$cnt]->No = $cnt; + $raList[$cnt]->PrevPps = self::_savePpsSetPnt($raList, $aPrev, $depth++); + $raList[$cnt]->NextPps = self::_savePpsSetPnt($raList, $aNext, $depth++); + $raList[$cnt]->DirPps = self::_savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++); + } + + return $cnt; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS/File.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS/File.php new file mode 100644 index 00000000000..68f50a5c8ce --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS/File.php @@ -0,0 +1,66 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +use PhpOffice\PhpSpreadsheet\Shared\OLE; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; + +/** + * Class for creating File PPS's for OLE containers. + * + * @author Xavier Noguer + * + * @category PhpSpreadsheet + */ +class File extends PPS +{ + /** + * The constructor. + * + * @param string $name The name of the file (in Unicode) + * + * @see OLE::ascToUcs() + */ + public function __construct($name) + { + parent::__construct(null, $name, OLE::OLE_PPS_TYPE_FILE, null, null, null, null, null, '', []); + } + + /** + * Initialization method. Has to be called right after OLE_PPS_File(). + * + * @return mixed true on success + */ + public function init() + { + return true; + } + + /** + * Append data to PPS. + * + * @param string $data The data to append + */ + public function append($data) + { + $this->_data .= $data; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS/Root.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS/Root.php new file mode 100644 index 00000000000..578e08fafe3 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLE/PPS/Root.php @@ -0,0 +1,466 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +use PhpOffice\PhpSpreadsheet\Shared\OLE; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; +use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; + +/** + * Class for creating Root PPS's for OLE containers. + * + * @author Xavier Noguer + * + * @category PhpSpreadsheet + */ +class Root extends PPS +{ + /** + * Directory for temporary files. + * + * @var string + */ + protected $tempDirectory; + + /** + * @var resource + */ + private $fileHandle; + + /** + * @var string + */ + private $tempFilename; + + /** + * @var int + */ + private $smallBlockSize; + + /** + * @var int + */ + private $bigBlockSize; + + /** + * @param int $time_1st A timestamp + * @param int $time_2nd A timestamp + * @param File[] $raChild + */ + public function __construct($time_1st, $time_2nd, $raChild) + { + $this->tempDirectory = \PhpOffice\PhpSpreadsheet\Shared\File::sysGetTempDir(); + + parent::__construct(null, OLE::ascToUcs('Root Entry'), OLE::OLE_PPS_TYPE_ROOT, null, null, null, $time_1st, $time_2nd, null, $raChild); + } + + /** + * Method for saving the whole OLE container (including files). + * In fact, if called with an empty argument (or '-'), it saves to a + * temporary file and then outputs it's contents to stdout. + * If a resource pointer to a stream created by fopen() is passed + * it will be used, but you have to close such stream by yourself. + * + * @param resource|string $filename the name of the file or stream where to save the OLE container + * + * @throws WriterException + * + * @return bool true on success + */ + public function save($filename) + { + // Initial Setting for saving + $this->bigBlockSize = pow( + 2, + (isset($this->bigBlockSize)) ? self::adjust2($this->bigBlockSize) : 9 + ); + $this->smallBlockSize = pow( + 2, + (isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6 + ); + + if (is_resource($filename)) { + $this->fileHandle = $filename; + } elseif ($filename == '-' || $filename == '') { + if ($this->tempDirectory === null) { + $this->tempDirectory = \PhpOffice\PhpSpreadsheet\Shared\File::sysGetTempDir(); + } + $this->tempFilename = tempnam($this->tempDirectory, 'OLE_PPS_Root'); + $this->fileHandle = fopen($this->tempFilename, 'w+b'); + if ($this->fileHandle == false) { + throw new WriterException("Can't create temporary file."); + } + } else { + $this->fileHandle = fopen($filename, 'wb'); + } + if ($this->fileHandle == false) { + throw new WriterException("Can't open $filename. It may be in use or protected."); + } + // Make an array of PPS's (for Save) + $aList = []; + PPS::_savePpsSetPnt($aList, [$this]); + // calculate values for header + list($iSBDcnt, $iBBcnt, $iPPScnt) = $this->_calcSize($aList); //, $rhInfo); + // Save Header + $this->_saveHeader($iSBDcnt, $iBBcnt, $iPPScnt); + + // Make Small Data string (write SBD) + $this->_data = $this->_makeSmallData($aList); + + // Write BB + $this->_saveBigData($iSBDcnt, $aList); + // Write PPS + $this->_savePps($aList); + // Write Big Block Depot and BDList and Adding Header informations + $this->_saveBbd($iSBDcnt, $iBBcnt, $iPPScnt); + + if (!is_resource($filename)) { + fclose($this->fileHandle); + } + + return true; + } + + /** + * Calculate some numbers. + * + * @param array $raList Reference to an array of PPS's + * + * @return float[] The array of numbers + */ + public function _calcSize(&$raList) + { + // Calculate Basic Setting + list($iSBDcnt, $iBBcnt, $iPPScnt) = [0, 0, 0]; + $iSmallLen = 0; + $iSBcnt = 0; + $iCount = count($raList); + for ($i = 0; $i < $iCount; ++$i) { + if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) { + $raList[$i]->Size = $raList[$i]->getDataLen(); + if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) { + $iSBcnt += floor($raList[$i]->Size / $this->smallBlockSize) + + (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0); + } else { + $iBBcnt += (floor($raList[$i]->Size / $this->bigBlockSize) + + (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0)); + } + } + } + $iSmallLen = $iSBcnt * $this->smallBlockSize; + $iSlCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE); + $iSBDcnt = floor($iSBcnt / $iSlCnt) + (($iSBcnt % $iSlCnt) ? 1 : 0); + $iBBcnt += (floor($iSmallLen / $this->bigBlockSize) + + (($iSmallLen % $this->bigBlockSize) ? 1 : 0)); + $iCnt = count($raList); + $iBdCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE; + $iPPScnt = (floor($iCnt / $iBdCnt) + (($iCnt % $iBdCnt) ? 1 : 0)); + + return [$iSBDcnt, $iBBcnt, $iPPScnt]; + } + + /** + * Helper function for caculating a magic value for block sizes. + * + * @param int $i2 The argument + * + * @see save() + * + * @return float + */ + private static function adjust2($i2) + { + $iWk = log($i2) / log(2); + + return ($iWk > floor($iWk)) ? floor($iWk) + 1 : $iWk; + } + + /** + * Save OLE header. + * + * @param int $iSBDcnt + * @param int $iBBcnt + * @param int $iPPScnt + */ + public function _saveHeader($iSBDcnt, $iBBcnt, $iPPScnt) + { + $FILE = $this->fileHandle; + + // Calculate Basic Setting + $iBlCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE; + $i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE; + + $iBdExL = 0; + $iAll = $iBBcnt + $iPPScnt + $iSBDcnt; + $iAllW = $iAll; + $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0); + $iBdCnt = floor(($iAll + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0); + + // Calculate BD count + if ($iBdCnt > $i1stBdL) { + while (1) { + ++$iBdExL; + ++$iAllW; + $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0); + $iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0); + if ($iBdCnt <= ($iBdExL * $iBlCnt + $i1stBdL)) { + break; + } + } + } + + // Save Header + fwrite( + $FILE, + "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . pack('v', 0x3b) + . pack('v', 0x03) + . pack('v', -2) + . pack('v', 9) + . pack('v', 6) + . pack('v', 0) + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . pack('V', $iBdCnt) + . pack('V', $iBBcnt + $iSBDcnt) //ROOT START + . pack('V', 0) + . pack('V', 0x1000) + . pack('V', $iSBDcnt ? 0 : -2) //Small Block Depot + . pack('V', $iSBDcnt) + ); + // Extra BDList Start, Count + if ($iBdCnt < $i1stBdL) { + fwrite( + $FILE, + pack('V', -2) // Extra BDList Start + . pack('V', 0)// Extra BDList Count + ); + } else { + fwrite($FILE, pack('V', $iAll + $iBdCnt) . pack('V', $iBdExL)); + } + + // BDList + for ($i = 0; $i < $i1stBdL && $i < $iBdCnt; ++$i) { + fwrite($FILE, pack('V', $iAll + $i)); + } + if ($i < $i1stBdL) { + $jB = $i1stBdL - $i; + for ($j = 0; $j < $jB; ++$j) { + fwrite($FILE, (pack('V', -1))); + } + } + } + + /** + * Saving big data (PPS's with data bigger than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). + * + * @param int $iStBlk + * @param array &$raList Reference to array of PPS's + */ + public function _saveBigData($iStBlk, &$raList) + { + $FILE = $this->fileHandle; + + // cycle through PPS's + $iCount = count($raList); + for ($i = 0; $i < $iCount; ++$i) { + if ($raList[$i]->Type != OLE::OLE_PPS_TYPE_DIR) { + $raList[$i]->Size = $raList[$i]->getDataLen(); + if (($raList[$i]->Size >= OLE::OLE_DATA_SIZE_SMALL) || (($raList[$i]->Type == OLE::OLE_PPS_TYPE_ROOT) && isset($raList[$i]->_data))) { + fwrite($FILE, $raList[$i]->_data); + + if ($raList[$i]->Size % $this->bigBlockSize) { + fwrite($FILE, str_repeat("\x00", $this->bigBlockSize - ($raList[$i]->Size % $this->bigBlockSize))); + } + // Set For PPS + $raList[$i]->startBlock = $iStBlk; + $iStBlk += + (floor($raList[$i]->Size / $this->bigBlockSize) + + (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0)); + } + } + } + } + + /** + * get small data (PPS's with data smaller than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). + * + * @param array &$raList Reference to array of PPS's + * + * @return string + */ + public function _makeSmallData(&$raList) + { + $sRes = ''; + $FILE = $this->fileHandle; + $iSmBlk = 0; + + $iCount = count($raList); + for ($i = 0; $i < $iCount; ++$i) { + // Make SBD, small data string + if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) { + if ($raList[$i]->Size <= 0) { + continue; + } + if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) { + $iSmbCnt = floor($raList[$i]->Size / $this->smallBlockSize) + + (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0); + // Add to SBD + $jB = $iSmbCnt - 1; + for ($j = 0; $j < $jB; ++$j) { + fwrite($FILE, pack('V', $j + $iSmBlk + 1)); + } + fwrite($FILE, pack('V', -2)); + + // Add to Data String(this will be written for RootEntry) + $sRes .= $raList[$i]->_data; + if ($raList[$i]->Size % $this->smallBlockSize) { + $sRes .= str_repeat("\x00", $this->smallBlockSize - ($raList[$i]->Size % $this->smallBlockSize)); + } + // Set for PPS + $raList[$i]->startBlock = $iSmBlk; + $iSmBlk += $iSmbCnt; + } + } + } + $iSbCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE); + if ($iSmBlk % $iSbCnt) { + $iB = $iSbCnt - ($iSmBlk % $iSbCnt); + for ($i = 0; $i < $iB; ++$i) { + fwrite($FILE, pack('V', -1)); + } + } + + return $sRes; + } + + /** + * Saves all the PPS's WKs. + * + * @param array $raList Reference to an array with all PPS's + */ + public function _savePps(&$raList) + { + // Save each PPS WK + $iC = count($raList); + for ($i = 0; $i < $iC; ++$i) { + fwrite($this->fileHandle, $raList[$i]->_getPpsWk()); + } + // Adjust for Block + $iCnt = count($raList); + $iBCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE; + if ($iCnt % $iBCnt) { + fwrite($this->fileHandle, str_repeat("\x00", ($iBCnt - ($iCnt % $iBCnt)) * OLE::OLE_PPS_SIZE)); + } + } + + /** + * Saving Big Block Depot. + * + * @param int $iSbdSize + * @param int $iBsize + * @param int $iPpsCnt + */ + public function _saveBbd($iSbdSize, $iBsize, $iPpsCnt) + { + $FILE = $this->fileHandle; + // Calculate Basic Setting + $iBbCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE; + $i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE; + + $iBdExL = 0; + $iAll = $iBsize + $iPpsCnt + $iSbdSize; + $iAllW = $iAll; + $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0); + $iBdCnt = floor(($iAll + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0); + // Calculate BD count + if ($iBdCnt > $i1stBdL) { + while (1) { + ++$iBdExL; + ++$iAllW; + $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0); + $iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0); + if ($iBdCnt <= ($iBdExL * $iBbCnt + $i1stBdL)) { + break; + } + } + } + + // Making BD + // Set for SBD + if ($iSbdSize > 0) { + for ($i = 0; $i < ($iSbdSize - 1); ++$i) { + fwrite($FILE, pack('V', $i + 1)); + } + fwrite($FILE, pack('V', -2)); + } + // Set for B + for ($i = 0; $i < ($iBsize - 1); ++$i) { + fwrite($FILE, pack('V', $i + $iSbdSize + 1)); + } + fwrite($FILE, pack('V', -2)); + + // Set for PPS + for ($i = 0; $i < ($iPpsCnt - 1); ++$i) { + fwrite($FILE, pack('V', $i + $iSbdSize + $iBsize + 1)); + } + fwrite($FILE, pack('V', -2)); + // Set for BBD itself ( 0xFFFFFFFD : BBD) + for ($i = 0; $i < $iBdCnt; ++$i) { + fwrite($FILE, pack('V', 0xFFFFFFFD)); + } + // Set for ExtraBDList + for ($i = 0; $i < $iBdExL; ++$i) { + fwrite($FILE, pack('V', 0xFFFFFFFC)); + } + // Adjust for Block + if (($iAllW + $iBdCnt) % $iBbCnt) { + $iBlock = ($iBbCnt - (($iAllW + $iBdCnt) % $iBbCnt)); + for ($i = 0; $i < $iBlock; ++$i) { + fwrite($FILE, pack('V', -1)); + } + } + // Extra BDList + if ($iBdCnt > $i1stBdL) { + $iN = 0; + $iNb = 0; + for ($i = $i1stBdL; $i < $iBdCnt; $i++, ++$iN) { + if ($iN >= ($iBbCnt - 1)) { + $iN = 0; + ++$iNb; + fwrite($FILE, pack('V', $iAll + $iBdCnt + $iNb)); + } + fwrite($FILE, pack('V', $iBsize + $iSbdSize + $iPpsCnt + $i)); + } + if (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)) { + $iB = ($iBbCnt - 1) - (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)); + for ($i = 0; $i < $iB; ++$i) { + fwrite($FILE, pack('V', -1)); + } + } + fwrite($FILE, pack('V', -2)); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLERead.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLERead.php new file mode 100644 index 00000000000..3af3970057b --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/OLERead.php @@ -0,0 +1,352 @@ +data = file_get_contents($pFilename, false, null, 0, 8); + + // Check OLE identifier + $identifierOle = pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1); + if ($this->data != $identifierOle) { + throw new ReaderException('The filename ' . $pFilename . ' is not recognised as an OLE file'); + } + + // Get the file data + $this->data = file_get_contents($pFilename); + + // Total number of sectors used for the SAT + $this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS); + + // SecID of the first sector of the directory stream + $this->rootStartBlock = self::getInt4d($this->data, self::ROOT_START_BLOCK_POS); + + // SecID of the first sector of the SSAT (or -2 if not extant) + $this->sbdStartBlock = self::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS); + + // SecID of the first sector of the MSAT (or -2 if no additional sectors are used) + $this->extensionBlock = self::getInt4d($this->data, self::EXTENSION_BLOCK_POS); + + // Total number of sectors used by MSAT + $this->numExtensionBlocks = self::getInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS); + + $bigBlockDepotBlocks = []; + $pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS; + + $bbdBlocks = $this->numBigBlockDepotBlocks; + + if ($this->numExtensionBlocks != 0) { + $bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4; + } + + for ($i = 0; $i < $bbdBlocks; ++$i) { + $bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); + $pos += 4; + } + + for ($j = 0; $j < $this->numExtensionBlocks; ++$j) { + $pos = ($this->extensionBlock + 1) * self::BIG_BLOCK_SIZE; + $blocksToRead = min($this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1); + + for ($i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; ++$i) { + $bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); + $pos += 4; + } + + $bbdBlocks += $blocksToRead; + if ($bbdBlocks < $this->numBigBlockDepotBlocks) { + $this->extensionBlock = self::getInt4d($this->data, $pos); + } + } + + $pos = 0; + $this->bigBlockChain = ''; + $bbs = self::BIG_BLOCK_SIZE / 4; + for ($i = 0; $i < $this->numBigBlockDepotBlocks; ++$i) { + $pos = ($bigBlockDepotBlocks[$i] + 1) * self::BIG_BLOCK_SIZE; + + $this->bigBlockChain .= substr($this->data, $pos, 4 * $bbs); + $pos += 4 * $bbs; + } + + $pos = 0; + $sbdBlock = $this->sbdStartBlock; + $this->smallBlockChain = ''; + while ($sbdBlock != -2) { + $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; + + $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); + $pos += 4 * $bbs; + + $sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4); + } + + // read the directory stream + $block = $this->rootStartBlock; + $this->entry = $this->_readData($block); + + $this->readPropertySets(); + } + + /** + * Extract binary stream data. + * + * @param int $stream + * + * @return string + */ + public function getStream($stream) + { + if ($stream === null) { + return null; + } + + $streamData = ''; + + if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) { + $rootdata = $this->_readData($this->props[$this->rootentry]['startBlock']); + + $block = $this->props[$stream]['startBlock']; + + while ($block != -2) { + $pos = $block * self::SMALL_BLOCK_SIZE; + $streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE); + + $block = self::getInt4d($this->smallBlockChain, $block * 4); + } + + return $streamData; + } + $numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE; + if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) { + ++$numBlocks; + } + + if ($numBlocks == 0) { + return ''; + } + + $block = $this->props[$stream]['startBlock']; + + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); + $block = self::getInt4d($this->bigBlockChain, $block * 4); + } + + return $streamData; + } + + /** + * Read a standard stream (by joining sectors using information from SAT). + * + * @param int $bl Sector ID where the stream starts + * + * @return string Data for standard stream + */ + private function _readData($bl) + { + $block = $bl; + $data = ''; + + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); + $block = self::getInt4d($this->bigBlockChain, $block * 4); + } + + return $data; + } + + /** + * Read entries in the directory stream. + */ + private function readPropertySets() + { + $offset = 0; + + // loop through entires, each entry is 128 bytes + $entryLen = strlen($this->entry); + while ($offset < $entryLen) { + // entry data (128 bytes) + $d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE); + + // size in bytes of name + $nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS + 1]) << 8); + + // type of entry + $type = ord($d[self::TYPE_POS]); + + // sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook) + // sectorID of first sector of the short-stream container stream, if this entry is root entry + $startBlock = self::getInt4d($d, self::START_BLOCK_POS); + + $size = self::getInt4d($d, self::SIZE_POS); + + $name = str_replace("\x00", '', substr($d, 0, $nameSize)); + + $this->props[] = [ + 'name' => $name, + 'type' => $type, + 'startBlock' => $startBlock, + 'size' => $size, + ]; + + // tmp helper to simplify checks + $upName = strtoupper($name); + + // Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook) + if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) { + $this->wrkbook = count($this->props) - 1; + } elseif ($upName === 'ROOT ENTRY' || $upName === 'R') { + // Root entry + $this->rootentry = count($this->props) - 1; + } + + // Summary information + if ($name == chr(5) . 'SummaryInformation') { + $this->summaryInformation = count($this->props) - 1; + } + + // Additional Document Summary information + if ($name == chr(5) . 'DocumentSummaryInformation') { + $this->documentSummaryInformation = count($this->props) - 1; + } + + $offset += self::PROPERTY_STORAGE_BLOCK_SIZE; + } + } + + /** + * Read 4 bytes of data at specified position. + * + * @param string $data + * @param int $pos + * + * @return int + */ + private static function getInt4d($data, $pos) + { + if ($pos < 0) { + // Invalid position + throw new ReaderException('Parameter pos=' . $pos . ' is invalid.'); + } + + $len = strlen($data); + if ($len < $pos + 4) { + $data .= str_repeat("\0", $pos + 4 - $len); + } + + // FIX: represent numbers correctly on 64-bit system + // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 + // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems + $_or_24 = ord($data[$pos + 3]); + if ($_or_24 >= 128) { + // negative number + $_ord_24 = -abs((256 - $_or_24) << 24); + } else { + $_ord_24 = ($_or_24 & 127) << 24; + } + + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/PasswordHasher.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/PasswordHasher.php new file mode 100644 index 00000000000..9b0080b9dbf --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/PasswordHasher.php @@ -0,0 +1,37 @@ +. + * + * @param string $pPassword Password to hash + * + * @return string Hashed password + */ + public static function hashPassword($pPassword) + { + $password = 0x0000; + $charPos = 1; // char position + + // split the plain text password in its component characters + $chars = preg_split('//', $pPassword, -1, PREG_SPLIT_NO_EMPTY); + foreach ($chars as $char) { + $value = ord($char) << $charPos++; // shifted ASCII value + $rotated_bits = $value >> 15; // rotated bits beyond bit 15 + $value &= 0x7fff; // first 15 bits + $password ^= ($value | $rotated_bits); + } + + $password ^= strlen($pPassword); + $password ^= 0xCE4B; + + return strtoupper(dechex($password)); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/StringHelper.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/StringHelper.php new file mode 100644 index 00000000000..a5702775074 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/StringHelper.php @@ -0,0 +1,724 @@ + chr(0), + "\x1B 1" => chr(1), + "\x1B 2" => chr(2), + "\x1B 3" => chr(3), + "\x1B 4" => chr(4), + "\x1B 5" => chr(5), + "\x1B 6" => chr(6), + "\x1B 7" => chr(7), + "\x1B 8" => chr(8), + "\x1B 9" => chr(9), + "\x1B :" => chr(10), + "\x1B ;" => chr(11), + "\x1B <" => chr(12), + "\x1B =" => chr(13), + "\x1B >" => chr(14), + "\x1B ?" => chr(15), + "\x1B!0" => chr(16), + "\x1B!1" => chr(17), + "\x1B!2" => chr(18), + "\x1B!3" => chr(19), + "\x1B!4" => chr(20), + "\x1B!5" => chr(21), + "\x1B!6" => chr(22), + "\x1B!7" => chr(23), + "\x1B!8" => chr(24), + "\x1B!9" => chr(25), + "\x1B!:" => chr(26), + "\x1B!;" => chr(27), + "\x1B!<" => chr(28), + "\x1B!=" => chr(29), + "\x1B!>" => chr(30), + "\x1B!?" => chr(31), + "\x1B'?" => chr(127), + "\x1B(0" => '€', // 128 in CP1252 + "\x1B(2" => '‚', // 130 in CP1252 + "\x1B(3" => 'ƒ', // 131 in CP1252 + "\x1B(4" => '„', // 132 in CP1252 + "\x1B(5" => '…', // 133 in CP1252 + "\x1B(6" => '†', // 134 in CP1252 + "\x1B(7" => '‡', // 135 in CP1252 + "\x1B(8" => 'ˆ', // 136 in CP1252 + "\x1B(9" => '‰', // 137 in CP1252 + "\x1B(:" => 'Š', // 138 in CP1252 + "\x1B(;" => '‹', // 139 in CP1252 + "\x1BNj" => 'Œ', // 140 in CP1252 + "\x1B(>" => 'Ž', // 142 in CP1252 + "\x1B)1" => '‘', // 145 in CP1252 + "\x1B)2" => '’', // 146 in CP1252 + "\x1B)3" => '“', // 147 in CP1252 + "\x1B)4" => '”', // 148 in CP1252 + "\x1B)5" => '•', // 149 in CP1252 + "\x1B)6" => '–', // 150 in CP1252 + "\x1B)7" => '—', // 151 in CP1252 + "\x1B)8" => '˜', // 152 in CP1252 + "\x1B)9" => '™', // 153 in CP1252 + "\x1B):" => 'š', // 154 in CP1252 + "\x1B);" => '›', // 155 in CP1252 + "\x1BNz" => 'œ', // 156 in CP1252 + "\x1B)>" => 'ž', // 158 in CP1252 + "\x1B)?" => 'Ÿ', // 159 in CP1252 + "\x1B*0" => ' ', // 160 in CP1252 + "\x1BN!" => '¡', // 161 in CP1252 + "\x1BN\"" => '¢', // 162 in CP1252 + "\x1BN#" => '£', // 163 in CP1252 + "\x1BN(" => '¤', // 164 in CP1252 + "\x1BN%" => '¥', // 165 in CP1252 + "\x1B*6" => '¦', // 166 in CP1252 + "\x1BN'" => '§', // 167 in CP1252 + "\x1BNH " => '¨', // 168 in CP1252 + "\x1BNS" => '©', // 169 in CP1252 + "\x1BNc" => 'ª', // 170 in CP1252 + "\x1BN+" => '«', // 171 in CP1252 + "\x1B*<" => '¬', // 172 in CP1252 + "\x1B*=" => '­', // 173 in CP1252 + "\x1BNR" => '®', // 174 in CP1252 + "\x1B*?" => '¯', // 175 in CP1252 + "\x1BN0" => '°', // 176 in CP1252 + "\x1BN1" => '±', // 177 in CP1252 + "\x1BN2" => '²', // 178 in CP1252 + "\x1BN3" => '³', // 179 in CP1252 + "\x1BNB " => '´', // 180 in CP1252 + "\x1BN5" => 'µ', // 181 in CP1252 + "\x1BN6" => '¶', // 182 in CP1252 + "\x1BN7" => '·', // 183 in CP1252 + "\x1B+8" => '¸', // 184 in CP1252 + "\x1BNQ" => '¹', // 185 in CP1252 + "\x1BNk" => 'º', // 186 in CP1252 + "\x1BN;" => '»', // 187 in CP1252 + "\x1BN<" => '¼', // 188 in CP1252 + "\x1BN=" => '½', // 189 in CP1252 + "\x1BN>" => '¾', // 190 in CP1252 + "\x1BN?" => '¿', // 191 in CP1252 + "\x1BNAA" => 'À', // 192 in CP1252 + "\x1BNBA" => 'Á', // 193 in CP1252 + "\x1BNCA" => 'Â', // 194 in CP1252 + "\x1BNDA" => 'Ã', // 195 in CP1252 + "\x1BNHA" => 'Ä', // 196 in CP1252 + "\x1BNJA" => 'Å', // 197 in CP1252 + "\x1BNa" => 'Æ', // 198 in CP1252 + "\x1BNKC" => 'Ç', // 199 in CP1252 + "\x1BNAE" => 'È', // 200 in CP1252 + "\x1BNBE" => 'É', // 201 in CP1252 + "\x1BNCE" => 'Ê', // 202 in CP1252 + "\x1BNHE" => 'Ë', // 203 in CP1252 + "\x1BNAI" => 'Ì', // 204 in CP1252 + "\x1BNBI" => 'Í', // 205 in CP1252 + "\x1BNCI" => 'Î', // 206 in CP1252 + "\x1BNHI" => 'Ï', // 207 in CP1252 + "\x1BNb" => 'Ð', // 208 in CP1252 + "\x1BNDN" => 'Ñ', // 209 in CP1252 + "\x1BNAO" => 'Ò', // 210 in CP1252 + "\x1BNBO" => 'Ó', // 211 in CP1252 + "\x1BNCO" => 'Ô', // 212 in CP1252 + "\x1BNDO" => 'Õ', // 213 in CP1252 + "\x1BNHO" => 'Ö', // 214 in CP1252 + "\x1B-7" => '×', // 215 in CP1252 + "\x1BNi" => 'Ø', // 216 in CP1252 + "\x1BNAU" => 'Ù', // 217 in CP1252 + "\x1BNBU" => 'Ú', // 218 in CP1252 + "\x1BNCU" => 'Û', // 219 in CP1252 + "\x1BNHU" => 'Ü', // 220 in CP1252 + "\x1B-=" => 'Ý', // 221 in CP1252 + "\x1BNl" => 'Þ', // 222 in CP1252 + "\x1BN{" => 'ß', // 223 in CP1252 + "\x1BNAa" => 'à', // 224 in CP1252 + "\x1BNBa" => 'á', // 225 in CP1252 + "\x1BNCa" => 'â', // 226 in CP1252 + "\x1BNDa" => 'ã', // 227 in CP1252 + "\x1BNHa" => 'ä', // 228 in CP1252 + "\x1BNJa" => 'å', // 229 in CP1252 + "\x1BNq" => 'æ', // 230 in CP1252 + "\x1BNKc" => 'ç', // 231 in CP1252 + "\x1BNAe" => 'è', // 232 in CP1252 + "\x1BNBe" => 'é', // 233 in CP1252 + "\x1BNCe" => 'ê', // 234 in CP1252 + "\x1BNHe" => 'ë', // 235 in CP1252 + "\x1BNAi" => 'ì', // 236 in CP1252 + "\x1BNBi" => 'í', // 237 in CP1252 + "\x1BNCi" => 'î', // 238 in CP1252 + "\x1BNHi" => 'ï', // 239 in CP1252 + "\x1BNs" => 'ð', // 240 in CP1252 + "\x1BNDn" => 'ñ', // 241 in CP1252 + "\x1BNAo" => 'ò', // 242 in CP1252 + "\x1BNBo" => 'ó', // 243 in CP1252 + "\x1BNCo" => 'ô', // 244 in CP1252 + "\x1BNDo" => 'õ', // 245 in CP1252 + "\x1BNHo" => 'ö', // 246 in CP1252 + "\x1B/7" => '÷', // 247 in CP1252 + "\x1BNy" => 'ø', // 248 in CP1252 + "\x1BNAu" => 'ù', // 249 in CP1252 + "\x1BNBu" => 'ú', // 250 in CP1252 + "\x1BNCu" => 'û', // 251 in CP1252 + "\x1BNHu" => 'ü', // 252 in CP1252 + "\x1B/=" => 'ý', // 253 in CP1252 + "\x1BN|" => 'þ', // 254 in CP1252 + "\x1BNHy" => 'ÿ', // 255 in CP1252 + ]; + } + + /** + * Get whether iconv extension is available. + * + * @return bool + */ + public static function getIsIconvEnabled() + { + if (isset(self::$isIconvEnabled)) { + return self::$isIconvEnabled; + } + + // Assume no problems with iconv + self::$isIconvEnabled = true; + + // Fail if iconv doesn't exist + if (!function_exists('iconv')) { + self::$isIconvEnabled = false; + } elseif (!@iconv('UTF-8', 'UTF-16LE', 'x')) { + // Sometimes iconv is not working, and e.g. iconv('UTF-8', 'UTF-16LE', 'x') just returns false, + self::$isIconvEnabled = false; + } elseif (defined('PHP_OS') && @stristr(PHP_OS, 'AIX') && defined('ICONV_IMPL') && (@strcasecmp(ICONV_IMPL, 'unknown') == 0) && defined('ICONV_VERSION') && (@strcasecmp(ICONV_VERSION, 'unknown') == 0)) { + // CUSTOM: IBM AIX iconv() does not work + self::$isIconvEnabled = false; + } + + // Deactivate iconv default options if they fail (as seen on IMB i) + if (self::$isIconvEnabled && !@iconv('UTF-8', 'UTF-16LE' . self::$iconvOptions, 'x')) { + self::$iconvOptions = ''; + } + + return self::$isIconvEnabled; + } + + private static function buildCharacterSets() + { + if (empty(self::$controlCharacters)) { + self::buildControlCharacters(); + } + + if (empty(self::$SYLKCharacters)) { + self::buildSYLKCharacters(); + } + } + + /** + * Convert from OpenXML escaped control character to PHP control character. + * + * Excel 2007 team: + * ---------------- + * That's correct, control characters are stored directly in the shared-strings table. + * We do encode characters that cannot be represented in XML using the following escape sequence: + * _xHHHH_ where H represents a hexadecimal character in the character's value... + * So you could end up with something like _x0008_ in a string (either in a cell value () + * element or in the shared string element. + * + * @param string $value Value to unescape + * + * @return string + */ + public static function controlCharacterOOXML2PHP($value) + { + self::buildCharacterSets(); + + return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $value); + } + + /** + * Convert from PHP control character to OpenXML escaped control character. + * + * Excel 2007 team: + * ---------------- + * That's correct, control characters are stored directly in the shared-strings table. + * We do encode characters that cannot be represented in XML using the following escape sequence: + * _xHHHH_ where H represents a hexadecimal character in the character's value... + * So you could end up with something like _x0008_ in a string (either in a cell value () + * element or in the shared string element. + * + * @param string $value Value to escape + * + * @return string + */ + public static function controlCharacterPHP2OOXML($value) + { + self::buildCharacterSets(); + + return str_replace(array_values(self::$controlCharacters), array_keys(self::$controlCharacters), $value); + } + + /** + * Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters. + * + * @param string $value + * + * @return string + */ + public static function sanitizeUTF8($value) + { + if (self::getIsIconvEnabled()) { + $value = @iconv('UTF-8', 'UTF-8', $value); + + return $value; + } + + $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); + + return $value; + } + + /** + * Check if a string contains UTF8 data. + * + * @param string $value + * + * @return bool + */ + public static function isUTF8($value) + { + return $value === '' || preg_match('/^./su', $value) === 1; + } + + /** + * Formats a numeric value as a string for output in various output writers forcing + * point as decimal separator in case locale is other than English. + * + * @param mixed $value + * + * @return string + */ + public static function formatNumber($value) + { + if (is_float($value)) { + return str_replace(',', '.', $value); + } + + return (string) $value; + } + + /** + * Converts a UTF-8 string into BIFF8 Unicode string data (8-bit string length) + * Writes the string using uncompressed notation, no rich text, no Asian phonetics + * If mbstring extension is not available, ASCII is assumed, and compressed notation is used + * although this will give wrong results for non-ASCII strings + * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. + * + * @param string $value UTF-8 encoded string + * @param mixed[] $arrcRuns Details of rich text runs in $value + * + * @return string + */ + public static function UTF8toBIFF8UnicodeShort($value, $arrcRuns = []) + { + // character count + $ln = self::countCharacters($value, 'UTF-8'); + // option flags + if (empty($arrcRuns)) { + $data = pack('CC', $ln, 0x0001); + // characters + $data .= self::convertEncoding($value, 'UTF-16LE', 'UTF-8'); + } else { + $data = pack('vC', $ln, 0x09); + $data .= pack('v', count($arrcRuns)); + // characters + $data .= self::convertEncoding($value, 'UTF-16LE', 'UTF-8'); + foreach ($arrcRuns as $cRun) { + $data .= pack('v', $cRun['strlen']); + $data .= pack('v', $cRun['fontidx']); + } + } + + return $data; + } + + /** + * Converts a UTF-8 string into BIFF8 Unicode string data (16-bit string length) + * Writes the string using uncompressed notation, no rich text, no Asian phonetics + * If mbstring extension is not available, ASCII is assumed, and compressed notation is used + * although this will give wrong results for non-ASCII strings + * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. + * + * @param string $value UTF-8 encoded string + * + * @return string + */ + public static function UTF8toBIFF8UnicodeLong($value) + { + // character count + $ln = self::countCharacters($value, 'UTF-8'); + + // characters + $chars = self::convertEncoding($value, 'UTF-16LE', 'UTF-8'); + + $data = pack('vC', $ln, 0x0001) . $chars; + + return $data; + } + + /** + * Convert string from one encoding to another. + * + * @param string $value + * @param string $to Encoding to convert to, e.g. 'UTF-8' + * @param string $from Encoding to convert from, e.g. 'UTF-16LE' + * + * @return string + */ + public static function convertEncoding($value, $to, $from) + { + if (self::getIsIconvEnabled()) { + $result = iconv($from, $to . self::$iconvOptions, $value); + if (false !== $result) { + return $result; + } + } + + return mb_convert_encoding($value, $to, $from); + } + + /** + * Get character count. + * + * @param string $value + * @param string $enc Encoding + * + * @return int Character count + */ + public static function countCharacters($value, $enc = 'UTF-8') + { + return mb_strlen($value, $enc); + } + + /** + * Get a substring of a UTF-8 encoded string. + * + * @param string $pValue UTF-8 encoded string + * @param int $pStart Start offset + * @param int $pLength Maximum number of characters in substring + * + * @return string + */ + public static function substring($pValue, $pStart, $pLength = 0) + { + return mb_substr($pValue, $pStart, $pLength, 'UTF-8'); + } + + /** + * Convert a UTF-8 encoded string to upper case. + * + * @param string $pValue UTF-8 encoded string + * + * @return string + */ + public static function strToUpper($pValue) + { + return mb_convert_case($pValue, MB_CASE_UPPER, 'UTF-8'); + } + + /** + * Convert a UTF-8 encoded string to lower case. + * + * @param string $pValue UTF-8 encoded string + * + * @return string + */ + public static function strToLower($pValue) + { + return mb_convert_case($pValue, MB_CASE_LOWER, 'UTF-8'); + } + + /** + * Convert a UTF-8 encoded string to title/proper case + * (uppercase every first character in each word, lower case all other characters). + * + * @param string $pValue UTF-8 encoded string + * + * @return string + */ + public static function strToTitle($pValue) + { + return mb_convert_case($pValue, MB_CASE_TITLE, 'UTF-8'); + } + + public static function mbIsUpper($char) + { + return mb_strtolower($char, 'UTF-8') != $char; + } + + public static function mbStrSplit($string) + { + // Split at all position not after the start: ^ + // and not before the end: $ + return preg_split('/(?_calculateFormulaValue($fractionFormula); + + return true; + } + + return false; + } + + // function convertToNumberIfFraction() + + /** + * Get the decimal separator. If it has not yet been set explicitly, try to obtain number + * formatting information from locale. + * + * @return string + */ + public static function getDecimalSeparator() + { + if (!isset(self::$decimalSeparator)) { + $localeconv = localeconv(); + self::$decimalSeparator = ($localeconv['decimal_point'] != '') + ? $localeconv['decimal_point'] : $localeconv['mon_decimal_point']; + + if (self::$decimalSeparator == '') { + // Default to . + self::$decimalSeparator = '.'; + } + } + + return self::$decimalSeparator; + } + + /** + * Set the decimal separator. Only used by NumberFormat::toFormattedString() + * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. + * + * @param string $pValue Character for decimal separator + */ + public static function setDecimalSeparator($pValue) + { + self::$decimalSeparator = $pValue; + } + + /** + * Get the thousands separator. If it has not yet been set explicitly, try to obtain number + * formatting information from locale. + * + * @return string + */ + public static function getThousandsSeparator() + { + if (!isset(self::$thousandsSeparator)) { + $localeconv = localeconv(); + self::$thousandsSeparator = ($localeconv['thousands_sep'] != '') + ? $localeconv['thousands_sep'] : $localeconv['mon_thousands_sep']; + + if (self::$thousandsSeparator == '') { + // Default to . + self::$thousandsSeparator = ','; + } + } + + return self::$thousandsSeparator; + } + + /** + * Set the thousands separator. Only used by NumberFormat::toFormattedString() + * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. + * + * @param string $pValue Character for thousands separator + */ + public static function setThousandsSeparator($pValue) + { + self::$thousandsSeparator = $pValue; + } + + /** + * Get the currency code. If it has not yet been set explicitly, try to obtain the + * symbol information from locale. + * + * @return string + */ + public static function getCurrencyCode() + { + if (!empty(self::$currencyCode)) { + return self::$currencyCode; + } + self::$currencyCode = '$'; + $localeconv = localeconv(); + if (!empty($localeconv['currency_symbol'])) { + self::$currencyCode = $localeconv['currency_symbol']; + + return self::$currencyCode; + } + if (!empty($localeconv['int_curr_symbol'])) { + self::$currencyCode = $localeconv['int_curr_symbol']; + + return self::$currencyCode; + } + + return self::$currencyCode; + } + + /** + * Set the currency code. Only used by NumberFormat::toFormattedString() + * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. + * + * @param string $pValue Character for currency code + */ + public static function setCurrencyCode($pValue) + { + self::$currencyCode = $pValue; + } + + /** + * Convert SYLK encoded string to UTF-8. + * + * @param string $pValue + * + * @return string UTF-8 encoded string + */ + public static function SYLKtoUTF8($pValue) + { + self::buildCharacterSets(); + + // If there is no escape character in the string there is nothing to do + if (strpos($pValue, '') === false) { + return $pValue; + } + + foreach (self::$SYLKCharacters as $k => $v) { + $pValue = str_replace($k, $v, $pValue); + } + + return $pValue; + } + + /** + * Retrieve any leading numeric part of a string, or return the full string if no leading numeric + * (handles basic integer or float, but not exponent or non decimal). + * + * @param string $value + * + * @return mixed string or only the leading numeric part of the string + */ + public static function testStringAsNumeric($value) + { + if (is_numeric($value)) { + return $value; + } + $v = (float) $value; + + return (is_numeric(substr($value, 0, strlen($v)))) ? $v : $value; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/TimeZone.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/TimeZone.php new file mode 100644 index 00000000000..e5a99b9b6b8 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/TimeZone.php @@ -0,0 +1,87 @@ +getTransitions($timestamp, $timestamp); + + return (count($transitions) > 0) ? $transitions[0]['offset'] : 0; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/BestFit.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/BestFit.php new file mode 100644 index 00000000000..d8e63d5e289 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/BestFit.php @@ -0,0 +1,463 @@ +error; + } + + public function getBestFitType() + { + return $this->bestFitType; + } + + /** + * Return the Y-Value for a specified value of X. + * + * @param float $xValue X-Value + * + * @return bool Y-Value + */ + public function getValueOfYForX($xValue) + { + return false; + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return bool X-Value + */ + public function getValueOfXForY($yValue) + { + return false; + } + + /** + * Return the original set of X-Values. + * + * @return float[] X-Values + */ + public function getXValues() + { + return $this->xValues; + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return bool + */ + public function getEquation($dp = 0) + { + return false; + } + + /** + * Return the Slope of the line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getSlope($dp = 0) + { + if ($dp != 0) { + return round($this->slope, $dp); + } + + return $this->slope; + } + + /** + * Return the standard error of the Slope. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getSlopeSE($dp = 0) + { + if ($dp != 0) { + return round($this->slopeSE, $dp); + } + + return $this->slopeSE; + } + + /** + * Return the Value of X where it intersects Y = 0. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getIntersect($dp = 0) + { + if ($dp != 0) { + return round($this->intersect, $dp); + } + + return $this->intersect; + } + + /** + * Return the standard error of the Intersect. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getIntersectSE($dp = 0) + { + if ($dp != 0) { + return round($this->intersectSE, $dp); + } + + return $this->intersectSE; + } + + /** + * Return the goodness of fit for this regression. + * + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getGoodnessOfFit($dp = 0) + { + if ($dp != 0) { + return round($this->goodnessOfFit, $dp); + } + + return $this->goodnessOfFit; + } + + /** + * Return the goodness of fit for this regression. + * + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getGoodnessOfFitPercent($dp = 0) + { + if ($dp != 0) { + return round($this->goodnessOfFit * 100, $dp); + } + + return $this->goodnessOfFit * 100; + } + + /** + * Return the standard deviation of the residuals for this regression. + * + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getStdevOfResiduals($dp = 0) + { + if ($dp != 0) { + return round($this->stdevOfResiduals, $dp); + } + + return $this->stdevOfResiduals; + } + + /** + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getSSRegression($dp = 0) + { + if ($dp != 0) { + return round($this->SSRegression, $dp); + } + + return $this->SSRegression; + } + + /** + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getSSResiduals($dp = 0) + { + if ($dp != 0) { + return round($this->SSResiduals, $dp); + } + + return $this->SSResiduals; + } + + /** + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getDFResiduals($dp = 0) + { + if ($dp != 0) { + return round($this->DFResiduals, $dp); + } + + return $this->DFResiduals; + } + + /** + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getF($dp = 0) + { + if ($dp != 0) { + return round($this->f, $dp); + } + + return $this->f; + } + + /** + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getCovariance($dp = 0) + { + if ($dp != 0) { + return round($this->covariance, $dp); + } + + return $this->covariance; + } + + /** + * @param int $dp Number of places of decimal precision to return + * + * @return float + */ + public function getCorrelation($dp = 0) + { + if ($dp != 0) { + return round($this->correlation, $dp); + } + + return $this->correlation; + } + + /** + * @return float[] + */ + public function getYBestFitValues() + { + return $this->yBestFitValues; + } + + protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const) + { + $SSres = $SScov = $SScor = $SStot = $SSsex = 0.0; + foreach ($this->xValues as $xKey => $xValue) { + $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue); + + $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY); + if ($const) { + $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY); + } else { + $SStot += $this->yValues[$xKey] * $this->yValues[$xKey]; + } + $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY); + if ($const) { + $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX); + } else { + $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey]; + } + } + + $this->SSResiduals = $SSres; + $this->DFResiduals = $this->valueCount - 1 - $const; + + if ($this->DFResiduals == 0.0) { + $this->stdevOfResiduals = 0.0; + } else { + $this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals); + } + if (($SStot == 0.0) || ($SSres == $SStot)) { + $this->goodnessOfFit = 1; + } else { + $this->goodnessOfFit = 1 - ($SSres / $SStot); + } + + $this->SSRegression = $this->goodnessOfFit * $SStot; + $this->covariance = $SScov / $this->valueCount; + $this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - pow($sumX, 2)) * ($this->valueCount * $sumY2 - pow($sumY, 2))); + $this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex); + $this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2)); + if ($this->SSResiduals != 0.0) { + if ($this->DFResiduals == 0.0) { + $this->f = 0.0; + } else { + $this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals); + } + } else { + if ($this->DFResiduals == 0.0) { + $this->f = 0.0; + } else { + $this->f = $this->SSRegression / $this->DFResiduals; + } + } + } + + /** + * @param float[] $yValues + * @param float[] $xValues + * @param bool $const + */ + protected function leastSquareFit(array $yValues, array $xValues, $const) + { + // calculate sums + $x_sum = array_sum($xValues); + $y_sum = array_sum($yValues); + $meanX = $x_sum / $this->valueCount; + $meanY = $y_sum / $this->valueCount; + $mBase = $mDivisor = $xx_sum = $xy_sum = $yy_sum = 0.0; + for ($i = 0; $i < $this->valueCount; ++$i) { + $xy_sum += $xValues[$i] * $yValues[$i]; + $xx_sum += $xValues[$i] * $xValues[$i]; + $yy_sum += $yValues[$i] * $yValues[$i]; + + if ($const) { + $mBase += ($xValues[$i] - $meanX) * ($yValues[$i] - $meanY); + $mDivisor += ($xValues[$i] - $meanX) * ($xValues[$i] - $meanX); + } else { + $mBase += $xValues[$i] * $yValues[$i]; + $mDivisor += $xValues[$i] * $xValues[$i]; + } + } + + // calculate slope + $this->slope = $mBase / $mDivisor; + + // calculate intersect + if ($const) { + $this->intersect = $meanY - ($this->slope * $meanX); + } else { + $this->intersect = 0; + } + + $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, $meanX, $meanY, $const); + } + + /** + * Define the regression. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + // Calculate number of points + $nY = count($yValues); + $nX = count($xValues); + + // Define X Values if necessary + if ($nX == 0) { + $xValues = range(1, $nY); + } elseif ($nY != $nX) { + // Ensure both arrays of points are the same size + $this->error = true; + } + + $this->valueCount = $nY; + $this->xValues = $xValues; + $this->yValues = $yValues; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php new file mode 100644 index 00000000000..03723d84d4e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php @@ -0,0 +1,120 @@ +getIntersect() * pow($this->getSlope(), ($xValue - $this->xOffset)); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return log(($yValue + $this->yOffset) / $this->getIntersect()) / log($this->getSlope()); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' * ' . $slope . '^X'; + } + + /** + * Return the Slope of the line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getSlope($dp = 0) + { + if ($dp != 0) { + return round(exp($this->slope), $dp); + } + + return exp($this->slope); + } + + /** + * Return the Value of X where it intersects Y = 0. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getIntersect($dp = 0) + { + if ($dp != 0) { + return round(exp($this->intersect), $dp); + } + + return exp($this->intersect); + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + private function exponentialRegression($yValues, $xValues, $const) + { + foreach ($yValues as &$value) { + if ($value < 0.0) { + $value = 0 - log(abs($value)); + } elseif ($value > 0.0) { + $value = log($value); + } + } + unset($value); + + $this->leastSquareFit($yValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + if (parent::__construct($yValues, $xValues) !== false) { + $this->exponentialRegression($yValues, $xValues, $const); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/LinearBestFit.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/LinearBestFit.php new file mode 100644 index 00000000000..367e9d6ec56 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/LinearBestFit.php @@ -0,0 +1,79 @@ +getIntersect() + $this->getSlope() * $xValue; + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return ($yValue - $this->getIntersect()) / $this->getSlope(); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' + ' . $slope . ' * X'; + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + private function linearRegression($yValues, $xValues, $const) + { + $this->leastSquareFit($yValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + if (parent::__construct($yValues, $xValues) !== false) { + $this->linearRegression($yValues, $xValues, $const); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php new file mode 100644 index 00000000000..9092cef8215 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php @@ -0,0 +1,88 @@ +getIntersect() + $this->getSlope() * log($xValue - $this->xOffset); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return exp(($yValue - $this->getIntersect()) / $this->getSlope()); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' + ' . $slope . ' * log(X)'; + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + private function logarithmicRegression($yValues, $xValues, $const) + { + foreach ($xValues as &$value) { + if ($value < 0.0) { + $value = 0 - log(abs($value)); + } elseif ($value > 0.0) { + $value = log($value); + } + } + unset($value); + + $this->leastSquareFit($yValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + if (parent::__construct($yValues, $xValues) !== false) { + $this->logarithmicRegression($yValues, $xValues, $const); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php new file mode 100644 index 00000000000..afcf5f477e4 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php @@ -0,0 +1,198 @@ +order; + } + + /** + * Return the Y-Value for a specified value of X. + * + * @param float $xValue X-Value + * + * @return float Y-Value + */ + public function getValueOfYForX($xValue) + { + $retVal = $this->getIntersect(); + $slope = $this->getSlope(); + foreach ($slope as $key => $value) { + if ($value != 0.0) { + $retVal += $value * pow($xValue, $key + 1); + } + } + + return $retVal; + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return ($yValue - $this->getIntersect()) / $this->getSlope(); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + $equation = 'Y = ' . $intersect; + foreach ($slope as $key => $value) { + if ($value != 0.0) { + $equation .= ' + ' . $value . ' * X'; + if ($key > 0) { + $equation .= '^' . ($key + 1); + } + } + } + + return $equation; + } + + /** + * Return the Slope of the line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getSlope($dp = 0) + { + if ($dp != 0) { + $coefficients = []; + foreach ($this->slope as $coefficient) { + $coefficients[] = round($coefficient, $dp); + } + + return $coefficients; + } + + return $this->slope; + } + + public function getCoefficients($dp = 0) + { + return array_merge([$this->getIntersect($dp)], $this->getSlope($dp)); + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param int $order Order of Polynomial for this regression + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function polynomialRegression($order, $yValues, $xValues) + { + // calculate sums + $x_sum = array_sum($xValues); + $y_sum = array_sum($yValues); + $xx_sum = $xy_sum = $yy_sum = 0; + for ($i = 0; $i < $this->valueCount; ++$i) { + $xy_sum += $xValues[$i] * $yValues[$i]; + $xx_sum += $xValues[$i] * $xValues[$i]; + $yy_sum += $yValues[$i] * $yValues[$i]; + } + /* + * This routine uses logic from the PHP port of polyfit version 0.1 + * written by Michael Bommarito and Paul Meagher + * + * The function fits a polynomial function of order $order through + * a series of x-y data points using least squares. + * + */ + $A = []; + $B = []; + for ($i = 0; $i < $this->valueCount; ++$i) { + for ($j = 0; $j <= $order; ++$j) { + $A[$i][$j] = pow($xValues[$i], $j); + } + } + for ($i = 0; $i < $this->valueCount; ++$i) { + $B[$i] = [$yValues[$i]]; + } + $matrixA = new Matrix($A); + $matrixB = new Matrix($B); + $C = $matrixA->solve($matrixB); + + $coefficients = []; + for ($i = 0; $i < $C->getRowDimension(); ++$i) { + $r = $C->get($i, 0); + if (abs($r) <= pow(10, -9)) { + $r = 0; + } + $coefficients[] = $r; + } + + $this->intersect = array_shift($coefficients); + $this->slope = $coefficients; + + $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0); + foreach ($this->xValues as $xKey => $xValue) { + $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue); + } + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param int $order Order of Polynomial for this regression + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($order, $yValues, $xValues = [], $const = true) + { + if (parent::__construct($yValues, $xValues) !== false) { + if ($order < $this->valueCount) { + $this->bestFitType .= '_' . $order; + $this->order = $order; + $this->polynomialRegression($order, $yValues, $xValues); + if (($this->getGoodnessOfFit() < 0.0) || ($this->getGoodnessOfFit() > 1.0)) { + $this->error = true; + } + } else { + $this->error = true; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/PowerBestFit.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/PowerBestFit.php new file mode 100644 index 00000000000..e1b3b8297cb --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/PowerBestFit.php @@ -0,0 +1,112 @@ +getIntersect() * pow(($xValue - $this->xOffset), $this->getSlope()); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return pow((($yValue + $this->yOffset) / $this->getIntersect()), (1 / $this->getSlope())); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' * X^' . $slope; + } + + /** + * Return the Value of X where it intersects Y = 0. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getIntersect($dp = 0) + { + if ($dp != 0) { + return round(exp($this->intersect), $dp); + } + + return exp($this->intersect); + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + private function powerRegression($yValues, $xValues, $const) + { + foreach ($xValues as &$value) { + if ($value < 0.0) { + $value = 0 - log(abs($value)); + } elseif ($value > 0.0) { + $value = log($value); + } + } + unset($value); + foreach ($yValues as &$value) { + if ($value < 0.0) { + $value = 0 - log(abs($value)); + } elseif ($value > 0.0) { + $value = log($value); + } + } + unset($value); + + $this->leastSquareFit($yValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + if (parent::__construct($yValues, $xValues) !== false) { + $this->powerRegression($yValues, $xValues, $const); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/Trend.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/Trend.php new file mode 100644 index 00000000000..1b7b3901078 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Trend/Trend.php @@ -0,0 +1,120 @@ +getGoodnessOfFit(); + } + if ($trendType != self::TREND_BEST_FIT_NO_POLY) { + foreach (self::$trendTypePolynomialOrders as $trendMethod) { + $order = substr($trendMethod, -1); + $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues, $const); + if ($bestFit[$trendMethod]->getError()) { + unset($bestFit[$trendMethod]); + } else { + $bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit(); + } + } + } + // Determine which of our Trend lines is the best fit, and then we return the instance of that Trend class + arsort($bestFitValue); + $bestFitType = key($bestFitValue); + + return $bestFit[$bestFitType]; + default: + return false; + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/XMLWriter.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/XMLWriter.php new file mode 100644 index 00000000000..4f7a6a06afc --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/XMLWriter.php @@ -0,0 +1,92 @@ +openMemory(); + } else { + // Create temporary filename + if ($pTemporaryStorageFolder === null) { + $pTemporaryStorageFolder = File::sysGetTempDir(); + } + $this->tempFileName = @tempnam($pTemporaryStorageFolder, 'xml'); + + // Open storage + if ($this->openUri($this->tempFileName) === false) { + // Fallback to memory... + $this->openMemory(); + } + } + + // Set default values + if (self::$debugEnabled) { + $this->setIndent(true); + } + } + + /** + * Destructor. + */ + public function __destruct() + { + // Unlink temporary files + if ($this->tempFileName != '') { + @unlink($this->tempFileName); + } + } + + /** + * Get written data. + * + * @return string + */ + public function getData() + { + if ($this->tempFileName == '') { + return $this->outputMemory(true); + } + $this->flush(); + + return file_get_contents($this->tempFileName); + } + + /** + * Wrapper method for writeRaw. + * + * @param string|string[] $text + * + * @return bool + */ + public function writeRawData($text) + { + if (is_array($text)) { + $text = implode("\n", $text); + } + + return $this->writeRaw(htmlspecialchars($text)); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Xls.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Xls.php new file mode 100644 index 00000000000..177779f8f10 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Shared/Xls.php @@ -0,0 +1,283 @@ +getParent()->getDefaultStyle()->getFont(); + + $columnDimensions = $sheet->getColumnDimensions(); + + // first find the true column width in pixels (uncollapsed and unhidden) + if (isset($columnDimensions[$col]) and $columnDimensions[$col]->getWidth() != -1) { + // then we have column dimension with explicit width + $columnDimension = $columnDimensions[$col]; + $width = $columnDimension->getWidth(); + $pixelWidth = Drawing::cellDimensionToPixels($width, $font); + } elseif ($sheet->getDefaultColumnDimension()->getWidth() != -1) { + // then we have default column dimension with explicit width + $defaultColumnDimension = $sheet->getDefaultColumnDimension(); + $width = $defaultColumnDimension->getWidth(); + $pixelWidth = Drawing::cellDimensionToPixels($width, $font); + } else { + // we don't even have any default column dimension. Width depends on default font + $pixelWidth = Font::getDefaultColumnWidthByFont($font, true); + } + + // now find the effective column width in pixels + if (isset($columnDimensions[$col]) and !$columnDimensions[$col]->getVisible()) { + $effectivePixelWidth = 0; + } else { + $effectivePixelWidth = $pixelWidth; + } + + return $effectivePixelWidth; + } + + /** + * Convert the height of a cell from user's units to pixels. By interpolation + * the relationship is: y = 4/3x. If the height hasn't been set by the user we + * use the default value. If the row is hidden we use a value of zero. + * + * @param Worksheet $sheet The sheet + * @param int $row The row index (1-based) + * + * @return int The width in pixels + */ + public static function sizeRow($sheet, $row = 1) + { + // default font of the workbook + $font = $sheet->getParent()->getDefaultStyle()->getFont(); + + $rowDimensions = $sheet->getRowDimensions(); + + // first find the true row height in pixels (uncollapsed and unhidden) + if (isset($rowDimensions[$row]) and $rowDimensions[$row]->getRowHeight() != -1) { + // then we have a row dimension + $rowDimension = $rowDimensions[$row]; + $rowHeight = $rowDimension->getRowHeight(); + $pixelRowHeight = (int) ceil(4 * $rowHeight / 3); // here we assume Arial 10 + } elseif ($sheet->getDefaultRowDimension()->getRowHeight() != -1) { + // then we have a default row dimension with explicit height + $defaultRowDimension = $sheet->getDefaultRowDimension(); + $rowHeight = $defaultRowDimension->getRowHeight(); + $pixelRowHeight = Drawing::pointsToPixels($rowHeight); + } else { + // we don't even have any default row dimension. Height depends on default font + $pointRowHeight = Font::getDefaultRowHeightByFont($font); + $pixelRowHeight = Font::fontSizeToPixels($pointRowHeight); + } + + // now find the effective row height in pixels + if (isset($rowDimensions[$row]) and !$rowDimensions[$row]->getVisible()) { + $effectivePixelRowHeight = 0; + } else { + $effectivePixelRowHeight = $pixelRowHeight; + } + + return $effectivePixelRowHeight; + } + + /** + * Get the horizontal distance in pixels between two anchors + * The distanceX is found as sum of all the spanning columns widths minus correction for the two offsets. + * + * @param Worksheet $sheet + * @param string $startColumn + * @param int $startOffsetX Offset within start cell measured in 1/1024 of the cell width + * @param string $endColumn + * @param int $endOffsetX Offset within end cell measured in 1/1024 of the cell width + * + * @return int Horizontal measured in pixels + */ + public static function getDistanceX(Worksheet $sheet, $startColumn = 'A', $startOffsetX = 0, $endColumn = 'A', $endOffsetX = 0) + { + $distanceX = 0; + + // add the widths of the spanning columns + $startColumnIndex = Coordinate::columnIndexFromString($startColumn); + $endColumnIndex = Coordinate::columnIndexFromString($endColumn); + for ($i = $startColumnIndex; $i <= $endColumnIndex; ++$i) { + $distanceX += self::sizeCol($sheet, Coordinate::stringFromColumnIndex($i)); + } + + // correct for offsetX in startcell + $distanceX -= (int) floor(self::sizeCol($sheet, $startColumn) * $startOffsetX / 1024); + + // correct for offsetX in endcell + $distanceX -= (int) floor(self::sizeCol($sheet, $endColumn) * (1 - $endOffsetX / 1024)); + + return $distanceX; + } + + /** + * Get the vertical distance in pixels between two anchors + * The distanceY is found as sum of all the spanning rows minus two offsets. + * + * @param Worksheet $sheet + * @param int $startRow (1-based) + * @param int $startOffsetY Offset within start cell measured in 1/256 of the cell height + * @param int $endRow (1-based) + * @param int $endOffsetY Offset within end cell measured in 1/256 of the cell height + * + * @return int Vertical distance measured in pixels + */ + public static function getDistanceY(Worksheet $sheet, $startRow = 1, $startOffsetY = 0, $endRow = 1, $endOffsetY = 0) + { + $distanceY = 0; + + // add the widths of the spanning rows + for ($row = $startRow; $row <= $endRow; ++$row) { + $distanceY += self::sizeRow($sheet, $row); + } + + // correct for offsetX in startcell + $distanceY -= (int) floor(self::sizeRow($sheet, $startRow) * $startOffsetY / 256); + + // correct for offsetX in endcell + $distanceY -= (int) floor(self::sizeRow($sheet, $endRow) * (1 - $endOffsetY / 256)); + + return $distanceY; + } + + /** + * Convert 1-cell anchor coordinates to 2-cell anchor coordinates + * This function is ported from PEAR Spreadsheet_Writer_Excel with small modifications. + * + * Calculate the vertices that define the position of the image as required by + * the OBJ record. + * + * +------------+------------+ + * | A | B | + * +-----+------------+------------+ + * | |(x1,y1) | | + * | 1 |(A1)._______|______ | + * | | | | | + * | | | | | + * +-----+----| BITMAP |-----+ + * | | | | | + * | 2 | |______________. | + * | | | (B2)| + * | | | (x2,y2)| + * +---- +------------+------------+ + * + * Example of a bitmap that covers some of the area from cell A1 to cell B2. + * + * Based on the width and height of the bitmap we need to calculate 8 vars: + * $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2. + * The width and height of the cells are also variable and have to be taken into + * account. + * The values of $col_start and $row_start are passed in from the calling + * function. The values of $col_end and $row_end are calculated by subtracting + * the width and height of the bitmap from the width and height of the + * underlying cells. + * The vertices are expressed as a percentage of the underlying cell width as + * follows (rhs values are in pixels): + * + * x1 = X / W *1024 + * y1 = Y / H *256 + * x2 = (X-1) / W *1024 + * y2 = (Y-1) / H *256 + * + * Where: X is distance from the left side of the underlying cell + * Y is distance from the top of the underlying cell + * W is the width of the cell + * H is the height of the cell + * + * @param Worksheet $sheet + * @param string $coordinates E.g. 'A1' + * @param int $offsetX Horizontal offset in pixels + * @param int $offsetY Vertical offset in pixels + * @param int $width Width in pixels + * @param int $height Height in pixels + * + * @return array + */ + public static function oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height) + { + list($column, $row) = Coordinate::coordinateFromString($coordinates); + $col_start = Coordinate::columnIndexFromString($column); + $row_start = $row - 1; + + $x1 = $offsetX; + $y1 = $offsetY; + + // Initialise end cell to the same as the start cell + $col_end = $col_start; // Col containing lower right corner of object + $row_end = $row_start; // Row containing bottom right corner of object + + // Zero the specified offset if greater than the cell dimensions + if ($x1 >= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_start))) { + $x1 = 0; + } + if ($y1 >= self::sizeRow($sheet, $row_start + 1)) { + $y1 = 0; + } + + $width = $width + $x1 - 1; + $height = $height + $y1 - 1; + + // Subtract the underlying cell widths to find the end cell of the image + while ($width >= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end))) { + $width -= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end)); + ++$col_end; + } + + // Subtract the underlying cell heights to find the end cell of the image + while ($height >= self::sizeRow($sheet, $row_end + 1)) { + $height -= self::sizeRow($sheet, $row_end + 1); + ++$row_end; + } + + // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell + // with zero height or width. + if (self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_start)) == 0) { + return; + } + if (self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end)) == 0) { + return; + } + if (self::sizeRow($sheet, $row_start + 1) == 0) { + return; + } + if (self::sizeRow($sheet, $row_end + 1) == 0) { + return; + } + + // Convert the pixel values to the percentage value expected by Excel + $x1 = $x1 / self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_start)) * 1024; + $y1 = $y1 / self::sizeRow($sheet, $row_start + 1) * 256; + $x2 = ($width + 1) / self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end)) * 1024; // Distance to right side of object + $y2 = ($height + 1) / self::sizeRow($sheet, $row_end + 1) * 256; // Distance to bottom of object + + $startCoordinates = Coordinate::stringFromColumnIndex($col_start) . ($row_start + 1); + $endCoordinates = Coordinate::stringFromColumnIndex($col_end) . ($row_end + 1); + + $twoAnchor = [ + 'startCoordinates' => $startCoordinates, + 'startOffsetX' => $x1, + 'startOffsetY' => $y1, + 'endCoordinates' => $endCoordinates, + 'endOffsetX' => $x2, + 'endOffsetY' => $y2, + ]; + + return $twoAnchor; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Spreadsheet.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Spreadsheet.php new file mode 100644 index 00000000000..150f71b1e4f --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Spreadsheet.php @@ -0,0 +1,1490 @@ +hasMacros; + } + + /** + * Define if a workbook has macros. + * + * @param bool $hasMacros true|false + */ + public function setHasMacros($hasMacros) + { + $this->hasMacros = (bool) $hasMacros; + } + + /** + * Set the macros code. + * + * @param string $macroCode string|null + */ + public function setMacrosCode($macroCode) + { + $this->macrosCode = $macroCode; + $this->setHasMacros($macroCode !== null); + } + + /** + * Return the macros code. + * + * @return null|string + */ + public function getMacrosCode() + { + return $this->macrosCode; + } + + /** + * Set the macros certificate. + * + * @param null|string $certificate + */ + public function setMacrosCertificate($certificate) + { + $this->macrosCertificate = $certificate; + } + + /** + * Is the project signed ? + * + * @return bool true|false + */ + public function hasMacrosCertificate() + { + return $this->macrosCertificate !== null; + } + + /** + * Return the macros certificate. + * + * @return null|string + */ + public function getMacrosCertificate() + { + return $this->macrosCertificate; + } + + /** + * Remove all macros, certificate from spreadsheet. + */ + public function discardMacros() + { + $this->hasMacros = false; + $this->macrosCode = null; + $this->macrosCertificate = null; + } + + /** + * set ribbon XML data. + * + * @param null|mixed $target + * @param null|mixed $xmlData + */ + public function setRibbonXMLData($target, $xmlData) + { + if ($target !== null && $xmlData !== null) { + $this->ribbonXMLData = ['target' => $target, 'data' => $xmlData]; + } else { + $this->ribbonXMLData = null; + } + } + + /** + * retrieve ribbon XML Data. + * + * return string|null|array + * + * @param string $what + * + * @return string + */ + public function getRibbonXMLData($what = 'all') //we need some constants here... + { + $returnData = null; + $what = strtolower($what); + switch ($what) { + case 'all': + $returnData = $this->ribbonXMLData; + + break; + case 'target': + case 'data': + if (is_array($this->ribbonXMLData) && isset($this->ribbonXMLData[$what])) { + $returnData = $this->ribbonXMLData[$what]; + } + + break; + } + + return $returnData; + } + + /** + * store binaries ribbon objects (pictures). + * + * @param null|mixed $BinObjectsNames + * @param null|mixed $BinObjectsData + */ + public function setRibbonBinObjects($BinObjectsNames, $BinObjectsData) + { + if ($BinObjectsNames !== null && $BinObjectsData !== null) { + $this->ribbonBinObjects = ['names' => $BinObjectsNames, 'data' => $BinObjectsData]; + } else { + $this->ribbonBinObjects = null; + } + } + + /** + * List of unparsed loaded data for export to same format with better compatibility. + * It has to be minimized when the library start to support currently unparsed data. + * + * @internal + * + * @return array + */ + public function getUnparsedLoadedData() + { + return $this->unparsedLoadedData; + } + + /** + * List of unparsed loaded data for export to same format with better compatibility. + * It has to be minimized when the library start to support currently unparsed data. + * + * @internal + * + * @param array $unparsedLoadedData + */ + public function setUnparsedLoadedData(array $unparsedLoadedData) + { + $this->unparsedLoadedData = $unparsedLoadedData; + } + + /** + * return the extension of a filename. Internal use for a array_map callback (php<5.3 don't like lambda function). + * + * @param mixed $path + * + * @return string + */ + private function getExtensionOnly($path) + { + return pathinfo($path, PATHINFO_EXTENSION); + } + + /** + * retrieve Binaries Ribbon Objects. + * + * @param string $what + * + * @return null|array + */ + public function getRibbonBinObjects($what = 'all') + { + $ReturnData = null; + $what = strtolower($what); + switch ($what) { + case 'all': + return $this->ribbonBinObjects; + + break; + case 'names': + case 'data': + if (is_array($this->ribbonBinObjects) && isset($this->ribbonBinObjects[$what])) { + $ReturnData = $this->ribbonBinObjects[$what]; + } + + break; + case 'types': + if (is_array($this->ribbonBinObjects) && + isset($this->ribbonBinObjects['data']) && is_array($this->ribbonBinObjects['data'])) { + $tmpTypes = array_keys($this->ribbonBinObjects['data']); + $ReturnData = array_unique(array_map([$this, 'getExtensionOnly'], $tmpTypes)); + } else { + $ReturnData = []; // the caller want an array... not null if empty + } + + break; + } + + return $ReturnData; + } + + /** + * This workbook have a custom UI ? + * + * @return bool + */ + public function hasRibbon() + { + return $this->ribbonXMLData !== null; + } + + /** + * This workbook have additionnal object for the ribbon ? + * + * @return bool + */ + public function hasRibbonBinObjects() + { + return $this->ribbonBinObjects !== null; + } + + /** + * Check if a sheet with a specified code name already exists. + * + * @param string $pSheetCodeName Name of the worksheet to check + * + * @return bool + */ + public function sheetCodeNameExists($pSheetCodeName) + { + return $this->getSheetByCodeName($pSheetCodeName) !== null; + } + + /** + * Get sheet by code name. Warning : sheet don't have always a code name ! + * + * @param string $pName Sheet name + * + * @return Worksheet + */ + public function getSheetByCodeName($pName) + { + $worksheetCount = count($this->workSheetCollection); + for ($i = 0; $i < $worksheetCount; ++$i) { + if ($this->workSheetCollection[$i]->getCodeName() == $pName) { + return $this->workSheetCollection[$i]; + } + } + + return null; + } + + /** + * Create a new PhpSpreadsheet with one Worksheet. + */ + public function __construct() + { + $this->uniqueID = uniqid('', true); + $this->calculationEngine = new Calculation($this); + + // Initialise worksheet collection and add one worksheet + $this->workSheetCollection = []; + $this->workSheetCollection[] = new Worksheet($this); + $this->activeSheetIndex = 0; + + // Create document properties + $this->properties = new Document\Properties(); + + // Create document security + $this->security = new Document\Security(); + + // Set named ranges + $this->namedRanges = []; + + // Create the cellXf supervisor + $this->cellXfSupervisor = new Style(true); + $this->cellXfSupervisor->bindParent($this); + + // Create the default style + $this->addCellXf(new Style()); + $this->addCellStyleXf(new Style()); + } + + /** + * Code to execute when this worksheet is unset(). + */ + public function __destruct() + { + $this->calculationEngine = null; + $this->disconnectWorksheets(); + } + + /** + * Disconnect all worksheets from this PhpSpreadsheet workbook object, + * typically so that the PhpSpreadsheet object can be unset. + */ + public function disconnectWorksheets() + { + $worksheet = null; + foreach ($this->workSheetCollection as $k => &$worksheet) { + $worksheet->disconnectCells(); + $this->workSheetCollection[$k] = null; + } + unset($worksheet); + $this->workSheetCollection = []; + } + + /** + * Return the calculation engine for this worksheet. + * + * @return Calculation + */ + public function getCalculationEngine() + { + return $this->calculationEngine; + } + + /** + * Get properties. + * + * @return Document\Properties + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Set properties. + * + * @param Document\Properties $pValue + */ + public function setProperties(Document\Properties $pValue) + { + $this->properties = $pValue; + } + + /** + * Get security. + * + * @return Document\Security + */ + public function getSecurity() + { + return $this->security; + } + + /** + * Set security. + * + * @param Document\Security $pValue + */ + public function setSecurity(Document\Security $pValue) + { + $this->security = $pValue; + } + + /** + * Get active sheet. + * + * @throws Exception + * + * @return Worksheet + */ + public function getActiveSheet() + { + return $this->getSheet($this->activeSheetIndex); + } + + /** + * Create sheet and add it to this workbook. + * + * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) + * + * @throws Exception + * + * @return Worksheet + */ + public function createSheet($sheetIndex = null) + { + $newSheet = new Worksheet($this); + $this->addSheet($newSheet, $sheetIndex); + + return $newSheet; + } + + /** + * Check if a sheet with a specified name already exists. + * + * @param string $pSheetName Name of the worksheet to check + * + * @return bool + */ + public function sheetNameExists($pSheetName) + { + return $this->getSheetByName($pSheetName) !== null; + } + + /** + * Add sheet. + * + * @param Worksheet $pSheet + * @param null|int $iSheetIndex Index where sheet should go (0,1,..., or null for last) + * + * @throws Exception + * + * @return Worksheet + */ + public function addSheet(Worksheet $pSheet, $iSheetIndex = null) + { + if ($this->sheetNameExists($pSheet->getTitle())) { + throw new Exception( + "Workbook already contains a worksheet named '{$pSheet->getTitle()}'. Rename this worksheet first." + ); + } + + if ($iSheetIndex === null) { + if ($this->activeSheetIndex < 0) { + $this->activeSheetIndex = 0; + } + $this->workSheetCollection[] = $pSheet; + } else { + // Insert the sheet at the requested index + array_splice( + $this->workSheetCollection, + $iSheetIndex, + 0, + [$pSheet] + ); + + // Adjust active sheet index if necessary + if ($this->activeSheetIndex >= $iSheetIndex) { + ++$this->activeSheetIndex; + } + } + + if ($pSheet->getParent() === null) { + $pSheet->rebindParent($this); + } + + return $pSheet; + } + + /** + * Remove sheet by index. + * + * @param int $pIndex Active sheet index + * + * @throws Exception + */ + public function removeSheetByIndex($pIndex) + { + $numSheets = count($this->workSheetCollection); + if ($pIndex > $numSheets - 1) { + throw new Exception( + "You tried to remove a sheet by the out of bounds index: {$pIndex}. The actual number of sheets is {$numSheets}." + ); + } + array_splice($this->workSheetCollection, $pIndex, 1); + + // Adjust active sheet index if necessary + if (($this->activeSheetIndex >= $pIndex) && + ($pIndex > count($this->workSheetCollection) - 1)) { + --$this->activeSheetIndex; + } + } + + /** + * Get sheet by index. + * + * @param int $pIndex Sheet index + * + * @throws Exception + * + * @return Worksheet + */ + public function getSheet($pIndex) + { + if (!isset($this->workSheetCollection[$pIndex])) { + $numSheets = $this->getSheetCount(); + + throw new Exception( + "Your requested sheet index: {$pIndex} is out of bounds. The actual number of sheets is {$numSheets}." + ); + } + + return $this->workSheetCollection[$pIndex]; + } + + /** + * Get all sheets. + * + * @return Worksheet[] + */ + public function getAllSheets() + { + return $this->workSheetCollection; + } + + /** + * Get sheet by name. + * + * @param string $pName Sheet name + * + * @return Worksheet + */ + public function getSheetByName($pName) + { + $worksheetCount = count($this->workSheetCollection); + for ($i = 0; $i < $worksheetCount; ++$i) { + if ($this->workSheetCollection[$i]->getTitle() === $pName) { + return $this->workSheetCollection[$i]; + } + } + + return null; + } + + /** + * Get index for sheet. + * + * @param Worksheet $pSheet + * + * @throws Exception + * + * @return int index + */ + public function getIndex(Worksheet $pSheet) + { + foreach ($this->workSheetCollection as $key => $value) { + if ($value->getHashCode() == $pSheet->getHashCode()) { + return $key; + } + } + + throw new Exception('Sheet does not exist.'); + } + + /** + * Set index for sheet by sheet name. + * + * @param string $sheetName Sheet name to modify index for + * @param int $newIndex New index for the sheet + * + * @throws Exception + * + * @return int New sheet index + */ + public function setIndexByName($sheetName, $newIndex) + { + $oldIndex = $this->getIndex($this->getSheetByName($sheetName)); + $pSheet = array_splice( + $this->workSheetCollection, + $oldIndex, + 1 + ); + array_splice( + $this->workSheetCollection, + $newIndex, + 0, + $pSheet + ); + + return $newIndex; + } + + /** + * Get sheet count. + * + * @return int + */ + public function getSheetCount() + { + return count($this->workSheetCollection); + } + + /** + * Get active sheet index. + * + * @return int Active sheet index + */ + public function getActiveSheetIndex() + { + return $this->activeSheetIndex; + } + + /** + * Set active sheet index. + * + * @param int $pIndex Active sheet index + * + * @throws Exception + * + * @return Worksheet + */ + public function setActiveSheetIndex($pIndex) + { + $numSheets = count($this->workSheetCollection); + + if ($pIndex > $numSheets - 1) { + throw new Exception( + "You tried to set a sheet active by the out of bounds index: {$pIndex}. The actual number of sheets is {$numSheets}." + ); + } + $this->activeSheetIndex = $pIndex; + + return $this->getActiveSheet(); + } + + /** + * Set active sheet index by name. + * + * @param string $pValue Sheet title + * + * @throws Exception + * + * @return Worksheet + */ + public function setActiveSheetIndexByName($pValue) + { + if (($worksheet = $this->getSheetByName($pValue)) instanceof Worksheet) { + $this->setActiveSheetIndex($this->getIndex($worksheet)); + + return $worksheet; + } + + throw new Exception('Workbook does not contain sheet:' . $pValue); + } + + /** + * Get sheet names. + * + * @return string[] + */ + public function getSheetNames() + { + $returnValue = []; + $worksheetCount = $this->getSheetCount(); + for ($i = 0; $i < $worksheetCount; ++$i) { + $returnValue[] = $this->getSheet($i)->getTitle(); + } + + return $returnValue; + } + + /** + * Add external sheet. + * + * @param Worksheet $pSheet External sheet to add + * @param null|int $iSheetIndex Index where sheet should go (0,1,..., or null for last) + * + * @throws Exception + * + * @return Worksheet + */ + public function addExternalSheet(Worksheet $pSheet, $iSheetIndex = null) + { + if ($this->sheetNameExists($pSheet->getTitle())) { + throw new Exception("Workbook already contains a worksheet named '{$pSheet->getTitle()}'. Rename the external sheet first."); + } + + // count how many cellXfs there are in this workbook currently, we will need this below + $countCellXfs = count($this->cellXfCollection); + + // copy all the shared cellXfs from the external workbook and append them to the current + foreach ($pSheet->getParent()->getCellXfCollection() as $cellXf) { + $this->addCellXf(clone $cellXf); + } + + // move sheet to this workbook + $pSheet->rebindParent($this); + + // update the cellXfs + foreach ($pSheet->getCoordinates(false) as $coordinate) { + $cell = $pSheet->getCell($coordinate); + $cell->setXfIndex($cell->getXfIndex() + $countCellXfs); + } + + return $this->addSheet($pSheet, $iSheetIndex); + } + + /** + * Get named ranges. + * + * @return NamedRange[] + */ + public function getNamedRanges() + { + return $this->namedRanges; + } + + /** + * Add named range. + * + * @param NamedRange $namedRange + * + * @return bool + */ + public function addNamedRange(NamedRange $namedRange) + { + if ($namedRange->getScope() == null) { + // global scope + $this->namedRanges[$namedRange->getName()] = $namedRange; + } else { + // local scope + $this->namedRanges[$namedRange->getScope()->getTitle() . '!' . $namedRange->getName()] = $namedRange; + } + + return true; + } + + /** + * Get named range. + * + * @param string $namedRange + * @param null|Worksheet $pSheet Scope. Use null for global scope + * + * @return null|NamedRange + */ + public function getNamedRange($namedRange, Worksheet $pSheet = null) + { + $returnValue = null; + + if ($namedRange != '' && ($namedRange !== null)) { + // first look for global defined name + if (isset($this->namedRanges[$namedRange])) { + $returnValue = $this->namedRanges[$namedRange]; + } + + // then look for local defined name (has priority over global defined name if both names exist) + if (($pSheet !== null) && isset($this->namedRanges[$pSheet->getTitle() . '!' . $namedRange])) { + $returnValue = $this->namedRanges[$pSheet->getTitle() . '!' . $namedRange]; + } + } + + return $returnValue; + } + + /** + * Remove named range. + * + * @param string $namedRange + * @param null|Worksheet $pSheet scope: use null for global scope + * + * @return Spreadsheet + */ + public function removeNamedRange($namedRange, Worksheet $pSheet = null) + { + if ($pSheet === null) { + if (isset($this->namedRanges[$namedRange])) { + unset($this->namedRanges[$namedRange]); + } + } else { + if (isset($this->namedRanges[$pSheet->getTitle() . '!' . $namedRange])) { + unset($this->namedRanges[$pSheet->getTitle() . '!' . $namedRange]); + } + } + + return $this; + } + + /** + * Get worksheet iterator. + * + * @return Iterator + */ + public function getWorksheetIterator() + { + return new Iterator($this); + } + + /** + * Copy workbook (!= clone!). + * + * @return Spreadsheet + */ + public function copy() + { + $copied = clone $this; + + $worksheetCount = count($this->workSheetCollection); + for ($i = 0; $i < $worksheetCount; ++$i) { + $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); + $this->workSheetCollection[$i]->rebindParent($this); + } + + return $copied; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + foreach ($this as $key => $val) { + if (is_object($val) || (is_array($val))) { + $this->{$key} = unserialize(serialize($val)); + } + } + } + + /** + * Get the workbook collection of cellXfs. + * + * @return Style[] + */ + public function getCellXfCollection() + { + return $this->cellXfCollection; + } + + /** + * Get cellXf by index. + * + * @param int $pIndex + * + * @return Style + */ + public function getCellXfByIndex($pIndex) + { + return $this->cellXfCollection[$pIndex]; + } + + /** + * Get cellXf by hash code. + * + * @param string $pValue + * + * @return false|Style + */ + public function getCellXfByHashCode($pValue) + { + foreach ($this->cellXfCollection as $cellXf) { + if ($cellXf->getHashCode() == $pValue) { + return $cellXf; + } + } + + return false; + } + + /** + * Check if style exists in style collection. + * + * @param Style $pCellStyle + * + * @return bool + */ + public function cellXfExists($pCellStyle) + { + return in_array($pCellStyle, $this->cellXfCollection, true); + } + + /** + * Get default style. + * + * @throws Exception + * + * @return Style + */ + public function getDefaultStyle() + { + if (isset($this->cellXfCollection[0])) { + return $this->cellXfCollection[0]; + } + + throw new Exception('No default style found for this workbook'); + } + + /** + * Add a cellXf to the workbook. + * + * @param Style $style + */ + public function addCellXf(Style $style) + { + $this->cellXfCollection[] = $style; + $style->setIndex(count($this->cellXfCollection) - 1); + } + + /** + * Remove cellXf by index. It is ensured that all cells get their xf index updated. + * + * @param int $pIndex Index to cellXf + * + * @throws Exception + */ + public function removeCellXfByIndex($pIndex) + { + if ($pIndex > count($this->cellXfCollection) - 1) { + throw new Exception('CellXf index is out of bounds.'); + } + + // first remove the cellXf + array_splice($this->cellXfCollection, $pIndex, 1); + + // then update cellXf indexes for cells + foreach ($this->workSheetCollection as $worksheet) { + foreach ($worksheet->getCoordinates(false) as $coordinate) { + $cell = $worksheet->getCell($coordinate); + $xfIndex = $cell->getXfIndex(); + if ($xfIndex > $pIndex) { + // decrease xf index by 1 + $cell->setXfIndex($xfIndex - 1); + } elseif ($xfIndex == $pIndex) { + // set to default xf index 0 + $cell->setXfIndex(0); + } + } + } + } + + /** + * Get the cellXf supervisor. + * + * @return Style + */ + public function getCellXfSupervisor() + { + return $this->cellXfSupervisor; + } + + /** + * Get the workbook collection of cellStyleXfs. + * + * @return Style[] + */ + public function getCellStyleXfCollection() + { + return $this->cellStyleXfCollection; + } + + /** + * Get cellStyleXf by index. + * + * @param int $pIndex Index to cellXf + * + * @return Style + */ + public function getCellStyleXfByIndex($pIndex) + { + return $this->cellStyleXfCollection[$pIndex]; + } + + /** + * Get cellStyleXf by hash code. + * + * @param string $pValue + * + * @return false|Style + */ + public function getCellStyleXfByHashCode($pValue) + { + foreach ($this->cellStyleXfCollection as $cellStyleXf) { + if ($cellStyleXf->getHashCode() == $pValue) { + return $cellStyleXf; + } + } + + return false; + } + + /** + * Add a cellStyleXf to the workbook. + * + * @param Style $pStyle + */ + public function addCellStyleXf(Style $pStyle) + { + $this->cellStyleXfCollection[] = $pStyle; + $pStyle->setIndex(count($this->cellStyleXfCollection) - 1); + } + + /** + * Remove cellStyleXf by index. + * + * @param int $pIndex Index to cellXf + * + * @throws Exception + */ + public function removeCellStyleXfByIndex($pIndex) + { + if ($pIndex > count($this->cellStyleXfCollection) - 1) { + throw new Exception('CellStyleXf index is out of bounds.'); + } + array_splice($this->cellStyleXfCollection, $pIndex, 1); + } + + /** + * Eliminate all unneeded cellXf and afterwards update the xfIndex for all cells + * and columns in the workbook. + */ + public function garbageCollect() + { + // how many references are there to each cellXf ? + $countReferencesCellXf = []; + foreach ($this->cellXfCollection as $index => $cellXf) { + $countReferencesCellXf[$index] = 0; + } + + foreach ($this->getWorksheetIterator() as $sheet) { + // from cells + foreach ($sheet->getCoordinates(false) as $coordinate) { + $cell = $sheet->getCell($coordinate); + ++$countReferencesCellXf[$cell->getXfIndex()]; + } + + // from row dimensions + foreach ($sheet->getRowDimensions() as $rowDimension) { + if ($rowDimension->getXfIndex() !== null) { + ++$countReferencesCellXf[$rowDimension->getXfIndex()]; + } + } + + // from column dimensions + foreach ($sheet->getColumnDimensions() as $columnDimension) { + ++$countReferencesCellXf[$columnDimension->getXfIndex()]; + } + } + + // remove cellXfs without references and create mapping so we can update xfIndex + // for all cells and columns + $countNeededCellXfs = 0; + foreach ($this->cellXfCollection as $index => $cellXf) { + if ($countReferencesCellXf[$index] > 0 || $index == 0) { // we must never remove the first cellXf + ++$countNeededCellXfs; + } else { + unset($this->cellXfCollection[$index]); + } + $map[$index] = $countNeededCellXfs - 1; + } + $this->cellXfCollection = array_values($this->cellXfCollection); + + // update the index for all cellXfs + foreach ($this->cellXfCollection as $i => $cellXf) { + $cellXf->setIndex($i); + } + + // make sure there is always at least one cellXf (there should be) + if (empty($this->cellXfCollection)) { + $this->cellXfCollection[] = new Style(); + } + + // update the xfIndex for all cells, row dimensions, column dimensions + foreach ($this->getWorksheetIterator() as $sheet) { + // for all cells + foreach ($sheet->getCoordinates(false) as $coordinate) { + $cell = $sheet->getCell($coordinate); + $cell->setXfIndex($map[$cell->getXfIndex()]); + } + + // for all row dimensions + foreach ($sheet->getRowDimensions() as $rowDimension) { + if ($rowDimension->getXfIndex() !== null) { + $rowDimension->setXfIndex($map[$rowDimension->getXfIndex()]); + } + } + + // for all column dimensions + foreach ($sheet->getColumnDimensions() as $columnDimension) { + $columnDimension->setXfIndex($map[$columnDimension->getXfIndex()]); + } + + // also do garbage collection for all the sheets + $sheet->garbageCollect(); + } + } + + /** + * Return the unique ID value assigned to this spreadsheet workbook. + * + * @return string + */ + public function getID() + { + return $this->uniqueID; + } + + /** + * Get the visibility of the horizonal scroll bar in the application. + * + * @return bool True if horizonal scroll bar is visible + */ + public function getShowHorizontalScroll() + { + return $this->showHorizontalScroll; + } + + /** + * Set the visibility of the horizonal scroll bar in the application. + * + * @param bool $showHorizontalScroll True if horizonal scroll bar is visible + */ + public function setShowHorizontalScroll($showHorizontalScroll) + { + $this->showHorizontalScroll = (bool) $showHorizontalScroll; + } + + /** + * Get the visibility of the vertical scroll bar in the application. + * + * @return bool True if vertical scroll bar is visible + */ + public function getShowVerticalScroll() + { + return $this->showVerticalScroll; + } + + /** + * Set the visibility of the vertical scroll bar in the application. + * + * @param bool $showVerticalScroll True if vertical scroll bar is visible + */ + public function setShowVerticalScroll($showVerticalScroll) + { + $this->showVerticalScroll = (bool) $showVerticalScroll; + } + + /** + * Get the visibility of the sheet tabs in the application. + * + * @return bool True if the sheet tabs are visible + */ + public function getShowSheetTabs() + { + return $this->showSheetTabs; + } + + /** + * Set the visibility of the sheet tabs in the application. + * + * @param bool $showSheetTabs True if sheet tabs are visible + */ + public function setShowSheetTabs($showSheetTabs) + { + $this->showSheetTabs = (bool) $showSheetTabs; + } + + /** + * Return whether the workbook window is minimized. + * + * @return bool true if workbook window is minimized + */ + public function getMinimized() + { + return $this->minimized; + } + + /** + * Set whether the workbook window is minimized. + * + * @param bool $minimized true if workbook window is minimized + */ + public function setMinimized($minimized) + { + $this->minimized = (bool) $minimized; + } + + /** + * Return whether to group dates when presenting the user with + * filtering optiomd in the user interface. + * + * @return bool true if workbook window is minimized + */ + public function getAutoFilterDateGrouping() + { + return $this->autoFilterDateGrouping; + } + + /** + * Set whether to group dates when presenting the user with + * filtering optiomd in the user interface. + * + * @param bool $autoFilterDateGrouping true if workbook window is minimized + */ + public function setAutoFilterDateGrouping($autoFilterDateGrouping) + { + $this->autoFilterDateGrouping = (bool) $autoFilterDateGrouping; + } + + /** + * Return the first sheet in the book view. + * + * @return int First sheet in book view + */ + public function getFirstSheetIndex() + { + return $this->firstSheetIndex; + } + + /** + * Set the first sheet in the book view. + * + * @param int $firstSheetIndex First sheet in book view + * + * @throws Exception if the given value is invalid + */ + public function setFirstSheetIndex($firstSheetIndex) + { + if ($firstSheetIndex >= 0) { + $this->firstSheetIndex = (int) $firstSheetIndex; + } else { + throw new Exception('First sheet index must be a positive integer.'); + } + } + + /** + * Return the visibility status of the workbook. + * + * This may be one of the following three values: + * - visibile + * + * @return string Visible status + */ + public function getVisibility() + { + return $this->visibility; + } + + /** + * Set the visibility status of the workbook. + * + * Valid values are: + * - 'visible' (self::VISIBILITY_VISIBLE): + * Workbook window is visible + * - 'hidden' (self::VISIBILITY_HIDDEN): + * Workbook window is hidden, but can be shown by the user + * via the user interface + * - 'veryHidden' (self::VISIBILITY_VERY_HIDDEN): + * Workbook window is hidden and cannot be shown in the + * user interface. + * + * @param string $visibility visibility status of the workbook + * + * @throws Exception if the given value is invalid + */ + public function setVisibility($visibility) + { + if ($visibility === null) { + $visibility = self::VISIBILITY_VISIBLE; + } + + if (in_array($visibility, self::$workbookViewVisibilityValues)) { + $this->visibility = $visibility; + } else { + throw new Exception('Invalid visibility value.'); + } + } + + /** + * Get the ratio between the workbook tabs bar and the horizontal scroll bar. + * TabRatio is assumed to be out of 1000 of the horizontal window width. + * + * @return int Ratio between the workbook tabs bar and the horizontal scroll bar + */ + public function getTabRatio() + { + return $this->tabRatio; + } + + /** + * Set the ratio between the workbook tabs bar and the horizontal scroll bar + * TabRatio is assumed to be out of 1000 of the horizontal window width. + * + * @param int $tabRatio Ratio between the tabs bar and the horizontal scroll bar + * + * @throws Exception if the given value is invalid + */ + public function setTabRatio($tabRatio) + { + if ($tabRatio >= 0 || $tabRatio <= 1000) { + $this->tabRatio = (int) $tabRatio; + } else { + throw new Exception('Tab ratio must be between 0 and 1000.'); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Alignment.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Alignment.php new file mode 100644 index 00000000000..b4df792b167 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Alignment.php @@ -0,0 +1,466 @@ +horizontal = null; + $this->vertical = null; + $this->textRotation = null; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Alignment + */ + public function getSharedComponent() + { + return $this->parent->getSharedComponent()->getAlignment(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['alignment' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getAlignment()->applyFromArray( + * [ + * 'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER, + * 'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER, + * 'textRotation' => 0, + * 'wrapText' => TRUE + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return Alignment + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells()) + ->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['horizontal'])) { + $this->setHorizontal($pStyles['horizontal']); + } + if (isset($pStyles['vertical'])) { + $this->setVertical($pStyles['vertical']); + } + if (isset($pStyles['textRotation'])) { + $this->setTextRotation($pStyles['textRotation']); + } + if (isset($pStyles['wrapText'])) { + $this->setWrapText($pStyles['wrapText']); + } + if (isset($pStyles['shrinkToFit'])) { + $this->setShrinkToFit($pStyles['shrinkToFit']); + } + if (isset($pStyles['indent'])) { + $this->setIndent($pStyles['indent']); + } + if (isset($pStyles['readOrder'])) { + $this->setReadOrder($pStyles['readOrder']); + } + } + + return $this; + } + + /** + * Get Horizontal. + * + * @return string + */ + public function getHorizontal() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHorizontal(); + } + + return $this->horizontal; + } + + /** + * Set Horizontal. + * + * @param string $pValue see self::HORIZONTAL_* + * + * @return Alignment + */ + public function setHorizontal($pValue) + { + if ($pValue == '') { + $pValue = self::HORIZONTAL_GENERAL; + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['horizontal' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->horizontal = $pValue; + } + + return $this; + } + + /** + * Get Vertical. + * + * @return string + */ + public function getVertical() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getVertical(); + } + + return $this->vertical; + } + + /** + * Set Vertical. + * + * @param string $pValue see self::VERTICAL_* + * + * @return Alignment + */ + public function setVertical($pValue) + { + if ($pValue == '') { + $pValue = self::VERTICAL_BOTTOM; + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['vertical' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->vertical = $pValue; + } + + return $this; + } + + /** + * Get TextRotation. + * + * @return int + */ + public function getTextRotation() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getTextRotation(); + } + + return $this->textRotation; + } + + /** + * Set TextRotation. + * + * @param int $pValue + * + * @throws PhpSpreadsheetException + * + * @return Alignment + */ + public function setTextRotation($pValue) + { + // Excel2007 value 255 => PhpSpreadsheet value -165 + if ($pValue == 255) { + $pValue = -165; + } + + // Set rotation + if (($pValue >= -90 && $pValue <= 90) || $pValue == -165) { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['textRotation' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->textRotation = $pValue; + } + } else { + throw new PhpSpreadsheetException('Text rotation should be a value between -90 and 90.'); + } + + return $this; + } + + /** + * Get Wrap Text. + * + * @return bool + */ + public function getWrapText() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getWrapText(); + } + + return $this->wrapText; + } + + /** + * Set Wrap Text. + * + * @param bool $pValue + * + * @return Alignment + */ + public function setWrapText($pValue) + { + if ($pValue == '') { + $pValue = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['wrapText' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->wrapText = $pValue; + } + + return $this; + } + + /** + * Get Shrink to fit. + * + * @return bool + */ + public function getShrinkToFit() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getShrinkToFit(); + } + + return $this->shrinkToFit; + } + + /** + * Set Shrink to fit. + * + * @param bool $pValue + * + * @return Alignment + */ + public function setShrinkToFit($pValue) + { + if ($pValue == '') { + $pValue = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['shrinkToFit' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->shrinkToFit = $pValue; + } + + return $this; + } + + /** + * Get indent. + * + * @return int + */ + public function getIndent() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getIndent(); + } + + return $this->indent; + } + + /** + * Set indent. + * + * @param int $pValue + * + * @return Alignment + */ + public function setIndent($pValue) + { + if ($pValue > 0) { + if ($this->getHorizontal() != self::HORIZONTAL_GENERAL && + $this->getHorizontal() != self::HORIZONTAL_LEFT && + $this->getHorizontal() != self::HORIZONTAL_RIGHT) { + $pValue = 0; // indent not supported + } + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['indent' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->indent = $pValue; + } + + return $this; + } + + /** + * Get read order. + * + * @return int + */ + public function getReadOrder() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getReadOrder(); + } + + return $this->readOrder; + } + + /** + * Set read order. + * + * @param int $pValue + * + * @return Alignment + */ + public function setReadOrder($pValue) + { + if ($pValue < 0 || $pValue > 2) { + $pValue = 0; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['readOrder' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->readOrder = $pValue; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->horizontal . + $this->vertical . + $this->textRotation . + ($this->wrapText ? 't' : 'f') . + ($this->shrinkToFit ? 't' : 'f') . + $this->indent . + $this->readOrder . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Border.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Border.php new file mode 100644 index 00000000000..c957cf595a9 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Border.php @@ -0,0 +1,239 @@ +color = new Color(Color::COLOR_BLACK, $isSupervisor); + + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->color->bindParent($this, 'color'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function getSharedComponent() + { + switch ($this->parentPropertyName) { + case 'allBorders': + case 'horizontal': + case 'inside': + case 'outline': + case 'vertical': + throw new PhpSpreadsheetException('Cannot get shared component for a pseudo-border.'); + + break; + case 'bottom': + return $this->parent->getSharedComponent()->getBottom(); + case 'diagonal': + return $this->parent->getSharedComponent()->getDiagonal(); + case 'left': + return $this->parent->getSharedComponent()->getLeft(); + case 'right': + return $this->parent->getSharedComponent()->getRight(); + case 'top': + return $this->parent->getSharedComponent()->getTop(); + } + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return $this->parent->getStyleArray([$this->parentPropertyName => $array]); + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->getTop()->applyFromArray( + * [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['borderStyle'])) { + $this->setBorderStyle($pStyles['borderStyle']); + } + if (isset($pStyles['color'])) { + $this->getColor()->applyFromArray($pStyles['color']); + } + } + + return $this; + } + + /** + * Get Border style. + * + * @return string + */ + public function getBorderStyle() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBorderStyle(); + } + + return $this->borderStyle; + } + + /** + * Set Border style. + * + * @param bool|string $pValue + * When passing a boolean, FALSE equates Border::BORDER_NONE + * and TRUE to Border::BORDER_MEDIUM + * + * @return Border + */ + public function setBorderStyle($pValue) + { + if (empty($pValue)) { + $pValue = self::BORDER_NONE; + } elseif (is_bool($pValue) && $pValue) { + $pValue = self::BORDER_MEDIUM; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['borderStyle' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->borderStyle = $pValue; + } + + return $this; + } + + /** + * Get Border Color. + * + * @return Color + */ + public function getColor() + { + return $this->color; + } + + /** + * Set Border Color. + * + * @param Color $pValue + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function setColor(Color $pValue) + { + // make sure parameter is a real color and not a supervisor + $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue; + + if ($this->isSupervisor) { + $styleArray = $this->getColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->color = $color; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->borderStyle . + $this->color->getHashCode() . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Borders.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Borders.php new file mode 100644 index 00000000000..a1d6759b936 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Borders.php @@ -0,0 +1,423 @@ +left = new Border($isSupervisor, $isConditional); + $this->right = new Border($isSupervisor, $isConditional); + $this->top = new Border($isSupervisor, $isConditional); + $this->bottom = new Border($isSupervisor, $isConditional); + $this->diagonal = new Border($isSupervisor, $isConditional); + $this->diagonalDirection = self::DIAGONAL_NONE; + + // Specially for supervisor + if ($isSupervisor) { + // Initialize pseudo-borders + $this->allBorders = new Border(true); + $this->outline = new Border(true); + $this->inside = new Border(true); + $this->vertical = new Border(true); + $this->horizontal = new Border(true); + + // bind parent if we are a supervisor + $this->left->bindParent($this, 'left'); + $this->right->bindParent($this, 'right'); + $this->top->bindParent($this, 'top'); + $this->bottom->bindParent($this, 'bottom'); + $this->diagonal->bindParent($this, 'diagonal'); + $this->allBorders->bindParent($this, 'allBorders'); + $this->outline->bindParent($this, 'outline'); + $this->inside->bindParent($this, 'inside'); + $this->vertical->bindParent($this, 'vertical'); + $this->horizontal->bindParent($this, 'horizontal'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Borders + */ + public function getSharedComponent() + { + return $this->parent->getSharedComponent()->getBorders(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['borders' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->applyFromArray( + * [ + * 'bottom' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ], + * 'top' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ] + * ); + * + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->applyFromArray( + * [ + * 'allBorders' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return Borders + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['left'])) { + $this->getLeft()->applyFromArray($pStyles['left']); + } + if (isset($pStyles['right'])) { + $this->getRight()->applyFromArray($pStyles['right']); + } + if (isset($pStyles['top'])) { + $this->getTop()->applyFromArray($pStyles['top']); + } + if (isset($pStyles['bottom'])) { + $this->getBottom()->applyFromArray($pStyles['bottom']); + } + if (isset($pStyles['diagonal'])) { + $this->getDiagonal()->applyFromArray($pStyles['diagonal']); + } + if (isset($pStyles['diagonalDirection'])) { + $this->setDiagonalDirection($pStyles['diagonalDirection']); + } + if (isset($pStyles['allBorders'])) { + $this->getLeft()->applyFromArray($pStyles['allBorders']); + $this->getRight()->applyFromArray($pStyles['allBorders']); + $this->getTop()->applyFromArray($pStyles['allBorders']); + $this->getBottom()->applyFromArray($pStyles['allBorders']); + } + } + + return $this; + } + + /** + * Get Left. + * + * @return Border + */ + public function getLeft() + { + return $this->left; + } + + /** + * Get Right. + * + * @return Border + */ + public function getRight() + { + return $this->right; + } + + /** + * Get Top. + * + * @return Border + */ + public function getTop() + { + return $this->top; + } + + /** + * Get Bottom. + * + * @return Border + */ + public function getBottom() + { + return $this->bottom; + } + + /** + * Get Diagonal. + * + * @return Border + */ + public function getDiagonal() + { + return $this->diagonal; + } + + /** + * Get AllBorders (pseudo-border). Only applies to supervisor. + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function getAllBorders() + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->allBorders; + } + + /** + * Get Outline (pseudo-border). Only applies to supervisor. + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function getOutline() + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->outline; + } + + /** + * Get Inside (pseudo-border). Only applies to supervisor. + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function getInside() + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->inside; + } + + /** + * Get Vertical (pseudo-border). Only applies to supervisor. + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function getVertical() + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->vertical; + } + + /** + * Get Horizontal (pseudo-border). Only applies to supervisor. + * + * @throws PhpSpreadsheetException + * + * @return Border + */ + public function getHorizontal() + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->horizontal; + } + + /** + * Get DiagonalDirection. + * + * @return int + */ + public function getDiagonalDirection() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getDiagonalDirection(); + } + + return $this->diagonalDirection; + } + + /** + * Set DiagonalDirection. + * + * @param int $pValue see self::DIAGONAL_* + * + * @return Borders + */ + public function setDiagonalDirection($pValue) + { + if ($pValue == '') { + $pValue = self::DIAGONAL_NONE; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['diagonalDirection' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->diagonalDirection = $pValue; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashcode(); + } + + return md5( + $this->getLeft()->getHashCode() . + $this->getRight()->getHashCode() . + $this->getTop()->getHashCode() . + $this->getBottom()->getHashCode() . + $this->getDiagonal()->getHashCode() . + $this->getDiagonalDirection() . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Color.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Color.php new file mode 100644 index 00000000000..8a1812d20b7 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Color.php @@ -0,0 +1,403 @@ +argb = $pARGB; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Color + */ + public function getSharedComponent() + { + switch ($this->parentPropertyName) { + case 'endColor': + return $this->parent->getSharedComponent()->getEndColor(); + case 'color': + return $this->parent->getSharedComponent()->getColor(); + case 'startColor': + return $this->parent->getSharedComponent()->getStartColor(); + } + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return $this->parent->getStyleArray([$this->parentPropertyName => $array]); + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFont()->getColor()->applyFromArray(['rgb' => '808080']); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return Color + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['rgb'])) { + $this->setRGB($pStyles['rgb']); + } + if (isset($pStyles['argb'])) { + $this->setARGB($pStyles['argb']); + } + } + + return $this; + } + + /** + * Get ARGB. + * + * @return string + */ + public function getARGB() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getARGB(); + } + + return $this->argb; + } + + /** + * Set ARGB. + * + * @param string $pValue see self::COLOR_* + * + * @return Color + */ + public function setARGB($pValue) + { + if ($pValue == '') { + $pValue = self::COLOR_BLACK; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['argb' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->argb = $pValue; + } + + return $this; + } + + /** + * Get RGB. + * + * @return string + */ + public function getRGB() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getRGB(); + } + + return substr($this->argb, 2); + } + + /** + * Set RGB. + * + * @param string $pValue RGB value + * + * @return Color + */ + public function setRGB($pValue) + { + if ($pValue == '') { + $pValue = '000000'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['argb' => 'FF' . $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->argb = 'FF' . $pValue; + } + + return $this; + } + + /** + * Get a specified colour component of an RGB value. + * + * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param int $offset Position within the RGB value to extract + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return string The extracted colour component + */ + private static function getColourComponent($RGB, $offset, $hex = true) + { + $colour = substr($RGB, $offset, 2); + if (!$hex) { + $colour = hexdec($colour); + } + + return $colour; + } + + /** + * Get the red colour component of an RGB value. + * + * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return string The red colour component + */ + public static function getRed($RGB, $hex = true) + { + return self::getColourComponent($RGB, strlen($RGB) - 6, $hex); + } + + /** + * Get the green colour component of an RGB value. + * + * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return string The green colour component + */ + public static function getGreen($RGB, $hex = true) + { + return self::getColourComponent($RGB, strlen($RGB) - 4, $hex); + } + + /** + * Get the blue colour component of an RGB value. + * + * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return string The blue colour component + */ + public static function getBlue($RGB, $hex = true) + { + return self::getColourComponent($RGB, strlen($RGB) - 2, $hex); + } + + /** + * Adjust the brightness of a color. + * + * @param string $hex The colour as an RGBA or RGB value (e.g. FF00CCCC or CCDDEE) + * @param float $adjustPercentage The percentage by which to adjust the colour as a float from -1 to 1 + * + * @return string The adjusted colour as an RGBA or RGB value (e.g. FF00CCCC or CCDDEE) + */ + public static function changeBrightness($hex, $adjustPercentage) + { + $rgba = (strlen($hex) == 8); + + $red = self::getRed($hex, false); + $green = self::getGreen($hex, false); + $blue = self::getBlue($hex, false); + if ($adjustPercentage > 0) { + $red += (255 - $red) * $adjustPercentage; + $green += (255 - $green) * $adjustPercentage; + $blue += (255 - $blue) * $adjustPercentage; + } else { + $red += $red * $adjustPercentage; + $green += $green * $adjustPercentage; + $blue += $blue * $adjustPercentage; + } + + if ($red < 0) { + $red = 0; + } elseif ($red > 255) { + $red = 255; + } + if ($green < 0) { + $green = 0; + } elseif ($green > 255) { + $green = 255; + } + if ($blue < 0) { + $blue = 0; + } elseif ($blue > 255) { + $blue = 255; + } + + $rgb = strtoupper( + str_pad(dechex($red), 2, '0', 0) . + str_pad(dechex($green), 2, '0', 0) . + str_pad(dechex($blue), 2, '0', 0) + ); + + return (($rgba) ? 'FF' : '') . $rgb; + } + + /** + * Get indexed color. + * + * @param int $pIndex Index entry point into the colour array + * @param bool $background Flag to indicate whether default background or foreground colour + * should be returned if the indexed colour doesn't exist + * + * @return Color + */ + public static function indexedColor($pIndex, $background = false) + { + // Clean parameter + $pIndex = (int) $pIndex; + + // Indexed colors + if (self::$indexedColors === null) { + self::$indexedColors = [ + 1 => 'FF000000', // System Colour #1 - Black + 2 => 'FFFFFFFF', // System Colour #2 - White + 3 => 'FFFF0000', // System Colour #3 - Red + 4 => 'FF00FF00', // System Colour #4 - Green + 5 => 'FF0000FF', // System Colour #5 - Blue + 6 => 'FFFFFF00', // System Colour #6 - Yellow + 7 => 'FFFF00FF', // System Colour #7- Magenta + 8 => 'FF00FFFF', // System Colour #8- Cyan + 9 => 'FF800000', // Standard Colour #9 + 10 => 'FF008000', // Standard Colour #10 + 11 => 'FF000080', // Standard Colour #11 + 12 => 'FF808000', // Standard Colour #12 + 13 => 'FF800080', // Standard Colour #13 + 14 => 'FF008080', // Standard Colour #14 + 15 => 'FFC0C0C0', // Standard Colour #15 + 16 => 'FF808080', // Standard Colour #16 + 17 => 'FF9999FF', // Chart Fill Colour #17 + 18 => 'FF993366', // Chart Fill Colour #18 + 19 => 'FFFFFFCC', // Chart Fill Colour #19 + 20 => 'FFCCFFFF', // Chart Fill Colour #20 + 21 => 'FF660066', // Chart Fill Colour #21 + 22 => 'FFFF8080', // Chart Fill Colour #22 + 23 => 'FF0066CC', // Chart Fill Colour #23 + 24 => 'FFCCCCFF', // Chart Fill Colour #24 + 25 => 'FF000080', // Chart Line Colour #25 + 26 => 'FFFF00FF', // Chart Line Colour #26 + 27 => 'FFFFFF00', // Chart Line Colour #27 + 28 => 'FF00FFFF', // Chart Line Colour #28 + 29 => 'FF800080', // Chart Line Colour #29 + 30 => 'FF800000', // Chart Line Colour #30 + 31 => 'FF008080', // Chart Line Colour #31 + 32 => 'FF0000FF', // Chart Line Colour #32 + 33 => 'FF00CCFF', // Standard Colour #33 + 34 => 'FFCCFFFF', // Standard Colour #34 + 35 => 'FFCCFFCC', // Standard Colour #35 + 36 => 'FFFFFF99', // Standard Colour #36 + 37 => 'FF99CCFF', // Standard Colour #37 + 38 => 'FFFF99CC', // Standard Colour #38 + 39 => 'FFCC99FF', // Standard Colour #39 + 40 => 'FFFFCC99', // Standard Colour #40 + 41 => 'FF3366FF', // Standard Colour #41 + 42 => 'FF33CCCC', // Standard Colour #42 + 43 => 'FF99CC00', // Standard Colour #43 + 44 => 'FFFFCC00', // Standard Colour #44 + 45 => 'FFFF9900', // Standard Colour #45 + 46 => 'FFFF6600', // Standard Colour #46 + 47 => 'FF666699', // Standard Colour #47 + 48 => 'FF969696', // Standard Colour #48 + 49 => 'FF003366', // Standard Colour #49 + 50 => 'FF339966', // Standard Colour #50 + 51 => 'FF003300', // Standard Colour #51 + 52 => 'FF333300', // Standard Colour #52 + 53 => 'FF993300', // Standard Colour #53 + 54 => 'FF993366', // Standard Colour #54 + 55 => 'FF333399', // Standard Colour #55 + 56 => 'FF333333', // Standard Colour #56 + ]; + } + + if (isset(self::$indexedColors[$pIndex])) { + return new self(self::$indexedColors[$pIndex]); + } + + if ($background) { + return new self(self::COLOR_WHITE); + } + + return new self(self::COLOR_BLACK); + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->argb . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Conditional.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Conditional.php new file mode 100644 index 00000000000..91a000dba4a --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Conditional.php @@ -0,0 +1,272 @@ +style = new Style(false, true); + } + + /** + * Get Condition type. + * + * @return string + */ + public function getConditionType() + { + return $this->conditionType; + } + + /** + * Set Condition type. + * + * @param string $pValue Condition type, see self::CONDITION_* + * + * @return Conditional + */ + public function setConditionType($pValue) + { + $this->conditionType = $pValue; + + return $this; + } + + /** + * Get Operator type. + * + * @return string + */ + public function getOperatorType() + { + return $this->operatorType; + } + + /** + * Set Operator type. + * + * @param string $pValue Conditional operator type, see self::OPERATOR_* + * + * @return Conditional + */ + public function setOperatorType($pValue) + { + $this->operatorType = $pValue; + + return $this; + } + + /** + * Get text. + * + * @return string + */ + public function getText() + { + return $this->text; + } + + /** + * Set text. + * + * @param string $value + * + * @return Conditional + */ + public function setText($value) + { + $this->text = $value; + + return $this; + } + + /** + * Get StopIfTrue. + * + * @return bool + */ + public function getStopIfTrue() + { + return $this->stopIfTrue; + } + + /** + * Set StopIfTrue. + * + * @param bool $value + * + * @return Conditional + */ + public function setStopIfTrue($value) + { + $this->stopIfTrue = $value; + + return $this; + } + + /** + * Get Conditions. + * + * @return string[] + */ + public function getConditions() + { + return $this->condition; + } + + /** + * Set Conditions. + * + * @param string[] $pValue Condition + * + * @return Conditional + */ + public function setConditions($pValue) + { + if (!is_array($pValue)) { + $pValue = [$pValue]; + } + $this->condition = $pValue; + + return $this; + } + + /** + * Add Condition. + * + * @param string $pValue Condition + * + * @return Conditional + */ + public function addCondition($pValue) + { + $this->condition[] = $pValue; + + return $this; + } + + /** + * Get Style. + * + * @return Style + */ + public function getStyle() + { + return $this->style; + } + + /** + * Set Style. + * + * @param Style $pValue + * + * @return Conditional + */ + public function setStyle(Style $pValue = null) + { + $this->style = $pValue; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->conditionType . + $this->operatorType . + implode(';', $this->condition) . + $this->style->getHashCode() . + __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Fill.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Fill.php new file mode 100644 index 00000000000..c2ad895ee12 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Fill.php @@ -0,0 +1,325 @@ +fillType = null; + } + $this->startColor = new Color(Color::COLOR_WHITE, $isSupervisor, $isConditional); + $this->endColor = new Color(Color::COLOR_BLACK, $isSupervisor, $isConditional); + + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->startColor->bindParent($this, 'startColor'); + $this->endColor->bindParent($this, 'endColor'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Fill + */ + public function getSharedComponent() + { + return $this->parent->getSharedComponent()->getFill(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['fill' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFill()->applyFromArray( + * [ + * 'fillType' => Fill::FILL_GRADIENT_LINEAR, + * 'rotation' => 0, + * 'startColor' => [ + * 'rgb' => '000000' + * ], + * 'endColor' => [ + * 'argb' => 'FFFFFFFF' + * ] + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return Fill + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['fillType'])) { + $this->setFillType($pStyles['fillType']); + } + if (isset($pStyles['rotation'])) { + $this->setRotation($pStyles['rotation']); + } + if (isset($pStyles['startColor'])) { + $this->getStartColor()->applyFromArray($pStyles['startColor']); + } + if (isset($pStyles['endColor'])) { + $this->getEndColor()->applyFromArray($pStyles['endColor']); + } + if (isset($pStyles['color'])) { + $this->getStartColor()->applyFromArray($pStyles['color']); + $this->getEndColor()->applyFromArray($pStyles['color']); + } + } + + return $this; + } + + /** + * Get Fill Type. + * + * @return string + */ + public function getFillType() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getFillType(); + } + + return $this->fillType; + } + + /** + * Set Fill Type. + * + * @param string $pValue Fill type, see self::FILL_* + * + * @return Fill + */ + public function setFillType($pValue) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['fillType' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->fillType = $pValue; + } + + return $this; + } + + /** + * Get Rotation. + * + * @return float + */ + public function getRotation() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getRotation(); + } + + return $this->rotation; + } + + /** + * Set Rotation. + * + * @param float $pValue + * + * @return Fill + */ + public function setRotation($pValue) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['rotation' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->rotation = $pValue; + } + + return $this; + } + + /** + * Get Start Color. + * + * @return Color + */ + public function getStartColor() + { + return $this->startColor; + } + + /** + * Set Start Color. + * + * @param Color $pValue + * + * @throws PhpSpreadsheetException + * + * @return Fill + */ + public function setStartColor(Color $pValue) + { + // make sure parameter is a real color and not a supervisor + $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue; + + if ($this->isSupervisor) { + $styleArray = $this->getStartColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->startColor = $color; + } + + return $this; + } + + /** + * Get End Color. + * + * @return Color + */ + public function getEndColor() + { + return $this->endColor; + } + + /** + * Set End Color. + * + * @param Color $pValue + * + * @throws PhpSpreadsheetException + * + * @return Fill + */ + public function setEndColor(Color $pValue) + { + // make sure parameter is a real color and not a supervisor + $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue; + + if ($this->isSupervisor) { + $styleArray = $this->getEndColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->endColor = $color; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->getFillType() . + $this->getRotation() . + $this->getStartColor()->getHashCode() . + $this->getEndColor()->getHashCode() . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Font.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Font.php new file mode 100644 index 00000000000..6d8e23b1954 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Font.php @@ -0,0 +1,556 @@ +name = null; + $this->size = null; + $this->bold = null; + $this->italic = null; + $this->superscript = null; + $this->subscript = null; + $this->underline = null; + $this->strikethrough = null; + $this->color = new Color(Color::COLOR_BLACK, $isSupervisor, $isConditional); + } else { + $this->color = new Color(Color::COLOR_BLACK, $isSupervisor); + } + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->color->bindParent($this, 'color'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Font + */ + public function getSharedComponent() + { + return $this->parent->getSharedComponent()->getFont(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['font' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFont()->applyFromArray( + * [ + * 'name' => 'Arial', + * 'bold' => TRUE, + * 'italic' => FALSE, + * 'underline' => \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE, + * 'strikethrough' => FALSE, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return Font + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['name'])) { + $this->setName($pStyles['name']); + } + if (isset($pStyles['bold'])) { + $this->setBold($pStyles['bold']); + } + if (isset($pStyles['italic'])) { + $this->setItalic($pStyles['italic']); + } + if (isset($pStyles['superscript'])) { + $this->setSuperscript($pStyles['superscript']); + } + if (isset($pStyles['subscript'])) { + $this->setSubscript($pStyles['subscript']); + } + if (isset($pStyles['underline'])) { + $this->setUnderline($pStyles['underline']); + } + if (isset($pStyles['strikethrough'])) { + $this->setStrikethrough($pStyles['strikethrough']); + } + if (isset($pStyles['color'])) { + $this->getColor()->applyFromArray($pStyles['color']); + } + if (isset($pStyles['size'])) { + $this->setSize($pStyles['size']); + } + } + + return $this; + } + + /** + * Get Name. + * + * @return string + */ + public function getName() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getName(); + } + + return $this->name; + } + + /** + * Set Name. + * + * @param string $pValue + * + * @return Font + */ + public function setName($pValue) + { + if ($pValue == '') { + $pValue = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['name' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->name = $pValue; + } + + return $this; + } + + /** + * Get Size. + * + * @return float + */ + public function getSize() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSize(); + } + + return $this->size; + } + + /** + * Set Size. + * + * @param float $pValue + * + * @return Font + */ + public function setSize($pValue) + { + if ($pValue == '') { + $pValue = 10; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['size' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->size = $pValue; + } + + return $this; + } + + /** + * Get Bold. + * + * @return bool + */ + public function getBold() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBold(); + } + + return $this->bold; + } + + /** + * Set Bold. + * + * @param bool $pValue + * + * @return Font + */ + public function setBold($pValue) + { + if ($pValue == '') { + $pValue = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['bold' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->bold = $pValue; + } + + return $this; + } + + /** + * Get Italic. + * + * @return bool + */ + public function getItalic() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getItalic(); + } + + return $this->italic; + } + + /** + * Set Italic. + * + * @param bool $pValue + * + * @return Font + */ + public function setItalic($pValue) + { + if ($pValue == '') { + $pValue = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['italic' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->italic = $pValue; + } + + return $this; + } + + /** + * Get Superscript. + * + * @return bool + */ + public function getSuperscript() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSuperscript(); + } + + return $this->superscript; + } + + /** + * Set Superscript. + * + * @param bool $pValue + * + * @return Font + */ + public function setSuperscript($pValue) + { + if ($pValue == '') { + $pValue = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['superscript' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->superscript = $pValue; + $this->subscript = !$pValue; + } + + return $this; + } + + /** + * Get Subscript. + * + * @return bool + */ + public function getSubscript() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSubscript(); + } + + return $this->subscript; + } + + /** + * Set Subscript. + * + * @param bool $pValue + * + * @return Font + */ + public function setSubscript($pValue) + { + if ($pValue == '') { + $pValue = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['subscript' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->subscript = $pValue; + $this->superscript = !$pValue; + } + + return $this; + } + + /** + * Get Underline. + * + * @return string + */ + public function getUnderline() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUnderline(); + } + + return $this->underline; + } + + /** + * Set Underline. + * + * @param bool|string $pValue \PhpOffice\PhpSpreadsheet\Style\Font underline type + * If a boolean is passed, then TRUE equates to UNDERLINE_SINGLE, + * false equates to UNDERLINE_NONE + * + * @return Font + */ + public function setUnderline($pValue) + { + if (is_bool($pValue)) { + $pValue = ($pValue) ? self::UNDERLINE_SINGLE : self::UNDERLINE_NONE; + } elseif ($pValue == '') { + $pValue = self::UNDERLINE_NONE; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['underline' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->underline = $pValue; + } + + return $this; + } + + /** + * Get Strikethrough. + * + * @return bool + */ + public function getStrikethrough() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikethrough(); + } + + return $this->strikethrough; + } + + /** + * Set Strikethrough. + * + * @param bool $pValue + * + * @return Font + */ + public function setStrikethrough($pValue) + { + if ($pValue == '') { + $pValue = false; + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['strikethrough' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->strikethrough = $pValue; + } + + return $this; + } + + /** + * Get Color. + * + * @return Color + */ + public function getColor() + { + return $this->color; + } + + /** + * Set Color. + * + * @param Color $pValue + * + * @throws PhpSpreadsheetException + * + * @return Font + */ + public function setColor(Color $pValue) + { + // make sure parameter is a real color and not a supervisor + $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue; + + if ($this->isSupervisor) { + $styleArray = $this->getColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->color = $color; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->name . + $this->size . + ($this->bold ? 't' : 'f') . + ($this->italic ? 't' : 'f') . + ($this->superscript ? 't' : 'f') . + ($this->subscript ? 't' : 'f') . + $this->underline . + ($this->strikethrough ? 't' : 'f') . + $this->color->getHashCode() . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/NumberFormat.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/NumberFormat.php new file mode 100644 index 00000000000..b6b803787cd --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/NumberFormat.php @@ -0,0 +1,757 @@ +formatCode = null; + $this->builtInFormatCode = false; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return NumberFormat + */ + public function getSharedComponent() + { + return $this->parent->getSharedComponent()->getNumberFormat(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['numberFormat' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray( + * [ + * 'formatCode' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return NumberFormat + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['formatCode'])) { + $this->setFormatCode($pStyles['formatCode']); + } + } + + return $this; + } + + /** + * Get Format Code. + * + * @return string + */ + public function getFormatCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getFormatCode(); + } + if ($this->builtInFormatCode !== false) { + return self::builtInFormatCode($this->builtInFormatCode); + } + + return $this->formatCode; + } + + /** + * Set Format Code. + * + * @param string $pValue see self::FORMAT_* + * + * @return NumberFormat + */ + public function setFormatCode($pValue) + { + if ($pValue == '') { + $pValue = self::FORMAT_GENERAL; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['formatCode' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->formatCode = $pValue; + $this->builtInFormatCode = self::builtInFormatCodeIndex($pValue); + } + + return $this; + } + + /** + * Get Built-In Format Code. + * + * @return int + */ + public function getBuiltInFormatCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBuiltInFormatCode(); + } + + return $this->builtInFormatCode; + } + + /** + * Set Built-In Format Code. + * + * @param int $pValue + * + * @return NumberFormat + */ + public function setBuiltInFormatCode($pValue) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['formatCode' => self::builtInFormatCode($pValue)]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->builtInFormatCode = $pValue; + $this->formatCode = self::builtInFormatCode($pValue); + } + + return $this; + } + + /** + * Fill built-in format codes. + */ + private static function fillBuiltInFormatCodes() + { + // [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance] + // 18.8.30. numFmt (Number Format) + // + // The ECMA standard defines built-in format IDs + // 14: "mm-dd-yy" + // 22: "m/d/yy h:mm" + // 37: "#,##0 ;(#,##0)" + // 38: "#,##0 ;[Red](#,##0)" + // 39: "#,##0.00;(#,##0.00)" + // 40: "#,##0.00;[Red](#,##0.00)" + // 47: "mmss.0" + // KOR fmt 55: "yyyy-mm-dd" + // Excel defines built-in format IDs + // 14: "m/d/yyyy" + // 22: "m/d/yyyy h:mm" + // 37: "#,##0_);(#,##0)" + // 38: "#,##0_);[Red](#,##0)" + // 39: "#,##0.00_);(#,##0.00)" + // 40: "#,##0.00_);[Red](#,##0.00)" + // 47: "mm:ss.0" + // KOR fmt 55: "yyyy/mm/dd" + + // Built-in format codes + if (self::$builtInFormats === null) { + self::$builtInFormats = []; + + // General + self::$builtInFormats[0] = self::FORMAT_GENERAL; + self::$builtInFormats[1] = '0'; + self::$builtInFormats[2] = '0.00'; + self::$builtInFormats[3] = '#,##0'; + self::$builtInFormats[4] = '#,##0.00'; + + self::$builtInFormats[9] = '0%'; + self::$builtInFormats[10] = '0.00%'; + self::$builtInFormats[11] = '0.00E+00'; + self::$builtInFormats[12] = '# ?/?'; + self::$builtInFormats[13] = '# ??/??'; + self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy'; + self::$builtInFormats[15] = 'd-mmm-yy'; + self::$builtInFormats[16] = 'd-mmm'; + self::$builtInFormats[17] = 'mmm-yy'; + self::$builtInFormats[18] = 'h:mm AM/PM'; + self::$builtInFormats[19] = 'h:mm:ss AM/PM'; + self::$builtInFormats[20] = 'h:mm'; + self::$builtInFormats[21] = 'h:mm:ss'; + self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm'; + + self::$builtInFormats[37] = '#,##0_);(#,##0)'; // Despite ECMA '#,##0 ;(#,##0)'; + self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; // Despite ECMA '#,##0 ;[Red](#,##0)'; + self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; // Despite ECMA '#,##0.00;(#,##0.00)'; + self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; // Despite ECMA '#,##0.00;[Red](#,##0.00)'; + + self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)'; + self::$builtInFormats[45] = 'mm:ss'; + self::$builtInFormats[46] = '[h]:mm:ss'; + self::$builtInFormats[47] = 'mm:ss.0'; // Despite ECMA 'mmss.0'; + self::$builtInFormats[48] = '##0.0E+0'; + self::$builtInFormats[49] = '@'; + + // CHT + self::$builtInFormats[27] = '[$-404]e/m/d'; + self::$builtInFormats[30] = 'm/d/yy'; + self::$builtInFormats[36] = '[$-404]e/m/d'; + self::$builtInFormats[50] = '[$-404]e/m/d'; + self::$builtInFormats[57] = '[$-404]e/m/d'; + + // THA + self::$builtInFormats[59] = 't0'; + self::$builtInFormats[60] = 't0.00'; + self::$builtInFormats[61] = 't#,##0'; + self::$builtInFormats[62] = 't#,##0.00'; + self::$builtInFormats[67] = 't0%'; + self::$builtInFormats[68] = 't0.00%'; + self::$builtInFormats[69] = 't# ?/?'; + self::$builtInFormats[70] = 't# ??/??'; + + // Flip array (for faster lookups) + self::$flippedBuiltInFormats = array_flip(self::$builtInFormats); + } + } + + /** + * Get built-in format code. + * + * @param int $pIndex + * + * @return string + */ + public static function builtInFormatCode($pIndex) + { + // Clean parameter + $pIndex = (int) $pIndex; + + // Ensure built-in format codes are available + self::fillBuiltInFormatCodes(); + + // Lookup format code + if (isset(self::$builtInFormats[$pIndex])) { + return self::$builtInFormats[$pIndex]; + } + + return ''; + } + + /** + * Get built-in format code index. + * + * @param string $formatCode + * + * @return bool|int + */ + public static function builtInFormatCodeIndex($formatCode) + { + // Ensure built-in format codes are available + self::fillBuiltInFormatCodes(); + + // Lookup format code + if (isset(self::$flippedBuiltInFormats[$formatCode])) { + return self::$flippedBuiltInFormats[$formatCode]; + } + + return false; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->formatCode . + $this->builtInFormatCode . + __CLASS__ + ); + } + + /** + * Search/replace values to convert Excel date/time format masks to PHP format masks. + * + * @var array + */ + private static $dateFormatReplacements = [ + // first remove escapes related to non-format characters + '\\' => '', + // 12-hour suffix + 'am/pm' => 'A', + // 4-digit year + 'e' => 'Y', + 'yyyy' => 'Y', + // 2-digit year + 'yy' => 'y', + // first letter of month - no php equivalent + 'mmmmm' => 'M', + // full month name + 'mmmm' => 'F', + // short month name + 'mmm' => 'M', + // mm is minutes if time, but can also be month w/leading zero + // so we try to identify times be the inclusion of a : separator in the mask + // It isn't perfect, but the best way I know how + ':mm' => ':i', + 'mm:' => 'i:', + // month leading zero + 'mm' => 'm', + // month no leading zero + 'm' => 'n', + // full day of week name + 'dddd' => 'l', + // short day of week name + 'ddd' => 'D', + // days leading zero + 'dd' => 'd', + // days no leading zero + 'd' => 'j', + // seconds + 'ss' => 's', + // fractional seconds - no php equivalent + '.s' => '', + ]; + + /** + * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock). + * + * @var array + */ + private static $dateFormatReplacements24 = [ + 'hh' => 'H', + 'h' => 'G', + ]; + + /** + * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock). + * + * @var array + */ + private static $dateFormatReplacements12 = [ + 'hh' => 'h', + 'h' => 'g', + ]; + + private static function setLowercaseCallback($matches) + { + return mb_strtolower($matches[0]); + } + + private static function escapeQuotesCallback($matches) + { + return '\\' . implode('\\', str_split($matches[1])); + } + + private static function formatAsDate(&$value, &$format) + { + // strip off first part containing e.g. [$-F800] or [$USD-409] + // general syntax: [$-] + // language info is in hexadecimal + // strip off chinese part like [DBNum1][$-804] + $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format); + + // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case; + // but we don't want to change any quoted strings + $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format); + + // Only process the non-quoted blocks for date format characters + $blocks = explode('"', $format); + foreach ($blocks as $key => &$block) { + if ($key % 2 == 0) { + $block = strtr($block, self::$dateFormatReplacements); + if (!strpos($block, 'A')) { + // 24-hour time format + // when [h]:mm format, the [h] should replace to the hours of the value * 24 + if (false !== strpos($block, '[h]')) { + $hours = (int) ($value * 24); + $block = str_replace('[h]', $hours, $block); + + continue; + } + $block = strtr($block, self::$dateFormatReplacements24); + } else { + // 12-hour time format + $block = strtr($block, self::$dateFormatReplacements12); + } + } + } + $format = implode('"', $blocks); + + // escape any quoted characters so that DateTime format() will render them correctly + $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format); + + $dateObj = Date::excelToDateTimeObject($value); + $value = $dateObj->format($format); + } + + private static function formatAsPercentage(&$value, &$format) + { + if ($format === self::FORMAT_PERCENTAGE) { + $value = round((100 * $value), 0) . '%'; + } else { + if (preg_match('/\.[#0]+/', $format, $m)) { + $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1); + $format = str_replace($m[0], $s, $format); + } + if (preg_match('/^[#0]+/', $format, $m)) { + $format = str_replace($m[0], strlen($m[0]), $format); + } + $format = '%' . str_replace('%', 'f%%', $format); + + $value = sprintf($format, 100 * $value); + } + } + + private static function formatAsFraction(&$value, &$format) + { + $sign = ($value < 0) ? '-' : ''; + + $integerPart = floor(abs($value)); + $decimalPart = trim(fmod(abs($value), 1), '0.'); + $decimalLength = strlen($decimalPart); + $decimalDivisor = pow(10, $decimalLength); + + $GCD = MathTrig::GCD($decimalPart, $decimalDivisor); + + $adjustedDecimalPart = $decimalPart / $GCD; + $adjustedDecimalDivisor = $decimalDivisor / $GCD; + + if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) { + if ($integerPart == 0) { + $integerPart = ''; + } + $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor"; + } else { + $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor; + $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor"; + } + } + + private static function complexNumberFormatMask($number, $mask) + { + $sign = ($number < 0.0); + $number = abs($number); + if (strpos($mask, '.') !== false) { + $numbers = explode('.', $number . '.0'); + $masks = explode('.', $mask . '.0'); + $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]); + $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]))); + + return (($sign) ? '-' : '') . $result1 . '.' . $result2; + } + + $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE); + if ($r > 1) { + $result = array_reverse($result[0]); + + foreach ($result as $block) { + $divisor = 1 . $block[0]; + $size = strlen($block[0]); + $offset = $block[1]; + + $blockValue = sprintf( + '%0' . $size . 'd', + fmod($number, $divisor) + ); + $number = floor($number / $divisor); + $mask = substr_replace($mask, $blockValue, $offset, $size); + } + if ($number > 0) { + $mask = substr_replace($mask, $number, $offset, 0); + } + $result = $mask; + } else { + $result = $number; + } + + return (($sign) ? '-' : '') . $result; + } + + /** + * Convert a value in a pre-defined format to a PHP string. + * + * @param mixed $value Value to format + * @param string $format Format code, see = self::FORMAT_* + * @param array $callBack Callback function for additional formatting of string + * + * @return string Formatted string + */ + public static function toFormattedString($value, $format, $callBack = null) + { + // For now we do not treat strings although section 4 of a format code affects strings + if (!is_numeric($value)) { + return $value; + } + + // For 'General' format code, we just pass the value although this is not entirely the way Excel does it, + // it seems to round numbers to a total of 10 digits. + if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) { + return $value; + } + + // Convert any other escaped characters to quoted strings, e.g. (\T to "T") + $format = preg_replace('/(\\\([^ ]))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format); + + // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal) + $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format); + + // Extract the relevant section depending on whether number is positive, negative, or zero? + // Text not supported yet. + // Here is how the sections apply to various values in Excel: + // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT] + // 2 sections: [POSITIVE/ZERO/TEXT] [NEGATIVE] + // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO] + // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT] + switch (count($sections)) { + case 1: + $format = $sections[0]; + + break; + case 2: + $format = ($value >= 0) ? $sections[0] : $sections[1]; + $value = abs($value); // Use the absolute value + break; + case 3: + $format = ($value > 0) ? + $sections[0] : (($value < 0) ? + $sections[1] : $sections[2]); + $value = abs($value); // Use the absolute value + break; + case 4: + $format = ($value > 0) ? + $sections[0] : (($value < 0) ? + $sections[1] : $sections[2]); + $value = abs($value); // Use the absolute value + break; + default: + // something is wrong, just use first section + $format = $sections[0]; + + break; + } + + // In Excel formats, "_" is used to add spacing, + // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space + $format = preg_replace('/_./', ' ', $format); + + // Save format with color information for later use below + $formatColor = $format; + + // Let's begin inspecting the format and converting the value to a formatted string + + // Check for date/time characters (not inside quotes) + if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) { + // datetime format + self::formatAsDate($value, $format); + } else { + // Strip color information + $color_regex = '/^\\[[a-zA-Z]+\\]/'; + $format = preg_replace($color_regex, '', $format); + if (preg_match('/%$/', $format)) { + // % number format + self::formatAsPercentage($value, $format); + } else { + if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) { + $value = 'EUR ' . sprintf('%1.2f', $value); + } else { + // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols + $format = str_replace(['"', '*'], '', $format); + + // Find out if we need thousands separator + // This is indicated by a comma enclosed by a digit placeholder: + // #,# or 0,0 + $useThousands = preg_match('/(#,#|0,0)/', $format); + if ($useThousands) { + $format = preg_replace('/0,0/', '00', $format); + $format = preg_replace('/#,#/', '##', $format); + } + + // Scale thousands, millions,... + // This is indicated by a number of commas after a digit placeholder: + // #, or 0.0,, + $scale = 1; // same as no scale + $matches = []; + if (preg_match('/(#|0)(,+)/', $format, $matches)) { + $scale = pow(1000, strlen($matches[2])); + + // strip the commas + $format = preg_replace('/0,+/', '0', $format); + $format = preg_replace('/#,+/', '#', $format); + } + + if (preg_match('/#?.*\?\/\?/', $format, $m)) { + if ($value != (int) $value) { + self::formatAsFraction($value, $format); + } + } else { + // Handle the number itself + + // scale number + $value = $value / $scale; + + // Strip # + $format = preg_replace('/\\#/', '0', $format); + + // Remove locale code [$-###] + $format = preg_replace('/\[\$\-.*\]/', '', $format); + + $n = '/\\[[^\\]]+\\]/'; + $m = preg_replace($n, '', $format); + $number_regex = '/(0+)(\\.?)(0*)/'; + if (preg_match($number_regex, $m, $matches)) { + $left = $matches[1]; + $dec = $matches[2]; + $right = $matches[3]; + + // minimun width of formatted number (including dot) + $minWidth = strlen($left) + strlen($dec) + strlen($right); + if ($useThousands) { + $value = number_format( + $value, + strlen($right), + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); + $value = preg_replace($number_regex, $value, $format); + } else { + if (preg_match('/[0#]E[+-]0/i', $format)) { + // Scientific format + $value = sprintf('%5.2E', $value); + } elseif (preg_match('/0([^\d\.]+)0/', $format)) { + $value = self::complexNumberFormatMask($value, $format); + } else { + $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; + $value = sprintf($sprintf_pattern, $value); + $value = preg_replace($number_regex, $value, $format); + } + } + } + } + if (preg_match('/\[\$(.*)\]/u', $format, $m)) { + // Currency or Accounting + $currencyCode = $m[1]; + list($currencyCode) = explode('-', $currencyCode); + if ($currencyCode == '') { + $currencyCode = StringHelper::getCurrencyCode(); + } + $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value); + } + } + } + } + + // Additional formatting provided by callback function + if ($callBack !== null) { + list($writerInstance, $function) = $callBack; + $value = $writerInstance->$function($value, $formatColor); + } + + return $value; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Protection.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Protection.php new file mode 100644 index 00000000000..b5feb53486e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Protection.php @@ -0,0 +1,190 @@ +locked = self::PROTECTION_INHERIT; + $this->hidden = self::PROTECTION_INHERIT; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Protection + */ + public function getSharedComponent() + { + return $this->parent->getSharedComponent()->getProtection(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['protection' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getLocked()->applyFromArray( + * [ + * 'locked' => TRUE, + * 'hidden' => FALSE + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * + * @throws PhpSpreadsheetException + * + * @return Protection + */ + public function applyFromArray(array $pStyles) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles)); + } else { + if (isset($pStyles['locked'])) { + $this->setLocked($pStyles['locked']); + } + if (isset($pStyles['hidden'])) { + $this->setHidden($pStyles['hidden']); + } + } + + return $this; + } + + /** + * Get locked. + * + * @return string + */ + public function getLocked() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getLocked(); + } + + return $this->locked; + } + + /** + * Set locked. + * + * @param string $pValue see self::PROTECTION_* + * + * @return Protection + */ + public function setLocked($pValue) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['locked' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->locked = $pValue; + } + + return $this; + } + + /** + * Get hidden. + * + * @return string + */ + public function getHidden() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHidden(); + } + + return $this->hidden; + } + + /** + * Set hidden. + * + * @param string $pValue see self::PROTECTION_* + * + * @return Protection + */ + public function setHidden($pValue) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['hidden' => $pValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->hidden = $pValue; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->locked . + $this->hidden . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Style.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Style.php new file mode 100644 index 00000000000..fbe07c2cdd2 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Style.php @@ -0,0 +1,641 @@ +conditionalStyles = []; + $this->font = new Font($isSupervisor, $isConditional); + $this->fill = new Fill($isSupervisor, $isConditional); + $this->borders = new Borders($isSupervisor, $isConditional); + $this->alignment = new Alignment($isSupervisor, $isConditional); + $this->numberFormat = new NumberFormat($isSupervisor, $isConditional); + $this->protection = new Protection($isSupervisor, $isConditional); + + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->font->bindParent($this); + $this->fill->bindParent($this); + $this->borders->bindParent($this); + $this->alignment->bindParent($this); + $this->numberFormat->bindParent($this); + $this->protection->bindParent($this); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Style + */ + public function getSharedComponent() + { + $activeSheet = $this->getActiveSheet(); + $selectedCell = $this->getActiveCell(); // e.g. 'A1' + + if ($activeSheet->cellExists($selectedCell)) { + $xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex(); + } else { + $xfIndex = 0; + } + + return $this->parent->getCellXfByIndex($xfIndex); + } + + /** + * Get parent. Only used for style supervisor. + * + * @return Spreadsheet + */ + public function getParent() + { + return $this->parent; + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['quotePrefix' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray( + * [ + * 'font' => [ + * 'name' => 'Arial', + * 'bold' => true, + * 'italic' => false, + * 'underline' => Font::UNDERLINE_DOUBLE, + * 'strikethrough' => false, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ], + * 'borders' => [ + * 'bottom' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ], + * 'top' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ], + * 'alignment' => [ + * 'horizontal' => Alignment::HORIZONTAL_CENTER, + * 'vertical' => Alignment::VERTICAL_CENTER, + * 'wrapText' => true, + * ], + * 'quotePrefix' => true + * ] + * ); + * + * + * @param array $pStyles Array containing style information + * @param bool $pAdvanced advanced mode for setting borders + * + * @return Style + */ + public function applyFromArray(array $pStyles, $pAdvanced = true) + { + if ($this->isSupervisor) { + $pRange = $this->getSelectedCells(); + + // Uppercase coordinate + $pRange = strtoupper($pRange); + + // Is it a cell range or a single cell? + if (strpos($pRange, ':') === false) { + $rangeA = $pRange; + $rangeB = $pRange; + } else { + list($rangeA, $rangeB) = explode(':', $pRange); + } + + // Calculate range outer borders + $rangeStart = Coordinate::coordinateFromString($rangeA); + $rangeEnd = Coordinate::coordinateFromString($rangeB); + + // Translate column into index + $rangeStart[0] = Coordinate::columnIndexFromString($rangeStart[0]); + $rangeEnd[0] = Coordinate::columnIndexFromString($rangeEnd[0]); + + // Make sure we can loop upwards on rows and columns + if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) { + $tmp = $rangeStart; + $rangeStart = $rangeEnd; + $rangeEnd = $tmp; + } + + // ADVANCED MODE: + if ($pAdvanced && isset($pStyles['borders'])) { + // 'allBorders' is a shorthand property for 'outline' and 'inside' and + // it applies to components that have not been set explicitly + if (isset($pStyles['borders']['allBorders'])) { + foreach (['outline', 'inside'] as $component) { + if (!isset($pStyles['borders'][$component])) { + $pStyles['borders'][$component] = $pStyles['borders']['allBorders']; + } + } + unset($pStyles['borders']['allBorders']); // not needed any more + } + // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left' + // it applies to components that have not been set explicitly + if (isset($pStyles['borders']['outline'])) { + foreach (['top', 'right', 'bottom', 'left'] as $component) { + if (!isset($pStyles['borders'][$component])) { + $pStyles['borders'][$component] = $pStyles['borders']['outline']; + } + } + unset($pStyles['borders']['outline']); // not needed any more + } + // 'inside' is a shorthand property for 'vertical' and 'horizontal' + // it applies to components that have not been set explicitly + if (isset($pStyles['borders']['inside'])) { + foreach (['vertical', 'horizontal'] as $component) { + if (!isset($pStyles['borders'][$component])) { + $pStyles['borders'][$component] = $pStyles['borders']['inside']; + } + } + unset($pStyles['borders']['inside']); // not needed any more + } + // width and height characteristics of selection, 1, 2, or 3 (for 3 or more) + $xMax = min($rangeEnd[0] - $rangeStart[0] + 1, 3); + $yMax = min($rangeEnd[1] - $rangeStart[1] + 1, 3); + + // loop through up to 3 x 3 = 9 regions + for ($x = 1; $x <= $xMax; ++$x) { + // start column index for region + $colStart = ($x == 3) ? + Coordinate::stringFromColumnIndex($rangeEnd[0]) + : Coordinate::stringFromColumnIndex($rangeStart[0] + $x - 1); + // end column index for region + $colEnd = ($x == 1) ? + Coordinate::stringFromColumnIndex($rangeStart[0]) + : Coordinate::stringFromColumnIndex($rangeEnd[0] - $xMax + $x); + + for ($y = 1; $y <= $yMax; ++$y) { + // which edges are touching the region + $edges = []; + if ($x == 1) { + // are we at left edge + $edges[] = 'left'; + } + if ($x == $xMax) { + // are we at right edge + $edges[] = 'right'; + } + if ($y == 1) { + // are we at top edge? + $edges[] = 'top'; + } + if ($y == $yMax) { + // are we at bottom edge? + $edges[] = 'bottom'; + } + + // start row index for region + $rowStart = ($y == 3) ? + $rangeEnd[1] : $rangeStart[1] + $y - 1; + + // end row index for region + $rowEnd = ($y == 1) ? + $rangeStart[1] : $rangeEnd[1] - $yMax + $y; + + // build range for region + $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd; + + // retrieve relevant style array for region + $regionStyles = $pStyles; + unset($regionStyles['borders']['inside']); + + // what are the inner edges of the region when looking at the selection + $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges); + + // inner edges that are not touching the region should take the 'inside' border properties if they have been set + foreach ($innerEdges as $innerEdge) { + switch ($innerEdge) { + case 'top': + case 'bottom': + // should pick up 'horizontal' border property if set + if (isset($pStyles['borders']['horizontal'])) { + $regionStyles['borders'][$innerEdge] = $pStyles['borders']['horizontal']; + } else { + unset($regionStyles['borders'][$innerEdge]); + } + + break; + case 'left': + case 'right': + // should pick up 'vertical' border property if set + if (isset($pStyles['borders']['vertical'])) { + $regionStyles['borders'][$innerEdge] = $pStyles['borders']['vertical']; + } else { + unset($regionStyles['borders'][$innerEdge]); + } + + break; + } + } + + // apply region style to region by calling applyFromArray() in simple mode + $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false); + } + } + + // restore initial cell selection range + $this->getActiveSheet()->getStyle($pRange); + + return $this; + } + + // SIMPLE MODE: + // Selection type, inspect + if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) { + $selectionType = 'COLUMN'; + } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) { + $selectionType = 'ROW'; + } else { + $selectionType = 'CELL'; + } + + // First loop through columns, rows, or cells to find out which styles are affected by this operation + switch ($selectionType) { + case 'COLUMN': + $oldXfIndexes = []; + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; + } + + break; + case 'ROW': + $oldXfIndexes = []; + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() == null) { + $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style + } else { + $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true; + } + } + + break; + case 'CELL': + $oldXfIndexes = []; + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true; + } + } + + break; + } + + // clone each of the affected styles, apply the style array, and add the new styles to the workbook + $workbook = $this->getActiveSheet()->getParent(); + foreach ($oldXfIndexes as $oldXfIndex => $dummy) { + $style = $workbook->getCellXfByIndex($oldXfIndex); + $newStyle = clone $style; + $newStyle->applyFromArray($pStyles); + + if ($existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode())) { + // there is already such cell Xf in our collection + $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex(); + } else { + // we don't have such a cell Xf, need to add + $workbook->addCellXf($newStyle); + $newXfIndexes[$oldXfIndex] = $newStyle->getIndex(); + } + } + + // Loop through columns, rows, or cells again and update the XF index + switch ($selectionType) { + case 'COLUMN': + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col); + $oldXfIndex = $columnDimension->getXfIndex(); + $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]); + } + + break; + case 'ROW': + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + $rowDimension = $this->getActiveSheet()->getRowDimension($row); + $oldXfIndex = $rowDimension->getXfIndex() === null ? + 0 : $rowDimension->getXfIndex(); // row without explicit style should be formatted based on default style + $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]); + } + + break; + case 'CELL': + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row); + $oldXfIndex = $cell->getXfIndex(); + $cell->setXfIndex($newXfIndexes[$oldXfIndex]); + } + } + + break; + } + } else { + // not a supervisor, just apply the style array directly on style object + if (isset($pStyles['fill'])) { + $this->getFill()->applyFromArray($pStyles['fill']); + } + if (isset($pStyles['font'])) { + $this->getFont()->applyFromArray($pStyles['font']); + } + if (isset($pStyles['borders'])) { + $this->getBorders()->applyFromArray($pStyles['borders']); + } + if (isset($pStyles['alignment'])) { + $this->getAlignment()->applyFromArray($pStyles['alignment']); + } + if (isset($pStyles['numberFormat'])) { + $this->getNumberFormat()->applyFromArray($pStyles['numberFormat']); + } + if (isset($pStyles['protection'])) { + $this->getProtection()->applyFromArray($pStyles['protection']); + } + if (isset($pStyles['quotePrefix'])) { + $this->quotePrefix = $pStyles['quotePrefix']; + } + } + + return $this; + } + + /** + * Get Fill. + * + * @return Fill + */ + public function getFill() + { + return $this->fill; + } + + /** + * Get Font. + * + * @return Font + */ + public function getFont() + { + return $this->font; + } + + /** + * Set font. + * + * @param Font $font + * + * @return Style + */ + public function setFont(Font $font) + { + $this->font = $font; + + return $this; + } + + /** + * Get Borders. + * + * @return Borders + */ + public function getBorders() + { + return $this->borders; + } + + /** + * Get Alignment. + * + * @return Alignment + */ + public function getAlignment() + { + return $this->alignment; + } + + /** + * Get Number Format. + * + * @return NumberFormat + */ + public function getNumberFormat() + { + return $this->numberFormat; + } + + /** + * Get Conditional Styles. Only used on supervisor. + * + * @return Conditional[] + */ + public function getConditionalStyles() + { + return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell()); + } + + /** + * Set Conditional Styles. Only used on supervisor. + * + * @param Conditional[] $pValue Array of conditional styles + * + * @return Style + */ + public function setConditionalStyles(array $pValue) + { + $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $pValue); + + return $this; + } + + /** + * Get Protection. + * + * @return Protection + */ + public function getProtection() + { + return $this->protection; + } + + /** + * Get quote prefix. + * + * @return bool + */ + public function getQuotePrefix() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getQuotePrefix(); + } + + return $this->quotePrefix; + } + + /** + * Set quote prefix. + * + * @param bool $pValue + * + * @return Style + */ + public function setQuotePrefix($pValue) + { + if ($pValue == '') { + $pValue = false; + } + if ($this->isSupervisor) { + $styleArray = ['quotePrefix' => $pValue]; + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->quotePrefix = (bool) $pValue; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + $hashConditionals = ''; + foreach ($this->conditionalStyles as $conditional) { + $hashConditionals .= $conditional->getHashCode(); + } + + return md5( + $this->fill->getHashCode() . + $this->font->getHashCode() . + $this->borders->getHashCode() . + $this->alignment->getHashCode() . + $this->numberFormat->getHashCode() . + $hashConditionals . + $this->protection->getHashCode() . + ($this->quotePrefix ? 't' : 'f') . + __CLASS__ + ); + } + + /** + * Get own index in style collection. + * + * @return int + */ + public function getIndex() + { + return $this->index; + } + + /** + * Set own index in style collection. + * + * @param int $pValue + */ + public function setIndex($pValue) + { + $this->index = $pValue; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Supervisor.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Supervisor.php new file mode 100644 index 00000000000..2d1a27266b1 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Style/Supervisor.php @@ -0,0 +1,117 @@ +isSupervisor = $isSupervisor; + } + + /** + * Bind parent. Only used for supervisor. + * + * @param Spreadsheet|Style $parent + * @param null|string $parentPropertyName + * + * @return Supervisor + */ + public function bindParent($parent, $parentPropertyName = null) + { + $this->parent = $parent; + $this->parentPropertyName = $parentPropertyName; + + return $this; + } + + /** + * Is this a supervisor or a cell style component? + * + * @return bool + */ + public function getIsSupervisor() + { + return $this->isSupervisor; + } + + /** + * Get the currently active sheet. Only used for supervisor. + * + * @return Worksheet + */ + public function getActiveSheet() + { + return $this->parent->getActiveSheet(); + } + + /** + * Get the currently active cell coordinate in currently active sheet. + * Only used for supervisor. + * + * @return string E.g. 'A1' + */ + public function getSelectedCells() + { + return $this->getActiveSheet()->getSelectedCells(); + } + + /** + * Get the currently active cell coordinate in currently active sheet. + * Only used for supervisor. + * + * @return string E.g. 'A1' + */ + public function getActiveCell() + { + return $this->getActiveSheet()->getActiveCell(); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if ((is_object($value)) && ($key != 'parent')) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter.php new file mode 100644 index 00000000000..494948a77f2 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -0,0 +1,873 @@ +range = $pRange; + $this->workSheet = $pSheet; + } + + /** + * Get AutoFilter Parent Worksheet. + * + * @return Worksheet + */ + public function getParent() + { + return $this->workSheet; + } + + /** + * Set AutoFilter Parent Worksheet. + * + * @param Worksheet $pSheet + * + * @return AutoFilter + */ + public function setParent(Worksheet $pSheet = null) + { + $this->workSheet = $pSheet; + + return $this; + } + + /** + * Get AutoFilter Range. + * + * @return string + */ + public function getRange() + { + return $this->range; + } + + /** + * Set AutoFilter Range. + * + * @param string $pRange Cell range (i.e. A1:E10) + * + * @throws PhpSpreadsheetException + * + * @return AutoFilter + */ + public function setRange($pRange) + { + // extract coordinate + list($worksheet, $pRange) = Worksheet::extractSheetTitle($pRange, true); + + if (strpos($pRange, ':') !== false) { + $this->range = $pRange; + } elseif (empty($pRange)) { + $this->range = ''; + } else { + throw new PhpSpreadsheetException('Autofilter must be set on a range of cells.'); + } + + if (empty($pRange)) { + // Discard all column rules + $this->columns = []; + } else { + // Discard any column rules that are no longer valid within this range + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range); + foreach ($this->columns as $key => $value) { + $colIndex = Coordinate::columnIndexFromString($key); + if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) { + unset($this->columns[$key]); + } + } + } + + return $this; + } + + /** + * Get all AutoFilter Columns. + * + * @return AutoFilter\Column[] + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Validate that the specified column is in the AutoFilter range. + * + * @param string $column Column name (e.g. A) + * + * @throws PhpSpreadsheetException + * + * @return int The column offset within the autofilter range + */ + public function testColumnInRange($column) + { + if (empty($this->range)) { + throw new PhpSpreadsheetException('No autofilter range is defined.'); + } + + $columnIndex = Coordinate::columnIndexFromString($column); + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range); + if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) { + throw new PhpSpreadsheetException('Column is outside of current autofilter range.'); + } + + return $columnIndex - $rangeStart[0]; + } + + /** + * Get a specified AutoFilter Column Offset within the defined AutoFilter range. + * + * @param string $pColumn Column name (e.g. A) + * + * @throws PhpSpreadsheetException + * + * @return int The offset of the specified column within the autofilter range + */ + public function getColumnOffset($pColumn) + { + return $this->testColumnInRange($pColumn); + } + + /** + * Get a specified AutoFilter Column. + * + * @param string $pColumn Column name (e.g. A) + * + * @throws PhpSpreadsheetException + * + * @return AutoFilter\Column + */ + public function getColumn($pColumn) + { + $this->testColumnInRange($pColumn); + + if (!isset($this->columns[$pColumn])) { + $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this); + } + + return $this->columns[$pColumn]; + } + + /** + * Get a specified AutoFilter Column by it's offset. + * + * @param int $pColumnOffset Column offset within range (starting from 0) + * + * @throws PhpSpreadsheetException + * + * @return AutoFilter\Column + */ + public function getColumnByOffset($pColumnOffset) + { + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range); + $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $pColumnOffset); + + return $this->getColumn($pColumn); + } + + /** + * Set AutoFilter. + * + * @param AutoFilter\Column|string $pColumn + * A simple string containing a Column ID like 'A' is permitted + * + * @throws PhpSpreadsheetException + * + * @return AutoFilter + */ + public function setColumn($pColumn) + { + if ((is_string($pColumn)) && (!empty($pColumn))) { + $column = $pColumn; + } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) { + $column = $pColumn->getColumnIndex(); + } else { + throw new PhpSpreadsheetException('Column is not within the autofilter range.'); + } + $this->testColumnInRange($column); + + if (is_string($pColumn)) { + $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this); + } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) { + $pColumn->setParent($this); + $this->columns[$column] = $pColumn; + } + ksort($this->columns); + + return $this; + } + + /** + * Clear a specified AutoFilter Column. + * + * @param string $pColumn Column name (e.g. A) + * + * @throws PhpSpreadsheetException + * + * @return AutoFilter + */ + public function clearColumn($pColumn) + { + $this->testColumnInRange($pColumn); + + if (isset($this->columns[$pColumn])) { + unset($this->columns[$pColumn]); + } + + return $this; + } + + /** + * Shift an AutoFilter Column Rule to a different column. + * + * Note: This method bypasses validation of the destination column to ensure it is within this AutoFilter range. + * Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value. + * Use with caution. + * + * @param string $fromColumn Column name (e.g. A) + * @param string $toColumn Column name (e.g. B) + * + * @return AutoFilter + */ + public function shiftColumn($fromColumn, $toColumn) + { + $fromColumn = strtoupper($fromColumn); + $toColumn = strtoupper($toColumn); + + if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) { + $this->columns[$fromColumn]->setParent(); + $this->columns[$fromColumn]->setColumnIndex($toColumn); + $this->columns[$toColumn] = $this->columns[$fromColumn]; + $this->columns[$toColumn]->setParent($this); + unset($this->columns[$fromColumn]); + + ksort($this->columns); + } + + return $this; + } + + /** + * Test if cell value is in the defined set of values. + * + * @param mixed $cellValue + * @param mixed[] $dataSet + * + * @return bool + */ + private static function filterTestInSimpleDataSet($cellValue, $dataSet) + { + $dataSetValues = $dataSet['filterValues']; + $blanks = $dataSet['blanks']; + if (($cellValue == '') || ($cellValue === null)) { + return $blanks; + } + + return in_array($cellValue, $dataSetValues); + } + + /** + * Test if cell value is in the defined set of Excel date values. + * + * @param mixed $cellValue + * @param mixed[] $dataSet + * + * @return bool + */ + private static function filterTestInDateGroupSet($cellValue, $dataSet) + { + $dateSet = $dataSet['filterValues']; + $blanks = $dataSet['blanks']; + if (($cellValue == '') || ($cellValue === null)) { + return $blanks; + } + + if (is_numeric($cellValue)) { + $dateValue = Date::excelToTimestamp($cellValue); + if ($cellValue < 1) { + // Just the time part + $dtVal = date('His', $dateValue); + $dateSet = $dateSet['time']; + } elseif ($cellValue == floor($cellValue)) { + // Just the date part + $dtVal = date('Ymd', $dateValue); + $dateSet = $dateSet['date']; + } else { + // date and time parts + $dtVal = date('YmdHis', $dateValue); + $dateSet = $dateSet['dateTime']; + } + foreach ($dateSet as $dateValue) { + // Use of substr to extract value at the appropriate group level + if (substr($dtVal, 0, strlen($dateValue)) == $dateValue) { + return true; + } + } + } + + return false; + } + + /** + * Test if cell value is within a set of values defined by a ruleset. + * + * @param mixed $cellValue + * @param mixed[] $ruleSet + * + * @return bool + */ + private static function filterTestInCustomDataSet($cellValue, $ruleSet) + { + $dataSet = $ruleSet['filterRules']; + $join = $ruleSet['join']; + $customRuleForBlanks = isset($ruleSet['customRuleForBlanks']) ? $ruleSet['customRuleForBlanks'] : false; + + if (!$customRuleForBlanks) { + // Blank cells are always ignored, so return a FALSE + if (($cellValue == '') || ($cellValue === null)) { + return false; + } + } + $returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND); + foreach ($dataSet as $rule) { + $retVal = false; + + if (is_numeric($rule['value'])) { + // Numeric values are tested using the appropriate operator + switch ($rule['operator']) { + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL: + $retVal = ($cellValue == $rule['value']); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: + $retVal = ($cellValue != $rule['value']); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN: + $retVal = ($cellValue > $rule['value']); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL: + $retVal = ($cellValue >= $rule['value']); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN: + $retVal = ($cellValue < $rule['value']); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL: + $retVal = ($cellValue <= $rule['value']); + + break; + } + } elseif ($rule['value'] == '') { + switch ($rule['operator']) { + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL: + $retVal = (($cellValue == '') || ($cellValue === null)); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: + $retVal = (($cellValue != '') && ($cellValue !== null)); + + break; + default: + $retVal = true; + + break; + } + } else { + // String values are always tested for equality, factoring in for wildcards (hence a regexp test) + $retVal = preg_match('/^' . $rule['value'] . '$/i', $cellValue); + } + // If there are multiple conditions, then we need to test both using the appropriate join operator + switch ($join) { + case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR: + $returnVal = $returnVal || $retVal; + // Break as soon as we have a TRUE match for OR joins, + // to avoid unnecessary additional code execution + if ($returnVal) { + return $returnVal; + } + + break; + case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND: + $returnVal = $returnVal && $retVal; + + break; + } + } + + return $returnVal; + } + + /** + * Test if cell date value is matches a set of values defined by a set of months. + * + * @param mixed $cellValue + * @param mixed[] $monthSet + * + * @return bool + */ + private static function filterTestInPeriodDateSet($cellValue, $monthSet) + { + // Blank cells are always ignored, so return a FALSE + if (($cellValue == '') || ($cellValue === null)) { + return false; + } + + if (is_numeric($cellValue)) { + $dateValue = date('m', Date::excelToTimestamp($cellValue)); + if (in_array($dateValue, $monthSet)) { + return true; + } + } + + return false; + } + + /** + * Search/Replace arrays to convert Excel wildcard syntax to a regexp syntax for preg_matching. + * + * @var array + */ + private static $fromReplace = ['\*', '\?', '~~', '~.*', '~.?']; + + private static $toReplace = ['.*', '.', '~', '\*', '\?']; + + /** + * Convert a dynamic rule daterange to a custom filter range expression for ease of calculation. + * + * @param string $dynamicRuleType + * @param AutoFilter\Column $filterColumn + * + * @return mixed[] + */ + private function dynamicFilterDateRange($dynamicRuleType, &$filterColumn) + { + $rDateType = Functions::getReturnDateType(); + Functions::setReturnDateType(Functions::RETURNDATE_PHP_NUMERIC); + $val = $maxVal = null; + + $ruleValues = []; + $baseDate = DateTime::DATENOW(); + // Calculate start/end dates for the required date range based on current date + switch ($dynamicRuleType) { + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK: + $baseDate = strtotime('-7 days', $baseDate); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK: + $baseDate = strtotime('-7 days', $baseDate); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH: + $baseDate = strtotime('-1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH: + $baseDate = strtotime('+1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER: + $baseDate = strtotime('-3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER: + $baseDate = strtotime('+3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR: + $baseDate = strtotime('-1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR: + $baseDate = strtotime('+1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); + + break; + } + + switch ($dynamicRuleType) { + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW: + $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate)); + $val = (int) Date::PHPToExcel($baseDate); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE: + $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate)); + $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR: + $maxVal = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 31, 12, date('Y', $baseDate))); + ++$maxVal; + $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER: + $thisMonth = date('m', $baseDate); + $thisQuarter = floor(--$thisMonth / 3); + $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), (1 + $thisQuarter) * 3, date('Y', $baseDate))); + ++$maxVal; + $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1 + $thisQuarter * 3, date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH: + $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), date('m', $baseDate), date('Y', $baseDate))); + ++$maxVal; + $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK: + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK: + $dayOfWeek = date('w', $baseDate); + $val = (int) Date::PHPToExcel($baseDate) - $dayOfWeek; + $maxVal = $val + 7; + + break; + } + + switch ($dynamicRuleType) { + // Adjust Today dates for Yesterday and Tomorrow + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY: + --$maxVal; + --$val; + + break; + case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW: + ++$maxVal; + ++$val; + + break; + } + + // Set the filter column rule attributes ready for writing + $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxVal]); + + // Set the rules for identifying rows for hide/show + $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val]; + $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxVal]; + Functions::setReturnDateType($rDateType); + + return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]]; + } + + private function calculateTopTenValue($columnID, $startRow, $endRow, $ruleType, $ruleValue) + { + $range = $columnID . $startRow . ':' . $columnID . $endRow; + $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false)); + + $dataValues = array_filter($dataValues); + if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) { + rsort($dataValues); + } else { + sort($dataValues); + } + + return array_pop(array_slice($dataValues, 0, $ruleValue)); + } + + /** + * Apply the AutoFilter rules to the AutoFilter Range. + * + * @throws PhpSpreadsheetException + * + * @return AutoFilter + */ + public function showHideRows() + { + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range); + + // The heading row should always be visible + $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true); + + $columnFilterTests = []; + foreach ($this->columns as $columnID => $filterColumn) { + $rules = $filterColumn->getRules(); + switch ($filterColumn->getFilterType()) { + case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER: + $ruleType = null; + $ruleValues = []; + // Build a list of the filter value selections + foreach ($rules as $rule) { + $ruleType = $rule->getRuleType(); + $ruleValues[] = $rule->getValue(); + } + // Test if we want to include blanks in our filter criteria + $blanks = false; + $ruleDataSet = array_filter($ruleValues); + if (count($ruleValues) != count($ruleDataSet)) { + $blanks = true; + } + if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_FILTER) { + // Filter on absolute values + $columnFilterTests[$columnID] = [ + 'method' => 'filterTestInSimpleDataSet', + 'arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks], + ]; + } else { + // Filter on date group values + $arguments = [ + 'date' => [], + 'time' => [], + 'dateTime' => [], + ]; + foreach ($ruleDataSet as $ruleValue) { + $date = $time = ''; + if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) && + ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')) { + $date .= sprintf('%04d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]); + } + if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) && + ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')) { + $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]); + } + if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) && + ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')) { + $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]); + } + if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) && + ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')) { + $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]); + } + if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) && + ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')) { + $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]); + } + if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) && + ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')) { + $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]); + } + $dateTime = $date . $time; + $arguments['date'][] = $date; + $arguments['time'][] = $time; + $arguments['dateTime'][] = $dateTime; + } + // Remove empty elements + $arguments['date'] = array_filter($arguments['date']); + $arguments['time'] = array_filter($arguments['time']); + $arguments['dateTime'] = array_filter($arguments['dateTime']); + $columnFilterTests[$columnID] = [ + 'method' => 'filterTestInDateGroupSet', + 'arguments' => ['filterValues' => $arguments, 'blanks' => $blanks], + ]; + } + + break; + case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER: + $customRuleForBlanks = false; + $ruleValues = []; + // Build a list of the filter value selections + foreach ($rules as $rule) { + $ruleValue = $rule->getValue(); + if (!is_numeric($ruleValue)) { + // Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards + $ruleValue = preg_quote($ruleValue); + $ruleValue = str_replace(self::$fromReplace, self::$toReplace, $ruleValue); + if (trim($ruleValue) == '') { + $customRuleForBlanks = true; + $ruleValue = trim($ruleValue); + } + } + $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue]; + } + $join = $filterColumn->getJoin(); + $columnFilterTests[$columnID] = [ + 'method' => 'filterTestInCustomDataSet', + 'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks], + ]; + + break; + case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER: + $ruleValues = []; + foreach ($rules as $rule) { + // We should only ever have one Dynamic Filter Rule anyway + $dynamicRuleType = $rule->getGrouping(); + if (($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) || + ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)) { + // Number (Average) based + // Calculate the average + $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')'; + $average = Calculation::getInstance()->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1')); + // Set above/below rule based on greaterThan or LessTan + $operator = ($dynamicRuleType === AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) + ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN + : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN; + $ruleValues[] = [ + 'operator' => $operator, + 'value' => $average, + ]; + $columnFilterTests[$columnID] = [ + 'method' => 'filterTestInCustomDataSet', + 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR], + ]; + } else { + // Date based + if ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') { + $periodType = ''; + $period = 0; + // Month or Quarter + sscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period); + if ($periodType == 'M') { + $ruleValues = [$period]; + } else { + --$period; + $periodEnd = (1 + $period) * 3; + $periodStart = 1 + $period * 3; + $ruleValues = range($periodStart, $periodEnd); + } + $columnFilterTests[$columnID] = [ + 'method' => 'filterTestInPeriodDateSet', + 'arguments' => $ruleValues, + ]; + $filterColumn->setAttributes([]); + } else { + // Date Range + $columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn); + + break; + } + } + } + + break; + case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER: + $ruleValues = []; + $dataRowCount = $rangeEnd[1] - $rangeStart[1]; + foreach ($rules as $rule) { + // We should only ever have one Dynamic Filter Rule anyway + $toptenRuleType = $rule->getGrouping(); + $ruleValue = $rule->getValue(); + $ruleOperator = $rule->getOperator(); + } + if ($ruleOperator === AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) { + $ruleValue = floor($ruleValue * ($dataRowCount / 100)); + } + if ($ruleValue < 1) { + $ruleValue = 1; + } + if ($ruleValue > 500) { + $ruleValue = 500; + } + + $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, $rangeEnd[1], $toptenRuleType, $ruleValue); + + $operator = ($toptenRuleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) + ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL + : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL; + $ruleValues[] = ['operator' => $operator, 'value' => $maxVal]; + $columnFilterTests[$columnID] = [ + 'method' => 'filterTestInCustomDataSet', + 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR], + ]; + $filterColumn->setAttributes(['maxVal' => $maxVal]); + + break; + } + } + + // Execute the column tests for each row in the autoFilter range to determine show/hide, + for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) { + $result = true; + foreach ($columnFilterTests as $columnID => $columnFilterTest) { + $cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue(); + // Execute the filter test + $result = $result && + call_user_func_array( + [self::class, $columnFilterTest['method']], + [$cellValue, $columnFilterTest['arguments']] + ); + // If filter test has resulted in FALSE, exit the loop straightaway rather than running any more tests + if (!$result) { + break; + } + } + // Set show/hide for the row based on the result of the autoFilter result + $this->workSheet->getRowDimension($row)->setVisible($result); + } + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + if ($key === 'workSheet') { + // Detach from worksheet + $this->{$key} = null; + } else { + $this->{$key} = clone $value; + } + } elseif ((is_array($value)) && ($key == 'columns')) { + // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects + $this->{$key} = []; + foreach ($value as $k => $v) { + $this->{$key}[$k] = clone $v; + // attach the new cloned Column to this new cloned Autofilter object + $this->{$key}[$k]->setParent($this); + } + } else { + $this->{$key} = $value; + } + } + } + + /** + * toString method replicates previous behavior by returning the range if object is + * referenced as a property of its parent. + */ + public function __toString() + { + return (string) $this->range; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter/Column.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter/Column.php new file mode 100644 index 00000000000..b5ab61e9713 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter/Column.php @@ -0,0 +1,388 @@ +columnIndex = $pColumn; + $this->parent = $pParent; + } + + /** + * Get AutoFilter Column Index. + * + * @return string + */ + public function getColumnIndex() + { + return $this->columnIndex; + } + + /** + * Set AutoFilter Column Index. + * + * @param string $pColumn Column (e.g. A) + * + * @throws PhpSpreadsheetException + * + * @return Column + */ + public function setColumnIndex($pColumn) + { + // Uppercase coordinate + $pColumn = strtoupper($pColumn); + if ($this->parent !== null) { + $this->parent->testColumnInRange($pColumn); + } + + $this->columnIndex = $pColumn; + + return $this; + } + + /** + * Get this Column's AutoFilter Parent. + * + * @return AutoFilter + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set this Column's AutoFilter Parent. + * + * @param AutoFilter $pParent + * + * @return Column + */ + public function setParent(AutoFilter $pParent = null) + { + $this->parent = $pParent; + + return $this; + } + + /** + * Get AutoFilter Type. + * + * @return string + */ + public function getFilterType() + { + return $this->filterType; + } + + /** + * Set AutoFilter Type. + * + * @param string $pFilterType + * + * @throws PhpSpreadsheetException + * + * @return Column + */ + public function setFilterType($pFilterType) + { + if (!in_array($pFilterType, self::$filterTypes)) { + throw new PhpSpreadsheetException('Invalid filter type for column AutoFilter.'); + } + + $this->filterType = $pFilterType; + + return $this; + } + + /** + * Get AutoFilter Multiple Rules And/Or Join. + * + * @return string + */ + public function getJoin() + { + return $this->join; + } + + /** + * Set AutoFilter Multiple Rules And/Or. + * + * @param string $pJoin And/Or + * + * @throws PhpSpreadsheetException + * + * @return Column + */ + public function setJoin($pJoin) + { + // Lowercase And/Or + $pJoin = strtolower($pJoin); + if (!in_array($pJoin, self::$ruleJoins)) { + throw new PhpSpreadsheetException('Invalid rule connection for column AutoFilter.'); + } + + $this->join = $pJoin; + + return $this; + } + + /** + * Set AutoFilter Attributes. + * + * @param string[] $attributes + * + * @return Column + */ + public function setAttributes(array $attributes) + { + $this->attributes = $attributes; + + return $this; + } + + /** + * Set An AutoFilter Attribute. + * + * @param string $pName Attribute Name + * @param string $pValue Attribute Value + * + * @return Column + */ + public function setAttribute($pName, $pValue) + { + $this->attributes[$pName] = $pValue; + + return $this; + } + + /** + * Get AutoFilter Column Attributes. + * + * @return string[] + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * Get specific AutoFilter Column Attribute. + * + * @param string $pName Attribute Name + * + * @return string + */ + public function getAttribute($pName) + { + if (isset($this->attributes[$pName])) { + return $this->attributes[$pName]; + } + + return null; + } + + /** + * Get all AutoFilter Column Rules. + * + * @return Column\Rule[] + */ + public function getRules() + { + return $this->ruleset; + } + + /** + * Get a specified AutoFilter Column Rule. + * + * @param int $pIndex Rule index in the ruleset array + * + * @return Column\Rule + */ + public function getRule($pIndex) + { + if (!isset($this->ruleset[$pIndex])) { + $this->ruleset[$pIndex] = new Column\Rule($this); + } + + return $this->ruleset[$pIndex]; + } + + /** + * Create a new AutoFilter Column Rule in the ruleset. + * + * @return Column\Rule + */ + public function createRule() + { + $this->ruleset[] = new Column\Rule($this); + + return end($this->ruleset); + } + + /** + * Add a new AutoFilter Column Rule to the ruleset. + * + * @param Column\Rule $pRule + * + * @return Column + */ + public function addRule(Column\Rule $pRule) + { + $pRule->setParent($this); + $this->ruleset[] = $pRule; + + return $this; + } + + /** + * Delete a specified AutoFilter Column Rule + * If the number of rules is reduced to 1, then we reset And/Or logic to Or. + * + * @param int $pIndex Rule index in the ruleset array + * + * @return Column + */ + public function deleteRule($pIndex) + { + if (isset($this->ruleset[$pIndex])) { + unset($this->ruleset[$pIndex]); + // If we've just deleted down to a single rule, then reset And/Or joining to Or + if (count($this->ruleset) <= 1) { + $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR); + } + } + + return $this; + } + + /** + * Delete all AutoFilter Column Rules. + * + * @return Column + */ + public function clearRules() + { + $this->ruleset = []; + $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR); + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if ($key === 'parent') { + // Detach from autofilter parent + $this->parent = null; + } elseif ($key === 'ruleset') { + // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects + $this->ruleset = []; + foreach ($value as $k => $v) { + $cloned = clone $v; + $cloned->setParent($this); // attach the new cloned Rule to this new cloned Autofilter Cloned object + $this->ruleset[$k] = $cloned; + } + } elseif (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php new file mode 100644 index 00000000000..450bccdb6ac --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php @@ -0,0 +1,455 @@ + + * + * + * + * + * + */ + const AUTOFILTER_COLUMN_RULE_EQUAL = 'equal'; + const AUTOFILTER_COLUMN_RULE_NOTEQUAL = 'notEqual'; + const AUTOFILTER_COLUMN_RULE_GREATERTHAN = 'greaterThan'; + const AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL = 'greaterThanOrEqual'; + const AUTOFILTER_COLUMN_RULE_LESSTHAN = 'lessThan'; + const AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL = 'lessThanOrEqual'; + + private static $operators = [ + self::AUTOFILTER_COLUMN_RULE_EQUAL, + self::AUTOFILTER_COLUMN_RULE_NOTEQUAL, + self::AUTOFILTER_COLUMN_RULE_GREATERTHAN, + self::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, + self::AUTOFILTER_COLUMN_RULE_LESSTHAN, + self::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL, + ]; + + const AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE = 'byValue'; + const AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT = 'byPercent'; + + private static $topTenValue = [ + self::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE, + self::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT, + ]; + + const AUTOFILTER_COLUMN_RULE_TOPTEN_TOP = 'top'; + const AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM = 'bottom'; + + private static $topTenType = [ + self::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP, + self::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM, + ]; + + // Rule Operators (Numeric, Boolean etc) +// const AUTOFILTER_COLUMN_RULE_BETWEEN = 'between'; // greaterThanOrEqual 1 && lessThanOrEqual 2 + // Rule Operators (Numeric Special) which are translated to standard numeric operators with calculated values +// const AUTOFILTER_COLUMN_RULE_TOPTEN = 'topTen'; // greaterThan calculated value +// const AUTOFILTER_COLUMN_RULE_TOPTENPERCENT = 'topTenPercent'; // greaterThan calculated value +// const AUTOFILTER_COLUMN_RULE_ABOVEAVERAGE = 'aboveAverage'; // Value is calculated as the average +// const AUTOFILTER_COLUMN_RULE_BELOWAVERAGE = 'belowAverage'; // Value is calculated as the average + // Rule Operators (String) which are set as wild-carded values +// const AUTOFILTER_COLUMN_RULE_BEGINSWITH = 'beginsWith'; // A* +// const AUTOFILTER_COLUMN_RULE_ENDSWITH = 'endsWith'; // *Z +// const AUTOFILTER_COLUMN_RULE_CONTAINS = 'contains'; // *B* +// const AUTOFILTER_COLUMN_RULE_DOESNTCONTAIN = 'notEqual'; // notEqual *B* + // Rule Operators (Date Special) which are translated to standard numeric operators with calculated values +// const AUTOFILTER_COLUMN_RULE_BEFORE = 'lessThan'; +// const AUTOFILTER_COLUMN_RULE_AFTER = 'greaterThan'; +// const AUTOFILTER_COLUMN_RULE_YESTERDAY = 'yesterday'; +// const AUTOFILTER_COLUMN_RULE_TODAY = 'today'; +// const AUTOFILTER_COLUMN_RULE_TOMORROW = 'tomorrow'; +// const AUTOFILTER_COLUMN_RULE_LASTWEEK = 'lastWeek'; +// const AUTOFILTER_COLUMN_RULE_THISWEEK = 'thisWeek'; +// const AUTOFILTER_COLUMN_RULE_NEXTWEEK = 'nextWeek'; +// const AUTOFILTER_COLUMN_RULE_LASTMONTH = 'lastMonth'; +// const AUTOFILTER_COLUMN_RULE_THISMONTH = 'thisMonth'; +// const AUTOFILTER_COLUMN_RULE_NEXTMONTH = 'nextMonth'; +// const AUTOFILTER_COLUMN_RULE_LASTQUARTER = 'lastQuarter'; +// const AUTOFILTER_COLUMN_RULE_THISQUARTER = 'thisQuarter'; +// const AUTOFILTER_COLUMN_RULE_NEXTQUARTER = 'nextQuarter'; +// const AUTOFILTER_COLUMN_RULE_LASTYEAR = 'lastYear'; +// const AUTOFILTER_COLUMN_RULE_THISYEAR = 'thisYear'; +// const AUTOFILTER_COLUMN_RULE_NEXTYEAR = 'nextYear'; +// const AUTOFILTER_COLUMN_RULE_YEARTODATE = 'yearToDate'; // +// const AUTOFILTER_COLUMN_RULE_ALLDATESINMONTH = 'allDatesInMonth'; // for Month/February +// const AUTOFILTER_COLUMN_RULE_ALLDATESINQUARTER = 'allDatesInQuarter'; // for Quarter 2 + + /** + * Autofilter Column. + * + * @var Column + */ + private $parent; + + /** + * Autofilter Rule Type. + * + * @var string + */ + private $ruleType = self::AUTOFILTER_RULETYPE_FILTER; + + /** + * Autofilter Rule Value. + * + * @var string + */ + private $value = ''; + + /** + * Autofilter Rule Operator. + * + * @var string + */ + private $operator = self::AUTOFILTER_COLUMN_RULE_EQUAL; + + /** + * DateTimeGrouping Group Value. + * + * @var string + */ + private $grouping = ''; + + /** + * Create a new Rule. + * + * @param Column $pParent + */ + public function __construct(Column $pParent = null) + { + $this->parent = $pParent; + } + + /** + * Get AutoFilter Rule Type. + * + * @return string + */ + public function getRuleType() + { + return $this->ruleType; + } + + /** + * Set AutoFilter Rule Type. + * + * @param string $pRuleType see self::AUTOFILTER_RULETYPE_* + * + * @throws PhpSpreadsheetException + * + * @return Rule + */ + public function setRuleType($pRuleType) + { + if (!in_array($pRuleType, self::$ruleTypes)) { + throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.'); + } + + $this->ruleType = $pRuleType; + + return $this; + } + + /** + * Get AutoFilter Rule Value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Set AutoFilter Rule Value. + * + * @param string|string[] $pValue + * + * @throws PhpSpreadsheetException + * + * @return Rule + */ + public function setValue($pValue) + { + if (is_array($pValue)) { + $grouping = -1; + foreach ($pValue as $key => $value) { + // Validate array entries + if (!in_array($key, self::$dateTimeGroups)) { + // Remove any invalid entries from the value array + unset($pValue[$key]); + } else { + // Work out what the dateTime grouping will be + $grouping = max($grouping, array_search($key, self::$dateTimeGroups)); + } + } + if (count($pValue) == 0) { + throw new PhpSpreadsheetException('Invalid rule value for column AutoFilter Rule.'); + } + // Set the dateTime grouping that we've anticipated + $this->setGrouping(self::$dateTimeGroups[$grouping]); + } + $this->value = $pValue; + + return $this; + } + + /** + * Get AutoFilter Rule Operator. + * + * @return string + */ + public function getOperator() + { + return $this->operator; + } + + /** + * Set AutoFilter Rule Operator. + * + * @param string $pOperator see self::AUTOFILTER_COLUMN_RULE_* + * + * @throws PhpSpreadsheetException + * + * @return Rule + */ + public function setOperator($pOperator) + { + if (empty($pOperator)) { + $pOperator = self::AUTOFILTER_COLUMN_RULE_EQUAL; + } + if ((!in_array($pOperator, self::$operators)) && + (!in_array($pOperator, self::$topTenValue))) { + throw new PhpSpreadsheetException('Invalid operator for column AutoFilter Rule.'); + } + $this->operator = $pOperator; + + return $this; + } + + /** + * Get AutoFilter Rule Grouping. + * + * @return string + */ + public function getGrouping() + { + return $this->grouping; + } + + /** + * Set AutoFilter Rule Grouping. + * + * @param string $pGrouping + * + * @throws PhpSpreadsheetException + * + * @return Rule + */ + public function setGrouping($pGrouping) + { + if (($pGrouping !== null) && + (!in_array($pGrouping, self::$dateTimeGroups)) && + (!in_array($pGrouping, self::$dynamicTypes)) && + (!in_array($pGrouping, self::$topTenType))) { + throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.'); + } + $this->grouping = $pGrouping; + + return $this; + } + + /** + * Set AutoFilter Rule. + * + * @param string $pOperator see self::AUTOFILTER_COLUMN_RULE_* + * @param string|string[] $pValue + * @param string $pGrouping + * + * @throws PhpSpreadsheetException + * + * @return Rule + */ + public function setRule($pOperator, $pValue, $pGrouping = null) + { + $this->setOperator($pOperator); + $this->setValue($pValue); + // Only set grouping if it's been passed in as a user-supplied argument, + // otherwise we're calculating it when we setValue() and don't want to overwrite that + // If the user supplies an argumnet for grouping, then on their own head be it + if ($pGrouping !== null) { + $this->setGrouping($pGrouping); + } + + return $this; + } + + /** + * Get this Rule's AutoFilter Column Parent. + * + * @return Column + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set this Rule's AutoFilter Column Parent. + * + * @param Column $pParent + * + * @return Rule + */ + public function setParent(Column $pParent = null) + { + $this->parent = $pParent; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + if ($key == 'parent') { + // Detach from autofilter column parent + $this->$key = null; + } else { + $this->$key = clone $value; + } + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/BaseDrawing.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/BaseDrawing.php new file mode 100644 index 00000000000..98b689729e1 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -0,0 +1,537 @@ +name = ''; + $this->description = ''; + $this->worksheet = null; + $this->coordinates = 'A1'; + $this->offsetX = 0; + $this->offsetY = 0; + $this->width = 0; + $this->height = 0; + $this->resizeProportional = true; + $this->rotation = 0; + $this->shadow = new Drawing\Shadow(); + + // Set image index + ++self::$imageCounter; + $this->imageIndex = self::$imageCounter; + } + + /** + * Get image index. + * + * @return int + */ + public function getImageIndex() + { + return $this->imageIndex; + } + + /** + * Get Name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set Name. + * + * @param string $pValue + * + * @return BaseDrawing + */ + public function setName($pValue) + { + $this->name = $pValue; + + return $this; + } + + /** + * Get Description. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set Description. + * + * @param string $description + * + * @return BaseDrawing + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Get Worksheet. + * + * @return Worksheet + */ + public function getWorksheet() + { + return $this->worksheet; + } + + /** + * Set Worksheet. + * + * @param Worksheet $pValue + * @param bool $pOverrideOld If a Worksheet has already been assigned, overwrite it and remove image from old Worksheet? + * + * @throws PhpSpreadsheetException + * + * @return BaseDrawing + */ + public function setWorksheet(Worksheet $pValue = null, $pOverrideOld = false) + { + if ($this->worksheet === null) { + // Add drawing to \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + $this->worksheet = $pValue; + $this->worksheet->getCell($this->coordinates); + $this->worksheet->getDrawingCollection()->append($this); + } else { + if ($pOverrideOld) { + // Remove drawing from old \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + $iterator = $this->worksheet->getDrawingCollection()->getIterator(); + + while ($iterator->valid()) { + if ($iterator->current()->getHashCode() == $this->getHashCode()) { + $this->worksheet->getDrawingCollection()->offsetUnset($iterator->key()); + $this->worksheet = null; + + break; + } + } + + // Set new \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + $this->setWorksheet($pValue); + } else { + throw new PhpSpreadsheetException('A Worksheet has already been assigned. Drawings can only exist on one \\PhpOffice\\PhpSpreadsheet\\Worksheet.'); + } + } + + return $this; + } + + /** + * Get Coordinates. + * + * @return string + */ + public function getCoordinates() + { + return $this->coordinates; + } + + /** + * Set Coordinates. + * + * @param string $pValue eg: 'A1' + * + * @return BaseDrawing + */ + public function setCoordinates($pValue) + { + $this->coordinates = $pValue; + + return $this; + } + + /** + * Get OffsetX. + * + * @return int + */ + public function getOffsetX() + { + return $this->offsetX; + } + + /** + * Set OffsetX. + * + * @param int $pValue + * + * @return BaseDrawing + */ + public function setOffsetX($pValue) + { + $this->offsetX = $pValue; + + return $this; + } + + /** + * Get OffsetY. + * + * @return int + */ + public function getOffsetY() + { + return $this->offsetY; + } + + /** + * Set OffsetY. + * + * @param int $pValue + * + * @return BaseDrawing + */ + public function setOffsetY($pValue) + { + $this->offsetY = $pValue; + + return $this; + } + + /** + * Get Width. + * + * @return int + */ + public function getWidth() + { + return $this->width; + } + + /** + * Set Width. + * + * @param int $pValue + * + * @return BaseDrawing + */ + public function setWidth($pValue) + { + // Resize proportional? + if ($this->resizeProportional && $pValue != 0) { + $ratio = $this->height / ($this->width != 0 ? $this->width : 1); + $this->height = round($ratio * $pValue); + } + + // Set width + $this->width = $pValue; + + return $this; + } + + /** + * Get Height. + * + * @return int + */ + public function getHeight() + { + return $this->height; + } + + /** + * Set Height. + * + * @param int $pValue + * + * @return BaseDrawing + */ + public function setHeight($pValue) + { + // Resize proportional? + if ($this->resizeProportional && $pValue != 0) { + $ratio = $this->width / ($this->height != 0 ? $this->height : 1); + $this->width = round($ratio * $pValue); + } + + // Set height + $this->height = $pValue; + + return $this; + } + + /** + * Set width and height with proportional resize. + * + * Example: + * + * $objDrawing->setResizeProportional(true); + * $objDrawing->setWidthAndHeight(160,120); + * + * + * @author Vincent@luo MSN:kele_100@hotmail.com + * + * @param int $width + * @param int $height + * + * @return BaseDrawing + */ + public function setWidthAndHeight($width, $height) + { + $xratio = $width / ($this->width != 0 ? $this->width : 1); + $yratio = $height / ($this->height != 0 ? $this->height : 1); + if ($this->resizeProportional && !($width == 0 || $height == 0)) { + if (($xratio * $this->height) < $height) { + $this->height = ceil($xratio * $this->height); + $this->width = $width; + } else { + $this->width = ceil($yratio * $this->width); + $this->height = $height; + } + } else { + $this->width = $width; + $this->height = $height; + } + + return $this; + } + + /** + * Get ResizeProportional. + * + * @return bool + */ + public function getResizeProportional() + { + return $this->resizeProportional; + } + + /** + * Set ResizeProportional. + * + * @param bool $pValue + * + * @return BaseDrawing + */ + public function setResizeProportional($pValue) + { + $this->resizeProportional = $pValue; + + return $this; + } + + /** + * Get Rotation. + * + * @return int + */ + public function getRotation() + { + return $this->rotation; + } + + /** + * Set Rotation. + * + * @param int $pValue + * + * @return BaseDrawing + */ + public function setRotation($pValue) + { + $this->rotation = $pValue; + + return $this; + } + + /** + * Get Shadow. + * + * @return Drawing\Shadow + */ + public function getShadow() + { + return $this->shadow; + } + + /** + * Set Shadow. + * + * @param Drawing\Shadow $pValue + * + * @return BaseDrawing + */ + public function setShadow(Drawing\Shadow $pValue = null) + { + $this->shadow = $pValue; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->name . + $this->description . + $this->worksheet->getHashCode() . + $this->coordinates . + $this->offsetX . + $this->offsetY . + $this->width . + $this->height . + $this->rotation . + $this->shadow->getHashCode() . + __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if ($key == 'worksheet') { + $this->worksheet = null; + } elseif (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } + + /** + * @param null|Hyperlink $pHyperlink + */ + public function setHyperlink(Hyperlink $pHyperlink = null) + { + $this->hyperlink = $pHyperlink; + } + + /** + * @return null|Hyperlink + */ + public function getHyperlink() + { + return $this->hyperlink; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/CellIterator.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/CellIterator.php new file mode 100644 index 00000000000..d97e33f7d10 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/CellIterator.php @@ -0,0 +1,61 @@ +worksheet); + } + + /** + * Get loop only existing cells. + * + * @return bool + */ + public function getIterateOnlyExistingCells() + { + return $this->onlyExistingCells; + } + + /** + * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary. + * + * @throws PhpSpreadsheetException + */ + abstract protected function adjustForExistingOnlyRange(); + + /** + * Set the iterator to loop only existing cells. + * + * @param bool $value + * + * @throws PhpSpreadsheetException + */ + public function setIterateOnlyExistingCells($value) + { + $this->onlyExistingCells = (bool) $value; + + $this->adjustForExistingOnlyRange(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Column.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Column.php new file mode 100644 index 00000000000..4baaae1f54a --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Column.php @@ -0,0 +1,64 @@ +parent = $parent; + $this->columnIndex = $columnIndex; + } + + /** + * Destructor. + */ + public function __destruct() + { + unset($this->parent); + } + + /** + * Get column index. + * + * @return string + */ + public function getColumnIndex() + { + return $this->columnIndex; + } + + /** + * Get cell iterator. + * + * @param int $startRow The row number at which to start iterating + * @param int $endRow Optionally, the row number at which to stop iterating + * + * @return ColumnCellIterator + */ + public function getCellIterator($startRow = 1, $endRow = null) + { + return new ColumnCellIterator($this->parent, $this->columnIndex, $startRow, $endRow); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnCellIterator.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnCellIterator.php new file mode 100644 index 00000000000..7e8f040d26a --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnCellIterator.php @@ -0,0 +1,197 @@ +worksheet = $subject; + $this->columnIndex = Coordinate::columnIndexFromString($columnIndex); + $this->resetEnd($endRow); + $this->resetStart($startRow); + } + + /** + * (Re)Set the start row and the current row pointer. + * + * @param int $startRow The row number at which to start iterating + * + * @throws PhpSpreadsheetException + * + * @return ColumnCellIterator + */ + public function resetStart($startRow = 1) + { + $this->startRow = $startRow; + $this->adjustForExistingOnlyRange(); + $this->seek($startRow); + + return $this; + } + + /** + * (Re)Set the end row. + * + * @param int $endRow The row number at which to stop iterating + * + * @throws PhpSpreadsheetException + * + * @return ColumnCellIterator + */ + public function resetEnd($endRow = null) + { + $this->endRow = ($endRow) ? $endRow : $this->worksheet->getHighestRow(); + $this->adjustForExistingOnlyRange(); + + return $this; + } + + /** + * Set the row pointer to the selected row. + * + * @param int $row The row number to set the current pointer at + * + * @throws PhpSpreadsheetException + * + * @return ColumnCellIterator + */ + public function seek($row = 1) + { + if (($row < $this->startRow) || ($row > $this->endRow)) { + throw new PhpSpreadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})"); + } elseif ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $row))) { + throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist'); + } + $this->currentRow = $row; + + return $this; + } + + /** + * Rewind the iterator to the starting row. + */ + public function rewind() + { + $this->currentRow = $this->startRow; + } + + /** + * Return the current cell in this worksheet column. + * + * @return null|\PhpOffice\PhpSpreadsheet\Cell\Cell + */ + public function current() + { + return $this->worksheet->getCellByColumnAndRow($this->columnIndex, $this->currentRow); + } + + /** + * Return the current iterator key. + * + * @return int + */ + public function key() + { + return $this->currentRow; + } + + /** + * Set the iterator to its next value. + */ + public function next() + { + do { + ++$this->currentRow; + } while (($this->onlyExistingCells) && + (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->currentRow)) && + ($this->currentRow <= $this->endRow)); + } + + /** + * Set the iterator to its previous value. + */ + public function prev() + { + do { + --$this->currentRow; + } while (($this->onlyExistingCells) && + (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->currentRow)) && + ($this->currentRow >= $this->startRow)); + } + + /** + * Indicate if more rows exist in the worksheet range of rows that we're iterating. + * + * @return bool + */ + public function valid() + { + return $this->currentRow <= $this->endRow && $this->currentRow >= $this->startRow; + } + + /** + * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary. + * + * @throws PhpSpreadsheetException + */ + protected function adjustForExistingOnlyRange() + { + if ($this->onlyExistingCells) { + while ((!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->startRow)) && + ($this->startRow <= $this->endRow)) { + ++$this->startRow; + } + if ($this->startRow > $this->endRow) { + throw new PhpSpreadsheetException('No cells exist within the specified range'); + } + while ((!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->endRow)) && + ($this->endRow >= $this->startRow)) { + --$this->endRow; + } + if ($this->endRow < $this->startRow) { + throw new PhpSpreadsheetException('No cells exist within the specified range'); + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnDimension.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnDimension.php new file mode 100644 index 00000000000..e2ea8af15f7 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnDimension.php @@ -0,0 +1,115 @@ +columnIndex = $pIndex; + + // set dimension as unformatted by default + parent::__construct(0); + } + + /** + * Get ColumnIndex. + * + * @return string + */ + public function getColumnIndex() + { + return $this->columnIndex; + } + + /** + * Set ColumnIndex. + * + * @param string $pValue + * + * @return ColumnDimension + */ + public function setColumnIndex($pValue) + { + $this->columnIndex = $pValue; + + return $this; + } + + /** + * Get Width. + * + * @return float + */ + public function getWidth() + { + return $this->width; + } + + /** + * Set Width. + * + * @param float $pValue + * + * @return ColumnDimension + */ + public function setWidth($pValue) + { + $this->width = $pValue; + + return $this; + } + + /** + * Get Auto Size. + * + * @return bool + */ + public function getAutoSize() + { + return $this->autoSize; + } + + /** + * Set Auto Size. + * + * @param bool $pValue + * + * @return ColumnDimension + */ + public function setAutoSize($pValue) + { + $this->autoSize = $pValue; + + return $this; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnIterator.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnIterator.php new file mode 100644 index 00000000000..d2b57aad2f5 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/ColumnIterator.php @@ -0,0 +1,175 @@ +worksheet = $worksheet; + $this->resetEnd($endColumn); + $this->resetStart($startColumn); + } + + /** + * Destructor. + */ + public function __destruct() + { + unset($this->worksheet); + } + + /** + * (Re)Set the start column and the current column pointer. + * + * @param string $startColumn The column address at which to start iterating + * + * @throws Exception + * + * @return ColumnIterator + */ + public function resetStart($startColumn = 'A') + { + $startColumnIndex = Coordinate::columnIndexFromString($startColumn); + if ($startColumnIndex > Coordinate::columnIndexFromString($this->worksheet->getHighestColumn())) { + throw new Exception("Start column ({$startColumn}) is beyond highest column ({$this->worksheet->getHighestColumn()})"); + } + + $this->startColumnIndex = $startColumnIndex; + if ($this->endColumnIndex < $this->startColumnIndex) { + $this->endColumnIndex = $this->startColumnIndex; + } + $this->seek($startColumn); + + return $this; + } + + /** + * (Re)Set the end column. + * + * @param string $endColumn The column address at which to stop iterating + * + * @return ColumnIterator + */ + public function resetEnd($endColumn = null) + { + $endColumn = $endColumn ? $endColumn : $this->worksheet->getHighestColumn(); + $this->endColumnIndex = Coordinate::columnIndexFromString($endColumn); + + return $this; + } + + /** + * Set the column pointer to the selected column. + * + * @param string $column The column address to set the current pointer at + * + * @throws PhpSpreadsheetException + * + * @return ColumnIterator + */ + public function seek($column = 'A') + { + $column = Coordinate::columnIndexFromString($column); + if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) { + throw new PhpSpreadsheetException("Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"); + } + $this->currentColumnIndex = $column; + + return $this; + } + + /** + * Rewind the iterator to the starting column. + */ + public function rewind() + { + $this->currentColumnIndex = $this->startColumnIndex; + } + + /** + * Return the current column in this worksheet. + * + * @return Column + */ + public function current() + { + return new Column($this->worksheet, Coordinate::stringFromColumnIndex($this->currentColumnIndex)); + } + + /** + * Return the current iterator key. + * + * @return string + */ + public function key() + { + return Coordinate::stringFromColumnIndex($this->currentColumnIndex); + } + + /** + * Set the iterator to its next value. + */ + public function next() + { + ++$this->currentColumnIndex; + } + + /** + * Set the iterator to its previous value. + */ + public function prev() + { + --$this->currentColumnIndex; + } + + /** + * Indicate if more columns exist in the worksheet range of columns that we're iterating. + * + * @return bool + */ + public function valid() + { + return $this->currentColumnIndex <= $this->endColumnIndex && $this->currentColumnIndex >= $this->startColumnIndex; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Dimension.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Dimension.php new file mode 100644 index 00000000000..44e66323206 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Dimension.php @@ -0,0 +1,165 @@ +xfIndex = $initialValue; + } + + /** + * Get Visible. + * + * @return bool + */ + public function getVisible() + { + return $this->visible; + } + + /** + * Set Visible. + * + * @param bool $pValue + * + * @return Dimension + */ + public function setVisible($pValue) + { + $this->visible = $pValue; + + return $this; + } + + /** + * Get Outline Level. + * + * @return int + */ + public function getOutlineLevel() + { + return $this->outlineLevel; + } + + /** + * Set Outline Level. + * Value must be between 0 and 7. + * + * @param int $pValue + * + * @throws PhpSpreadsheetException + * + * @return Dimension + */ + public function setOutlineLevel($pValue) + { + if ($pValue < 0 || $pValue > 7) { + throw new PhpSpreadsheetException('Outline level must range between 0 and 7.'); + } + + $this->outlineLevel = $pValue; + + return $this; + } + + /** + * Get Collapsed. + * + * @return bool + */ + public function getCollapsed() + { + return $this->collapsed; + } + + /** + * Set Collapsed. + * + * @param bool $pValue + * + * @return Dimension + */ + public function setCollapsed($pValue) + { + $this->collapsed = $pValue; + + return $this; + } + + /** + * Get index to cellXf. + * + * @return int + */ + public function getXfIndex() + { + return $this->xfIndex; + } + + /** + * Set index to cellXf. + * + * @param int $pValue + * + * @return Dimension + */ + public function setXfIndex($pValue) + { + $this->xfIndex = $pValue; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Drawing.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Drawing.php new file mode 100644 index 00000000000..ed26006c048 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Drawing.php @@ -0,0 +1,116 @@ +path = ''; + + // Initialize parent + parent::__construct(); + } + + /** + * Get Filename. + * + * @return string + */ + public function getFilename() + { + return basename($this->path); + } + + /** + * Get indexed filename (using image index). + * + * @return string + */ + public function getIndexedFilename() + { + $fileName = $this->getFilename(); + $fileName = str_replace(' ', '_', $fileName); + + return str_replace('.' . $this->getExtension(), '', $fileName) . $this->getImageIndex() . '.' . $this->getExtension(); + } + + /** + * Get Extension. + * + * @return string + */ + public function getExtension() + { + $exploded = explode('.', basename($this->path)); + + return $exploded[count($exploded) - 1]; + } + + /** + * Get Path. + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Set Path. + * + * @param string $pValue File path + * @param bool $pVerifyFile Verify file + * + * @throws PhpSpreadsheetException + * + * @return Drawing + */ + public function setPath($pValue, $pVerifyFile = true) + { + if ($pVerifyFile) { + if (file_exists($pValue)) { + $this->path = $pValue; + + if ($this->width == 0 && $this->height == 0) { + // Get width/height + list($this->width, $this->height) = getimagesize($pValue); + } + } else { + throw new PhpSpreadsheetException("File $pValue not found!"); + } + } else { + $this->path = $pValue; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->path . + parent::getHashCode() . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Drawing/Shadow.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Drawing/Shadow.php new file mode 100644 index 00000000000..a1e05d60001 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Drawing/Shadow.php @@ -0,0 +1,289 @@ +visible = false; + $this->blurRadius = 6; + $this->distance = 2; + $this->direction = 0; + $this->alignment = self::SHADOW_BOTTOM_RIGHT; + $this->color = new Color(Color::COLOR_BLACK); + $this->alpha = 50; + } + + /** + * Get Visible. + * + * @return bool + */ + public function getVisible() + { + return $this->visible; + } + + /** + * Set Visible. + * + * @param bool $pValue + * + * @return Shadow + */ + public function setVisible($pValue) + { + $this->visible = $pValue; + + return $this; + } + + /** + * Get Blur radius. + * + * @return int + */ + public function getBlurRadius() + { + return $this->blurRadius; + } + + /** + * Set Blur radius. + * + * @param int $pValue + * + * @return Shadow + */ + public function setBlurRadius($pValue) + { + $this->blurRadius = $pValue; + + return $this; + } + + /** + * Get Shadow distance. + * + * @return int + */ + public function getDistance() + { + return $this->distance; + } + + /** + * Set Shadow distance. + * + * @param int $pValue + * + * @return Shadow + */ + public function setDistance($pValue) + { + $this->distance = $pValue; + + return $this; + } + + /** + * Get Shadow direction (in degrees). + * + * @return int + */ + public function getDirection() + { + return $this->direction; + } + + /** + * Set Shadow direction (in degrees). + * + * @param int $pValue + * + * @return Shadow + */ + public function setDirection($pValue) + { + $this->direction = $pValue; + + return $this; + } + + /** + * Get Shadow alignment. + * + * @return int + */ + public function getAlignment() + { + return $this->alignment; + } + + /** + * Set Shadow alignment. + * + * @param int $pValue + * + * @return Shadow + */ + public function setAlignment($pValue) + { + $this->alignment = $pValue; + + return $this; + } + + /** + * Get Color. + * + * @return Color + */ + public function getColor() + { + return $this->color; + } + + /** + * Set Color. + * + * @param Color $pValue + * + * @return Shadow + */ + public function setColor(Color $pValue = null) + { + $this->color = $pValue; + + return $this; + } + + /** + * Get Alpha. + * + * @return int + */ + public function getAlpha() + { + return $this->alpha; + } + + /** + * Set Alpha. + * + * @param int $pValue + * + * @return Shadow + */ + public function setAlpha($pValue) + { + $this->alpha = $pValue; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + ($this->visible ? 't' : 'f') . + $this->blurRadius . + $this->distance . + $this->direction . + $this->alignment . + $this->color->getHashCode() . + $this->alpha . + __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/HeaderFooter.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/HeaderFooter.php new file mode 100644 index 00000000000..a78f4fccaeb --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/HeaderFooter.php @@ -0,0 +1,491 @@ + + * Header/Footer Formatting Syntax taken from Office Open XML Part 4 - Markup Language Reference, page 1970:. + * + * There are a number of formatting codes that can be written inline with the actual header / footer text, which + * affect the formatting in the header or footer. + * + * Example: This example shows the text "Center Bold Header" on the first line (center section), and the date on + * the second line (center section). + * &CCenter &"-,Bold"Bold&"-,Regular"Header_x000A_&D + * + * General Rules: + * There is no required order in which these codes must appear. + * + * The first occurrence of the following codes turns the formatting ON, the second occurrence turns it OFF again: + * - strikethrough + * - superscript + * - subscript + * Superscript and subscript cannot both be ON at same time. Whichever comes first wins and the other is ignored, + * while the first is ON. + * &L - code for "left section" (there are three header / footer locations, "left", "center", and "right"). When + * two or more occurrences of this section marker exist, the contents from all markers are concatenated, in the + * order of appearance, and placed into the left section. + * &P - code for "current page #" + * &N - code for "total pages" + * &font size - code for "text font size", where font size is a font size in points. + * &K - code for "text font color" + * RGB Color is specified as RRGGBB + * Theme Color is specifed as TTSNN where TT is the theme color Id, S is either "+" or "-" of the tint/shade + * value, NN is the tint/shade value. + * &S - code for "text strikethrough" on / off + * &X - code for "text super script" on / off + * &Y - code for "text subscript" on / off + * &C - code for "center section". When two or more occurrences of this section marker exist, the contents + * from all markers are concatenated, in the order of appearance, and placed into the center section. + * + * &D - code for "date" + * &T - code for "time" + * &G - code for "picture as background" + * &U - code for "text single underline" + * &E - code for "double underline" + * &R - code for "right section". When two or more occurrences of this section marker exist, the contents + * from all markers are concatenated, in the order of appearance, and placed into the right section. + * &Z - code for "this workbook's file path" + * &F - code for "this workbook's file name" + * &A - code for "sheet tab name" + * &+ - code for add to page #. + * &- - code for subtract from page #. + * &"font name,font type" - code for "text font name" and "text font type", where font name and font type + * are strings specifying the name and type of the font, separated by a comma. When a hyphen appears in font + * name, it means "none specified". Both of font name and font type can be localized values. + * &"-,Bold" - code for "bold font style" + * &B - also means "bold font style". + * &"-,Regular" - code for "regular font style" + * &"-,Italic" - code for "italic font style" + * &I - also means "italic font style" + * &"-,Bold Italic" code for "bold italic font style" + * &O - code for "outline style" + * &H - code for "shadow style" + * + */ +class HeaderFooter +{ + // Header/footer image location + const IMAGE_HEADER_LEFT = 'LH'; + const IMAGE_HEADER_CENTER = 'CH'; + const IMAGE_HEADER_RIGHT = 'RH'; + const IMAGE_FOOTER_LEFT = 'LF'; + const IMAGE_FOOTER_CENTER = 'CF'; + const IMAGE_FOOTER_RIGHT = 'RF'; + + /** + * OddHeader. + * + * @var string + */ + private $oddHeader = ''; + + /** + * OddFooter. + * + * @var string + */ + private $oddFooter = ''; + + /** + * EvenHeader. + * + * @var string + */ + private $evenHeader = ''; + + /** + * EvenFooter. + * + * @var string + */ + private $evenFooter = ''; + + /** + * FirstHeader. + * + * @var string + */ + private $firstHeader = ''; + + /** + * FirstFooter. + * + * @var string + */ + private $firstFooter = ''; + + /** + * Different header for Odd/Even, defaults to false. + * + * @var bool + */ + private $differentOddEven = false; + + /** + * Different header for first page, defaults to false. + * + * @var bool + */ + private $differentFirst = false; + + /** + * Scale with document, defaults to true. + * + * @var bool + */ + private $scaleWithDocument = true; + + /** + * Align with margins, defaults to true. + * + * @var bool + */ + private $alignWithMargins = true; + + /** + * Header/footer images. + * + * @var HeaderFooterDrawing[] + */ + private $headerFooterImages = []; + + /** + * Create a new HeaderFooter. + */ + public function __construct() + { + } + + /** + * Get OddHeader. + * + * @return string + */ + public function getOddHeader() + { + return $this->oddHeader; + } + + /** + * Set OddHeader. + * + * @param string $pValue + * + * @return HeaderFooter + */ + public function setOddHeader($pValue) + { + $this->oddHeader = $pValue; + + return $this; + } + + /** + * Get OddFooter. + * + * @return string + */ + public function getOddFooter() + { + return $this->oddFooter; + } + + /** + * Set OddFooter. + * + * @param string $pValue + * + * @return HeaderFooter + */ + public function setOddFooter($pValue) + { + $this->oddFooter = $pValue; + + return $this; + } + + /** + * Get EvenHeader. + * + * @return string + */ + public function getEvenHeader() + { + return $this->evenHeader; + } + + /** + * Set EvenHeader. + * + * @param string $pValue + * + * @return HeaderFooter + */ + public function setEvenHeader($pValue) + { + $this->evenHeader = $pValue; + + return $this; + } + + /** + * Get EvenFooter. + * + * @return string + */ + public function getEvenFooter() + { + return $this->evenFooter; + } + + /** + * Set EvenFooter. + * + * @param string $pValue + * + * @return HeaderFooter + */ + public function setEvenFooter($pValue) + { + $this->evenFooter = $pValue; + + return $this; + } + + /** + * Get FirstHeader. + * + * @return string + */ + public function getFirstHeader() + { + return $this->firstHeader; + } + + /** + * Set FirstHeader. + * + * @param string $pValue + * + * @return HeaderFooter + */ + public function setFirstHeader($pValue) + { + $this->firstHeader = $pValue; + + return $this; + } + + /** + * Get FirstFooter. + * + * @return string + */ + public function getFirstFooter() + { + return $this->firstFooter; + } + + /** + * Set FirstFooter. + * + * @param string $pValue + * + * @return HeaderFooter + */ + public function setFirstFooter($pValue) + { + $this->firstFooter = $pValue; + + return $this; + } + + /** + * Get DifferentOddEven. + * + * @return bool + */ + public function getDifferentOddEven() + { + return $this->differentOddEven; + } + + /** + * Set DifferentOddEven. + * + * @param bool $pValue + * + * @return HeaderFooter + */ + public function setDifferentOddEven($pValue) + { + $this->differentOddEven = $pValue; + + return $this; + } + + /** + * Get DifferentFirst. + * + * @return bool + */ + public function getDifferentFirst() + { + return $this->differentFirst; + } + + /** + * Set DifferentFirst. + * + * @param bool $pValue + * + * @return HeaderFooter + */ + public function setDifferentFirst($pValue) + { + $this->differentFirst = $pValue; + + return $this; + } + + /** + * Get ScaleWithDocument. + * + * @return bool + */ + public function getScaleWithDocument() + { + return $this->scaleWithDocument; + } + + /** + * Set ScaleWithDocument. + * + * @param bool $pValue + * + * @return HeaderFooter + */ + public function setScaleWithDocument($pValue) + { + $this->scaleWithDocument = $pValue; + + return $this; + } + + /** + * Get AlignWithMargins. + * + * @return bool + */ + public function getAlignWithMargins() + { + return $this->alignWithMargins; + } + + /** + * Set AlignWithMargins. + * + * @param bool $pValue + * + * @return HeaderFooter + */ + public function setAlignWithMargins($pValue) + { + $this->alignWithMargins = $pValue; + + return $this; + } + + /** + * Add header/footer image. + * + * @param HeaderFooterDrawing $image + * @param string $location + * + * @return HeaderFooter + */ + public function addImage(HeaderFooterDrawing $image, $location = self::IMAGE_HEADER_LEFT) + { + $this->headerFooterImages[$location] = $image; + + return $this; + } + + /** + * Remove header/footer image. + * + * @param string $location + * + * @return HeaderFooter + */ + public function removeImage($location = self::IMAGE_HEADER_LEFT) + { + if (isset($this->headerFooterImages[$location])) { + unset($this->headerFooterImages[$location]); + } + + return $this; + } + + /** + * Set header/footer images. + * + * @param HeaderFooterDrawing[] $images + * + * @return HeaderFooter + */ + public function setImages(array $images) + { + $this->headerFooterImages = $images; + + return $this; + } + + /** + * Get header/footer images. + * + * @return HeaderFooterDrawing[] + */ + public function getImages() + { + // Sort array + $images = []; + if (isset($this->headerFooterImages[self::IMAGE_HEADER_LEFT])) { + $images[self::IMAGE_HEADER_LEFT] = $this->headerFooterImages[self::IMAGE_HEADER_LEFT]; + } + if (isset($this->headerFooterImages[self::IMAGE_HEADER_CENTER])) { + $images[self::IMAGE_HEADER_CENTER] = $this->headerFooterImages[self::IMAGE_HEADER_CENTER]; + } + if (isset($this->headerFooterImages[self::IMAGE_HEADER_RIGHT])) { + $images[self::IMAGE_HEADER_RIGHT] = $this->headerFooterImages[self::IMAGE_HEADER_RIGHT]; + } + if (isset($this->headerFooterImages[self::IMAGE_FOOTER_LEFT])) { + $images[self::IMAGE_FOOTER_LEFT] = $this->headerFooterImages[self::IMAGE_FOOTER_LEFT]; + } + if (isset($this->headerFooterImages[self::IMAGE_FOOTER_CENTER])) { + $images[self::IMAGE_FOOTER_CENTER] = $this->headerFooterImages[self::IMAGE_FOOTER_CENTER]; + } + if (isset($this->headerFooterImages[self::IMAGE_FOOTER_RIGHT])) { + $images[self::IMAGE_FOOTER_RIGHT] = $this->headerFooterImages[self::IMAGE_FOOTER_RIGHT]; + } + $this->headerFooterImages = $images; + + return $this->headerFooterImages; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php new file mode 100644 index 00000000000..b42c7324c18 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php @@ -0,0 +1,24 @@ +getPath() . + $this->name . + $this->offsetX . + $this->offsetY . + $this->width . + $this->height . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Iterator.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Iterator.php new file mode 100644 index 00000000000..d8797a34736 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Iterator.php @@ -0,0 +1,87 @@ +subject = $subject; + } + + /** + * Destructor. + */ + public function __destruct() + { + unset($this->subject); + } + + /** + * Rewind iterator. + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Current Worksheet. + * + * @return Worksheet + */ + public function current() + { + return $this->subject->getSheet($this->position); + } + + /** + * Current key. + * + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * Next value. + */ + public function next() + { + ++$this->position; + } + + /** + * Are there more Worksheet instances available? + * + * @return bool + */ + public function valid() + { + return $this->position < $this->subject->getSheetCount() && $this->position >= 0; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/MemoryDrawing.php new file mode 100644 index 00000000000..6012e93e6ea --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -0,0 +1,169 @@ +imageResource = null; + $this->renderingFunction = self::RENDERING_DEFAULT; + $this->mimeType = self::MIMETYPE_DEFAULT; + $this->uniqueName = md5(rand(0, 9999) . time() . rand(0, 9999)); + + // Initialize parent + parent::__construct(); + } + + /** + * Get image resource. + * + * @return resource + */ + public function getImageResource() + { + return $this->imageResource; + } + + /** + * Set image resource. + * + * @param resource $value + * + * @return MemoryDrawing + */ + public function setImageResource($value) + { + $this->imageResource = $value; + + if ($this->imageResource !== null) { + // Get width/height + $this->width = imagesx($this->imageResource); + $this->height = imagesy($this->imageResource); + } + + return $this; + } + + /** + * Get rendering function. + * + * @return string + */ + public function getRenderingFunction() + { + return $this->renderingFunction; + } + + /** + * Set rendering function. + * + * @param string $value see self::RENDERING_* + * + * @return MemoryDrawing + */ + public function setRenderingFunction($value) + { + $this->renderingFunction = $value; + + return $this; + } + + /** + * Get mime type. + * + * @return string + */ + public function getMimeType() + { + return $this->mimeType; + } + + /** + * Set mime type. + * + * @param string $value see self::MIMETYPE_* + * + * @return MemoryDrawing + */ + public function setMimeType($value) + { + $this->mimeType = $value; + + return $this; + } + + /** + * Get indexed filename (using image index). + * + * @return string + */ + public function getIndexedFilename() + { + $extension = strtolower($this->getMimeType()); + $extension = explode('/', $extension); + $extension = $extension[1]; + + return $this->uniqueName . $this->getImageIndex() . '.' . $extension; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->renderingFunction . + $this->mimeType . + $this->uniqueName . + parent::getHashCode() . + __CLASS__ + ); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/PageMargins.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/PageMargins.php new file mode 100644 index 00000000000..eb44a1094c5 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/PageMargins.php @@ -0,0 +1,214 @@ +left; + } + + /** + * Set Left. + * + * @param float $pValue + * + * @return PageMargins + */ + public function setLeft($pValue) + { + $this->left = $pValue; + + return $this; + } + + /** + * Get Right. + * + * @return float + */ + public function getRight() + { + return $this->right; + } + + /** + * Set Right. + * + * @param float $pValue + * + * @return PageMargins + */ + public function setRight($pValue) + { + $this->right = $pValue; + + return $this; + } + + /** + * Get Top. + * + * @return float + */ + public function getTop() + { + return $this->top; + } + + /** + * Set Top. + * + * @param float $pValue + * + * @return PageMargins + */ + public function setTop($pValue) + { + $this->top = $pValue; + + return $this; + } + + /** + * Get Bottom. + * + * @return float + */ + public function getBottom() + { + return $this->bottom; + } + + /** + * Set Bottom. + * + * @param float $pValue + * + * @return PageMargins + */ + public function setBottom($pValue) + { + $this->bottom = $pValue; + + return $this; + } + + /** + * Get Header. + * + * @return float + */ + public function getHeader() + { + return $this->header; + } + + /** + * Set Header. + * + * @param float $pValue + * + * @return PageMargins + */ + public function setHeader($pValue) + { + $this->header = $pValue; + + return $this; + } + + /** + * Get Footer. + * + * @return float + */ + public function getFooter() + { + return $this->footer; + } + + /** + * Set Footer. + * + * @param float $pValue + * + * @return PageMargins + */ + public function setFooter($pValue) + { + $this->footer = $pValue; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/PageSetup.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/PageSetup.php new file mode 100644 index 00000000000..ab007f61d54 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/PageSetup.php @@ -0,0 +1,851 @@ + + * Paper size taken from Office Open XML Part 4 - Markup Language Reference, page 1988:. + * + * 1 = Letter paper (8.5 in. by 11 in.) + * 2 = Letter small paper (8.5 in. by 11 in.) + * 3 = Tabloid paper (11 in. by 17 in.) + * 4 = Ledger paper (17 in. by 11 in.) + * 5 = Legal paper (8.5 in. by 14 in.) + * 6 = Statement paper (5.5 in. by 8.5 in.) + * 7 = Executive paper (7.25 in. by 10.5 in.) + * 8 = A3 paper (297 mm by 420 mm) + * 9 = A4 paper (210 mm by 297 mm) + * 10 = A4 small paper (210 mm by 297 mm) + * 11 = A5 paper (148 mm by 210 mm) + * 12 = B4 paper (250 mm by 353 mm) + * 13 = B5 paper (176 mm by 250 mm) + * 14 = Folio paper (8.5 in. by 13 in.) + * 15 = Quarto paper (215 mm by 275 mm) + * 16 = Standard paper (10 in. by 14 in.) + * 17 = Standard paper (11 in. by 17 in.) + * 18 = Note paper (8.5 in. by 11 in.) + * 19 = #9 envelope (3.875 in. by 8.875 in.) + * 20 = #10 envelope (4.125 in. by 9.5 in.) + * 21 = #11 envelope (4.5 in. by 10.375 in.) + * 22 = #12 envelope (4.75 in. by 11 in.) + * 23 = #14 envelope (5 in. by 11.5 in.) + * 24 = C paper (17 in. by 22 in.) + * 25 = D paper (22 in. by 34 in.) + * 26 = E paper (34 in. by 44 in.) + * 27 = DL envelope (110 mm by 220 mm) + * 28 = C5 envelope (162 mm by 229 mm) + * 29 = C3 envelope (324 mm by 458 mm) + * 30 = C4 envelope (229 mm by 324 mm) + * 31 = C6 envelope (114 mm by 162 mm) + * 32 = C65 envelope (114 mm by 229 mm) + * 33 = B4 envelope (250 mm by 353 mm) + * 34 = B5 envelope (176 mm by 250 mm) + * 35 = B6 envelope (176 mm by 125 mm) + * 36 = Italy envelope (110 mm by 230 mm) + * 37 = Monarch envelope (3.875 in. by 7.5 in.). + * 38 = 6 3/4 envelope (3.625 in. by 6.5 in.) + * 39 = US standard fanfold (14.875 in. by 11 in.) + * 40 = German standard fanfold (8.5 in. by 12 in.) + * 41 = German legal fanfold (8.5 in. by 13 in.) + * 42 = ISO B4 (250 mm by 353 mm) + * 43 = Japanese double postcard (200 mm by 148 mm) + * 44 = Standard paper (9 in. by 11 in.) + * 45 = Standard paper (10 in. by 11 in.) + * 46 = Standard paper (15 in. by 11 in.) + * 47 = Invite envelope (220 mm by 220 mm) + * 50 = Letter extra paper (9.275 in. by 12 in.) + * 51 = Legal extra paper (9.275 in. by 15 in.) + * 52 = Tabloid extra paper (11.69 in. by 18 in.) + * 53 = A4 extra paper (236 mm by 322 mm) + * 54 = Letter transverse paper (8.275 in. by 11 in.) + * 55 = A4 transverse paper (210 mm by 297 mm) + * 56 = Letter extra transverse paper (9.275 in. by 12 in.) + * 57 = SuperA/SuperA/A4 paper (227 mm by 356 mm) + * 58 = SuperB/SuperB/A3 paper (305 mm by 487 mm) + * 59 = Letter plus paper (8.5 in. by 12.69 in.) + * 60 = A4 plus paper (210 mm by 330 mm) + * 61 = A5 transverse paper (148 mm by 210 mm) + * 62 = JIS B5 transverse paper (182 mm by 257 mm) + * 63 = A3 extra paper (322 mm by 445 mm) + * 64 = A5 extra paper (174 mm by 235 mm) + * 65 = ISO B5 extra paper (201 mm by 276 mm) + * 66 = A2 paper (420 mm by 594 mm) + * 67 = A3 transverse paper (297 mm by 420 mm) + * 68 = A3 extra transverse paper (322 mm by 445 mm) + * + * + * @category PhpSpreadsheet + * + * @copyright Copyright (c) 2006 - 2016 PhpSpreadsheet (https://github.com/PHPOffice/PhpSpreadsheet) + */ +class PageSetup +{ + // Paper size + const PAPERSIZE_LETTER = 1; + const PAPERSIZE_LETTER_SMALL = 2; + const PAPERSIZE_TABLOID = 3; + const PAPERSIZE_LEDGER = 4; + const PAPERSIZE_LEGAL = 5; + const PAPERSIZE_STATEMENT = 6; + const PAPERSIZE_EXECUTIVE = 7; + const PAPERSIZE_A3 = 8; + const PAPERSIZE_A4 = 9; + const PAPERSIZE_A4_SMALL = 10; + const PAPERSIZE_A5 = 11; + const PAPERSIZE_B4 = 12; + const PAPERSIZE_B5 = 13; + const PAPERSIZE_FOLIO = 14; + const PAPERSIZE_QUARTO = 15; + const PAPERSIZE_STANDARD_1 = 16; + const PAPERSIZE_STANDARD_2 = 17; + const PAPERSIZE_NOTE = 18; + const PAPERSIZE_NO9_ENVELOPE = 19; + const PAPERSIZE_NO10_ENVELOPE = 20; + const PAPERSIZE_NO11_ENVELOPE = 21; + const PAPERSIZE_NO12_ENVELOPE = 22; + const PAPERSIZE_NO14_ENVELOPE = 23; + const PAPERSIZE_C = 24; + const PAPERSIZE_D = 25; + const PAPERSIZE_E = 26; + const PAPERSIZE_DL_ENVELOPE = 27; + const PAPERSIZE_C5_ENVELOPE = 28; + const PAPERSIZE_C3_ENVELOPE = 29; + const PAPERSIZE_C4_ENVELOPE = 30; + const PAPERSIZE_C6_ENVELOPE = 31; + const PAPERSIZE_C65_ENVELOPE = 32; + const PAPERSIZE_B4_ENVELOPE = 33; + const PAPERSIZE_B5_ENVELOPE = 34; + const PAPERSIZE_B6_ENVELOPE = 35; + const PAPERSIZE_ITALY_ENVELOPE = 36; + const PAPERSIZE_MONARCH_ENVELOPE = 37; + const PAPERSIZE_6_3_4_ENVELOPE = 38; + const PAPERSIZE_US_STANDARD_FANFOLD = 39; + const PAPERSIZE_GERMAN_STANDARD_FANFOLD = 40; + const PAPERSIZE_GERMAN_LEGAL_FANFOLD = 41; + const PAPERSIZE_ISO_B4 = 42; + const PAPERSIZE_JAPANESE_DOUBLE_POSTCARD = 43; + const PAPERSIZE_STANDARD_PAPER_1 = 44; + const PAPERSIZE_STANDARD_PAPER_2 = 45; + const PAPERSIZE_STANDARD_PAPER_3 = 46; + const PAPERSIZE_INVITE_ENVELOPE = 47; + const PAPERSIZE_LETTER_EXTRA_PAPER = 48; + const PAPERSIZE_LEGAL_EXTRA_PAPER = 49; + const PAPERSIZE_TABLOID_EXTRA_PAPER = 50; + const PAPERSIZE_A4_EXTRA_PAPER = 51; + const PAPERSIZE_LETTER_TRANSVERSE_PAPER = 52; + const PAPERSIZE_A4_TRANSVERSE_PAPER = 53; + const PAPERSIZE_LETTER_EXTRA_TRANSVERSE_PAPER = 54; + const PAPERSIZE_SUPERA_SUPERA_A4_PAPER = 55; + const PAPERSIZE_SUPERB_SUPERB_A3_PAPER = 56; + const PAPERSIZE_LETTER_PLUS_PAPER = 57; + const PAPERSIZE_A4_PLUS_PAPER = 58; + const PAPERSIZE_A5_TRANSVERSE_PAPER = 59; + const PAPERSIZE_JIS_B5_TRANSVERSE_PAPER = 60; + const PAPERSIZE_A3_EXTRA_PAPER = 61; + const PAPERSIZE_A5_EXTRA_PAPER = 62; + const PAPERSIZE_ISO_B5_EXTRA_PAPER = 63; + const PAPERSIZE_A2_PAPER = 64; + const PAPERSIZE_A3_TRANSVERSE_PAPER = 65; + const PAPERSIZE_A3_EXTRA_TRANSVERSE_PAPER = 66; + + // Page orientation + const ORIENTATION_DEFAULT = 'default'; + const ORIENTATION_LANDSCAPE = 'landscape'; + const ORIENTATION_PORTRAIT = 'portrait'; + + // Print Range Set Method + const SETPRINTRANGE_OVERWRITE = 'O'; + const SETPRINTRANGE_INSERT = 'I'; + + /** + * Paper size. + * + * @var int + */ + private $paperSize = self::PAPERSIZE_LETTER; + + /** + * Orientation. + * + * @var string + */ + private $orientation = self::ORIENTATION_DEFAULT; + + /** + * Scale (Print Scale). + * + * Print scaling. Valid values range from 10 to 400 + * This setting is overridden when fitToWidth and/or fitToHeight are in use + * + * @var int? + */ + private $scale = 100; + + /** + * Fit To Page + * Whether scale or fitToWith / fitToHeight applies. + * + * @var bool + */ + private $fitToPage = false; + + /** + * Fit To Height + * Number of vertical pages to fit on. + * + * @var int? + */ + private $fitToHeight = 1; + + /** + * Fit To Width + * Number of horizontal pages to fit on. + * + * @var int? + */ + private $fitToWidth = 1; + + /** + * Columns to repeat at left. + * + * @var array Containing start column and end column, empty array if option unset + */ + private $columnsToRepeatAtLeft = ['', '']; + + /** + * Rows to repeat at top. + * + * @var array Containing start row number and end row number, empty array if option unset + */ + private $rowsToRepeatAtTop = [0, 0]; + + /** + * Center page horizontally. + * + * @var bool + */ + private $horizontalCentered = false; + + /** + * Center page vertically. + * + * @var bool + */ + private $verticalCentered = false; + + /** + * Print area. + * + * @var string + */ + private $printArea; + + /** + * First page number. + * + * @var int + */ + private $firstPageNumber; + + /** + * Create a new PageSetup. + */ + public function __construct() + { + } + + /** + * Get Paper Size. + * + * @return int + */ + public function getPaperSize() + { + return $this->paperSize; + } + + /** + * Set Paper Size. + * + * @param int $pValue see self::PAPERSIZE_* + * + * @return PageSetup + */ + public function setPaperSize($pValue) + { + $this->paperSize = $pValue; + + return $this; + } + + /** + * Get Orientation. + * + * @return string + */ + public function getOrientation() + { + return $this->orientation; + } + + /** + * Set Orientation. + * + * @param string $pValue see self::ORIENTATION_* + * + * @return PageSetup + */ + public function setOrientation($pValue) + { + $this->orientation = $pValue; + + return $this; + } + + /** + * Get Scale. + * + * @return int? + */ + public function getScale() + { + return $this->scale; + } + + /** + * Set Scale. + * Print scaling. Valid values range from 10 to 400 + * This setting is overridden when fitToWidth and/or fitToHeight are in use. + * + * @param null|int $pValue + * @param bool $pUpdate Update fitToPage so scaling applies rather than fitToHeight / fitToWidth + * + * @throws PhpSpreadsheetException + * + * @return PageSetup + */ + public function setScale($pValue, $pUpdate = true) + { + // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface, + // but it is apparently still able to handle any scale >= 0, where 0 results in 100 + if (($pValue >= 0) || $pValue === null) { + $this->scale = $pValue; + if ($pUpdate) { + $this->fitToPage = false; + } + } else { + throw new PhpSpreadsheetException('Scale must not be negative'); + } + + return $this; + } + + /** + * Get Fit To Page. + * + * @return bool + */ + public function getFitToPage() + { + return $this->fitToPage; + } + + /** + * Set Fit To Page. + * + * @param bool $pValue + * + * @return PageSetup + */ + public function setFitToPage($pValue) + { + $this->fitToPage = $pValue; + + return $this; + } + + /** + * Get Fit To Height. + * + * @return int? + */ + public function getFitToHeight() + { + return $this->fitToHeight; + } + + /** + * Set Fit To Height. + * + * @param null|int $pValue + * @param bool $pUpdate Update fitToPage so it applies rather than scaling + * + * @return PageSetup + */ + public function setFitToHeight($pValue, $pUpdate = true) + { + $this->fitToHeight = $pValue; + if ($pUpdate) { + $this->fitToPage = true; + } + + return $this; + } + + /** + * Get Fit To Width. + * + * @return int? + */ + public function getFitToWidth() + { + return $this->fitToWidth; + } + + /** + * Set Fit To Width. + * + * @param null|int $pValue + * @param bool $pUpdate Update fitToPage so it applies rather than scaling + * + * @return PageSetup + */ + public function setFitToWidth($pValue, $pUpdate = true) + { + $this->fitToWidth = $pValue; + if ($pUpdate) { + $this->fitToPage = true; + } + + return $this; + } + + /** + * Is Columns to repeat at left set? + * + * @return bool + */ + public function isColumnsToRepeatAtLeftSet() + { + if (is_array($this->columnsToRepeatAtLeft)) { + if ($this->columnsToRepeatAtLeft[0] != '' && $this->columnsToRepeatAtLeft[1] != '') { + return true; + } + } + + return false; + } + + /** + * Get Columns to repeat at left. + * + * @return array Containing start column and end column, empty array if option unset + */ + public function getColumnsToRepeatAtLeft() + { + return $this->columnsToRepeatAtLeft; + } + + /** + * Set Columns to repeat at left. + * + * @param array $pValue Containing start column and end column, empty array if option unset + * + * @return PageSetup + */ + public function setColumnsToRepeatAtLeft(array $pValue) + { + $this->columnsToRepeatAtLeft = $pValue; + + return $this; + } + + /** + * Set Columns to repeat at left by start and end. + * + * @param string $pStart eg: 'A' + * @param string $pEnd eg: 'B' + * + * @return PageSetup + */ + public function setColumnsToRepeatAtLeftByStartAndEnd($pStart, $pEnd) + { + $this->columnsToRepeatAtLeft = [$pStart, $pEnd]; + + return $this; + } + + /** + * Is Rows to repeat at top set? + * + * @return bool + */ + public function isRowsToRepeatAtTopSet() + { + if (is_array($this->rowsToRepeatAtTop)) { + if ($this->rowsToRepeatAtTop[0] != 0 && $this->rowsToRepeatAtTop[1] != 0) { + return true; + } + } + + return false; + } + + /** + * Get Rows to repeat at top. + * + * @return array Containing start column and end column, empty array if option unset + */ + public function getRowsToRepeatAtTop() + { + return $this->rowsToRepeatAtTop; + } + + /** + * Set Rows to repeat at top. + * + * @param array $pValue Containing start column and end column, empty array if option unset + * + * @return PageSetup + */ + public function setRowsToRepeatAtTop(array $pValue) + { + $this->rowsToRepeatAtTop = $pValue; + + return $this; + } + + /** + * Set Rows to repeat at top by start and end. + * + * @param int $pStart eg: 1 + * @param int $pEnd eg: 1 + * + * @return PageSetup + */ + public function setRowsToRepeatAtTopByStartAndEnd($pStart, $pEnd) + { + $this->rowsToRepeatAtTop = [$pStart, $pEnd]; + + return $this; + } + + /** + * Get center page horizontally. + * + * @return bool + */ + public function getHorizontalCentered() + { + return $this->horizontalCentered; + } + + /** + * Set center page horizontally. + * + * @param bool $value + * + * @return PageSetup + */ + public function setHorizontalCentered($value) + { + $this->horizontalCentered = $value; + + return $this; + } + + /** + * Get center page vertically. + * + * @return bool + */ + public function getVerticalCentered() + { + return $this->verticalCentered; + } + + /** + * Set center page vertically. + * + * @param bool $value + * + * @return PageSetup + */ + public function setVerticalCentered($value) + { + $this->verticalCentered = $value; + + return $this; + } + + /** + * Get print area. + * + * @param int $index Identifier for a specific print area range if several ranges have been set + * Default behaviour, or a index value of 0, will return all ranges as a comma-separated string + * Otherwise, the specific range identified by the value of $index will be returned + * Print areas are numbered from 1 + * + * @throws PhpSpreadsheetException + * + * @return string + */ + public function getPrintArea($index = 0) + { + if ($index == 0) { + return $this->printArea; + } + $printAreas = explode(',', $this->printArea); + if (isset($printAreas[$index - 1])) { + return $printAreas[$index - 1]; + } + + throw new PhpSpreadsheetException('Requested Print Area does not exist'); + } + + /** + * Is print area set? + * + * @param int $index Identifier for a specific print area range if several ranges have been set + * Default behaviour, or an index value of 0, will identify whether any print range is set + * Otherwise, existence of the range identified by the value of $index will be returned + * Print areas are numbered from 1 + * + * @return bool + */ + public function isPrintAreaSet($index = 0) + { + if ($index == 0) { + return $this->printArea !== null; + } + $printAreas = explode(',', $this->printArea); + + return isset($printAreas[$index - 1]); + } + + /** + * Clear a print area. + * + * @param int $index Identifier for a specific print area range if several ranges have been set + * Default behaviour, or an index value of 0, will clear all print ranges that are set + * Otherwise, the range identified by the value of $index will be removed from the series + * Print areas are numbered from 1 + * + * @return PageSetup + */ + public function clearPrintArea($index = 0) + { + if ($index == 0) { + $this->printArea = null; + } else { + $printAreas = explode(',', $this->printArea); + if (isset($printAreas[$index - 1])) { + unset($printAreas[$index - 1]); + $this->printArea = implode(',', $printAreas); + } + } + + return $this; + } + + /** + * Set print area. e.g. 'A1:D10' or 'A1:D10,G5:M20'. + * + * @param string $value + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * When the method is "O"verwrite, then a positive integer index will overwrite that indexed + * entry in the print areas list; a negative index value will identify which entry to + * overwrite working bacward through the print area to the list, with the last entry as -1. + * Specifying an index value of 0, will overwrite all existing print ranges. + * When the method is "I"nsert, then a positive index will insert after that indexed entry in + * the print areas list, while a negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * @param string $method Determines the method used when setting multiple print areas + * Default behaviour, or the "O" method, overwrites existing print area + * The "I" method, inserts the new print area before any specified index, or at the end of the list + * + * @throws PhpSpreadsheetException + * + * @return PageSetup + */ + public function setPrintArea($value, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE) + { + if (strpos($value, '!') !== false) { + throw new PhpSpreadsheetException('Cell coordinate must not specify a worksheet.'); + } elseif (strpos($value, ':') === false) { + throw new PhpSpreadsheetException('Cell coordinate must be a range of cells.'); + } elseif (strpos($value, '$') !== false) { + throw new PhpSpreadsheetException('Cell coordinate must not be absolute.'); + } + $value = strtoupper($value); + + if ($method == self::SETPRINTRANGE_OVERWRITE) { + if ($index == 0) { + $this->printArea = $value; + } else { + $printAreas = explode(',', $this->printArea); + if ($index < 0) { + $index = count($printAreas) - abs($index) + 1; + } + if (($index <= 0) || ($index > count($printAreas))) { + throw new PhpSpreadsheetException('Invalid index for setting print range.'); + } + $printAreas[$index - 1] = $value; + $this->printArea = implode(',', $printAreas); + } + } elseif ($method == self::SETPRINTRANGE_INSERT) { + if ($index == 0) { + $this->printArea .= ($this->printArea == '') ? $value : ',' . $value; + } else { + $printAreas = explode(',', $this->printArea); + if ($index < 0) { + $index = abs($index) - 1; + } + if ($index > count($printAreas)) { + throw new PhpSpreadsheetException('Invalid index for setting print range.'); + } + $printAreas = array_merge(array_slice($printAreas, 0, $index), [$value], array_slice($printAreas, $index)); + $this->printArea = implode(',', $printAreas); + } + } else { + throw new PhpSpreadsheetException('Invalid method for setting print range.'); + } + + return $this; + } + + /** + * Add a new print area (e.g. 'A1:D10' or 'A1:D10,G5:M20') to the list of print areas. + * + * @param string $value + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * A positive index will insert after that indexed entry in the print areas list, while a + * negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * + * @throws PhpSpreadsheetException + * + * @return PageSetup + */ + public function addPrintArea($value, $index = -1) + { + return $this->setPrintArea($value, $index, self::SETPRINTRANGE_INSERT); + } + + /** + * Set print area. + * + * @param int $column1 Column 1 + * @param int $row1 Row 1 + * @param int $column2 Column 2 + * @param int $row2 Row 2 + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * When the method is "O"verwrite, then a positive integer index will overwrite that indexed + * entry in the print areas list; a negative index value will identify which entry to + * overwrite working backward through the print area to the list, with the last entry as -1. + * Specifying an index value of 0, will overwrite all existing print ranges. + * When the method is "I"nsert, then a positive index will insert after that indexed entry in + * the print areas list, while a negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * @param string $method Determines the method used when setting multiple print areas + * Default behaviour, or the "O" method, overwrites existing print area + * The "I" method, inserts the new print area before any specified index, or at the end of the list + * + * @throws PhpSpreadsheetException + * + * @return PageSetup + */ + public function setPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE) + { + return $this->setPrintArea( + Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2, + $index, + $method + ); + } + + /** + * Add a new print area to the list of print areas. + * + * @param int $column1 Start Column for the print area + * @param int $row1 Start Row for the print area + * @param int $column2 End Column for the print area + * @param int $row2 End Row for the print area + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * A positive index will insert after that indexed entry in the print areas list, while a + * negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * + * @throws PhpSpreadsheetException + * + * @return PageSetup + */ + public function addPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = -1) + { + return $this->setPrintArea( + Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2, + $index, + self::SETPRINTRANGE_INSERT + ); + } + + /** + * Get first page number. + * + * @return int + */ + public function getFirstPageNumber() + { + return $this->firstPageNumber; + } + + /** + * Set first page number. + * + * @param int $value + * + * @return PageSetup + */ + public function setFirstPageNumber($value) + { + $this->firstPageNumber = $value; + + return $this; + } + + /** + * Reset first page number. + * + * @return PageSetup + */ + public function resetFirstPageNumber() + { + return $this->setFirstPageNumber(null); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Protection.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Protection.php new file mode 100644 index 00000000000..1815f45bf07 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Protection.php @@ -0,0 +1,586 @@ +sheet || + $this->objects || + $this->scenarios || + $this->formatCells || + $this->formatColumns || + $this->formatRows || + $this->insertColumns || + $this->insertRows || + $this->insertHyperlinks || + $this->deleteColumns || + $this->deleteRows || + $this->selectLockedCells || + $this->sort || + $this->autoFilter || + $this->pivotTables || + $this->selectUnlockedCells; + } + + /** + * Get Sheet. + * + * @return bool + */ + public function getSheet() + { + return $this->sheet; + } + + /** + * Set Sheet. + * + * @param bool $pValue + * + * @return Protection + */ + public function setSheet($pValue) + { + $this->sheet = $pValue; + + return $this; + } + + /** + * Get Objects. + * + * @return bool + */ + public function getObjects() + { + return $this->objects; + } + + /** + * Set Objects. + * + * @param bool $pValue + * + * @return Protection + */ + public function setObjects($pValue) + { + $this->objects = $pValue; + + return $this; + } + + /** + * Get Scenarios. + * + * @return bool + */ + public function getScenarios() + { + return $this->scenarios; + } + + /** + * Set Scenarios. + * + * @param bool $pValue + * + * @return Protection + */ + public function setScenarios($pValue) + { + $this->scenarios = $pValue; + + return $this; + } + + /** + * Get FormatCells. + * + * @return bool + */ + public function getFormatCells() + { + return $this->formatCells; + } + + /** + * Set FormatCells. + * + * @param bool $pValue + * + * @return Protection + */ + public function setFormatCells($pValue) + { + $this->formatCells = $pValue; + + return $this; + } + + /** + * Get FormatColumns. + * + * @return bool + */ + public function getFormatColumns() + { + return $this->formatColumns; + } + + /** + * Set FormatColumns. + * + * @param bool $pValue + * + * @return Protection + */ + public function setFormatColumns($pValue) + { + $this->formatColumns = $pValue; + + return $this; + } + + /** + * Get FormatRows. + * + * @return bool + */ + public function getFormatRows() + { + return $this->formatRows; + } + + /** + * Set FormatRows. + * + * @param bool $pValue + * + * @return Protection + */ + public function setFormatRows($pValue) + { + $this->formatRows = $pValue; + + return $this; + } + + /** + * Get InsertColumns. + * + * @return bool + */ + public function getInsertColumns() + { + return $this->insertColumns; + } + + /** + * Set InsertColumns. + * + * @param bool $pValue + * + * @return Protection + */ + public function setInsertColumns($pValue) + { + $this->insertColumns = $pValue; + + return $this; + } + + /** + * Get InsertRows. + * + * @return bool + */ + public function getInsertRows() + { + return $this->insertRows; + } + + /** + * Set InsertRows. + * + * @param bool $pValue + * + * @return Protection + */ + public function setInsertRows($pValue) + { + $this->insertRows = $pValue; + + return $this; + } + + /** + * Get InsertHyperlinks. + * + * @return bool + */ + public function getInsertHyperlinks() + { + return $this->insertHyperlinks; + } + + /** + * Set InsertHyperlinks. + * + * @param bool $pValue + * + * @return Protection + */ + public function setInsertHyperlinks($pValue) + { + $this->insertHyperlinks = $pValue; + + return $this; + } + + /** + * Get DeleteColumns. + * + * @return bool + */ + public function getDeleteColumns() + { + return $this->deleteColumns; + } + + /** + * Set DeleteColumns. + * + * @param bool $pValue + * + * @return Protection + */ + public function setDeleteColumns($pValue) + { + $this->deleteColumns = $pValue; + + return $this; + } + + /** + * Get DeleteRows. + * + * @return bool + */ + public function getDeleteRows() + { + return $this->deleteRows; + } + + /** + * Set DeleteRows. + * + * @param bool $pValue + * + * @return Protection + */ + public function setDeleteRows($pValue) + { + $this->deleteRows = $pValue; + + return $this; + } + + /** + * Get SelectLockedCells. + * + * @return bool + */ + public function getSelectLockedCells() + { + return $this->selectLockedCells; + } + + /** + * Set SelectLockedCells. + * + * @param bool $pValue + * + * @return Protection + */ + public function setSelectLockedCells($pValue) + { + $this->selectLockedCells = $pValue; + + return $this; + } + + /** + * Get Sort. + * + * @return bool + */ + public function getSort() + { + return $this->sort; + } + + /** + * Set Sort. + * + * @param bool $pValue + * + * @return Protection + */ + public function setSort($pValue) + { + $this->sort = $pValue; + + return $this; + } + + /** + * Get AutoFilter. + * + * @return bool + */ + public function getAutoFilter() + { + return $this->autoFilter; + } + + /** + * Set AutoFilter. + * + * @param bool $pValue + * + * @return Protection + */ + public function setAutoFilter($pValue) + { + $this->autoFilter = $pValue; + + return $this; + } + + /** + * Get PivotTables. + * + * @return bool + */ + public function getPivotTables() + { + return $this->pivotTables; + } + + /** + * Set PivotTables. + * + * @param bool $pValue + * + * @return Protection + */ + public function setPivotTables($pValue) + { + $this->pivotTables = $pValue; + + return $this; + } + + /** + * Get SelectUnlockedCells. + * + * @return bool + */ + public function getSelectUnlockedCells() + { + return $this->selectUnlockedCells; + } + + /** + * Set SelectUnlockedCells. + * + * @param bool $pValue + * + * @return Protection + */ + public function setSelectUnlockedCells($pValue) + { + $this->selectUnlockedCells = $pValue; + + return $this; + } + + /** + * Get Password (hashed). + * + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Set Password. + * + * @param string $pValue + * @param bool $pAlreadyHashed If the password has already been hashed, set this to true + * + * @return Protection + */ + public function setPassword($pValue, $pAlreadyHashed = false) + { + if (!$pAlreadyHashed) { + $pValue = PasswordHasher::hashPassword($pValue); + } + $this->password = $pValue; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Row.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Row.php new file mode 100644 index 00000000000..2a379d2cd34 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Row.php @@ -0,0 +1,74 @@ +worksheet = $worksheet; + $this->rowIndex = $rowIndex; + } + + /** + * Destructor. + */ + public function __destruct() + { + unset($this->worksheet); + } + + /** + * Get row index. + * + * @return int + */ + public function getRowIndex() + { + return $this->rowIndex; + } + + /** + * Get cell iterator. + * + * @param string $startColumn The column address at which to start iterating + * @param string $endColumn Optionally, the column address at which to stop iterating + * + * @return RowCellIterator + */ + public function getCellIterator($startColumn = 'A', $endColumn = null) + { + return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn); + } + + /** + * Returns bound worksheet. + * + * @return Worksheet + */ + public function getWorksheet() + { + return $this->worksheet; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowCellIterator.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowCellIterator.php new file mode 100644 index 00000000000..59ef329c8b9 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowCellIterator.php @@ -0,0 +1,195 @@ +worksheet = $worksheet; + $this->rowIndex = $rowIndex; + $this->resetEnd($endColumn); + $this->resetStart($startColumn); + } + + /** + * (Re)Set the start column and the current column pointer. + * + * @param string $startColumn The column address at which to start iterating + * + * @throws PhpSpreadsheetException + * + * @return RowCellIterator + */ + public function resetStart($startColumn = 'A') + { + $this->startColumnIndex = Coordinate::columnIndexFromString($startColumn); + $this->adjustForExistingOnlyRange(); + $this->seek(Coordinate::stringFromColumnIndex($this->startColumnIndex)); + + return $this; + } + + /** + * (Re)Set the end column. + * + * @param string $endColumn The column address at which to stop iterating + * + * @throws PhpSpreadsheetException + * + * @return RowCellIterator + */ + public function resetEnd($endColumn = null) + { + $endColumn = $endColumn ? $endColumn : $this->worksheet->getHighestColumn(); + $this->endColumnIndex = Coordinate::columnIndexFromString($endColumn); + $this->adjustForExistingOnlyRange(); + + return $this; + } + + /** + * Set the column pointer to the selected column. + * + * @param string $column The column address to set the current pointer at + * + * @throws PhpSpreadsheetException + * + * @return RowCellIterator + */ + public function seek($column = 'A') + { + $column = Coordinate::columnIndexFromString($column); + if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) { + throw new PhpSpreadsheetException("Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"); + } elseif ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($column, $this->rowIndex))) { + throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist'); + } + $this->currentColumnIndex = $column; + + return $this; + } + + /** + * Rewind the iterator to the starting column. + */ + public function rewind() + { + $this->currentColumnIndex = $this->startColumnIndex; + } + + /** + * Return the current cell in this worksheet row. + * + * @return \PhpOffice\PhpSpreadsheet\Cell\Cell + */ + public function current() + { + return $this->worksheet->getCellByColumnAndRow($this->currentColumnIndex, $this->rowIndex); + } + + /** + * Return the current iterator key. + * + * @return string + */ + public function key() + { + return Coordinate::stringFromColumnIndex($this->currentColumnIndex); + } + + /** + * Set the iterator to its next value. + */ + public function next() + { + do { + ++$this->currentColumnIndex; + } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex <= $this->endColumnIndex)); + } + + /** + * Set the iterator to its previous value. + * + * @throws PhpSpreadsheetException + */ + public function prev() + { + do { + --$this->currentColumnIndex; + } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex >= $this->startColumnIndex)); + } + + /** + * Indicate if more columns exist in the worksheet range of columns that we're iterating. + * + * @return bool + */ + public function valid() + { + return $this->currentColumnIndex <= $this->endColumnIndex && $this->currentColumnIndex >= $this->startColumnIndex; + } + + /** + * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary. + * + * @throws PhpSpreadsheetException + */ + protected function adjustForExistingOnlyRange() + { + if ($this->onlyExistingCells) { + while ((!$this->worksheet->cellExistsByColumnAndRow($this->startColumnIndex, $this->rowIndex)) && ($this->startColumnIndex <= $this->endColumnIndex)) { + ++$this->startColumnIndex; + } + if ($this->startColumnIndex > $this->endColumnIndex) { + throw new PhpSpreadsheetException('No cells exist within the specified range'); + } + while ((!$this->worksheet->cellExistsByColumnAndRow($this->endColumnIndex, $this->rowIndex)) && ($this->endColumnIndex >= $this->startColumnIndex)) { + --$this->endColumnIndex; + } + if ($this->endColumnIndex < $this->startColumnIndex) { + throw new PhpSpreadsheetException('No cells exist within the specified range'); + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowDimension.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowDimension.php new file mode 100644 index 00000000000..e4346404d23 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowDimension.php @@ -0,0 +1,115 @@ +rowIndex = $pIndex; + + // set dimension as unformatted by default + parent::__construct(null); + } + + /** + * Get Row Index. + * + * @return int + */ + public function getRowIndex() + { + return $this->rowIndex; + } + + /** + * Set Row Index. + * + * @param int $pValue + * + * @return RowDimension + */ + public function setRowIndex($pValue) + { + $this->rowIndex = $pValue; + + return $this; + } + + /** + * Get Row Height. + * + * @return float + */ + public function getRowHeight() + { + return $this->height; + } + + /** + * Set Row Height. + * + * @param float $pValue + * + * @return RowDimension + */ + public function setRowHeight($pValue) + { + $this->height = $pValue; + + return $this; + } + + /** + * Get ZeroHeight. + * + * @return bool + */ + public function getZeroHeight() + { + return $this->zeroHeight; + } + + /** + * Set ZeroHeight. + * + * @param bool $pValue + * + * @return RowDimension + */ + public function setZeroHeight($pValue) + { + $this->zeroHeight = $pValue; + + return $this; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowIterator.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowIterator.php new file mode 100644 index 00000000000..433cea6a12a --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/RowIterator.php @@ -0,0 +1,170 @@ +subject = $subject; + $this->resetEnd($endRow); + $this->resetStart($startRow); + } + + /** + * Destructor. + */ + public function __destruct() + { + unset($this->subject); + } + + /** + * (Re)Set the start row and the current row pointer. + * + * @param int $startRow The row number at which to start iterating + * + * @throws PhpSpreadsheetException + * + * @return RowIterator + */ + public function resetStart($startRow = 1) + { + if ($startRow > $this->subject->getHighestRow()) { + throw new PhpSpreadsheetException("Start row ({$startRow}) is beyond highest row ({$this->subject->getHighestRow()})"); + } + + $this->startRow = $startRow; + if ($this->endRow < $this->startRow) { + $this->endRow = $this->startRow; + } + $this->seek($startRow); + + return $this; + } + + /** + * (Re)Set the end row. + * + * @param int $endRow The row number at which to stop iterating + * + * @return RowIterator + */ + public function resetEnd($endRow = null) + { + $this->endRow = ($endRow) ? $endRow : $this->subject->getHighestRow(); + + return $this; + } + + /** + * Set the row pointer to the selected row. + * + * @param int $row The row number to set the current pointer at + * + * @throws PhpSpreadsheetException + * + * @return RowIterator + */ + public function seek($row = 1) + { + if (($row < $this->startRow) || ($row > $this->endRow)) { + throw new PhpSpreadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})"); + } + $this->position = $row; + + return $this; + } + + /** + * Rewind the iterator to the starting row. + */ + public function rewind() + { + $this->position = $this->startRow; + } + + /** + * Return the current row in this worksheet. + * + * @return Row + */ + public function current() + { + return new Row($this->subject, $this->position); + } + + /** + * Return the current iterator key. + * + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * Set the iterator to its next value. + */ + public function next() + { + ++$this->position; + } + + /** + * Set the iterator to its previous value. + */ + public function prev() + { + --$this->position; + } + + /** + * Indicate if more rows exist in the worksheet range of rows that we're iterating. + * + * @return bool + */ + public function valid() + { + return $this->position <= $this->endRow && $this->position >= $this->startRow; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/SheetView.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/SheetView.php new file mode 100644 index 00000000000..172823245e0 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/SheetView.php @@ -0,0 +1,171 @@ +zoomScale; + } + + /** + * Set ZoomScale. + * Valid values range from 10 to 400. + * + * @param int $pValue + * + * @throws PhpSpreadsheetException + * + * @return SheetView + */ + public function setZoomScale($pValue) + { + // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface, + // but it is apparently still able to handle any scale >= 1 + if (($pValue >= 1) || $pValue === null) { + $this->zoomScale = $pValue; + } else { + throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.'); + } + + return $this; + } + + /** + * Get ZoomScaleNormal. + * + * @return int + */ + public function getZoomScaleNormal() + { + return $this->zoomScaleNormal; + } + + /** + * Set ZoomScale. + * Valid values range from 10 to 400. + * + * @param int $pValue + * + * @throws PhpSpreadsheetException + * + * @return SheetView + */ + public function setZoomScaleNormal($pValue) + { + if (($pValue >= 1) || $pValue === null) { + $this->zoomScaleNormal = $pValue; + } else { + throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.'); + } + + return $this; + } + + /** + * Get View. + * + * @return string + */ + public function getView() + { + return $this->sheetviewType; + } + + /** + * Set View. + * + * Valid values are + * 'normal' self::SHEETVIEW_NORMAL + * 'pageLayout' self::SHEETVIEW_PAGE_LAYOUT + * 'pageBreakPreview' self::SHEETVIEW_PAGE_BREAK_PREVIEW + * + * @param string $pValue + * + * @throws PhpSpreadsheetException + * + * @return SheetView + */ + public function setView($pValue) + { + // MS Excel 2007 allows setting the view to 'normal', 'pageLayout' or 'pageBreakPreview' via the user interface + if ($pValue === null) { + $pValue = self::SHEETVIEW_NORMAL; + } + if (in_array($pValue, self::$sheetViewTypes)) { + $this->sheetviewType = $pValue; + } else { + throw new PhpSpreadsheetException('Invalid sheetview layout type.'); + } + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Worksheet.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Worksheet.php new file mode 100644 index 00000000000..d0224037616 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Worksheet/Worksheet.php @@ -0,0 +1,3066 @@ +parent = $parent; + $this->setTitle($pTitle, false); + // setTitle can change $pTitle + $this->setCodeName($this->getTitle()); + $this->setSheetState(self::SHEETSTATE_VISIBLE); + + $this->cellCollection = CellsFactory::getInstance($this); + // Set page setup + $this->pageSetup = new PageSetup(); + // Set page margins + $this->pageMargins = new PageMargins(); + // Set page header/footer + $this->headerFooter = new HeaderFooter(); + // Set sheet view + $this->sheetView = new SheetView(); + // Drawing collection + $this->drawingCollection = new \ArrayObject(); + // Chart collection + $this->chartCollection = new \ArrayObject(); + // Protection + $this->protection = new Protection(); + // Default row dimension + $this->defaultRowDimension = new RowDimension(null); + // Default column dimension + $this->defaultColumnDimension = new ColumnDimension(null); + $this->autoFilter = new AutoFilter(null, $this); + } + + /** + * Disconnect all cells from this Worksheet object, + * typically so that the worksheet object can be unset. + */ + public function disconnectCells() + { + if ($this->cellCollection !== null) { + $this->cellCollection->unsetWorksheetCells(); + $this->cellCollection = null; + } + // detach ourself from the workbook, so that it can then delete this worksheet successfully + $this->parent = null; + } + + /** + * Code to execute when this worksheet is unset(). + */ + public function __destruct() + { + Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title); + + $this->disconnectCells(); + } + + /** + * Return the cell collection. + * + * @return Cells + */ + public function getCellCollection() + { + return $this->cellCollection; + } + + /** + * Get array of invalid characters for sheet title. + * + * @return array + */ + public static function getInvalidCharacters() + { + return self::$invalidCharacters; + } + + /** + * Check sheet code name for valid Excel syntax. + * + * @param string $pValue The string to check + * + * @throws Exception + * + * @return string The valid string + */ + private static function checkSheetCodeName($pValue) + { + $CharCount = Shared\StringHelper::countCharacters($pValue); + if ($CharCount == 0) { + throw new Exception('Sheet code name cannot be empty.'); + } + // Some of the printable ASCII characters are invalid: * : / \ ? [ ] and first and last characters cannot be a "'" + if ((str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) || + (Shared\StringHelper::substring($pValue, -1, 1) == '\'') || + (Shared\StringHelper::substring($pValue, 0, 1) == '\'')) { + throw new Exception('Invalid character found in sheet code name'); + } + + // Enforce maximum characters allowed for sheet title + if ($CharCount > self::SHEET_TITLE_MAXIMUM_LENGTH) { + throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.'); + } + + return $pValue; + } + + /** + * Check sheet title for valid Excel syntax. + * + * @param string $pValue The string to check + * + * @throws Exception + * + * @return string The valid string + */ + private static function checkSheetTitle($pValue) + { + // Some of the printable ASCII characters are invalid: * : / \ ? [ ] + if (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) { + throw new Exception('Invalid character found in sheet title'); + } + + // Enforce maximum characters allowed for sheet title + if (Shared\StringHelper::countCharacters($pValue) > self::SHEET_TITLE_MAXIMUM_LENGTH) { + throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.'); + } + + return $pValue; + } + + /** + * Get a sorted list of all cell coordinates currently held in the collection by row and column. + * + * @param bool $sorted Also sort the cell collection? + * + * @return string[] + */ + public function getCoordinates($sorted = true) + { + if ($this->cellCollection == null) { + return []; + } + + if ($sorted) { + return $this->cellCollection->getSortedCoordinates(); + } + + return $this->cellCollection->getCoordinates(); + } + + /** + * Get collection of row dimensions. + * + * @return RowDimension[] + */ + public function getRowDimensions() + { + return $this->rowDimensions; + } + + /** + * Get default row dimension. + * + * @return RowDimension + */ + public function getDefaultRowDimension() + { + return $this->defaultRowDimension; + } + + /** + * Get collection of column dimensions. + * + * @return ColumnDimension[] + */ + public function getColumnDimensions() + { + return $this->columnDimensions; + } + + /** + * Get default column dimension. + * + * @return ColumnDimension + */ + public function getDefaultColumnDimension() + { + return $this->defaultColumnDimension; + } + + /** + * Get collection of drawings. + * + * @return BaseDrawing[] + */ + public function getDrawingCollection() + { + return $this->drawingCollection; + } + + /** + * Get collection of charts. + * + * @return Chart[] + */ + public function getChartCollection() + { + return $this->chartCollection; + } + + /** + * Add chart. + * + * @param Chart $pChart + * @param null|int $iChartIndex Index where chart should go (0,1,..., or null for last) + * + * @return Chart + */ + public function addChart(Chart $pChart, $iChartIndex = null) + { + $pChart->setWorksheet($this); + if ($iChartIndex === null) { + $this->chartCollection[] = $pChart; + } else { + // Insert the chart at the requested index + array_splice($this->chartCollection, $iChartIndex, 0, [$pChart]); + } + + return $pChart; + } + + /** + * Return the count of charts on this worksheet. + * + * @return int The number of charts + */ + public function getChartCount() + { + return count($this->chartCollection); + } + + /** + * Get a chart by its index position. + * + * @param string $index Chart index position + * + * @return Chart|false + */ + public function getChartByIndex($index) + { + $chartCount = count($this->chartCollection); + if ($chartCount == 0) { + return false; + } + if ($index === null) { + $index = --$chartCount; + } + if (!isset($this->chartCollection[$index])) { + return false; + } + + return $this->chartCollection[$index]; + } + + /** + * Return an array of the names of charts on this worksheet. + * + * @return string[] The names of charts + */ + public function getChartNames() + { + $chartNames = []; + foreach ($this->chartCollection as $chart) { + $chartNames[] = $chart->getName(); + } + + return $chartNames; + } + + /** + * Get a chart by name. + * + * @param string $chartName Chart name + * + * @return Chart|false + */ + public function getChartByName($chartName) + { + $chartCount = count($this->chartCollection); + if ($chartCount == 0) { + return false; + } + foreach ($this->chartCollection as $index => $chart) { + if ($chart->getName() == $chartName) { + return $this->chartCollection[$index]; + } + } + + return false; + } + + /** + * Refresh column dimensions. + * + * @return Worksheet + */ + public function refreshColumnDimensions() + { + $currentColumnDimensions = $this->getColumnDimensions(); + $newColumnDimensions = []; + + foreach ($currentColumnDimensions as $objColumnDimension) { + $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension; + } + + $this->columnDimensions = $newColumnDimensions; + + return $this; + } + + /** + * Refresh row dimensions. + * + * @return Worksheet + */ + public function refreshRowDimensions() + { + $currentRowDimensions = $this->getRowDimensions(); + $newRowDimensions = []; + + foreach ($currentRowDimensions as $objRowDimension) { + $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension; + } + + $this->rowDimensions = $newRowDimensions; + + return $this; + } + + /** + * Calculate worksheet dimension. + * + * @return string String containing the dimension of this worksheet + */ + public function calculateWorksheetDimension() + { + // Return + return 'A1' . ':' . $this->getHighestColumn() . $this->getHighestRow(); + } + + /** + * Calculate worksheet data dimension. + * + * @return string String containing the dimension of this worksheet that actually contain data + */ + public function calculateWorksheetDataDimension() + { + // Return + return 'A1' . ':' . $this->getHighestDataColumn() . $this->getHighestDataRow(); + } + + /** + * Calculate widths for auto-size columns. + * + * @return Worksheet; + */ + public function calculateColumnWidths() + { + // initialize $autoSizes array + $autoSizes = []; + foreach ($this->getColumnDimensions() as $colDimension) { + if ($colDimension->getAutoSize()) { + $autoSizes[$colDimension->getColumnIndex()] = -1; + } + } + + // There is only something to do if there are some auto-size columns + if (!empty($autoSizes)) { + // build list of cells references that participate in a merge + $isMergeCell = []; + foreach ($this->getMergeCells() as $cells) { + foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) { + $isMergeCell[$cellReference] = true; + } + } + + // loop through all cells in the worksheet + foreach ($this->getCoordinates(false) as $coordinate) { + $cell = $this->getCell($coordinate, false); + if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) { + //Determine if cell is in merge range + $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]); + + //By default merged cells should be ignored + $isMergedButProceed = false; + + //The only exception is if it's a merge range value cell of a 'vertical' randge (1 column wide) + if ($isMerged && $cell->isMergeRangeValueCell()) { + $range = $cell->getMergeRange(); + $rangeBoundaries = Coordinate::rangeDimension($range); + if ($rangeBoundaries[0] == 1) { + $isMergedButProceed = true; + } + } + + // Determine width if cell does not participate in a merge or does and is a value cell of 1-column wide range + if (!$isMerged || $isMergedButProceed) { + // Calculated value + // To formatted string + $cellValue = NumberFormat::toFormattedString( + $cell->getCalculatedValue(), + $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode() + ); + + $autoSizes[$this->cellCollection->getCurrentColumn()] = max( + (float) $autoSizes[$this->cellCollection->getCurrentColumn()], + (float) Shared\Font::calculateColumnWidth( + $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(), + $cellValue, + $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(), + $this->getParent()->getDefaultStyle()->getFont() + ) + ); + } + } + } + + // adjust column widths + foreach ($autoSizes as $columnIndex => $width) { + if ($width == -1) { + $width = $this->getDefaultColumnDimension()->getWidth(); + } + $this->getColumnDimension($columnIndex)->setWidth($width); + } + } + + return $this; + } + + /** + * Get parent. + * + * @return Spreadsheet + */ + public function getParent() + { + return $this->parent; + } + + /** + * Re-bind parent. + * + * @param Spreadsheet $parent + * + * @return Worksheet + */ + public function rebindParent(Spreadsheet $parent) + { + if ($this->parent !== null) { + $namedRanges = $this->parent->getNamedRanges(); + foreach ($namedRanges as $namedRange) { + $parent->addNamedRange($namedRange); + } + + $this->parent->removeSheetByIndex( + $this->parent->getIndex($this) + ); + } + $this->parent = $parent; + + return $this; + } + + /** + * Get title. + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set title. + * + * @param string $pValue String containing the dimension of this worksheet + * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should + * be updated to reflect the new sheet name. + * This should be left as the default true, unless you are + * certain that no formula cells on any worksheet contain + * references to this worksheet + * @param bool $validate False to skip validation of new title. WARNING: This should only be set + * at parse time (by Readers), where titles can be assumed to be valid. + * + * @return Worksheet + */ + public function setTitle($pValue, $updateFormulaCellReferences = true, $validate = true) + { + // Is this a 'rename' or not? + if ($this->getTitle() == $pValue) { + return $this; + } + + // Old title + $oldTitle = $this->getTitle(); + + if ($validate) { + // Syntax check + self::checkSheetTitle($pValue); + + if ($this->parent) { + // Is there already such sheet name? + if ($this->parent->sheetNameExists($pValue)) { + // Use name, but append with lowest possible integer + + if (Shared\StringHelper::countCharacters($pValue) > 29) { + $pValue = Shared\StringHelper::substring($pValue, 0, 29); + } + $i = 1; + while ($this->parent->sheetNameExists($pValue . ' ' . $i)) { + ++$i; + if ($i == 10) { + if (Shared\StringHelper::countCharacters($pValue) > 28) { + $pValue = Shared\StringHelper::substring($pValue, 0, 28); + } + } elseif ($i == 100) { + if (Shared\StringHelper::countCharacters($pValue) > 27) { + $pValue = Shared\StringHelper::substring($pValue, 0, 27); + } + } + } + + $pValue .= " $i"; + } + } + } + + // Set title + $this->title = $pValue; + $this->dirty = true; + + if ($this->parent && $this->parent->getCalculationEngine()) { + // New title + $newTitle = $this->getTitle(); + $this->parent->getCalculationEngine() + ->renameCalculationCacheForWorksheet($oldTitle, $newTitle); + if ($updateFormulaCellReferences) { + ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle); + } + } + + return $this; + } + + /** + * Get sheet state. + * + * @return string Sheet state (visible, hidden, veryHidden) + */ + public function getSheetState() + { + return $this->sheetState; + } + + /** + * Set sheet state. + * + * @param string $value Sheet state (visible, hidden, veryHidden) + * + * @return Worksheet + */ + public function setSheetState($value) + { + $this->sheetState = $value; + + return $this; + } + + /** + * Get page setup. + * + * @return PageSetup + */ + public function getPageSetup() + { + return $this->pageSetup; + } + + /** + * Set page setup. + * + * @param PageSetup $pValue + * + * @return Worksheet + */ + public function setPageSetup(PageSetup $pValue) + { + $this->pageSetup = $pValue; + + return $this; + } + + /** + * Get page margins. + * + * @return PageMargins + */ + public function getPageMargins() + { + return $this->pageMargins; + } + + /** + * Set page margins. + * + * @param PageMargins $pValue + * + * @return Worksheet + */ + public function setPageMargins(PageMargins $pValue) + { + $this->pageMargins = $pValue; + + return $this; + } + + /** + * Get page header/footer. + * + * @return HeaderFooter + */ + public function getHeaderFooter() + { + return $this->headerFooter; + } + + /** + * Set page header/footer. + * + * @param HeaderFooter $pValue + * + * @return Worksheet + */ + public function setHeaderFooter(HeaderFooter $pValue) + { + $this->headerFooter = $pValue; + + return $this; + } + + /** + * Get sheet view. + * + * @return SheetView + */ + public function getSheetView() + { + return $this->sheetView; + } + + /** + * Set sheet view. + * + * @param SheetView $pValue + * + * @return Worksheet + */ + public function setSheetView(SheetView $pValue) + { + $this->sheetView = $pValue; + + return $this; + } + + /** + * Get Protection. + * + * @return Protection + */ + public function getProtection() + { + return $this->protection; + } + + /** + * Set Protection. + * + * @param Protection $pValue + * + * @return Worksheet + */ + public function setProtection(Protection $pValue) + { + $this->protection = $pValue; + $this->dirty = true; + + return $this; + } + + /** + * Get highest worksheet column. + * + * @param string $row Return the data highest column for the specified row, + * or the highest column of any row if no row number is passed + * + * @return string Highest column name + */ + public function getHighestColumn($row = null) + { + if ($row == null) { + return $this->cachedHighestColumn; + } + + return $this->getHighestDataColumn($row); + } + + /** + * Get highest worksheet column that contains data. + * + * @param string $row Return the highest data column for the specified row, + * or the highest data column of any row if no row number is passed + * + * @return string Highest column name that contains data + */ + public function getHighestDataColumn($row = null) + { + return $this->cellCollection->getHighestColumn($row); + } + + /** + * Get highest worksheet row. + * + * @param string $column Return the highest data row for the specified column, + * or the highest row of any column if no column letter is passed + * + * @return int Highest row number + */ + public function getHighestRow($column = null) + { + if ($column == null) { + return $this->cachedHighestRow; + } + + return $this->getHighestDataRow($column); + } + + /** + * Get highest worksheet row that contains data. + * + * @param string $column Return the highest data row for the specified column, + * or the highest data row of any column if no column letter is passed + * + * @return string Highest row number that contains data + */ + public function getHighestDataRow($column = null) + { + return $this->cellCollection->getHighestRow($column); + } + + /** + * Get highest worksheet column and highest row that have cell records. + * + * @return array Highest column name and highest row number + */ + public function getHighestRowAndColumn() + { + return $this->cellCollection->getHighestRowAndColumn(); + } + + /** + * Set a cell value. + * + * @param string $pCoordinate Coordinate of the cell, eg: 'A1' + * @param mixed $pValue Value of the cell + * + * @return Worksheet + */ + public function setCellValue($pCoordinate, $pValue) + { + $this->getCell($pCoordinate)->setValue($pValue); + + return $this; + } + + /** + * Set a cell value by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * @param mixed $value Value of the cell + * + * @return Worksheet + */ + public function setCellValueByColumnAndRow($columnIndex, $row, $value) + { + $this->getCellByColumnAndRow($columnIndex, $row)->setValue($value); + + return $this; + } + + /** + * Set a cell value. + * + * @param string $pCoordinate Coordinate of the cell, eg: 'A1' + * @param mixed $pValue Value of the cell + * @param string $pDataType Explicit data type, see DataType::TYPE_* + * + * @return Worksheet + */ + public function setCellValueExplicit($pCoordinate, $pValue, $pDataType) + { + // Set value + $this->getCell($pCoordinate)->setValueExplicit($pValue, $pDataType); + + return $this; + } + + /** + * Set a cell value by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * @param mixed $value Value of the cell + * @param string $dataType Explicit data type, see DataType::TYPE_* + * + * @return Worksheet + */ + public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType) + { + $this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType); + + return $this; + } + + /** + * Get cell at a specific coordinate. + * + * @param string $pCoordinate Coordinate of the cell, eg: 'A1' + * @param bool $createIfNotExists Flag indicating whether a new cell should be created if it doesn't + * already exist, or a null should be returned instead + * + * @throws Exception + * + * @return null|Cell Cell that was found/created or null + */ + public function getCell($pCoordinate, $createIfNotExists = true) + { + // Uppercase coordinate + $pCoordinateUpper = strtoupper($pCoordinate); + + // Check cell collection + if ($this->cellCollection->has($pCoordinateUpper)) { + return $this->cellCollection->get($pCoordinateUpper); + } + + // Worksheet reference? + if (strpos($pCoordinate, '!') !== false) { + $worksheetReference = self::extractSheetTitle($pCoordinate, true); + + return $this->parent->getSheetByName($worksheetReference[0])->getCell(strtoupper($worksheetReference[1]), $createIfNotExists); + } + + // Named range? + if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) && + (preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $pCoordinate, $matches))) { + $namedRange = NamedRange::resolveRange($pCoordinate, $this); + if ($namedRange !== null) { + $pCoordinate = $namedRange->getRange(); + + return $namedRange->getWorksheet()->getCell($pCoordinate, $createIfNotExists); + } + } + + if (Coordinate::coordinateIsRange($pCoordinate)) { + throw new Exception('Cell coordinate can not be a range of cells.'); + } elseif (strpos($pCoordinate, '$') !== false) { + throw new Exception('Cell coordinate must not be absolute.'); + } + + // Create new cell object, if required + return $createIfNotExists ? $this->createNewCell($pCoordinateUpper) : null; + } + + /** + * Get cell at a specific coordinate by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * @param bool $createIfNotExists Flag indicating whether a new cell should be created if it doesn't + * already exist, or a null should be returned instead + * + * @return null|Cell Cell that was found/created or null + */ + public function getCellByColumnAndRow($columnIndex, $row, $createIfNotExists = true) + { + $columnLetter = Coordinate::stringFromColumnIndex($columnIndex); + $coordinate = $columnLetter . $row; + + if ($this->cellCollection->has($coordinate)) { + return $this->cellCollection->get($coordinate); + } + + // Create new cell object, if required + return $createIfNotExists ? $this->createNewCell($coordinate) : null; + } + + /** + * Create a new cell at the specified coordinate. + * + * @param string $pCoordinate Coordinate of the cell + * + * @return Cell Cell that was created + */ + private function createNewCell($pCoordinate) + { + $cell = new Cell(null, DataType::TYPE_NULL, $this); + $this->cellCollection->add($pCoordinate, $cell); + $this->cellCollectionIsSorted = false; + + // Coordinates + $aCoordinates = Coordinate::coordinateFromString($pCoordinate); + if (Coordinate::columnIndexFromString($this->cachedHighestColumn) < Coordinate::columnIndexFromString($aCoordinates[0])) { + $this->cachedHighestColumn = $aCoordinates[0]; + } + if ($aCoordinates[1] > $this->cachedHighestRow) { + $this->cachedHighestRow = $aCoordinates[1]; + } + + // Cell needs appropriate xfIndex from dimensions records + // but don't create dimension records if they don't already exist + $rowDimension = $this->getRowDimension($aCoordinates[1], false); + $columnDimension = $this->getColumnDimension($aCoordinates[0], false); + + if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) { + // then there is a row dimension with explicit style, assign it to the cell + $cell->setXfIndex($rowDimension->getXfIndex()); + } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) { + // then there is a column dimension, assign it to the cell + $cell->setXfIndex($columnDimension->getXfIndex()); + } + + return $cell; + } + + /** + * Does the cell at a specific coordinate exist? + * + * @param string $pCoordinate Coordinate of the cell eg: 'A1' + * + * @throws Exception + * + * @return bool + */ + public function cellExists($pCoordinate) + { + // Worksheet reference? + if (strpos($pCoordinate, '!') !== false) { + $worksheetReference = self::extractSheetTitle($pCoordinate, true); + + return $this->parent->getSheetByName($worksheetReference[0])->cellExists(strtoupper($worksheetReference[1])); + } + + // Named range? + if ((!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) && + (preg_match('/^' . Calculation::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $pCoordinate, $matches))) { + $namedRange = NamedRange::resolveRange($pCoordinate, $this); + if ($namedRange !== null) { + $pCoordinate = $namedRange->getRange(); + if ($this->getHashCode() != $namedRange->getWorksheet()->getHashCode()) { + if (!$namedRange->getLocalOnly()) { + return $namedRange->getWorksheet()->cellExists($pCoordinate); + } + + throw new Exception('Named range ' . $namedRange->getName() . ' is not accessible from within sheet ' . $this->getTitle()); + } + } else { + return false; + } + } + + // Uppercase coordinate + $pCoordinate = strtoupper($pCoordinate); + + if (Coordinate::coordinateIsRange($pCoordinate)) { + throw new Exception('Cell coordinate can not be a range of cells.'); + } elseif (strpos($pCoordinate, '$') !== false) { + throw new Exception('Cell coordinate must not be absolute.'); + } + + // Cell exists? + return $this->cellCollection->has($pCoordinate); + } + + /** + * Cell at a specific coordinate by using numeric cell coordinates exists? + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * + * @return bool + */ + public function cellExistsByColumnAndRow($columnIndex, $row) + { + return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row); + } + + /** + * Get row dimension at a specific row. + * + * @param int $pRow Numeric index of the row + * @param bool $create + * + * @return RowDimension + */ + public function getRowDimension($pRow, $create = true) + { + // Found + $found = null; + + // Get row dimension + if (!isset($this->rowDimensions[$pRow])) { + if (!$create) { + return null; + } + $this->rowDimensions[$pRow] = new RowDimension($pRow); + + $this->cachedHighestRow = max($this->cachedHighestRow, $pRow); + } + + return $this->rowDimensions[$pRow]; + } + + /** + * Get column dimension at a specific column. + * + * @param string $pColumn String index of the column eg: 'A' + * @param bool $create + * + * @return ColumnDimension + */ + public function getColumnDimension($pColumn, $create = true) + { + // Uppercase coordinate + $pColumn = strtoupper($pColumn); + + // Fetch dimensions + if (!isset($this->columnDimensions[$pColumn])) { + if (!$create) { + return null; + } + $this->columnDimensions[$pColumn] = new ColumnDimension($pColumn); + + if (Coordinate::columnIndexFromString($this->cachedHighestColumn) < Coordinate::columnIndexFromString($pColumn)) { + $this->cachedHighestColumn = $pColumn; + } + } + + return $this->columnDimensions[$pColumn]; + } + + /** + * Get column dimension at a specific column by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * + * @return ColumnDimension + */ + public function getColumnDimensionByColumn($columnIndex) + { + return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex)); + } + + /** + * Get styles. + * + * @return Style[] + */ + public function getStyles() + { + return $this->styles; + } + + /** + * Get style for cell. + * + * @param string $pCellCoordinate Cell coordinate (or range) to get style for, eg: 'A1' + * + * @throws Exception + * + * @return Style + */ + public function getStyle($pCellCoordinate) + { + // set this sheet as active + $this->parent->setActiveSheetIndex($this->parent->getIndex($this)); + + // set cell coordinate as active + $this->setSelectedCells(strtoupper($pCellCoordinate)); + + return $this->parent->getCellXfSupervisor(); + } + + /** + * Get conditional styles for a cell. + * + * @param string $pCoordinate eg: 'A1' + * + * @return Conditional[] + */ + public function getConditionalStyles($pCoordinate) + { + $pCoordinate = strtoupper($pCoordinate); + if (!isset($this->conditionalStylesCollection[$pCoordinate])) { + $this->conditionalStylesCollection[$pCoordinate] = []; + } + + return $this->conditionalStylesCollection[$pCoordinate]; + } + + /** + * Do conditional styles exist for this cell? + * + * @param string $pCoordinate eg: 'A1' + * + * @return bool + */ + public function conditionalStylesExists($pCoordinate) + { + return isset($this->conditionalStylesCollection[strtoupper($pCoordinate)]); + } + + /** + * Removes conditional styles for a cell. + * + * @param string $pCoordinate eg: 'A1' + * + * @return Worksheet + */ + public function removeConditionalStyles($pCoordinate) + { + unset($this->conditionalStylesCollection[strtoupper($pCoordinate)]); + + return $this; + } + + /** + * Get collection of conditional styles. + * + * @return array + */ + public function getConditionalStylesCollection() + { + return $this->conditionalStylesCollection; + } + + /** + * Set conditional styles. + * + * @param string $pCoordinate eg: 'A1' + * @param $pValue Conditional[] + * + * @return Worksheet + */ + public function setConditionalStyles($pCoordinate, $pValue) + { + $this->conditionalStylesCollection[strtoupper($pCoordinate)] = $pValue; + + return $this; + } + + /** + * Get style for cell by using numeric cell coordinates. + * + * @param int $columnIndex1 Numeric column coordinate of the cell + * @param int $row1 Numeric row coordinate of the cell + * @param null|int $columnIndex2 Numeric column coordinate of the range cell + * @param null|int $row2 Numeric row coordinate of the range cell + * + * @return Style + */ + public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null) + { + if ($columnIndex2 !== null && $row2 !== null) { + $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + + return $this->getStyle($cellRange); + } + + return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1); + } + + /** + * Duplicate cell style to a range of cells. + * + * Please note that this will overwrite existing cell styles for cells in range! + * + * @param Style $pCellStyle Cell style to duplicate + * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1") + * + * @throws Exception + * + * @return Worksheet + */ + public function duplicateStyle(Style $pCellStyle, $pRange) + { + // Add the style to the workbook if necessary + $workbook = $this->parent; + if ($existingStyle = $this->parent->getCellXfByHashCode($pCellStyle->getHashCode())) { + // there is already such cell Xf in our collection + $xfIndex = $existingStyle->getIndex(); + } else { + // we don't have such a cell Xf, need to add + $workbook->addCellXf($pCellStyle); + $xfIndex = $pCellStyle->getIndex(); + } + + // Calculate range outer borders + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($pRange . ':' . $pRange); + + // Make sure we can loop upwards on rows and columns + if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) { + $tmp = $rangeStart; + $rangeStart = $rangeEnd; + $rangeEnd = $tmp; + } + + // Loop through cells and apply styles + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex); + } + } + + return $this; + } + + /** + * Duplicate conditional style to a range of cells. + * + * Please note that this will overwrite existing cell styles for cells in range! + * + * @param Conditional[] $pCellStyle Cell style to duplicate + * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1") + * + * @throws Exception + * + * @return Worksheet + */ + public function duplicateConditionalStyle(array $pCellStyle, $pRange = '') + { + foreach ($pCellStyle as $cellStyle) { + if (!($cellStyle instanceof Conditional)) { + throw new Exception('Style is not a conditional style'); + } + } + + // Calculate range outer borders + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($pRange . ':' . $pRange); + + // Make sure we can loop upwards on rows and columns + if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) { + $tmp = $rangeStart; + $rangeStart = $rangeEnd; + $rangeEnd = $tmp; + } + + // Loop through cells and apply styles + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $pCellStyle); + } + } + + return $this; + } + + /** + * Set break on a cell. + * + * @param string $pCoordinate Cell coordinate (e.g. A1) + * @param int $pBreak Break type (type of Worksheet::BREAK_*) + * + * @throws Exception + * + * @return Worksheet + */ + public function setBreak($pCoordinate, $pBreak) + { + // Uppercase coordinate + $pCoordinate = strtoupper($pCoordinate); + + if ($pCoordinate != '') { + if ($pBreak == self::BREAK_NONE) { + if (isset($this->breaks[$pCoordinate])) { + unset($this->breaks[$pCoordinate]); + } + } else { + $this->breaks[$pCoordinate] = $pBreak; + } + } else { + throw new Exception('No cell coordinate specified.'); + } + + return $this; + } + + /** + * Set break on a cell by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * @param int $break Break type (type of Worksheet::BREAK_*) + * + * @return Worksheet + */ + public function setBreakByColumnAndRow($columnIndex, $row, $break) + { + return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break); + } + + /** + * Get breaks. + * + * @return array[] + */ + public function getBreaks() + { + return $this->breaks; + } + + /** + * Set merge on a cell range. + * + * @param string $pRange Cell range (e.g. A1:E1) + * + * @throws Exception + * + * @return Worksheet + */ + public function mergeCells($pRange) + { + // Uppercase coordinate + $pRange = strtoupper($pRange); + + if (strpos($pRange, ':') !== false) { + $this->mergeCells[$pRange] = $pRange; + + // make sure cells are created + + // get the cells in the range + $aReferences = Coordinate::extractAllCellReferencesInRange($pRange); + + // create upper left cell if it does not already exist + $upperLeft = $aReferences[0]; + if (!$this->cellExists($upperLeft)) { + $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); + } + + // Blank out the rest of the cells in the range (if they exist) + $count = count($aReferences); + for ($i = 1; $i < $count; ++$i) { + if ($this->cellExists($aReferences[$i])) { + $this->getCell($aReferences[$i])->setValueExplicit(null, DataType::TYPE_NULL); + } + } + } else { + throw new Exception('Merge must be set on a range of cells.'); + } + + return $this; + } + + /** + * Set merge on a cell range by using numeric cell coordinates. + * + * @param int $columnIndex1 Numeric column coordinate of the first cell + * @param int $row1 Numeric row coordinate of the first cell + * @param int $columnIndex2 Numeric column coordinate of the last cell + * @param int $row2 Numeric row coordinate of the last cell + * + * @throws Exception + * + * @return Worksheet + */ + public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + { + $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + + return $this->mergeCells($cellRange); + } + + /** + * Remove merge on a cell range. + * + * @param string $pRange Cell range (e.g. A1:E1) + * + * @throws Exception + * + * @return Worksheet + */ + public function unmergeCells($pRange) + { + // Uppercase coordinate + $pRange = strtoupper($pRange); + + if (strpos($pRange, ':') !== false) { + if (isset($this->mergeCells[$pRange])) { + unset($this->mergeCells[$pRange]); + } else { + throw new Exception('Cell range ' . $pRange . ' not known as merged.'); + } + } else { + throw new Exception('Merge can only be removed from a range of cells.'); + } + + return $this; + } + + /** + * Remove merge on a cell range by using numeric cell coordinates. + * + * @param int $columnIndex1 Numeric column coordinate of the first cell + * @param int $row1 Numeric row coordinate of the first cell + * @param int $columnIndex2 Numeric column coordinate of the last cell + * @param int $row2 Numeric row coordinate of the last cell + * + * @throws Exception + * + * @return Worksheet + */ + public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + { + $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + + return $this->unmergeCells($cellRange); + } + + /** + * Get merge cells array. + * + * @return array[] + */ + public function getMergeCells() + { + return $this->mergeCells; + } + + /** + * Set merge cells array for the entire sheet. Use instead mergeCells() to merge + * a single cell range. + * + * @param array $pValue + * + * @return Worksheet + */ + public function setMergeCells(array $pValue) + { + $this->mergeCells = $pValue; + + return $this; + } + + /** + * Set protection on a cell range. + * + * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1) + * @param string $pPassword Password to unlock the protection + * @param bool $pAlreadyHashed If the password has already been hashed, set this to true + * + * @return Worksheet + */ + public function protectCells($pRange, $pPassword, $pAlreadyHashed = false) + { + // Uppercase coordinate + $pRange = strtoupper($pRange); + + if (!$pAlreadyHashed) { + $pPassword = Shared\PasswordHasher::hashPassword($pPassword); + } + $this->protectedCells[$pRange] = $pPassword; + + return $this; + } + + /** + * Set protection on a cell range by using numeric cell coordinates. + * + * @param int $columnIndex1 Numeric column coordinate of the first cell + * @param int $row1 Numeric row coordinate of the first cell + * @param int $columnIndex2 Numeric column coordinate of the last cell + * @param int $row2 Numeric row coordinate of the last cell + * @param string $password Password to unlock the protection + * @param bool $alreadyHashed If the password has already been hashed, set this to true + * + * @return Worksheet + */ + public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false) + { + $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + + return $this->protectCells($cellRange, $password, $alreadyHashed); + } + + /** + * Remove protection on a cell range. + * + * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1) + * + * @throws Exception + * + * @return Worksheet + */ + public function unprotectCells($pRange) + { + // Uppercase coordinate + $pRange = strtoupper($pRange); + + if (isset($this->protectedCells[$pRange])) { + unset($this->protectedCells[$pRange]); + } else { + throw new Exception('Cell range ' . $pRange . ' not known as protected.'); + } + + return $this; + } + + /** + * Remove protection on a cell range by using numeric cell coordinates. + * + * @param int $columnIndex1 Numeric column coordinate of the first cell + * @param int $row1 Numeric row coordinate of the first cell + * @param int $columnIndex2 Numeric column coordinate of the last cell + * @param int $row2 Numeric row coordinate of the last cell + * + * @throws Exception + * + * @return Worksheet + */ + public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + { + $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + + return $this->unprotectCells($cellRange); + } + + /** + * Get protected cells. + * + * @return array[] + */ + public function getProtectedCells() + { + return $this->protectedCells; + } + + /** + * Get Autofilter. + * + * @return AutoFilter + */ + public function getAutoFilter() + { + return $this->autoFilter; + } + + /** + * Set AutoFilter. + * + * @param AutoFilter|string $pValue + * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility + * + * @throws Exception + * + * @return Worksheet + */ + public function setAutoFilter($pValue) + { + if (is_string($pValue)) { + $this->autoFilter->setRange($pValue); + } elseif (is_object($pValue) && ($pValue instanceof AutoFilter)) { + $this->autoFilter = $pValue; + } + + return $this; + } + + /** + * Set Autofilter Range by using numeric cell coordinates. + * + * @param int $columnIndex1 Numeric column coordinate of the first cell + * @param int $row1 Numeric row coordinate of the first cell + * @param int $columnIndex2 Numeric column coordinate of the second cell + * @param int $row2 Numeric row coordinate of the second cell + * + * @throws Exception + * + * @return Worksheet + */ + public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + { + return $this->setAutoFilter( + Coordinate::stringFromColumnIndex($columnIndex1) . $row1 + . ':' . + Coordinate::stringFromColumnIndex($columnIndex2) . $row2 + ); + } + + /** + * Remove autofilter. + * + * @return Worksheet + */ + public function removeAutoFilter() + { + $this->autoFilter->setRange(null); + + return $this; + } + + /** + * Get Freeze Pane. + * + * @return string + */ + public function getFreezePane() + { + return $this->freezePane; + } + + /** + * Freeze Pane. + * + * Examples: + * + * - A2 will freeze the rows above cell A2 (i.e row 1) + * - B1 will freeze the columns to the left of cell B1 (i.e column A) + * - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A) + * + * @param null|string $cell Position of the split + * @param null|string $topLeftCell default position of the right bottom pane + * + * @throws Exception + * + * @return Worksheet + */ + public function freezePane($cell, $topLeftCell = null) + { + if (is_string($cell) && Coordinate::coordinateIsRange($cell)) { + throw new Exception('Freeze pane can not be set on a range of cells.'); + } + + if ($cell !== null && $topLeftCell === null) { + $coordinate = Coordinate::coordinateFromString($cell); + $topLeftCell = $coordinate[0] . $coordinate[1]; + } + + $this->freezePane = $cell; + $this->topLeftCell = $topLeftCell; + + return $this; + } + + /** + * Freeze Pane by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * + * @return Worksheet + */ + public function freezePaneByColumnAndRow($columnIndex, $row) + { + return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row); + } + + /** + * Unfreeze Pane. + * + * @return Worksheet + */ + public function unfreezePane() + { + return $this->freezePane(null); + } + + /** + * Get the default position of the right bottom pane. + * + * @return int + */ + public function getTopLeftCell() + { + return $this->topLeftCell; + } + + /** + * Insert a new row, updating all possible related data. + * + * @param int $pBefore Insert before this one + * @param int $pNumRows Number of rows to insert + * + * @throws Exception + * + * @return Worksheet + */ + public function insertNewRowBefore($pBefore, $pNumRows = 1) + { + if ($pBefore >= 1) { + $objReferenceHelper = ReferenceHelper::getInstance(); + $objReferenceHelper->insertNewBefore('A' . $pBefore, 0, $pNumRows, $this); + } else { + throw new Exception('Rows can only be inserted before at least row 1.'); + } + + return $this; + } + + /** + * Insert a new column, updating all possible related data. + * + * @param int $pBefore Insert before this one, eg: 'A' + * @param int $pNumCols Number of columns to insert + * + * @throws Exception + * + * @return Worksheet + */ + public function insertNewColumnBefore($pBefore, $pNumCols = 1) + { + if (!is_numeric($pBefore)) { + $objReferenceHelper = ReferenceHelper::getInstance(); + $objReferenceHelper->insertNewBefore($pBefore . '1', $pNumCols, 0, $this); + } else { + throw new Exception('Column references should not be numeric.'); + } + + return $this; + } + + /** + * Insert a new column, updating all possible related data. + * + * @param int $beforeColumnIndex Insert before this one (numeric column coordinate of the cell) + * @param int $pNumCols Number of columns to insert + * + * @throws Exception + * + * @return Worksheet + */ + public function insertNewColumnBeforeByIndex($beforeColumnIndex, $pNumCols = 1) + { + if ($beforeColumnIndex >= 1) { + return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $pNumCols); + } + + throw new Exception('Columns can only be inserted before at least column A (1).'); + } + + /** + * Delete a row, updating all possible related data. + * + * @param int $pRow Remove starting with this one + * @param int $pNumRows Number of rows to remove + * + * @throws Exception + * + * @return Worksheet + */ + public function removeRow($pRow, $pNumRows = 1) + { + if ($pRow >= 1) { + $highestRow = $this->getHighestDataRow(); + $objReferenceHelper = ReferenceHelper::getInstance(); + $objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this); + for ($r = 0; $r < $pNumRows; ++$r) { + $this->getCellCollection()->removeRow($highestRow); + --$highestRow; + } + } else { + throw new Exception('Rows to be deleted should at least start from row 1.'); + } + + return $this; + } + + /** + * Remove a column, updating all possible related data. + * + * @param string $pColumn Remove starting with this one, eg: 'A' + * @param int $pNumCols Number of columns to remove + * + * @throws Exception + * + * @return Worksheet + */ + public function removeColumn($pColumn, $pNumCols = 1) + { + if (!is_numeric($pColumn)) { + $highestColumn = $this->getHighestDataColumn(); + $pColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($pColumn) + $pNumCols); + $objReferenceHelper = ReferenceHelper::getInstance(); + $objReferenceHelper->insertNewBefore($pColumn . '1', -$pNumCols, 0, $this); + for ($c = 0; $c < $pNumCols; ++$c) { + $this->getCellCollection()->removeColumn($highestColumn); + $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1); + } + } else { + throw new Exception('Column references should not be numeric.'); + } + + return $this; + } + + /** + * Remove a column, updating all possible related data. + * + * @param int $columnIndex Remove starting with this one (numeric column coordinate of the cell) + * @param int $numColumns Number of columns to remove + * + * @throws Exception + * + * @return Worksheet + */ + public function removeColumnByIndex($columnIndex, $numColumns = 1) + { + if ($columnIndex >= 1) { + return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns); + } + + throw new Exception('Columns to be deleted should at least start from column A (1)'); + } + + /** + * Show gridlines? + * + * @return bool + */ + public function getShowGridlines() + { + return $this->showGridlines; + } + + /** + * Set show gridlines. + * + * @param bool $pValue Show gridlines (true/false) + * + * @return Worksheet + */ + public function setShowGridlines($pValue) + { + $this->showGridlines = $pValue; + + return $this; + } + + /** + * Print gridlines? + * + * @return bool + */ + public function getPrintGridlines() + { + return $this->printGridlines; + } + + /** + * Set print gridlines. + * + * @param bool $pValue Print gridlines (true/false) + * + * @return Worksheet + */ + public function setPrintGridlines($pValue) + { + $this->printGridlines = $pValue; + + return $this; + } + + /** + * Show row and column headers? + * + * @return bool + */ + public function getShowRowColHeaders() + { + return $this->showRowColHeaders; + } + + /** + * Set show row and column headers. + * + * @param bool $pValue Show row and column headers (true/false) + * + * @return Worksheet + */ + public function setShowRowColHeaders($pValue) + { + $this->showRowColHeaders = $pValue; + + return $this; + } + + /** + * Show summary below? (Row/Column outlining). + * + * @return bool + */ + public function getShowSummaryBelow() + { + return $this->showSummaryBelow; + } + + /** + * Set show summary below. + * + * @param bool $pValue Show summary below (true/false) + * + * @return Worksheet + */ + public function setShowSummaryBelow($pValue) + { + $this->showSummaryBelow = $pValue; + + return $this; + } + + /** + * Show summary right? (Row/Column outlining). + * + * @return bool + */ + public function getShowSummaryRight() + { + return $this->showSummaryRight; + } + + /** + * Set show summary right. + * + * @param bool $pValue Show summary right (true/false) + * + * @return Worksheet + */ + public function setShowSummaryRight($pValue) + { + $this->showSummaryRight = $pValue; + + return $this; + } + + /** + * Get comments. + * + * @return Comment[] + */ + public function getComments() + { + return $this->comments; + } + + /** + * Set comments array for the entire sheet. + * + * @param Comment[] $pValue + * + * @return Worksheet + */ + public function setComments(array $pValue) + { + $this->comments = $pValue; + + return $this; + } + + /** + * Get comment for cell. + * + * @param string $pCellCoordinate Cell coordinate to get comment for, eg: 'A1' + * + * @throws Exception + * + * @return Comment + */ + public function getComment($pCellCoordinate) + { + // Uppercase coordinate + $pCellCoordinate = strtoupper($pCellCoordinate); + + if (Coordinate::coordinateIsRange($pCellCoordinate)) { + throw new Exception('Cell coordinate string can not be a range of cells.'); + } elseif (strpos($pCellCoordinate, '$') !== false) { + throw new Exception('Cell coordinate string must not be absolute.'); + } elseif ($pCellCoordinate == '') { + throw new Exception('Cell coordinate can not be zero-length string.'); + } + + // Check if we already have a comment for this cell. + if (isset($this->comments[$pCellCoordinate])) { + return $this->comments[$pCellCoordinate]; + } + + // If not, create a new comment. + $newComment = new Comment(); + $this->comments[$pCellCoordinate] = $newComment; + + return $newComment; + } + + /** + * Get comment for cell by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * + * @return Comment + */ + public function getCommentByColumnAndRow($columnIndex, $row) + { + return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row); + } + + /** + * Get active cell. + * + * @return string Example: 'A1' + */ + public function getActiveCell() + { + return $this->activeCell; + } + + /** + * Get selected cells. + * + * @return string + */ + public function getSelectedCells() + { + return $this->selectedCells; + } + + /** + * Selected cell. + * + * @param string $pCoordinate Cell (i.e. A1) + * + * @return Worksheet + */ + public function setSelectedCell($pCoordinate) + { + return $this->setSelectedCells($pCoordinate); + } + + /** + * Select a range of cells. + * + * @param string $pCoordinate Cell range, examples: 'A1', 'B2:G5', 'A:C', '3:6' + * + * @return Worksheet + */ + public function setSelectedCells($pCoordinate) + { + // Uppercase coordinate + $pCoordinate = strtoupper($pCoordinate); + + // Convert 'A' to 'A:A' + $pCoordinate = preg_replace('/^([A-Z]+)$/', '${1}:${1}', $pCoordinate); + + // Convert '1' to '1:1' + $pCoordinate = preg_replace('/^(\d+)$/', '${1}:${1}', $pCoordinate); + + // Convert 'A:C' to 'A1:C1048576' + $pCoordinate = preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $pCoordinate); + + // Convert '1:3' to 'A1:XFD3' + $pCoordinate = preg_replace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $pCoordinate); + + if (Coordinate::coordinateIsRange($pCoordinate)) { + list($first) = Coordinate::splitRange($pCoordinate); + $this->activeCell = $first[0]; + } else { + $this->activeCell = $pCoordinate; + } + $this->selectedCells = $pCoordinate; + + return $this; + } + + /** + * Selected cell by using numeric cell coordinates. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * + * @throws Exception + * + * @return Worksheet + */ + public function setSelectedCellByColumnAndRow($columnIndex, $row) + { + return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row); + } + + /** + * Get right-to-left. + * + * @return bool + */ + public function getRightToLeft() + { + return $this->rightToLeft; + } + + /** + * Set right-to-left. + * + * @param bool $value Right-to-left true/false + * + * @return Worksheet + */ + public function setRightToLeft($value) + { + $this->rightToLeft = $value; + + return $this; + } + + /** + * Fill worksheet from values in array. + * + * @param array $source Source array + * @param mixed $nullValue Value in source array that stands for blank cell + * @param string $startCell Insert array starting from this cell address as the top left coordinate + * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array + * + * @throws Exception + * + * @return Worksheet + */ + public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false) + { + // Convert a 1-D array to 2-D (for ease of looping) + if (!is_array(end($source))) { + $source = [$source]; + } + + // start coordinate + list($startColumn, $startRow) = Coordinate::coordinateFromString($startCell); + + // Loop through $source + foreach ($source as $rowData) { + $currentColumn = $startColumn; + foreach ($rowData as $cellValue) { + if ($strictNullComparison) { + if ($cellValue !== $nullValue) { + // Set cell value + $this->getCell($currentColumn . $startRow)->setValue($cellValue); + } + } else { + if ($cellValue != $nullValue) { + // Set cell value + $this->getCell($currentColumn . $startRow)->setValue($cellValue); + } + } + ++$currentColumn; + } + ++$startRow; + } + + return $this; + } + + /** + * Create array from a range of cells. + * + * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1") + * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist + * @param bool $calculateFormulas Should formulas be calculated? + * @param bool $formatData Should formatting be applied to cell values? + * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero + * True - Return rows and columns indexed by their actual row and column IDs + * + * @return array + */ + public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false) + { + // Returnvalue + $returnValue = []; + // Identify the range that we need to extract from the worksheet + list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($pRange); + $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]); + $minRow = $rangeStart[1]; + $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]); + $maxRow = $rangeEnd[1]; + + ++$maxCol; + // Loop through rows + $r = -1; + for ($row = $minRow; $row <= $maxRow; ++$row) { + $rRef = ($returnCellRef) ? $row : ++$r; + $c = -1; + // Loop through columns in the current row + for ($col = $minCol; $col != $maxCol; ++$col) { + $cRef = ($returnCellRef) ? $col : ++$c; + // Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen + // so we test and retrieve directly against cellCollection + if ($this->cellCollection->has($col . $row)) { + // Cell exists + $cell = $this->cellCollection->get($col . $row); + if ($cell->getValue() !== null) { + if ($cell->getValue() instanceof RichText) { + $returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText(); + } else { + if ($calculateFormulas) { + $returnValue[$rRef][$cRef] = $cell->getCalculatedValue(); + } else { + $returnValue[$rRef][$cRef] = $cell->getValue(); + } + } + + if ($formatData) { + $style = $this->parent->getCellXfByIndex($cell->getXfIndex()); + $returnValue[$rRef][$cRef] = NumberFormat::toFormattedString( + $returnValue[$rRef][$cRef], + ($style && $style->getNumberFormat()) ? $style->getNumberFormat()->getFormatCode() : NumberFormat::FORMAT_GENERAL + ); + } + } else { + // Cell holds a NULL + $returnValue[$rRef][$cRef] = $nullValue; + } + } else { + // Cell doesn't exist + $returnValue[$rRef][$cRef] = $nullValue; + } + } + } + + // Return + return $returnValue; + } + + /** + * Create array from a range of cells. + * + * @param string $pNamedRange Name of the Named Range + * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist + * @param bool $calculateFormulas Should formulas be calculated? + * @param bool $formatData Should formatting be applied to cell values? + * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero + * True - Return rows and columns indexed by their actual row and column IDs + * + * @throws Exception + * + * @return array + */ + public function namedRangeToArray($pNamedRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false) + { + $namedRange = NamedRange::resolveRange($pNamedRange, $this); + if ($namedRange !== null) { + $pWorkSheet = $namedRange->getWorksheet(); + $pCellRange = $namedRange->getRange(); + + return $pWorkSheet->rangeToArray($pCellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef); + } + + throw new Exception('Named Range ' . $pNamedRange . ' does not exist.'); + } + + /** + * Create array from worksheet. + * + * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist + * @param bool $calculateFormulas Should formulas be calculated? + * @param bool $formatData Should formatting be applied to cell values? + * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero + * True - Return rows and columns indexed by their actual row and column IDs + * + * @return array + */ + public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false) + { + // Garbage collect... + $this->garbageCollect(); + + // Identify the range that we need to extract from the worksheet + $maxCol = $this->getHighestColumn(); + $maxRow = $this->getHighestRow(); + + // Return + return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef); + } + + /** + * Get row iterator. + * + * @param int $startRow The row number at which to start iterating + * @param int $endRow The row number at which to stop iterating + * + * @return RowIterator + */ + public function getRowIterator($startRow = 1, $endRow = null) + { + return new RowIterator($this, $startRow, $endRow); + } + + /** + * Get column iterator. + * + * @param string $startColumn The column address at which to start iterating + * @param string $endColumn The column address at which to stop iterating + * + * @return ColumnIterator + */ + public function getColumnIterator($startColumn = 'A', $endColumn = null) + { + return new ColumnIterator($this, $startColumn, $endColumn); + } + + /** + * Run PhpSpreadsheet garbage collector. + * + * @return Worksheet + */ + public function garbageCollect() + { + // Flush cache + $this->cellCollection->get('A1'); + + // Lookup highest column and highest row if cells are cleaned + $colRow = $this->cellCollection->getHighestRowAndColumn(); + $highestRow = $colRow['row']; + $highestColumn = Coordinate::columnIndexFromString($colRow['column']); + + // Loop through column dimensions + foreach ($this->columnDimensions as $dimension) { + $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex())); + } + + // Loop through row dimensions + foreach ($this->rowDimensions as $dimension) { + $highestRow = max($highestRow, $dimension->getRowIndex()); + } + + // Cache values + if ($highestColumn < 1) { + $this->cachedHighestColumn = 'A'; + } else { + $this->cachedHighestColumn = Coordinate::stringFromColumnIndex($highestColumn); + } + $this->cachedHighestRow = $highestRow; + + // Return + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->dirty) { + $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__); + $this->dirty = false; + } + + return $this->hash; + } + + /** + * Extract worksheet title from range. + * + * Example: extractSheetTitle("testSheet!A1") ==> 'A1' + * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1']; + * + * @param string $pRange Range to extract title from + * @param bool $returnRange Return range? (see example) + * + * @return mixed + */ + public static function extractSheetTitle($pRange, $returnRange = false) + { + // Sheet title included? + if (($sep = strrpos($pRange, '!')) === false) { + return $returnRange ? ['', $pRange] : ''; + } + + if ($returnRange) { + return [substr($pRange, 0, $sep), substr($pRange, $sep + 1)]; + } + + return substr($pRange, $sep + 1); + } + + /** + * Get hyperlink. + * + * @param string $pCellCoordinate Cell coordinate to get hyperlink for, eg: 'A1' + * + * @return Hyperlink + */ + public function getHyperlink($pCellCoordinate) + { + // return hyperlink if we already have one + if (isset($this->hyperlinkCollection[$pCellCoordinate])) { + return $this->hyperlinkCollection[$pCellCoordinate]; + } + + // else create hyperlink + $this->hyperlinkCollection[$pCellCoordinate] = new Hyperlink(); + + return $this->hyperlinkCollection[$pCellCoordinate]; + } + + /** + * Set hyperlink. + * + * @param string $pCellCoordinate Cell coordinate to insert hyperlink, eg: 'A1' + * @param null|Hyperlink $pHyperlink + * + * @return Worksheet + */ + public function setHyperlink($pCellCoordinate, Hyperlink $pHyperlink = null) + { + if ($pHyperlink === null) { + unset($this->hyperlinkCollection[$pCellCoordinate]); + } else { + $this->hyperlinkCollection[$pCellCoordinate] = $pHyperlink; + } + + return $this; + } + + /** + * Hyperlink at a specific coordinate exists? + * + * @param string $pCoordinate eg: 'A1' + * + * @return bool + */ + public function hyperlinkExists($pCoordinate) + { + return isset($this->hyperlinkCollection[$pCoordinate]); + } + + /** + * Get collection of hyperlinks. + * + * @return Hyperlink[] + */ + public function getHyperlinkCollection() + { + return $this->hyperlinkCollection; + } + + /** + * Get data validation. + * + * @param string $pCellCoordinate Cell coordinate to get data validation for, eg: 'A1' + * + * @return DataValidation + */ + public function getDataValidation($pCellCoordinate) + { + // return data validation if we already have one + if (isset($this->dataValidationCollection[$pCellCoordinate])) { + return $this->dataValidationCollection[$pCellCoordinate]; + } + + // else create data validation + $this->dataValidationCollection[$pCellCoordinate] = new DataValidation(); + + return $this->dataValidationCollection[$pCellCoordinate]; + } + + /** + * Set data validation. + * + * @param string $pCellCoordinate Cell coordinate to insert data validation, eg: 'A1' + * @param null|DataValidation $pDataValidation + * + * @return Worksheet + */ + public function setDataValidation($pCellCoordinate, DataValidation $pDataValidation = null) + { + if ($pDataValidation === null) { + unset($this->dataValidationCollection[$pCellCoordinate]); + } else { + $this->dataValidationCollection[$pCellCoordinate] = $pDataValidation; + } + + return $this; + } + + /** + * Data validation at a specific coordinate exists? + * + * @param string $pCoordinate eg: 'A1' + * + * @return bool + */ + public function dataValidationExists($pCoordinate) + { + return isset($this->dataValidationCollection[$pCoordinate]); + } + + /** + * Get collection of data validations. + * + * @return DataValidation[] + */ + public function getDataValidationCollection() + { + return $this->dataValidationCollection; + } + + /** + * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet. + * + * @param string $range + * + * @return string Adjusted range value + */ + public function shrinkRangeToFit($range) + { + $maxCol = $this->getHighestColumn(); + $maxRow = $this->getHighestRow(); + $maxCol = Coordinate::columnIndexFromString($maxCol); + + $rangeBlocks = explode(' ', $range); + foreach ($rangeBlocks as &$rangeSet) { + $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet); + + if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) { + $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol); + } + if ($rangeBoundaries[0][1] > $maxRow) { + $rangeBoundaries[0][1] = $maxRow; + } + if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) { + $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol); + } + if ($rangeBoundaries[1][1] > $maxRow) { + $rangeBoundaries[1][1] = $maxRow; + } + $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1]; + } + unset($rangeSet); + $stRange = implode(' ', $rangeBlocks); + + return $stRange; + } + + /** + * Get tab color. + * + * @return Color + */ + public function getTabColor() + { + if ($this->tabColor === null) { + $this->tabColor = new Color(); + } + + return $this->tabColor; + } + + /** + * Reset tab color. + * + * @return Worksheet + */ + public function resetTabColor() + { + $this->tabColor = null; + unset($this->tabColor); + + return $this; + } + + /** + * Tab color set? + * + * @return bool + */ + public function isTabColorSet() + { + return $this->tabColor !== null; + } + + /** + * Copy worksheet (!= clone!). + * + * @return Worksheet + */ + public function copy() + { + $copied = clone $this; + + return $copied; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + foreach ($this as $key => $val) { + if ($key == 'parent') { + continue; + } + + if (is_object($val) || (is_array($val))) { + if ($key == 'cellCollection') { + $newCollection = $this->cellCollection->cloneCellCollection($this); + $this->cellCollection = $newCollection; + } elseif ($key == 'drawingCollection') { + $currentCollection = $this->drawingCollection; + $this->drawingCollection = new ArrayObject(); + foreach ($currentCollection as $item) { + if (is_object($item)) { + $newDrawing = clone $item; + $newDrawing->setWorksheet($this); + } + } + } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) { + $newAutoFilter = clone $this->autoFilter; + $this->autoFilter = $newAutoFilter; + $this->autoFilter->setParent($this); + } else { + $this->{$key} = unserialize(serialize($val)); + } + } + } + } + + /** + * Define the code name of the sheet. + * + * @param string $pValue Same rule as Title minus space not allowed (but, like Excel, change + * silently space to underscore) + * @param bool $validate False to skip validation of new title. WARNING: This should only be set + * at parse time (by Readers), where titles can be assumed to be valid. + * + * @throws Exception + * + * @return Worksheet + */ + public function setCodeName($pValue, $validate = true) + { + // Is this a 'rename' or not? + if ($this->getCodeName() == $pValue) { + return $this; + } + + if ($validate) { + $pValue = str_replace(' ', '_', $pValue); //Excel does this automatically without flinching, we are doing the same + + // Syntax check + // throw an exception if not valid + self::checkSheetCodeName($pValue); + + // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_' + + if ($this->getParent()) { + // Is there already such sheet name? + if ($this->getParent()->sheetCodeNameExists($pValue)) { + // Use name, but append with lowest possible integer + + if (Shared\StringHelper::countCharacters($pValue) > 29) { + $pValue = Shared\StringHelper::substring($pValue, 0, 29); + } + $i = 1; + while ($this->getParent()->sheetCodeNameExists($pValue . '_' . $i)) { + ++$i; + if ($i == 10) { + if (Shared\StringHelper::countCharacters($pValue) > 28) { + $pValue = Shared\StringHelper::substring($pValue, 0, 28); + } + } elseif ($i == 100) { + if (Shared\StringHelper::countCharacters($pValue) > 27) { + $pValue = Shared\StringHelper::substring($pValue, 0, 27); + } + } + } + + $pValue = $pValue . '_' . $i; // ok, we have a valid name + } + } + } + + $this->codeName = $pValue; + + return $this; + } + + /** + * Return the code name of the sheet. + * + * @return null|string + */ + public function getCodeName() + { + return $this->codeName; + } + + /** + * Sheet has a code name ? + * + * @return bool + */ + public function hasCodeName() + { + return $this->codeName !== null; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/BaseWriter.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/BaseWriter.php new file mode 100644 index 00000000000..122783f30c6 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/BaseWriter.php @@ -0,0 +1,141 @@ +includeCharts; + } + + /** + * Set write charts in workbook + * Set to true, to advise the Writer to include any charts that exist in the PhpSpreadsheet object. + * Set to false (the default) to ignore charts. + * + * @param bool $pValue + * + * @return IWriter + */ + public function setIncludeCharts($pValue) + { + $this->includeCharts = (bool) $pValue; + + return $this; + } + + /** + * Get Pre-Calculate Formulas flag + * If this is true (the default), then the writer will recalculate all formulae in a workbook when saving, + * so that the pre-calculated values are immediately available to MS Excel or other office spreadsheet + * viewer when opening the file + * If false, then formulae are not calculated on save. This is faster for saving in PhpSpreadsheet, but slower + * when opening the resulting file in MS Excel, because Excel has to recalculate the formulae itself. + * + * @return bool + */ + public function getPreCalculateFormulas() + { + return $this->preCalculateFormulas; + } + + /** + * Set Pre-Calculate Formulas + * Set to true (the default) to advise the Writer to calculate all formulae on save + * Set to false to prevent precalculation of formulae on save. + * + * @param bool $pValue Pre-Calculate Formulas? + * + * @return IWriter + */ + public function setPreCalculateFormulas($pValue) + { + $this->preCalculateFormulas = (bool) $pValue; + + return $this; + } + + /** + * Get use disk caching where possible? + * + * @return bool + */ + public function getUseDiskCaching() + { + return $this->useDiskCaching; + } + + /** + * Set use disk caching where possible? + * + * @param bool $pValue + * @param string $pDirectory Disk caching directory + * + * @throws Exception when directory does not exist + * + * @return IWriter + */ + public function setUseDiskCaching($pValue, $pDirectory = null) + { + $this->useDiskCaching = $pValue; + + if ($pDirectory !== null) { + if (is_dir($pDirectory)) { + $this->diskCachingDirectory = $pDirectory; + } else { + throw new Exception("Directory does not exist: $pDirectory"); + } + } + + return $this; + } + + /** + * Get disk caching directory. + * + * @return string + */ + public function getDiskCachingDirectory() + { + return $this->diskCachingDirectory; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Csv.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Csv.php new file mode 100644 index 00000000000..ae38ab73218 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Csv.php @@ -0,0 +1,342 @@ +spreadsheet = $spreadsheet; + } + + /** + * Save PhpSpreadsheet to file. + * + * @param string $pFilename + * + * @throws Exception + */ + public function save($pFilename) + { + // Fetch sheet + $sheet = $this->spreadsheet->getSheet($this->sheetIndex); + + $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog(); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false); + $saveArrayReturnType = Calculation::getArrayReturnType(); + Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE); + + // Open file + $fileHandle = fopen($pFilename, 'wb+'); + if ($fileHandle === false) { + throw new Exception("Could not open file $pFilename for writing."); + } + + if ($this->excelCompatibility) { + $this->setUseBOM(true); // Enforce UTF-8 BOM Header + $this->setIncludeSeparatorLine(true); // Set separator line + $this->setEnclosure('"'); // Set enclosure to " + $this->setDelimiter(';'); // Set delimiter to a semi-colon + $this->setLineEnding("\r\n"); + } + if ($this->useBOM) { + // Write the UTF-8 BOM code if required + fwrite($fileHandle, "\xEF\xBB\xBF"); + } + if ($this->includeSeparatorLine) { + // Write the separator line if required + fwrite($fileHandle, 'sep=' . $this->getDelimiter() . $this->lineEnding); + } + + // Identify the range that we need to extract from the worksheet + $maxCol = $sheet->getHighestDataColumn(); + $maxRow = $sheet->getHighestDataRow(); + + // Write rows to file + for ($row = 1; $row <= $maxRow; ++$row) { + // Convert the row to an array... + $cellsArray = $sheet->rangeToArray('A' . $row . ':' . $maxCol . $row, '', $this->preCalculateFormulas); + // ... and write to the file + $this->writeLine($fileHandle, $cellsArray[0]); + } + + // Close file + fclose($fileHandle); + + Calculation::setArrayReturnType($saveArrayReturnType); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); + } + + /** + * Get delimiter. + * + * @return string + */ + public function getDelimiter() + { + return $this->delimiter; + } + + /** + * Set delimiter. + * + * @param string $pValue Delimiter, defaults to ',' + * + * @return CSV + */ + public function setDelimiter($pValue) + { + $this->delimiter = $pValue; + + return $this; + } + + /** + * Get enclosure. + * + * @return string + */ + public function getEnclosure() + { + return $this->enclosure; + } + + /** + * Set enclosure. + * + * @param string $pValue Enclosure, defaults to " + * + * @return CSV + */ + public function setEnclosure($pValue) + { + if ($pValue == '') { + $pValue = null; + } + $this->enclosure = $pValue; + + return $this; + } + + /** + * Get line ending. + * + * @return string + */ + public function getLineEnding() + { + return $this->lineEnding; + } + + /** + * Set line ending. + * + * @param string $pValue Line ending, defaults to OS line ending (PHP_EOL) + * + * @return CSV + */ + public function setLineEnding($pValue) + { + $this->lineEnding = $pValue; + + return $this; + } + + /** + * Get whether BOM should be used. + * + * @return bool + */ + public function getUseBOM() + { + return $this->useBOM; + } + + /** + * Set whether BOM should be used. + * + * @param bool $pValue Use UTF-8 byte-order mark? Defaults to false + * + * @return CSV + */ + public function setUseBOM($pValue) + { + $this->useBOM = $pValue; + + return $this; + } + + /** + * Get whether a separator line should be included. + * + * @return bool + */ + public function getIncludeSeparatorLine() + { + return $this->includeSeparatorLine; + } + + /** + * Set whether a separator line should be included as the first line of the file. + * + * @param bool $pValue Use separator line? Defaults to false + * + * @return CSV + */ + public function setIncludeSeparatorLine($pValue) + { + $this->includeSeparatorLine = $pValue; + + return $this; + } + + /** + * Get whether the file should be saved with full Excel Compatibility. + * + * @return bool + */ + public function getExcelCompatibility() + { + return $this->excelCompatibility; + } + + /** + * Set whether the file should be saved with full Excel Compatibility. + * + * @param bool $pValue Set the file to be written as a fully Excel compatible csv file + * Note that this overrides other settings such as useBOM, enclosure and delimiter + * + * @return CSV + */ + public function setExcelCompatibility($pValue) + { + $this->excelCompatibility = $pValue; + + return $this; + } + + /** + * Get sheet index. + * + * @return int + */ + public function getSheetIndex() + { + return $this->sheetIndex; + } + + /** + * Set sheet index. + * + * @param int $pValue Sheet index + * + * @return CSV + */ + public function setSheetIndex($pValue) + { + $this->sheetIndex = $pValue; + + return $this; + } + + /** + * Write line to CSV file. + * + * @param resource $pFileHandle PHP filehandle + * @param array $pValues Array containing values in a row + */ + private function writeLine($pFileHandle, array $pValues) + { + // No leading delimiter + $writeDelimiter = false; + + // Build the line + $line = ''; + + foreach ($pValues as $element) { + // Escape enclosures + $element = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $element); + + // Add delimiter + if ($writeDelimiter) { + $line .= $this->delimiter; + } else { + $writeDelimiter = true; + } + + // Add enclosed string + $line .= $this->enclosure . $element . $this->enclosure; + } + + // Add line ending + $line .= $this->lineEnding; + + // Write to file + fwrite($pFileHandle, $line); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Exception.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Exception.php new file mode 100644 index 00000000000..92e6f5f4d4c --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Exception.php @@ -0,0 +1,9 @@ +spreadsheet = $spreadsheet; + $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont(); + } + + /** + * Save Spreadsheet to file. + * + * @param string $pFilename + * + * @throws WriterException + */ + public function save($pFilename) + { + // garbage collect + $this->spreadsheet->garbageCollect(); + + $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog(); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false); + $saveArrayReturnType = Calculation::getArrayReturnType(); + Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE); + + // Build CSS + $this->buildCSS(!$this->useInlineCss); + + // Open file + $fileHandle = fopen($pFilename, 'wb+'); + if ($fileHandle === false) { + throw new WriterException("Could not open file $pFilename for writing."); + } + + // Write headers + fwrite($fileHandle, $this->generateHTMLHeader(!$this->useInlineCss)); + + // Write navigation (tabs) + if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) { + fwrite($fileHandle, $this->generateNavigation()); + } + + // Write data + fwrite($fileHandle, $this->generateSheetData()); + + // Write footer + fwrite($fileHandle, $this->generateHTMLFooter()); + + // Close file + fclose($fileHandle); + + Calculation::setArrayReturnType($saveArrayReturnType); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); + } + + /** + * Map VAlign. + * + * @param string $vAlign Vertical alignment + * + * @return string + */ + private function mapVAlign($vAlign) + { + switch ($vAlign) { + case Alignment::VERTICAL_BOTTOM: + return 'bottom'; + case Alignment::VERTICAL_TOP: + return 'top'; + case Alignment::VERTICAL_CENTER: + case Alignment::VERTICAL_JUSTIFY: + return 'middle'; + default: + return 'baseline'; + } + } + + /** + * Map HAlign. + * + * @param string $hAlign Horizontal alignment + * + * @return false|string + */ + private function mapHAlign($hAlign) + { + switch ($hAlign) { + case Alignment::HORIZONTAL_GENERAL: + return false; + case Alignment::HORIZONTAL_LEFT: + return 'left'; + case Alignment::HORIZONTAL_RIGHT: + return 'right'; + case Alignment::HORIZONTAL_CENTER: + case Alignment::HORIZONTAL_CENTER_CONTINUOUS: + return 'center'; + case Alignment::HORIZONTAL_JUSTIFY: + return 'justify'; + default: + return false; + } + } + + /** + * Map border style. + * + * @param int $borderStyle Sheet index + * + * @return string + */ + private function mapBorderStyle($borderStyle) + { + switch ($borderStyle) { + case Border::BORDER_NONE: + return 'none'; + case Border::BORDER_DASHDOT: + return '1px dashed'; + case Border::BORDER_DASHDOTDOT: + return '1px dotted'; + case Border::BORDER_DASHED: + return '1px dashed'; + case Border::BORDER_DOTTED: + return '1px dotted'; + case Border::BORDER_DOUBLE: + return '3px double'; + case Border::BORDER_HAIR: + return '1px solid'; + case Border::BORDER_MEDIUM: + return '2px solid'; + case Border::BORDER_MEDIUMDASHDOT: + return '2px dashed'; + case Border::BORDER_MEDIUMDASHDOTDOT: + return '2px dotted'; + case Border::BORDER_MEDIUMDASHED: + return '2px dashed'; + case Border::BORDER_SLANTDASHDOT: + return '2px dashed'; + case Border::BORDER_THICK: + return '3px solid'; + case Border::BORDER_THIN: + return '1px solid'; + default: + // map others to thin + return '1px solid'; + } + } + + /** + * Get sheet index. + * + * @return int + */ + public function getSheetIndex() + { + return $this->sheetIndex; + } + + /** + * Set sheet index. + * + * @param int $pValue Sheet index + * + * @return HTML + */ + public function setSheetIndex($pValue) + { + $this->sheetIndex = $pValue; + + return $this; + } + + /** + * Get sheet index. + * + * @return bool + */ + public function getGenerateSheetNavigationBlock() + { + return $this->generateSheetNavigationBlock; + } + + /** + * Set sheet index. + * + * @param bool $pValue Flag indicating whether the sheet navigation block should be generated or not + * + * @return HTML + */ + public function setGenerateSheetNavigationBlock($pValue) + { + $this->generateSheetNavigationBlock = (bool) $pValue; + + return $this; + } + + /** + * Write all sheets (resets sheetIndex to NULL). + */ + public function writeAllSheets() + { + $this->sheetIndex = null; + + return $this; + } + + /** + * Generate HTML header. + * + * @param bool $pIncludeStyles Include styles? + * + * @throws WriterException + * + * @return string + */ + public function generateHTMLHeader($pIncludeStyles = false) + { + // Construct HTML + $properties = $this->spreadsheet->getProperties(); + $html = '' . PHP_EOL; + $html .= '' . PHP_EOL; + $html .= ' ' . PHP_EOL; + $html .= ' ' . PHP_EOL; + $html .= ' ' . PHP_EOL; + if ($properties->getTitle() > '') { + $html .= ' ' . htmlspecialchars($properties->getTitle()) . '' . PHP_EOL; + } + if ($properties->getCreator() > '') { + $html .= ' ' . PHP_EOL; + } + if ($properties->getTitle() > '') { + $html .= ' ' . PHP_EOL; + } + if ($properties->getDescription() > '') { + $html .= ' ' . PHP_EOL; + } + if ($properties->getSubject() > '') { + $html .= ' ' . PHP_EOL; + } + if ($properties->getKeywords() > '') { + $html .= ' ' . PHP_EOL; + } + if ($properties->getCategory() > '') { + $html .= ' ' . PHP_EOL; + } + if ($properties->getCompany() > '') { + $html .= ' ' . PHP_EOL; + } + if ($properties->getManager() > '') { + $html .= ' ' . PHP_EOL; + } + + if ($pIncludeStyles) { + $html .= $this->generateStyles(true); + } + + $html .= ' ' . PHP_EOL; + $html .= '' . PHP_EOL; + $html .= ' ' . PHP_EOL; + + return $html; + } + + /** + * Generate sheet data. + * + * @throws WriterException + * + * @return string + */ + public function generateSheetData() + { + // Ensure that Spans have been calculated? + if ($this->sheetIndex !== null || !$this->spansAreCalculated) { + $this->calculateSpans(); + } + + // Fetch sheets + $sheets = []; + if ($this->sheetIndex === null) { + $sheets = $this->spreadsheet->getAllSheets(); + } else { + $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); + } + + // Construct HTML + $html = ''; + + // Loop all sheets + $sheetId = 0; + foreach ($sheets as $sheet) { + // Write table header + $html .= $this->generateTableHeader($sheet); + + // Get worksheet dimension + $dimension = explode(':', $sheet->calculateWorksheetDimension()); + $dimension[0] = Coordinate::coordinateFromString($dimension[0]); + $dimension[0][0] = Coordinate::columnIndexFromString($dimension[0][0]); + $dimension[1] = Coordinate::coordinateFromString($dimension[1]); + $dimension[1][0] = Coordinate::columnIndexFromString($dimension[1][0]); + + // row min,max + $rowMin = $dimension[0][1]; + $rowMax = $dimension[1][1]; + + // calculate start of

'; + $html .= $this->writeImageInCell($pSheet, $col . $row); + if ($this->includeCharts) { + $html .= $this->writeChartInCell($pSheet, $col . $row); + } + $html .= '
' . PHP_EOL; + } else { + $style = isset($this->cssStyles['table']) ? + $this->assembleCSS($this->cssStyles['table']) : ''; + + if ($this->isPdf && $pSheet->getShowGridlines()) { + $html .= '
' . PHP_EOL; + } else { + $html .= '
' . PHP_EOL; + } + } + + // Write elements + $highestColumnIndex = Coordinate::columnIndexFromString($pSheet->getHighestColumn()) - 1; + $i = -1; + while ($i++ < $highestColumnIndex) { + if (!$this->isPdf) { + if (!$this->useInlineCss) { + $html .= ' ' . PHP_EOL; + } else { + $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ? + $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : ''; + $html .= ' ' . PHP_EOL; + } + } + } + + return $html; + } + + /** + * Generate table footer. + */ + private function generateTableFooter() + { + $html = '
' . PHP_EOL; + + return $html; + } + + /** + * Generate row. + * + * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + * @param array $pValues Array containing cells in a row + * @param int $pRow Row number (0-based) + * @param string $cellType eg: 'td' + * + * @throws WriterException + * + * @return string + */ + private function generateRow(Worksheet $pSheet, array $pValues, $pRow, $cellType) + { + // Construct HTML + $html = ''; + + // Sheet index + $sheetIndex = $pSheet->getParent()->getIndex($pSheet); + + // Dompdf and breaks + if ($this->isPdf && count($pSheet->getBreaks()) > 0) { + $breaks = $pSheet->getBreaks(); + + // check if a break is needed before this row + if (isset($breaks['A' . $pRow])) { + // close table:
+ $html .= $this->generateTableFooter(); + + // insert page break + $html .= '
'; + + // open table again: + etc. + $html .= $this->generateTableHeader($pSheet); + } + } + + // Write row start + if (!$this->useInlineCss) { + $html .= ' ' . PHP_EOL; + } else { + $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) + ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : ''; + + $html .= ' ' . PHP_EOL; + } + + // Write cells + $colNum = 0; + foreach ($pValues as $cellAddress) { + $cell = ($cellAddress > '') ? $pSheet->getCell($cellAddress) : ''; + $coordinate = Coordinate::stringFromColumnIndex($colNum + 1) . ($pRow + 1); + if (!$this->useInlineCss) { + $cssClass = 'column' . $colNum; + } else { + $cssClass = []; + if ($cellType == 'th') { + if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) { + $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum]; + } + } else { + if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) { + $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum]; + } + } + } + $colSpan = 1; + $rowSpan = 1; + + // initialize + $cellData = ' '; + + // Cell + if ($cell instanceof Cell) { + $cellData = ''; + if ($cell->getParent() === null) { + $cell->attach($pSheet); + } + // Value + if ($cell->getValue() instanceof RichText) { + // Loop through rich text elements + $elements = $cell->getValue()->getRichTextElements(); + foreach ($elements as $element) { + // Rich text start? + if ($element instanceof Run) { + $cellData .= ''; + + if ($element->getFont()->getSuperscript()) { + $cellData .= ''; + } elseif ($element->getFont()->getSubscript()) { + $cellData .= ''; + } + } + + // Convert UTF8 data to PCDATA + $cellText = $element->getText(); + $cellData .= htmlspecialchars($cellText); + + if ($element instanceof Run) { + if ($element->getFont()->getSuperscript()) { + $cellData .= ''; + } elseif ($element->getFont()->getSubscript()) { + $cellData .= ''; + } + + $cellData .= ''; + } + } + } else { + if ($this->preCalculateFormulas) { + $cellData = NumberFormat::toFormattedString( + $cell->getCalculatedValue(), + $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(), + [$this, 'formatColor'] + ); + } else { + $cellData = NumberFormat::toFormattedString( + $cell->getValue(), + $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(), + [$this, 'formatColor'] + ); + } + $cellData = htmlspecialchars($cellData); + if ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { + $cellData = '' . $cellData . ''; + } elseif ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) { + $cellData = '' . $cellData . ''; + } + } + + // Converts the cell content so that spaces occuring at beginning of each new line are replaced by   + // Example: " Hello\n to the world" is converted to "  Hello\n to the world" + $cellData = preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); + + // convert newline "\n" to '
' + $cellData = nl2br($cellData); + + // Extend CSS class? + if (!$this->useInlineCss) { + $cssClass .= ' style' . $cell->getXfIndex(); + $cssClass .= ' ' . $cell->getDataType(); + } else { + if ($cellType == 'th') { + if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) { + $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]); + } + } else { + if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) { + $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]); + } + } + + // General horizontal alignment: Actual horizontal alignment depends on dataType + $sharedStyle = $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex()); + if ($sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL + && isset($this->cssStyles['.' . $cell->getDataType()]['text-align']) + ) { + $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align']; + } + } + } + + // Hyperlink? + if ($pSheet->hyperlinkExists($coordinate) && !$pSheet->getHyperlink($coordinate)->isInternal()) { + $cellData = '' . $cellData . ''; + } + + // Should the cell be written or is it swallowed by a rowspan or colspan? + $writeCell = !(isset($this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]) + && $this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]); + + // Colspan and Rowspan + $colspan = 1; + $rowspan = 1; + if (isset($this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])) { + $spans = $this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]; + $rowSpan = $spans['rowspan']; + $colSpan = $spans['colspan']; + + // Also apply style from last cell in merge to fix borders - + // relies on !important for non-none border declarations in createCSSStyleBorder + $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($pRow + $rowSpan); + if (!$this->useInlineCss) { + $cssClass .= ' style' . $pSheet->getCell($endCellCoord)->getXfIndex(); + } + } + + // Write + if ($writeCell) { + // Column start + $html .= ' <' . $cellType; + if (!$this->useInlineCss) { + $html .= ' class="' . $cssClass . '"'; + } else { + //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf ** + // We must explicitly write the width of the + $width = 0; + $i = $colNum - 1; + $e = $colNum + $colSpan - 1; + while ($i++ < $e) { + if (isset($this->columnWidths[$sheetIndex][$i])) { + $width += $this->columnWidths[$sheetIndex][$i]; + } + } + $cssClass['width'] = $width . 'pt'; + + // We must also explicitly write the height of the + if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) { + $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height']; + $cssClass['height'] = $height; + } + //** end of redundant code ** + + $html .= ' style="' . $this->assembleCSS($cssClass) . '"'; + } + if ($colSpan > 1) { + $html .= ' colspan="' . $colSpan . '"'; + } + if ($rowSpan > 1) { + $html .= ' rowspan="' . $rowSpan . '"'; + } + $html .= '>'; + + $html .= $this->writeComment($pSheet, $coordinate); + + // Image? + $html .= $this->writeImageInCell($pSheet, $coordinate); + + // Chart? + if ($this->includeCharts) { + $html .= $this->writeChartInCell($pSheet, $coordinate); + } + + // Cell data + $html .= $cellData; + + // Column end + $html .= '' . PHP_EOL; + } + + // Next column + ++$colNum; + } + + // Write row end + $html .= ' ' . PHP_EOL; + + // Return + return $html; + } + + /** + * Takes array where of CSS properties / values and converts to CSS string. + * + * @param array $pValue + * + * @return string + */ + private function assembleCSS(array $pValue = []) + { + $pairs = []; + foreach ($pValue as $property => $value) { + $pairs[] = $property . ':' . $value; + } + $string = implode('; ', $pairs); + + return $string; + } + + /** + * Get images root. + * + * @return string + */ + public function getImagesRoot() + { + return $this->imagesRoot; + } + + /** + * Set images root. + * + * @param string $pValue + * + * @return HTML + */ + public function setImagesRoot($pValue) + { + $this->imagesRoot = $pValue; + + return $this; + } + + /** + * Get embed images. + * + * @return bool + */ + public function getEmbedImages() + { + return $this->embedImages; + } + + /** + * Set embed images. + * + * @param bool $pValue + * + * @return HTML + */ + public function setEmbedImages($pValue) + { + $this->embedImages = $pValue; + + return $this; + } + + /** + * Get use inline CSS? + * + * @return bool + */ + public function getUseInlineCss() + { + return $this->useInlineCss; + } + + /** + * Set use inline CSS? + * + * @param bool $pValue + * + * @return HTML + */ + public function setUseInlineCss($pValue) + { + $this->useInlineCss = $pValue; + + return $this; + } + + /** + * Add color to formatted string as inline style. + * + * @param string $pValue Plain formatted value without color + * @param string $pFormat Format code + * + * @return string + */ + public function formatColor($pValue, $pFormat) + { + // Color information, e.g. [Red] is always at the beginning + $color = null; // initialize + $matches = []; + + $color_regex = '/^\\[[a-zA-Z]+\\]/'; + if (preg_match($color_regex, $pFormat, $matches)) { + $color = str_replace(['[', ']'], '', $matches[0]); + $color = strtolower($color); + } + + // convert to PCDATA + $value = htmlspecialchars($pValue); + + // color span tag + if ($color !== null) { + $value = '' . $value . ''; + } + + return $value; + } + + /** + * Calculate information about HTML colspan and rowspan which is not always the same as Excel's. + */ + private function calculateSpans() + { + // Identify all cells that should be omitted in HTML due to cell merge. + // In HTML only the upper-left cell should be written and it should have + // appropriate rowspan / colspan attribute + $sheetIndexes = $this->sheetIndex !== null ? + [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1); + + foreach ($sheetIndexes as $sheetIndex) { + $sheet = $this->spreadsheet->getSheet($sheetIndex); + + $candidateSpannedRow = []; + + // loop through all Excel merged cells + foreach ($sheet->getMergeCells() as $cells) { + list($cells) = Coordinate::splitRange($cells); + $first = $cells[0]; + $last = $cells[1]; + + list($fc, $fr) = Coordinate::coordinateFromString($first); + $fc = Coordinate::columnIndexFromString($fc) - 1; + + list($lc, $lr) = Coordinate::coordinateFromString($last); + $lc = Coordinate::columnIndexFromString($lc) - 1; + + // loop through the individual cells in the individual merge + $r = $fr - 1; + while ($r++ < $lr) { + // also, flag this row as a HTML row that is candidate to be omitted + $candidateSpannedRow[$r] = $r; + + $c = $fc - 1; + while ($c++ < $lc) { + if (!($c == $fc && $r == $fr)) { + // not the upper-left cell (should not be written in HTML) + $this->isSpannedCell[$sheetIndex][$r][$c] = [ + 'baseCell' => [$fr, $fc], + ]; + } else { + // upper-left is the base cell that should hold the colspan/rowspan attribute + $this->isBaseCell[$sheetIndex][$r][$c] = [ + 'xlrowspan' => $lr - $fr + 1, // Excel rowspan + 'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change + 'xlcolspan' => $lc - $fc + 1, // Excel colspan + 'colspan' => $lc - $fc + 1, // HTML colspan, value may change + ]; + } + } + } + } + + // Identify which rows should be omitted in HTML. These are the rows where all the cells + // participate in a merge and the where base cells are somewhere above. + $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn()); + foreach ($candidateSpannedRow as $rowIndex) { + if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) { + if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) { + $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex; + } + } + } + + // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1 + if (isset($this->isSpannedRow[$sheetIndex])) { + foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) { + $adjustedBaseCells = []; + $c = -1; + $e = $countColumns - 1; + while ($c++ < $e) { + $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell']; + + if (!in_array($baseCell, $adjustedBaseCells)) { + // subtract rowspan by 1 + --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan']; + $adjustedBaseCells[] = $baseCell; + } + } + } + } + + // TODO: Same for columns + } + + // We have calculated the spans + $this->spansAreCalculated = true; + } + + private function setMargins(Worksheet $pSheet) + { + $htmlPage = '@page { '; + $htmlBody = 'body { '; + + $left = StringHelper::formatNumber($pSheet->getPageMargins()->getLeft()) . 'in; '; + $htmlPage .= 'margin-left: ' . $left; + $htmlBody .= 'margin-left: ' . $left; + $right = StringHelper::formatNumber($pSheet->getPageMargins()->getRight()) . 'in; '; + $htmlPage .= 'margin-right: ' . $right; + $htmlBody .= 'margin-right: ' . $right; + $top = StringHelper::formatNumber($pSheet->getPageMargins()->getTop()) . 'in; '; + $htmlPage .= 'margin-top: ' . $top; + $htmlBody .= 'margin-top: ' . $top; + $bottom = StringHelper::formatNumber($pSheet->getPageMargins()->getBottom()) . 'in; '; + $htmlPage .= 'margin-bottom: ' . $bottom; + $htmlBody .= 'margin-bottom: ' . $bottom; + + $htmlPage .= "}\n"; + $htmlBody .= "}\n"; + + return "\n"; + } + + /** + * Write a comment in the same format as LibreOffice. + * + * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092 + * + * @param Worksheet $pSheet + * @param string $coordinate + * + * @return string + */ + private function writeComment(Worksheet $pSheet, $coordinate) + { + $result = ''; + if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) { + $result .= ''; + $result .= '
' . nl2br($pSheet->getComment($coordinate)->getText()->getPlainText()) . '
'; + $result .= PHP_EOL; + } + + return $result; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/IWriter.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/IWriter.php new file mode 100644 index 00000000000..9ce45a1946a --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/IWriter.php @@ -0,0 +1,24 @@ +setSpreadsheet($spreadsheet); + + $writerPartsArray = [ + 'content' => Content::class, + 'meta' => Meta::class, + 'meta_inf' => MetaInf::class, + 'mimetype' => Mimetype::class, + 'settings' => Settings::class, + 'styles' => Styles::class, + 'thumbnails' => Thumbnails::class, + ]; + + foreach ($writerPartsArray as $writer => $class) { + $this->writerParts[$writer] = new $class($this); + } + } + + /** + * Get writer part. + * + * @param string $pPartName Writer part name + * + * @return null|Ods\WriterPart + */ + public function getWriterPart($pPartName) + { + if ($pPartName != '' && isset($this->writerParts[strtolower($pPartName)])) { + return $this->writerParts[strtolower($pPartName)]; + } + + return null; + } + + /** + * Save PhpSpreadsheet to file. + * + * @param string $pFilename + * + * @throws WriterException + */ + public function save($pFilename) + { + if (!$this->spreadSheet) { + throw new WriterException('PhpSpreadsheet object unassigned.'); + } + + // garbage collect + $this->spreadSheet->garbageCollect(); + + // If $pFilename is php://output or php://stdout, make it a temporary file... + $originalFilename = $pFilename; + if (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { + $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); + if ($pFilename == '') { + $pFilename = $originalFilename; + } + } + + $zip = $this->createZip($pFilename); + + $zip->addFromString('META-INF/manifest.xml', $this->getWriterPart('meta_inf')->writeManifest()); + $zip->addFromString('Thumbnails/thumbnail.png', $this->getWriterPart('thumbnails')->writeThumbnail()); + $zip->addFromString('content.xml', $this->getWriterPart('content')->write()); + $zip->addFromString('meta.xml', $this->getWriterPart('meta')->write()); + $zip->addFromString('mimetype', $this->getWriterPart('mimetype')->write()); + $zip->addFromString('settings.xml', $this->getWriterPart('settings')->write()); + $zip->addFromString('styles.xml', $this->getWriterPart('styles')->write()); + + // Close file + if ($zip->close() === false) { + throw new WriterException("Could not close zip file $pFilename."); + } + + // If a temporary file was used, copy it to the correct file stream + if ($originalFilename != $pFilename) { + if (copy($pFilename, $originalFilename) === false) { + throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); + } + @unlink($pFilename); + } + } + + /** + * Create zip object. + * + * @param string $pFilename + * + * @throws WriterException + * + * @return ZipArchive + */ + private function createZip($pFilename) + { + // Create new ZIP file and open it for writing + $zip = new ZipArchive(); + + if (file_exists($pFilename)) { + unlink($pFilename); + } + // Try opening the ZIP file + if ($zip->open($pFilename, ZipArchive::OVERWRITE) !== true) { + if ($zip->open($pFilename, ZipArchive::CREATE) !== true) { + throw new WriterException("Could not open $pFilename for writing."); + } + } + + return $zip; + } + + /** + * Get Spreadsheet object. + * + * @throws WriterException + * + * @return Spreadsheet + */ + public function getSpreadsheet() + { + if ($this->spreadSheet !== null) { + return $this->spreadSheet; + } + + throw new WriterException('No PhpSpreadsheet assigned.'); + } + + /** + * Set Spreadsheet object. + * + * @param Spreadsheet $spreadsheet PhpSpreadsheet object + * + * @return self + */ + public function setSpreadsheet(Spreadsheet $spreadsheet) + { + $this->spreadSheet = $spreadsheet; + + return $this; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Cell/Comment.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Cell/Comment.php new file mode 100644 index 00000000000..2f543be51b8 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Cell/Comment.php @@ -0,0 +1,33 @@ + + */ +class Comment +{ + public static function write(XMLWriter $objWriter, Cell $cell) + { + $comments = $cell->getWorksheet()->getComments(); + if (!isset($comments[$cell->getCoordinate()])) { + return; + } + $comment = $comments[$cell->getCoordinate()]; + + $objWriter->startElement('office:annotation'); + $objWriter->writeAttribute('svg:width', $comment->getWidth()); + $objWriter->writeAttribute('svg:height', $comment->getHeight()); + $objWriter->writeAttribute('svg:x', $comment->getMarginLeft()); + $objWriter->writeAttribute('svg:y', $comment->getMarginTop()); + $objWriter->writeElement('dc:creator', $comment->getAuthor()); + $objWriter->writeElement('text:p', $comment->getText()->getPlainText()); + $objWriter->endElement(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Content.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Content.php new file mode 100644 index 00000000000..11de0fd3733 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Content.php @@ -0,0 +1,395 @@ + + */ +class Content extends WriterPart +{ + const NUMBER_COLS_REPEATED_MAX = 1024; + const NUMBER_ROWS_REPEATED_MAX = 1048576; + const CELL_STYLE_PREFIX = 'ce'; + + /** + * Write content.xml to XML format. + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @return string XML Output + */ + public function write() + { + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8'); + + // Content + $objWriter->startElement('office:document-content'); + $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'); + $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'); + $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'); + $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'); + $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'); + $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'); + $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'); + $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'); + $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0'); + $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'); + $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'); + $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'); + $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML'); + $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'); + $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'); + $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office'); + $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer'); + $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc'); + $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events'); + $objWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms'); + $objWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'); + $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report'); + $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2'); + $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); + $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#'); + $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table'); + $objWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0'); + $objWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0'); + $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/'); + $objWriter->writeAttribute('office:version', '1.2'); + + $objWriter->writeElement('office:scripts'); + $objWriter->writeElement('office:font-face-decls'); + + // Styles XF + $objWriter->startElement('office:automatic-styles'); + $this->writeXfStyles($objWriter, $this->getParentWriter()->getSpreadsheet()); + $objWriter->endElement(); + + $objWriter->startElement('office:body'); + $objWriter->startElement('office:spreadsheet'); + $objWriter->writeElement('table:calculation-settings'); + + $this->writeSheets($objWriter); + + $objWriter->writeElement('table:named-expressions'); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + return $objWriter->getData(); + } + + /** + * Write sheets. + * + * @param XMLWriter $objWriter + */ + private function writeSheets(XMLWriter $objWriter) + { + $spreadsheet = $this->getParentWriter()->getSpreadsheet(); // @var $spreadsheet Spreadsheet + + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $objWriter->startElement('table:table'); + $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle()); + $objWriter->writeElement('office:forms'); + $objWriter->startElement('table:table-column'); + $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); + $objWriter->endElement(); + $this->writeRows($objWriter, $spreadsheet->getSheet($i)); + $objWriter->endElement(); + } + } + + /** + * Write rows of the specified sheet. + * + * @param XMLWriter $objWriter + * @param Worksheet $sheet + */ + private function writeRows(XMLWriter $objWriter, Worksheet $sheet) + { + $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX; + $span_row = 0; + $rows = $sheet->getRowIterator(); + while ($rows->valid()) { + --$numberRowsRepeated; + $row = $rows->current(); + if ($row->getCellIterator()->valid()) { + if ($span_row) { + $objWriter->startElement('table:table-row'); + if ($span_row > 1) { + $objWriter->writeAttribute('table:number-rows-repeated', $span_row); + } + $objWriter->startElement('table:table-cell'); + $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); + $objWriter->endElement(); + $objWriter->endElement(); + $span_row = 0; + } + $objWriter->startElement('table:table-row'); + $this->writeCells($objWriter, $row); + $objWriter->endElement(); + } else { + ++$span_row; + } + $rows->next(); + } + } + + /** + * Write cells of the specified row. + * + * @param XMLWriter $objWriter + * @param Row $row + * + * @throws Exception + */ + private function writeCells(XMLWriter $objWriter, Row $row) + { + $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX; + $prevColumn = -1; + $cells = $row->getCellIterator(); + while ($cells->valid()) { + /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */ + $cell = $cells->current(); + $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1; + + $this->writeCellSpan($objWriter, $column, $prevColumn); + $objWriter->startElement('table:table-cell'); + $this->writeCellMerge($objWriter, $cell); + + // Style XF + $style = $cell->getXfIndex(); + if ($style !== null) { + $objWriter->writeAttribute('table:style-name', self::CELL_STYLE_PREFIX . $style); + } + + switch ($cell->getDataType()) { + case DataType::TYPE_BOOL: + $objWriter->writeAttribute('office:value-type', 'boolean'); + $objWriter->writeAttribute('office:value', $cell->getValue()); + $objWriter->writeElement('text:p', $cell->getValue()); + + break; + case DataType::TYPE_ERROR: + throw new Exception('Writing of error not implemented yet.'); + + break; + case DataType::TYPE_FORMULA: + $formulaValue = $cell->getValue(); + if ($this->getParentWriter()->getPreCalculateFormulas()) { + try { + $formulaValue = $cell->getCalculatedValue(); + } catch (Exception $e) { + // don't do anything + } + } + $objWriter->writeAttribute('table:formula', 'of:' . $cell->getValue()); + if (is_numeric($formulaValue)) { + $objWriter->writeAttribute('office:value-type', 'float'); + } else { + $objWriter->writeAttribute('office:value-type', 'string'); + } + $objWriter->writeAttribute('office:value', $formulaValue); + $objWriter->writeElement('text:p', $formulaValue); + + break; + case DataType::TYPE_INLINE: + throw new Exception('Writing of inline not implemented yet.'); + + break; + case DataType::TYPE_NUMERIC: + $objWriter->writeAttribute('office:value-type', 'float'); + $objWriter->writeAttribute('office:value', $cell->getValue()); + $objWriter->writeElement('text:p', $cell->getValue()); + + break; + case DataType::TYPE_STRING: + $objWriter->writeAttribute('office:value-type', 'string'); + $objWriter->writeElement('text:p', $cell->getValue()); + + break; + } + Comment::write($objWriter, $cell); + $objWriter->endElement(); + $prevColumn = $column; + $cells->next(); + } + $numberColsRepeated = $numberColsRepeated - $prevColumn - 1; + if ($numberColsRepeated > 0) { + if ($numberColsRepeated > 1) { + $objWriter->startElement('table:table-cell'); + $objWriter->writeAttribute('table:number-columns-repeated', $numberColsRepeated); + $objWriter->endElement(); + } else { + $objWriter->writeElement('table:table-cell'); + } + } + } + + /** + * Write span. + * + * @param XMLWriter $objWriter + * @param int $curColumn + * @param int $prevColumn + */ + private function writeCellSpan(XMLWriter $objWriter, $curColumn, $prevColumn) + { + $diff = $curColumn - $prevColumn - 1; + if (1 === $diff) { + $objWriter->writeElement('table:table-cell'); + } elseif ($diff > 1) { + $objWriter->startElement('table:table-cell'); + $objWriter->writeAttribute('table:number-columns-repeated', $diff); + $objWriter->endElement(); + } + } + + /** + * Write XF cell styles. + * + * @param XMLWriter $writer + * @param Spreadsheet $spreadsheet + */ + private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet) + { + foreach ($spreadsheet->getCellXfCollection() as $style) { + $writer->startElement('style:style'); + $writer->writeAttribute('style:name', self::CELL_STYLE_PREFIX . $style->getIndex()); + $writer->writeAttribute('style:family', 'table-cell'); + $writer->writeAttribute('style:parent-style-name', 'Default'); + + // style:text-properties + + // Font + $writer->startElement('style:text-properties'); + + $font = $style->getFont(); + + if ($font->getBold()) { + $writer->writeAttribute('fo:font-weight', 'bold'); + $writer->writeAttribute('style:font-weight-complex', 'bold'); + $writer->writeAttribute('style:font-weight-asian', 'bold'); + } + + if ($font->getItalic()) { + $writer->writeAttribute('fo:font-style', 'italic'); + } + + if ($color = $font->getColor()) { + $writer->writeAttribute('fo:color', sprintf('#%s', $color->getRGB())); + } + + if ($family = $font->getName()) { + $writer->writeAttribute('fo:font-family', $family); + } + + if ($size = $font->getSize()) { + $writer->writeAttribute('fo:font-size', sprintf('%.1fpt', $size)); + } + + if ($font->getUnderline() && $font->getUnderline() != Font::UNDERLINE_NONE) { + $writer->writeAttribute('style:text-underline-style', 'solid'); + $writer->writeAttribute('style:text-underline-width', 'auto'); + $writer->writeAttribute('style:text-underline-color', 'font-color'); + + switch ($font->getUnderline()) { + case Font::UNDERLINE_DOUBLE: + $writer->writeAttribute('style:text-underline-type', 'double'); + + break; + case Font::UNDERLINE_SINGLE: + $writer->writeAttribute('style:text-underline-type', 'single'); + + break; + } + } + + $writer->endElement(); // Close style:text-properties + + // style:table-cell-properties + + $writer->startElement('style:table-cell-properties'); + $writer->writeAttribute('style:rotation-align', 'none'); + + // Fill + if ($fill = $style->getFill()) { + switch ($fill->getFillType()) { + case Fill::FILL_SOLID: + $writer->writeAttribute('fo:background-color', sprintf( + '#%s', + strtolower($fill->getStartColor()->getRGB()) + )); + + break; + case Fill::FILL_GRADIENT_LINEAR: + case Fill::FILL_GRADIENT_PATH: + /// TODO :: To be implemented + break; + case Fill::FILL_NONE: + default: + } + } + + $writer->endElement(); // Close style:table-cell-properties + + // End + + $writer->endElement(); // Close style:style + } + } + + /** + * Write attributes for merged cell. + * + * @param XMLWriter $objWriter + * @param Cell $cell + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + private function writeCellMerge(XMLWriter $objWriter, Cell $cell) + { + if (!$cell->isMergeRangeValueCell()) { + return; + } + + $mergeRange = Coordinate::splitRange($cell->getMergeRange()); + list($startCell, $endCell) = $mergeRange[0]; + $start = Coordinate::coordinateFromString($startCell); + $end = Coordinate::coordinateFromString($endCell); + $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1; + $rowSpan = $end[1] - $start[1] + 1; + + $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan); + $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Meta.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Meta.php new file mode 100644 index 00000000000..ffe5eff7925 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Meta.php @@ -0,0 +1,77 @@ +getParentWriter()->getSpreadsheet(); + } + + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8'); + + // Meta + $objWriter->startElement('office:document-meta'); + + $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'); + $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'); + $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office'); + $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#'); + $objWriter->writeAttribute('office:version', '1.2'); + + $objWriter->startElement('office:meta'); + + $objWriter->writeElement('meta:initial-creator', $spreadsheet->getProperties()->getCreator()); + $objWriter->writeElement('dc:creator', $spreadsheet->getProperties()->getCreator()); + $objWriter->writeElement('meta:creation-date', date(DATE_W3C, $spreadsheet->getProperties()->getCreated())); + $objWriter->writeElement('dc:date', date(DATE_W3C, $spreadsheet->getProperties()->getCreated())); + $objWriter->writeElement('dc:title', $spreadsheet->getProperties()->getTitle()); + $objWriter->writeElement('dc:description', $spreadsheet->getProperties()->getDescription()); + $objWriter->writeElement('dc:subject', $spreadsheet->getProperties()->getSubject()); + $keywords = explode(' ', $spreadsheet->getProperties()->getKeywords()); + foreach ($keywords as $keyword) { + $objWriter->writeElement('meta:keyword', $keyword); + } + + // + $objWriter->startElement('meta:user-defined'); + $objWriter->writeAttribute('meta:name', 'Company'); + $objWriter->writeRaw($spreadsheet->getProperties()->getCompany()); + $objWriter->endElement(); + + $objWriter->startElement('meta:user-defined'); + $objWriter->writeAttribute('meta:name', 'category'); + $objWriter->writeRaw($spreadsheet->getProperties()->getCategory()); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/MetaInf.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/MetaInf.php new file mode 100644 index 00000000000..1ec9d1eb44b --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/MetaInf.php @@ -0,0 +1,62 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8'); + + // Manifest + $objWriter->startElement('manifest:manifest'); + $objWriter->writeAttribute('xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'); + $objWriter->writeAttribute('manifest:version', '1.2'); + + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', '/'); + $objWriter->writeAttribute('manifest:version', '1.2'); + $objWriter->writeAttribute('manifest:media-type', 'application/vnd.oasis.opendocument.spreadsheet'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'meta.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'settings.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'content.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'Thumbnails/thumbnail.png'); + $objWriter->writeAttribute('manifest:media-type', 'image/png'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'styles.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Mimetype.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Mimetype.php new file mode 100644 index 00000000000..d0fed2b379b --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Mimetype.php @@ -0,0 +1,20 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8'); + + // Settings + $objWriter->startElement('office:document-settings'); + $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'); + $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + $objWriter->writeAttribute('xmlns:config', 'urn:oasis:names:tc:opendocument:xmlns:config:1.0'); + $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office'); + $objWriter->writeAttribute('office:version', '1.2'); + + $objWriter->startElement('office:settings'); + $objWriter->startElement('config:config-item-set'); + $objWriter->writeAttribute('config:name', 'ooo:view-settings'); + $objWriter->startElement('config:config-item-map-indexed'); + $objWriter->writeAttribute('config:name', 'Views'); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->startElement('config:config-item-set'); + $objWriter->writeAttribute('config:name', 'ooo:configuration-settings'); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Styles.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Styles.php new file mode 100644 index 00000000000..eaf5cad9526 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Styles.php @@ -0,0 +1,70 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8'); + + // Content + $objWriter->startElement('office:document-styles'); + $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'); + $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'); + $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'); + $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'); + $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'); + $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'); + $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'); + $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'); + $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0'); + $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'); + $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'); + $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'); + $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML'); + $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'); + $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'); + $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office'); + $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer'); + $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc'); + $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events'); + $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report'); + $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2'); + $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); + $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#'); + $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table'); + $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/'); + $objWriter->writeAttribute('office:version', '1.2'); + + $objWriter->writeElement('office:font-face-decls'); + $objWriter->writeElement('office:styles'); + $objWriter->writeElement('office:automatic-styles'); + $objWriter->writeElement('office:master-styles'); + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Thumbnails.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Thumbnails.php new file mode 100644 index 00000000000..a29a14adbe8 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Ods/Thumbnails.php @@ -0,0 +1,20 @@ +parentWriter; + } + + /** + * Set parent Ods writer. + * + * @param Ods $writer + */ + public function __construct(Ods $writer) + { + $this->parentWriter = $writer; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf.php new file mode 100644 index 00000000000..b80083ae466 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf.php @@ -0,0 +1,283 @@ + 'LETTER', // (8.5 in. by 11 in.) + PageSetup::PAPERSIZE_LETTER_SMALL => 'LETTER', // (8.5 in. by 11 in.) + PageSetup::PAPERSIZE_TABLOID => [792.00, 1224.00], // (11 in. by 17 in.) + PageSetup::PAPERSIZE_LEDGER => [1224.00, 792.00], // (17 in. by 11 in.) + PageSetup::PAPERSIZE_LEGAL => 'LEGAL', // (8.5 in. by 14 in.) + PageSetup::PAPERSIZE_STATEMENT => [396.00, 612.00], // (5.5 in. by 8.5 in.) + PageSetup::PAPERSIZE_EXECUTIVE => 'EXECUTIVE', // (7.25 in. by 10.5 in.) + PageSetup::PAPERSIZE_A3 => 'A3', // (297 mm by 420 mm) + PageSetup::PAPERSIZE_A4 => 'A4', // (210 mm by 297 mm) + PageSetup::PAPERSIZE_A4_SMALL => 'A4', // (210 mm by 297 mm) + PageSetup::PAPERSIZE_A5 => 'A5', // (148 mm by 210 mm) + PageSetup::PAPERSIZE_B4 => 'B4', // (250 mm by 353 mm) + PageSetup::PAPERSIZE_B5 => 'B5', // (176 mm by 250 mm) + PageSetup::PAPERSIZE_FOLIO => 'FOLIO', // (8.5 in. by 13 in.) + PageSetup::PAPERSIZE_QUARTO => [609.45, 779.53], // (215 mm by 275 mm) + PageSetup::PAPERSIZE_STANDARD_1 => [720.00, 1008.00], // (10 in. by 14 in.) + PageSetup::PAPERSIZE_STANDARD_2 => [792.00, 1224.00], // (11 in. by 17 in.) + PageSetup::PAPERSIZE_NOTE => 'LETTER', // (8.5 in. by 11 in.) + PageSetup::PAPERSIZE_NO9_ENVELOPE => [279.00, 639.00], // (3.875 in. by 8.875 in.) + PageSetup::PAPERSIZE_NO10_ENVELOPE => [297.00, 684.00], // (4.125 in. by 9.5 in.) + PageSetup::PAPERSIZE_NO11_ENVELOPE => [324.00, 747.00], // (4.5 in. by 10.375 in.) + PageSetup::PAPERSIZE_NO12_ENVELOPE => [342.00, 792.00], // (4.75 in. by 11 in.) + PageSetup::PAPERSIZE_NO14_ENVELOPE => [360.00, 828.00], // (5 in. by 11.5 in.) + PageSetup::PAPERSIZE_C => [1224.00, 1584.00], // (17 in. by 22 in.) + PageSetup::PAPERSIZE_D => [1584.00, 2448.00], // (22 in. by 34 in.) + PageSetup::PAPERSIZE_E => [2448.00, 3168.00], // (34 in. by 44 in.) + PageSetup::PAPERSIZE_DL_ENVELOPE => [311.81, 623.62], // (110 mm by 220 mm) + PageSetup::PAPERSIZE_C5_ENVELOPE => 'C5', // (162 mm by 229 mm) + PageSetup::PAPERSIZE_C3_ENVELOPE => 'C3', // (324 mm by 458 mm) + PageSetup::PAPERSIZE_C4_ENVELOPE => 'C4', // (229 mm by 324 mm) + PageSetup::PAPERSIZE_C6_ENVELOPE => 'C6', // (114 mm by 162 mm) + PageSetup::PAPERSIZE_C65_ENVELOPE => [323.15, 649.13], // (114 mm by 229 mm) + PageSetup::PAPERSIZE_B4_ENVELOPE => 'B4', // (250 mm by 353 mm) + PageSetup::PAPERSIZE_B5_ENVELOPE => 'B5', // (176 mm by 250 mm) + PageSetup::PAPERSIZE_B6_ENVELOPE => [498.90, 354.33], // (176 mm by 125 mm) + PageSetup::PAPERSIZE_ITALY_ENVELOPE => [311.81, 651.97], // (110 mm by 230 mm) + PageSetup::PAPERSIZE_MONARCH_ENVELOPE => [279.00, 540.00], // (3.875 in. by 7.5 in.) + PageSetup::PAPERSIZE_6_3_4_ENVELOPE => [261.00, 468.00], // (3.625 in. by 6.5 in.) + PageSetup::PAPERSIZE_US_STANDARD_FANFOLD => [1071.00, 792.00], // (14.875 in. by 11 in.) + PageSetup::PAPERSIZE_GERMAN_STANDARD_FANFOLD => [612.00, 864.00], // (8.5 in. by 12 in.) + PageSetup::PAPERSIZE_GERMAN_LEGAL_FANFOLD => 'FOLIO', // (8.5 in. by 13 in.) + PageSetup::PAPERSIZE_ISO_B4 => 'B4', // (250 mm by 353 mm) + PageSetup::PAPERSIZE_JAPANESE_DOUBLE_POSTCARD => [566.93, 419.53], // (200 mm by 148 mm) + PageSetup::PAPERSIZE_STANDARD_PAPER_1 => [648.00, 792.00], // (9 in. by 11 in.) + PageSetup::PAPERSIZE_STANDARD_PAPER_2 => [720.00, 792.00], // (10 in. by 11 in.) + PageSetup::PAPERSIZE_STANDARD_PAPER_3 => [1080.00, 792.00], // (15 in. by 11 in.) + PageSetup::PAPERSIZE_INVITE_ENVELOPE => [623.62, 623.62], // (220 mm by 220 mm) + PageSetup::PAPERSIZE_LETTER_EXTRA_PAPER => [667.80, 864.00], // (9.275 in. by 12 in.) + PageSetup::PAPERSIZE_LEGAL_EXTRA_PAPER => [667.80, 1080.00], // (9.275 in. by 15 in.) + PageSetup::PAPERSIZE_TABLOID_EXTRA_PAPER => [841.68, 1296.00], // (11.69 in. by 18 in.) + PageSetup::PAPERSIZE_A4_EXTRA_PAPER => [668.98, 912.76], // (236 mm by 322 mm) + PageSetup::PAPERSIZE_LETTER_TRANSVERSE_PAPER => [595.80, 792.00], // (8.275 in. by 11 in.) + PageSetup::PAPERSIZE_A4_TRANSVERSE_PAPER => 'A4', // (210 mm by 297 mm) + PageSetup::PAPERSIZE_LETTER_EXTRA_TRANSVERSE_PAPER => [667.80, 864.00], // (9.275 in. by 12 in.) + PageSetup::PAPERSIZE_SUPERA_SUPERA_A4_PAPER => [643.46, 1009.13], // (227 mm by 356 mm) + PageSetup::PAPERSIZE_SUPERB_SUPERB_A3_PAPER => [864.57, 1380.47], // (305 mm by 487 mm) + PageSetup::PAPERSIZE_LETTER_PLUS_PAPER => [612.00, 913.68], // (8.5 in. by 12.69 in.) + PageSetup::PAPERSIZE_A4_PLUS_PAPER => [595.28, 935.43], // (210 mm by 330 mm) + PageSetup::PAPERSIZE_A5_TRANSVERSE_PAPER => 'A5', // (148 mm by 210 mm) + PageSetup::PAPERSIZE_JIS_B5_TRANSVERSE_PAPER => [515.91, 728.50], // (182 mm by 257 mm) + PageSetup::PAPERSIZE_A3_EXTRA_PAPER => [912.76, 1261.42], // (322 mm by 445 mm) + PageSetup::PAPERSIZE_A5_EXTRA_PAPER => [493.23, 666.14], // (174 mm by 235 mm) + PageSetup::PAPERSIZE_ISO_B5_EXTRA_PAPER => [569.76, 782.36], // (201 mm by 276 mm) + PageSetup::PAPERSIZE_A2_PAPER => 'A2', // (420 mm by 594 mm) + PageSetup::PAPERSIZE_A3_TRANSVERSE_PAPER => 'A3', // (297 mm by 420 mm) + PageSetup::PAPERSIZE_A3_EXTRA_TRANSVERSE_PAPER => [912.76, 1261.42], // (322 mm by 445 mm) + ]; + + /** + * Create a new PDF Writer instance. + * + * @param Spreadsheet $spreadsheet Spreadsheet object + */ + public function __construct(Spreadsheet $spreadsheet) + { + parent::__construct($spreadsheet); + $this->setUseInlineCss(true); + $this->tempDir = File::sysGetTempDir(); + } + + /** + * Get Font. + * + * @return string + */ + public function getFont() + { + return $this->font; + } + + /** + * Set font. Examples: + * 'arialunicid0-chinese-simplified' + * 'arialunicid0-chinese-traditional' + * 'arialunicid0-korean' + * 'arialunicid0-japanese'. + * + * @param string $fontName + * + * @return Pdf + */ + public function setFont($fontName) + { + $this->font = $fontName; + + return $this; + } + + /** + * Get Paper Size. + * + * @return int + */ + public function getPaperSize() + { + return $this->paperSize; + } + + /** + * Set Paper Size. + * + * @param string $pValue Paper size see PageSetup::PAPERSIZE_* + * + * @return self + */ + public function setPaperSize($pValue) + { + $this->paperSize = $pValue; + + return $this; + } + + /** + * Get Orientation. + * + * @return string + */ + public function getOrientation() + { + return $this->orientation; + } + + /** + * Set Orientation. + * + * @param string $pValue Page orientation see PageSetup::ORIENTATION_* + * + * @return self + */ + public function setOrientation($pValue) + { + $this->orientation = $pValue; + + return $this; + } + + /** + * Get temporary storage directory. + * + * @return string + */ + public function getTempDir() + { + return $this->tempDir; + } + + /** + * Set temporary storage directory. + * + * @param string $pValue Temporary storage directory + * + * @throws WriterException when directory does not exist + * + * @return self + */ + public function setTempDir($pValue) + { + if (is_dir($pValue)) { + $this->tempDir = $pValue; + } else { + throw new WriterException("Directory does not exist: $pValue"); + } + + return $this; + } + + /** + * Save Spreadsheet to PDF file, pre-save. + * + * @param string $pFilename Name of the file to save as + * + * @throws WriterException + * + * @return resource + */ + protected function prepareForSave($pFilename) + { + // garbage collect + $this->spreadsheet->garbageCollect(); + + $this->saveArrayReturnType = Calculation::getArrayReturnType(); + Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE); + + // Open file + $fileHandle = fopen($pFilename, 'w'); + if ($fileHandle === false) { + throw new WriterException("Could not open file $pFilename for writing."); + } + + // Set PDF + $this->isPdf = true; + // Build CSS + $this->buildCSS(true); + + return $fileHandle; + } + + /** + * Save PhpSpreadsheet to PDF file, post-save. + * + * @param resource $fileHandle + */ + protected function restoreStateAfterSave($fileHandle) + { + // Close file + fclose($fileHandle); + + Calculation::setArrayReturnType($this->saveArrayReturnType); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Dompdf.php new file mode 100644 index 00000000000..3c3044d711b --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -0,0 +1,78 @@ +getSheetIndex() === null) { + $orientation = ($this->spreadsheet->getSheet(0)->getPageSetup()->getOrientation() + == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->spreadsheet->getSheet(0)->getPageSetup()->getPaperSize(); + } else { + $orientation = ($this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getOrientation() + == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getPaperSize(); + } + + $orientation = ($orientation == 'L') ? 'landscape' : 'portrait'; + + // Override Page Orientation + if ($this->getOrientation() !== null) { + $orientation = ($this->getOrientation() == PageSetup::ORIENTATION_DEFAULT) + ? PageSetup::ORIENTATION_PORTRAIT + : $this->getOrientation(); + } + // Override Paper Size + if ($this->getPaperSize() !== null) { + $printPaperSize = $this->getPaperSize(); + } + + if (isset(self::$paperSizes[$printPaperSize])) { + $paperSize = self::$paperSizes[$printPaperSize]; + } + + // Create PDF + $pdf = $this->createExternalWriterInstance(); + $pdf->setPaper(strtolower($paperSize), $orientation); + + $pdf->loadHtml( + $this->generateHTMLHeader(false) . + $this->generateSheetData() . + $this->generateHTMLFooter() + ); + $pdf->render(); + + // Write to file + fwrite($fileHandle, $pdf->output()); + + parent::restoreStateAfterSave($fileHandle); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Mpdf.php new file mode 100644 index 00000000000..fd2664a8238 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -0,0 +1,112 @@ +getSheetIndex()) { + $orientation = ($this->spreadsheet->getSheet(0)->getPageSetup()->getOrientation() + == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->spreadsheet->getSheet(0)->getPageSetup()->getPaperSize(); + } else { + $orientation = ($this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getOrientation() + == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getPaperSize(); + } + $this->setOrientation($orientation); + + // Override Page Orientation + if (null !== $this->getOrientation()) { + $orientation = ($this->getOrientation() == PageSetup::ORIENTATION_DEFAULT) + ? PageSetup::ORIENTATION_PORTRAIT + : $this->getOrientation(); + } + $orientation = strtoupper($orientation); + + // Override Paper Size + if (null !== $this->getPaperSize()) { + $printPaperSize = $this->getPaperSize(); + } + + if (isset(self::$paperSizes[$printPaperSize])) { + $paperSize = self::$paperSizes[$printPaperSize]; + } + + // Create PDF + $config = ['tempDir' => $this->tempDir]; + $pdf = $this->createExternalWriterInstance($config); + $ortmp = $orientation; + $pdf->_setPageSize(strtoupper($paperSize), $ortmp); + $pdf->DefOrientation = $orientation; + $pdf->AddPageByArray([ + 'orientation' => $orientation, + 'margin-left' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getLeft()), + 'margin-right' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getRight()), + 'margin-top' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getTop()), + 'margin-bottom' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getBottom()), + ]); + + // Document info + $pdf->SetTitle($this->spreadsheet->getProperties()->getTitle()); + $pdf->SetAuthor($this->spreadsheet->getProperties()->getCreator()); + $pdf->SetSubject($this->spreadsheet->getProperties()->getSubject()); + $pdf->SetKeywords($this->spreadsheet->getProperties()->getKeywords()); + $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); + + $pdf->WriteHTML($this->generateHTMLHeader(false)); + $html = $this->generateSheetData(); + foreach (\array_chunk(\explode(PHP_EOL, $html), 1000) as $lines) { + $pdf->WriteHTML(\implode(PHP_EOL, $lines)); + } + $pdf->WriteHTML($this->generateHTMLFooter()); + + // Write to file + fwrite($fileHandle, $pdf->Output('', 'S')); + + parent::restoreStateAfterSave($fileHandle); + } + + /** + * Convert inches to mm. + * + * @param float $inches + * + * @return float + */ + private function inchesToMm($inches) + { + return $inches * 25.4; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Tcpdf.php new file mode 100644 index 00000000000..8a97b8fed5d --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -0,0 +1,98 @@ +getSheetIndex() === null) { + $orientation = ($this->spreadsheet->getSheet(0)->getPageSetup()->getOrientation() + == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->spreadsheet->getSheet(0)->getPageSetup()->getPaperSize(); + $printMargins = $this->spreadsheet->getSheet(0)->getPageMargins(); + } else { + $orientation = ($this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getOrientation() + == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getPaperSize(); + $printMargins = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageMargins(); + } + + // Override Page Orientation + if ($this->getOrientation() !== null) { + $orientation = ($this->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) + ? 'L' + : 'P'; + } + // Override Paper Size + if ($this->getPaperSize() !== null) { + $printPaperSize = $this->getPaperSize(); + } + + if (isset(self::$paperSizes[$printPaperSize])) { + $paperSize = self::$paperSizes[$printPaperSize]; + } + + // Create PDF + $pdf = $this->createExternalWriterInstance($orientation, 'pt', $paperSize); + $pdf->setFontSubsetting(false); + // Set margins, converting inches to points (using 72 dpi) + $pdf->SetMargins($printMargins->getLeft() * 72, $printMargins->getTop() * 72, $printMargins->getRight() * 72); + $pdf->SetAutoPageBreak(true, $printMargins->getBottom() * 72); + + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + + $pdf->AddPage(); + + // Set the appropriate font + $pdf->SetFont($this->getFont()); + $pdf->writeHTML( + $this->generateHTMLHeader(false) . + $this->generateSheetData() . + $this->generateHTMLFooter() + ); + + // Document info + $pdf->SetTitle($this->spreadsheet->getProperties()->getTitle()); + $pdf->SetAuthor($this->spreadsheet->getProperties()->getCreator()); + $pdf->SetSubject($this->spreadsheet->getProperties()->getSubject()); + $pdf->SetKeywords($this->spreadsheet->getProperties()->getKeywords()); + $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); + + // Write to file + fwrite($fileHandle, $pdf->output($pFilename, 'S')); + + parent::restoreStateAfterSave($fileHandle); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls.php new file mode 100644 index 00000000000..6dff1342aed --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls.php @@ -0,0 +1,946 @@ +spreadsheet = $spreadsheet; + + $this->parser = new Xls\Parser(); + } + + /** + * Save Spreadsheet to file. + * + * @param string $pFilename + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + */ + public function save($pFilename) + { + // garbage collect + $this->spreadsheet->garbageCollect(); + + $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog(); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false); + $saveDateReturnType = Functions::getReturnDateType(); + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + + // initialize colors array + $this->colors = []; + + // Initialise workbook writer + $this->writerWorkbook = new Xls\Workbook($this->spreadsheet, $this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser); + + // Initialise worksheet writers + $countSheets = $this->spreadsheet->getSheetCount(); + for ($i = 0; $i < $countSheets; ++$i) { + $this->writerWorksheets[$i] = new Xls\Worksheet($this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser, $this->preCalculateFormulas, $this->spreadsheet->getSheet($i)); + } + + // build Escher objects. Escher objects for workbooks needs to be build before Escher object for workbook. + $this->buildWorksheetEschers(); + $this->buildWorkbookEscher(); + + // add 15 identical cell style Xfs + // for now, we use the first cellXf instead of cellStyleXf + $cellXfCollection = $this->spreadsheet->getCellXfCollection(); + for ($i = 0; $i < 15; ++$i) { + $this->writerWorkbook->addXfWriter($cellXfCollection[0], true); + } + + // add all the cell Xfs + foreach ($this->spreadsheet->getCellXfCollection() as $style) { + $this->writerWorkbook->addXfWriter($style, false); + } + + // add fonts from rich text eleemnts + for ($i = 0; $i < $countSheets; ++$i) { + foreach ($this->writerWorksheets[$i]->phpSheet->getCoordinates() as $coordinate) { + $cell = $this->writerWorksheets[$i]->phpSheet->getCell($coordinate); + $cVal = $cell->getValue(); + if ($cVal instanceof RichText) { + $elements = $cVal->getRichTextElements(); + foreach ($elements as $element) { + if ($element instanceof Run) { + $font = $element->getFont(); + $this->writerWorksheets[$i]->fontHashIndex[$font->getHashCode()] = $this->writerWorkbook->addFont($font); + } + } + } + } + } + + // initialize OLE file + $workbookStreamName = 'Workbook'; + $OLE = new File(OLE::ascToUcs($workbookStreamName)); + + // Write the worksheet streams before the global workbook stream, + // because the byte sizes of these are needed in the global workbook stream + $worksheetSizes = []; + for ($i = 0; $i < $countSheets; ++$i) { + $this->writerWorksheets[$i]->close(); + $worksheetSizes[] = $this->writerWorksheets[$i]->_datasize; + } + + // add binary data for global workbook stream + $OLE->append($this->writerWorkbook->writeWorkbook($worksheetSizes)); + + // add binary data for sheet streams + for ($i = 0; $i < $countSheets; ++$i) { + $OLE->append($this->writerWorksheets[$i]->getData()); + } + + $this->documentSummaryInformation = $this->writeDocumentSummaryInformation(); + // initialize OLE Document Summary Information + if (isset($this->documentSummaryInformation) && !empty($this->documentSummaryInformation)) { + $OLE_DocumentSummaryInformation = new File(OLE::ascToUcs(chr(5) . 'DocumentSummaryInformation')); + $OLE_DocumentSummaryInformation->append($this->documentSummaryInformation); + } + + $this->summaryInformation = $this->writeSummaryInformation(); + // initialize OLE Summary Information + if (isset($this->summaryInformation) && !empty($this->summaryInformation)) { + $OLE_SummaryInformation = new File(OLE::ascToUcs(chr(5) . 'SummaryInformation')); + $OLE_SummaryInformation->append($this->summaryInformation); + } + + // define OLE Parts + $arrRootData = [$OLE]; + // initialize OLE Properties file + if (isset($OLE_SummaryInformation)) { + $arrRootData[] = $OLE_SummaryInformation; + } + // initialize OLE Extended Properties file + if (isset($OLE_DocumentSummaryInformation)) { + $arrRootData[] = $OLE_DocumentSummaryInformation; + } + + $root = new Root(time(), time(), $arrRootData); + // save the OLE file + $root->save($pFilename); + + Functions::setReturnDateType($saveDateReturnType); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); + } + + /** + * Build the Worksheet Escher objects. + */ + private function buildWorksheetEschers() + { + // 1-based index to BstoreContainer + $blipIndex = 0; + $lastReducedSpId = 0; + $lastSpId = 0; + + foreach ($this->spreadsheet->getAllsheets() as $sheet) { + // sheet index + $sheetIndex = $sheet->getParent()->getIndex($sheet); + + $escher = null; + + // check if there are any shapes for this sheet + $filterRange = $sheet->getAutoFilter()->getRange(); + if (count($sheet->getDrawingCollection()) == 0 && empty($filterRange)) { + continue; + } + + // create intermediate Escher object + $escher = new Escher(); + + // dgContainer + $dgContainer = new DgContainer(); + + // set the drawing index (we use sheet index + 1) + $dgId = $sheet->getParent()->getIndex($sheet) + 1; + $dgContainer->setDgId($dgId); + $escher->setDgContainer($dgContainer); + + // spgrContainer + $spgrContainer = new SpgrContainer(); + $dgContainer->setSpgrContainer($spgrContainer); + + // add one shape which is the group shape + $spContainer = new SpContainer(); + $spContainer->setSpgr(true); + $spContainer->setSpType(0); + $spContainer->setSpId(($sheet->getParent()->getIndex($sheet) + 1) << 10); + $spgrContainer->addChild($spContainer); + + // add the shapes + + $countShapes[$sheetIndex] = 0; // count number of shapes (minus group shape), in sheet + + foreach ($sheet->getDrawingCollection() as $drawing) { + ++$blipIndex; + + ++$countShapes[$sheetIndex]; + + // add the shape + $spContainer = new SpContainer(); + + // set the shape type + $spContainer->setSpType(0x004B); + // set the shape flag + $spContainer->setSpFlag(0x02); + + // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index) + $reducedSpId = $countShapes[$sheetIndex]; + $spId = $reducedSpId | ($sheet->getParent()->getIndex($sheet) + 1) << 10; + $spContainer->setSpId($spId); + + // keep track of last reducedSpId + $lastReducedSpId = $reducedSpId; + + // keep track of last spId + $lastSpId = $spId; + + // set the BLIP index + $spContainer->setOPT(0x4104, $blipIndex); + + // set coordinates and offsets, client anchor + $coordinates = $drawing->getCoordinates(); + $offsetX = $drawing->getOffsetX(); + $offsetY = $drawing->getOffsetY(); + $width = $drawing->getWidth(); + $height = $drawing->getHeight(); + + $twoAnchor = \PhpOffice\PhpSpreadsheet\Shared\Xls::oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height); + + $spContainer->setStartCoordinates($twoAnchor['startCoordinates']); + $spContainer->setStartOffsetX($twoAnchor['startOffsetX']); + $spContainer->setStartOffsetY($twoAnchor['startOffsetY']); + $spContainer->setEndCoordinates($twoAnchor['endCoordinates']); + $spContainer->setEndOffsetX($twoAnchor['endOffsetX']); + $spContainer->setEndOffsetY($twoAnchor['endOffsetY']); + + $spgrContainer->addChild($spContainer); + } + + // AutoFilters + if (!empty($filterRange)) { + $rangeBounds = Coordinate::rangeBoundaries($filterRange); + $iNumColStart = $rangeBounds[0][0]; + $iNumColEnd = $rangeBounds[1][0]; + + $iInc = $iNumColStart; + while ($iInc <= $iNumColEnd) { + ++$countShapes[$sheetIndex]; + + // create an Drawing Object for the dropdown + $oDrawing = new BaseDrawing(); + // get the coordinates of drawing + $cDrawing = Coordinate::stringFromColumnIndex($iInc) . $rangeBounds[0][1]; + $oDrawing->setCoordinates($cDrawing); + $oDrawing->setWorksheet($sheet); + + // add the shape + $spContainer = new SpContainer(); + // set the shape type + $spContainer->setSpType(0x00C9); + // set the shape flag + $spContainer->setSpFlag(0x01); + + // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index) + $reducedSpId = $countShapes[$sheetIndex]; + $spId = $reducedSpId | ($sheet->getParent()->getIndex($sheet) + 1) << 10; + $spContainer->setSpId($spId); + + // keep track of last reducedSpId + $lastReducedSpId = $reducedSpId; + + // keep track of last spId + $lastSpId = $spId; + + $spContainer->setOPT(0x007F, 0x01040104); // Protection -> fLockAgainstGrouping + $spContainer->setOPT(0x00BF, 0x00080008); // Text -> fFitTextToShape + $spContainer->setOPT(0x01BF, 0x00010000); // Fill Style -> fNoFillHitTest + $spContainer->setOPT(0x01FF, 0x00080000); // Line Style -> fNoLineDrawDash + $spContainer->setOPT(0x03BF, 0x000A0000); // Group Shape -> fPrint + + // set coordinates and offsets, client anchor + $endCoordinates = Coordinate::stringFromColumnIndex($iInc); + $endCoordinates .= $rangeBounds[0][1] + 1; + + $spContainer->setStartCoordinates($cDrawing); + $spContainer->setStartOffsetX(0); + $spContainer->setStartOffsetY(0); + $spContainer->setEndCoordinates($endCoordinates); + $spContainer->setEndOffsetX(0); + $spContainer->setEndOffsetY(0); + + $spgrContainer->addChild($spContainer); + ++$iInc; + } + } + + // identifier clusters, used for workbook Escher object + $this->IDCLs[$dgId] = $lastReducedSpId; + + // set last shape index + $dgContainer->setLastSpId($lastSpId); + + // set the Escher object + $this->writerWorksheets[$sheetIndex]->setEscher($escher); + } + } + + /** + * Build the Escher object corresponding to the MSODRAWINGGROUP record. + */ + private function buildWorkbookEscher() + { + $escher = null; + + // any drawings in this workbook? + $found = false; + foreach ($this->spreadsheet->getAllSheets() as $sheet) { + if (count($sheet->getDrawingCollection()) > 0) { + $found = true; + + break; + } + } + + // nothing to do if there are no drawings + if (!$found) { + return; + } + + // if we reach here, then there are drawings in the workbook + $escher = new Escher(); + + // dggContainer + $dggContainer = new DggContainer(); + $escher->setDggContainer($dggContainer); + + // set IDCLs (identifier clusters) + $dggContainer->setIDCLs($this->IDCLs); + + // this loop is for determining maximum shape identifier of all drawing + $spIdMax = 0; + $totalCountShapes = 0; + $countDrawings = 0; + + foreach ($this->spreadsheet->getAllsheets() as $sheet) { + $sheetCountShapes = 0; // count number of shapes (minus group shape), in sheet + + if (count($sheet->getDrawingCollection()) > 0) { + ++$countDrawings; + + foreach ($sheet->getDrawingCollection() as $drawing) { + ++$sheetCountShapes; + ++$totalCountShapes; + + $spId = $sheetCountShapes | ($this->spreadsheet->getIndex($sheet) + 1) << 10; + $spIdMax = max($spId, $spIdMax); + } + } + } + + $dggContainer->setSpIdMax($spIdMax + 1); + $dggContainer->setCDgSaved($countDrawings); + $dggContainer->setCSpSaved($totalCountShapes + $countDrawings); // total number of shapes incl. one group shapes per drawing + + // bstoreContainer + $bstoreContainer = new BstoreContainer(); + $dggContainer->setBstoreContainer($bstoreContainer); + + // the BSE's (all the images) + foreach ($this->spreadsheet->getAllsheets() as $sheet) { + foreach ($sheet->getDrawingCollection() as $drawing) { + if (!extension_loaded('gd')) { + throw new RuntimeException('Saving images in xls requires gd extension'); + } + if ($drawing instanceof Drawing) { + $filename = $drawing->getPath(); + + list($imagesx, $imagesy, $imageFormat) = getimagesize($filename); + + switch ($imageFormat) { + case 1: // GIF, not supported by BIFF8, we convert to PNG + $blipType = BSE::BLIPTYPE_PNG; + ob_start(); + imagepng(imagecreatefromgif($filename)); + $blipData = ob_get_contents(); + ob_end_clean(); + + break; + case 2: // JPEG + $blipType = BSE::BLIPTYPE_JPEG; + $blipData = file_get_contents($filename); + + break; + case 3: // PNG + $blipType = BSE::BLIPTYPE_PNG; + $blipData = file_get_contents($filename); + + break; + case 6: // Windows DIB (BMP), we convert to PNG + $blipType = BSE::BLIPTYPE_PNG; + ob_start(); + imagepng(SharedDrawing::imagecreatefrombmp($filename)); + $blipData = ob_get_contents(); + ob_end_clean(); + + break; + default: + continue 2; + } + + $blip = new Blip(); + $blip->setData($blipData); + + $BSE = new BSE(); + $BSE->setBlipType($blipType); + $BSE->setBlip($blip); + + $bstoreContainer->addBSE($BSE); + } elseif ($drawing instanceof MemoryDrawing) { + switch ($drawing->getRenderingFunction()) { + case MemoryDrawing::RENDERING_JPEG: + $blipType = BSE::BLIPTYPE_JPEG; + $renderingFunction = 'imagejpeg'; + + break; + case MemoryDrawing::RENDERING_GIF: + case MemoryDrawing::RENDERING_PNG: + case MemoryDrawing::RENDERING_DEFAULT: + $blipType = BSE::BLIPTYPE_PNG; + $renderingFunction = 'imagepng'; + + break; + } + + ob_start(); + call_user_func($renderingFunction, $drawing->getImageResource()); + $blipData = ob_get_contents(); + ob_end_clean(); + + $blip = new Blip(); + $blip->setData($blipData); + + $BSE = new BSE(); + $BSE->setBlipType($blipType); + $BSE->setBlip($blip); + + $bstoreContainer->addBSE($BSE); + } + } + } + + // Set the Escher object + $this->writerWorkbook->setEscher($escher); + } + + /** + * Build the OLE Part for DocumentSummary Information. + * + * @return string + */ + private function writeDocumentSummaryInformation() + { + // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark) + $data = pack('v', 0xFFFE); + // offset: 2; size: 2; + $data .= pack('v', 0x0000); + // offset: 4; size: 2; OS version + $data .= pack('v', 0x0106); + // offset: 6; size: 2; OS indicator + $data .= pack('v', 0x0002); + // offset: 8; size: 16 + $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00); + // offset: 24; size: 4; section count + $data .= pack('V', 0x0001); + + // offset: 28; size: 16; first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae + $data .= pack('vvvvvvvv', 0xD502, 0xD5CD, 0x2E9C, 0x101B, 0x9793, 0x0008, 0x2C2B, 0xAEF9); + // offset: 44; size: 4; offset of the start + $data .= pack('V', 0x30); + + // SECTION + $dataSection = []; + $dataSection_NumProps = 0; + $dataSection_Summary = ''; + $dataSection_Content = ''; + + // GKPIDDSI_CODEPAGE: CodePage + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x01], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer + 'data' => ['data' => 1252], + ]; + ++$dataSection_NumProps; + + // GKPIDDSI_CATEGORY : Category + if ($this->spreadsheet->getProperties()->getCategory()) { + $dataProp = $this->spreadsheet->getProperties()->getCategory(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x02], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x1E], + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + // GKPIDDSI_VERSION :Version of the application that wrote the property storage + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x17], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x03], + 'data' => ['pack' => 'V', 'data' => 0x000C0000], + ]; + ++$dataSection_NumProps; + // GKPIDDSI_SCALE : FALSE + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x0B], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x0B], + 'data' => ['data' => false], + ]; + ++$dataSection_NumProps; + // GKPIDDSI_LINKSDIRTY : True if any of the values for the linked properties have changed outside of the application + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x10], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x0B], + 'data' => ['data' => false], + ]; + ++$dataSection_NumProps; + // GKPIDDSI_SHAREDOC : FALSE + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x13], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x0B], + 'data' => ['data' => false], + ]; + ++$dataSection_NumProps; + // GKPIDDSI_HYPERLINKSCHANGED : True if any of the values for the _PID_LINKS (hyperlink text) have changed outside of the application + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x16], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x0B], + 'data' => ['data' => false], + ]; + ++$dataSection_NumProps; + + // GKPIDDSI_DOCSPARTS + // MS-OSHARED p75 (2.3.3.2.2.1) + // Structure is VtVecUnalignedLpstrValue (2.3.3.1.9) + // cElements + $dataProp = pack('v', 0x0001); + $dataProp .= pack('v', 0x0000); + // array of UnalignedLpstr + // cch + $dataProp .= pack('v', 0x000A); + $dataProp .= pack('v', 0x0000); + // value + $dataProp .= 'Worksheet' . chr(0); + + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x0D], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x101E], + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + + // GKPIDDSI_HEADINGPAIR + // VtVecHeadingPairValue + // cElements + $dataProp = pack('v', 0x0002); + $dataProp .= pack('v', 0x0000); + // Array of vtHeadingPair + // vtUnalignedString - headingString + // stringType + $dataProp .= pack('v', 0x001E); + // padding + $dataProp .= pack('v', 0x0000); + // UnalignedLpstr + // cch + $dataProp .= pack('v', 0x0013); + $dataProp .= pack('v', 0x0000); + // value + $dataProp .= 'Feuilles de calcul'; + // vtUnalignedString - headingParts + // wType : 0x0003 = 32 bit signed integer + $dataProp .= pack('v', 0x0300); + // padding + $dataProp .= pack('v', 0x0000); + // value + $dataProp .= pack('v', 0x0100); + $dataProp .= pack('v', 0x0000); + $dataProp .= pack('v', 0x0000); + $dataProp .= pack('v', 0x0000); + + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x0C], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x100C], + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + + // 4 Section Length + // 4 Property count + // 8 * $dataSection_NumProps (8 = ID (4) + OffSet(4)) + $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8; + foreach ($dataSection as $dataProp) { + // Summary + $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']); + // Offset + $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset); + // DataType + $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']); + // Data + if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer + $dataSection_Content .= pack('V', $dataProp['data']['data']); + + $dataSection_Content_Offset += 4 + 4; + } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer + $dataSection_Content .= pack('V', $dataProp['data']['data']); + + $dataSection_Content_Offset += 4 + 4; + } elseif ($dataProp['type']['data'] == 0x0B) { // Boolean + if ($dataProp['data']['data'] == false) { + $dataSection_Content .= pack('V', 0x0000); + } else { + $dataSection_Content .= pack('V', 0x0001); + } + $dataSection_Content_Offset += 4 + 4; + } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length + // Null-terminated string + $dataProp['data']['data'] .= chr(0); + $dataProp['data']['length'] += 1; + // Complete the string with null string for being a %4 + $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4)); + $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT); + + $dataSection_Content .= pack('V', $dataProp['data']['length']); + $dataSection_Content .= $dataProp['data']['data']; + + $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']); + } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + $dataSection_Content .= $dataProp['data']['data']; + + $dataSection_Content_Offset += 4 + 8; + } else { + // Data Type Not Used at the moment + $dataSection_Content .= $dataProp['data']['data']; + + $dataSection_Content_Offset += 4 + $dataProp['data']['length']; + } + } + // Now $dataSection_Content_Offset contains the size of the content + + // section header + // offset: $secOffset; size: 4; section length + // + x Size of the content (summary + content) + $data .= pack('V', $dataSection_Content_Offset); + // offset: $secOffset+4; size: 4; property count + $data .= pack('V', $dataSection_NumProps); + // Section Summary + $data .= $dataSection_Summary; + // Section Content + $data .= $dataSection_Content; + + return $data; + } + + /** + * Build the OLE Part for Summary Information. + * + * @return string + */ + private function writeSummaryInformation() + { + // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark) + $data = pack('v', 0xFFFE); + // offset: 2; size: 2; + $data .= pack('v', 0x0000); + // offset: 4; size: 2; OS version + $data .= pack('v', 0x0106); + // offset: 6; size: 2; OS indicator + $data .= pack('v', 0x0002); + // offset: 8; size: 16 + $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00); + // offset: 24; size: 4; section count + $data .= pack('V', 0x0001); + + // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9 + $data .= pack('vvvvvvvv', 0x85E0, 0xF29F, 0x4FF9, 0x1068, 0x91AB, 0x0008, 0x272B, 0xD9B3); + // offset: 44; size: 4; offset of the start + $data .= pack('V', 0x30); + + // SECTION + $dataSection = []; + $dataSection_NumProps = 0; + $dataSection_Summary = ''; + $dataSection_Content = ''; + + // CodePage : CP-1252 + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x01], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer + 'data' => ['data' => 1252], + ]; + ++$dataSection_NumProps; + + // Title + if ($this->spreadsheet->getProperties()->getTitle()) { + $dataProp = $this->spreadsheet->getProperties()->getTitle(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x02], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Subject + if ($this->spreadsheet->getProperties()->getSubject()) { + $dataProp = $this->spreadsheet->getProperties()->getSubject(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x03], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Author (Creator) + if ($this->spreadsheet->getProperties()->getCreator()) { + $dataProp = $this->spreadsheet->getProperties()->getCreator(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x04], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Keywords + if ($this->spreadsheet->getProperties()->getKeywords()) { + $dataProp = $this->spreadsheet->getProperties()->getKeywords(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x05], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Comments (Description) + if ($this->spreadsheet->getProperties()->getDescription()) { + $dataProp = $this->spreadsheet->getProperties()->getDescription(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x06], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Last Saved By (LastModifiedBy) + if ($this->spreadsheet->getProperties()->getLastModifiedBy()) { + $dataProp = $this->spreadsheet->getProperties()->getLastModifiedBy(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x08], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x1E], // null-terminated string prepended by dword string length + 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Created Date/Time + if ($this->spreadsheet->getProperties()->getCreated()) { + $dataProp = $this->spreadsheet->getProperties()->getCreated(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x0C], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x40], // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + 'data' => ['data' => OLE::localDateToOLE($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Modified Date/Time + if ($this->spreadsheet->getProperties()->getModified()) { + $dataProp = $this->spreadsheet->getProperties()->getModified(); + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x0D], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x40], // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + 'data' => ['data' => OLE::localDateToOLE($dataProp)], + ]; + ++$dataSection_NumProps; + } + // Security + $dataSection[] = [ + 'summary' => ['pack' => 'V', 'data' => 0x13], + 'offset' => ['pack' => 'V'], + 'type' => ['pack' => 'V', 'data' => 0x03], // 4 byte signed integer + 'data' => ['data' => 0x00], + ]; + ++$dataSection_NumProps; + + // 4 Section Length + // 4 Property count + // 8 * $dataSection_NumProps (8 = ID (4) + OffSet(4)) + $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8; + foreach ($dataSection as $dataProp) { + // Summary + $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']); + // Offset + $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset); + // DataType + $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']); + // Data + if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer + $dataSection_Content .= pack('V', $dataProp['data']['data']); + + $dataSection_Content_Offset += 4 + 4; + } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer + $dataSection_Content .= pack('V', $dataProp['data']['data']); + + $dataSection_Content_Offset += 4 + 4; + } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length + // Null-terminated string + $dataProp['data']['data'] .= chr(0); + $dataProp['data']['length'] += 1; + // Complete the string with null string for being a %4 + $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4)); + $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT); + + $dataSection_Content .= pack('V', $dataProp['data']['length']); + $dataSection_Content .= $dataProp['data']['data']; + + $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']); + } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + $dataSection_Content .= $dataProp['data']['data']; + + $dataSection_Content_Offset += 4 + 8; + } + // Data Type Not Used at the moment + } + // Now $dataSection_Content_Offset contains the size of the content + + // section header + // offset: $secOffset; size: 4; section length + // + x Size of the content (summary + content) + $data .= pack('V', $dataSection_Content_Offset); + // offset: $secOffset+4; size: 4; property count + $data .= pack('V', $dataSection_NumProps); + // Section Summary + $data .= $dataSection_Summary; + // Section Content + $data .= $dataSection_Content; + + return $data; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/BIFFwriter.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/BIFFwriter.php new file mode 100644 index 00000000000..3b2eb9af273 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/BIFFwriter.php @@ -0,0 +1,224 @@ + +// * +// * The majority of this is _NOT_ my code. I simply ported it from the +// * PERL Spreadsheet::WriteExcel module. +// * +// * The author of the Spreadsheet::WriteExcel module is John McNamara +// * +// * +// * I _DO_ maintain this code, and John McNamara has nothing to do with the +// * porting of this code to PHP. Any questions directly related to this +// * class library should be directed to me. +// * +// * License Information: +// * +// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets +// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com +// * +// * This library is free software; you can redistribute it and/or +// * modify it under the terms of the GNU Lesser General Public +// * License as published by the Free Software Foundation; either +// * version 2.1 of the License, or (at your option) any later version. +// * +// * This library is distributed in the hope that it will be useful, +// * but WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Lesser General Public License for more details. +// * +// * You should have received a copy of the GNU Lesser General Public +// * License along with this library; if not, write to the Free Software +// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// */ +class BIFFwriter +{ + /** + * The byte order of this architecture. 0 => little endian, 1 => big endian. + * + * @var int + */ + private static $byteOrder; + + /** + * The string containing the data of the BIFF stream. + * + * @var string + */ + public $_data; + + /** + * The size of the data in bytes. Should be the same as strlen($this->_data). + * + * @var int + */ + public $_datasize; + + /** + * The maximum length for a BIFF record (excluding record header and length field). See addContinue(). + * + * @var int + * + * @see addContinue() + */ + private $limit = 8224; + + /** + * Constructor. + */ + public function __construct() + { + $this->_data = ''; + $this->_datasize = 0; + } + + /** + * Determine the byte order and store it as class data to avoid + * recalculating it for each call to new(). + * + * @return int + */ + public static function getByteOrder() + { + if (!isset(self::$byteOrder)) { + // Check if "pack" gives the required IEEE 64bit float + $teststr = pack('d', 1.2345); + $number = pack('C8', 0x8D, 0x97, 0x6E, 0x12, 0x83, 0xC0, 0xF3, 0x3F); + if ($number == $teststr) { + $byte_order = 0; // Little Endian + } elseif ($number == strrev($teststr)) { + $byte_order = 1; // Big Endian + } else { + // Give up. I'll fix this in a later version. + throw new WriterException('Required floating point format not supported on this platform.'); + } + self::$byteOrder = $byte_order; + } + + return self::$byteOrder; + } + + /** + * General storage function. + * + * @param string $data binary data to append + */ + protected function append($data) + { + if (strlen($data) - 4 > $this->limit) { + $data = $this->addContinue($data); + } + $this->_data .= $data; + $this->_datasize += strlen($data); + } + + /** + * General storage function like append, but returns string instead of modifying $this->_data. + * + * @param string $data binary data to write + * + * @return string + */ + public function writeData($data) + { + if (strlen($data) - 4 > $this->limit) { + $data = $this->addContinue($data); + } + $this->_datasize += strlen($data); + + return $data; + } + + /** + * Writes Excel BOF record to indicate the beginning of a stream or + * sub-stream in the BIFF file. + * + * @param int $type type of BIFF file to write: 0x0005 Workbook, + * 0x0010 Worksheet + */ + protected function storeBof($type) + { + $record = 0x0809; // Record identifier (BIFF5-BIFF8) + $length = 0x0010; + + // by inspection of real files, MS Office Excel 2007 writes the following + $unknown = pack('VV', 0x000100D1, 0x00000406); + + $build = 0x0DBB; // Excel 97 + $year = 0x07CC; // Excel 97 + + $version = 0x0600; // BIFF8 + + $header = pack('vv', $record, $length); + $data = pack('vvvv', $version, $type, $build, $year); + $this->append($header . $data . $unknown); + } + + /** + * Writes Excel EOF record to indicate the end of a BIFF stream. + */ + protected function storeEof() + { + $record = 0x000A; // Record identifier + $length = 0x0000; // Number of bytes to follow + + $header = pack('vv', $record, $length); + $this->append($header); + } + + /** + * Writes Excel EOF record to indicate the end of a BIFF stream. + */ + public function writeEof() + { + $record = 0x000A; // Record identifier + $length = 0x0000; // Number of bytes to follow + $header = pack('vv', $record, $length); + + return $this->writeData($header); + } + + /** + * Excel limits the size of BIFF records. In Excel 5 the limit is 2084 bytes. In + * Excel 97 the limit is 8228 bytes. Records that are longer than these limits + * must be split up into CONTINUE blocks. + * + * This function takes a long BIFF record and inserts CONTINUE records as + * necessary. + * + * @param string $data The original binary data to be written + * + * @return string A very convenient string of continue blocks + */ + private function addContinue($data) + { + $limit = $this->limit; + $record = 0x003C; // Record identifier + + // The first 2080/8224 bytes remain intact. However, we have to change + // the length field of the record. + $tmp = substr($data, 0, 2) . pack('v', $limit) . substr($data, 4, $limit); + + $header = pack('vv', $record, $limit); // Headers for continue records + + // Retrieve chunks of 2080/8224 bytes +4 for the header. + $data_length = strlen($data); + for ($i = $limit + 4; $i < ($data_length - $limit); $i += $limit) { + $tmp .= $header; + $tmp .= substr($data, $i, $limit); + } + + // Retrieve the last chunk of data + $header = pack('vv', $record, strlen($data) - $i); + $tmp .= $header; + $tmp .= substr($data, $i); + + return $tmp; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Escher.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Escher.php new file mode 100644 index 00000000000..1dcef8072fc --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Escher.php @@ -0,0 +1,510 @@ +object = $object; + } + + /** + * Process the object to be written. + * + * @return string + */ + public function close() + { + // initialize + $this->data = ''; + + switch (get_class($this->object)) { + case \PhpOffice\PhpSpreadsheet\Shared\Escher::class: + if ($dggContainer = $this->object->getDggContainer()) { + $writer = new self($dggContainer); + $this->data = $writer->close(); + } elseif ($dgContainer = $this->object->getDgContainer()) { + $writer = new self($dgContainer); + $this->data = $writer->close(); + $this->spOffsets = $writer->getSpOffsets(); + $this->spTypes = $writer->getSpTypes(); + } + + break; + case DggContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // write the dgg + $recVer = 0x0; + $recInstance = 0x0000; + $recType = 0xF006; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + // dgg data + $dggData = + pack( + 'VVVV', + $this->object->getSpIdMax(), // maximum shape identifier increased by one + $this->object->getCDgSaved() + 1, // number of file identifier clusters increased by one + $this->object->getCSpSaved(), + $this->object->getCDgSaved() // count total number of drawings saved + ); + + // add file identifier clusters (one per drawing) + $IDCLs = $this->object->getIDCLs(); + + foreach ($IDCLs as $dgId => $maxReducedSpId) { + $dggData .= pack('VV', $dgId, $maxReducedSpId + 1); + } + + $header = pack('vvV', $recVerInstance, $recType, strlen($dggData)); + $innerData .= $header . $dggData; + + // write the bstoreContainer + if ($bstoreContainer = $this->object->getBstoreContainer()) { + $writer = new self($bstoreContainer); + $innerData .= $writer->close(); + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF000; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + + break; + case BstoreContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // treat the inner data + if ($BSECollection = $this->object->getBSECollection()) { + foreach ($BSECollection as $BSE) { + $writer = new self($BSE); + $innerData .= $writer->close(); + } + } + + // write the record + $recVer = 0xF; + $recInstance = count($this->object->getBSECollection()); + $recType = 0xF001; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + + break; + case BSE::class: + // this is a semi-container record + + // initialize + $innerData = ''; + + // here we treat the inner data + if ($blip = $this->object->getBlip()) { + $writer = new self($blip); + $innerData .= $writer->close(); + } + + // initialize + $data = ''; + + $btWin32 = $this->object->getBlipType(); + $btMacOS = $this->object->getBlipType(); + $data .= pack('CC', $btWin32, $btMacOS); + + $rgbUid = pack('VVVV', 0, 0, 0, 0); // todo + $data .= $rgbUid; + + $tag = 0; + $size = strlen($innerData); + $cRef = 1; + $foDelay = 0; //todo + $unused1 = 0x0; + $cbName = 0x0; + $unused2 = 0x0; + $unused3 = 0x0; + $data .= pack('vVVVCCCC', $tag, $size, $cRef, $foDelay, $unused1, $cbName, $unused2, $unused3); + + $data .= $innerData; + + // write the record + $recVer = 0x2; + $recInstance = $this->object->getBlipType(); + $recType = 0xF007; + $length = strlen($data); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header; + + $this->data .= $data; + + break; + case Blip::class: + // this is an atom record + + // write the record + switch ($this->object->getParent()->getBlipType()) { + case BSE::BLIPTYPE_JPEG: + // initialize + $innerData = ''; + + $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo + $innerData .= $rgbUid1; + + $tag = 0xFF; // todo + $innerData .= pack('C', $tag); + + $innerData .= $this->object->getData(); + + $recVer = 0x0; + $recInstance = 0x46A; + $recType = 0xF01D; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header; + + $this->data .= $innerData; + + break; + case BSE::BLIPTYPE_PNG: + // initialize + $innerData = ''; + + $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo + $innerData .= $rgbUid1; + + $tag = 0xFF; // todo + $innerData .= pack('C', $tag); + + $innerData .= $this->object->getData(); + + $recVer = 0x0; + $recInstance = 0x6E0; + $recType = 0xF01E; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header; + + $this->data .= $innerData; + + break; + } + + break; + case DgContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // write the dg + $recVer = 0x0; + $recInstance = $this->object->getDgId(); + $recType = 0xF008; + $length = 8; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + // number of shapes in this drawing (including group shape) + $countShapes = count($this->object->getSpgrContainer()->getChildren()); + $innerData .= $header . pack('VV', $countShapes, $this->object->getLastSpId()); + + // write the spgrContainer + if ($spgrContainer = $this->object->getSpgrContainer()) { + $writer = new self($spgrContainer); + $innerData .= $writer->close(); + + // get the shape offsets relative to the spgrContainer record + $spOffsets = $writer->getSpOffsets(); + $spTypes = $writer->getSpTypes(); + + // save the shape offsets relative to dgContainer + foreach ($spOffsets as &$spOffset) { + $spOffset += 24; // add length of dgContainer header data (8 bytes) plus dg data (16 bytes) + } + + $this->spOffsets = $spOffsets; + $this->spTypes = $spTypes; + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF002; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + + break; + case SpgrContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // initialize spape offsets + $totalSize = 8; + $spOffsets = []; + $spTypes = []; + + // treat the inner data + foreach ($this->object->getChildren() as $spContainer) { + $writer = new self($spContainer); + $spData = $writer->close(); + $innerData .= $spData; + + // save the shape offsets (where new shape records begin) + $totalSize += strlen($spData); + $spOffsets[] = $totalSize; + + $spTypes = array_merge($spTypes, $writer->getSpTypes()); + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF003; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + $this->spOffsets = $spOffsets; + $this->spTypes = $spTypes; + + break; + case SpContainer::class: + // initialize + $data = ''; + + // build the data + + // write group shape record, if necessary? + if ($this->object->getSpgr()) { + $recVer = 0x1; + $recInstance = 0x0000; + $recType = 0xF009; + $length = 0x00000010; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $data .= $header . pack('VVVV', 0, 0, 0, 0); + } + $this->spTypes[] = ($this->object->getSpType()); + + // write the shape record + $recVer = 0x2; + $recInstance = $this->object->getSpType(); // shape type + $recType = 0xF00A; + $length = 0x00000008; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $data .= $header . pack('VV', $this->object->getSpId(), $this->object->getSpgr() ? 0x0005 : 0x0A00); + + // the options + if ($this->object->getOPTCollection()) { + $optData = ''; + + $recVer = 0x3; + $recInstance = count($this->object->getOPTCollection()); + $recType = 0xF00B; + foreach ($this->object->getOPTCollection() as $property => $value) { + $optData .= pack('vV', $property, $value); + } + $length = strlen($optData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + $data .= $header . $optData; + } + + // the client anchor + if ($this->object->getStartCoordinates()) { + $clientAnchorData = ''; + + $recVer = 0x0; + $recInstance = 0x0; + $recType = 0xF010; + + // start coordinates + list($column, $row) = Coordinate::coordinateFromString($this->object->getStartCoordinates()); + $c1 = Coordinate::columnIndexFromString($column) - 1; + $r1 = $row - 1; + + // start offsetX + $startOffsetX = $this->object->getStartOffsetX(); + + // start offsetY + $startOffsetY = $this->object->getStartOffsetY(); + + // end coordinates + list($column, $row) = Coordinate::coordinateFromString($this->object->getEndCoordinates()); + $c2 = Coordinate::columnIndexFromString($column) - 1; + $r2 = $row - 1; + + // end offsetX + $endOffsetX = $this->object->getEndOffsetX(); + + // end offsetY + $endOffsetY = $this->object->getEndOffsetY(); + + $clientAnchorData = pack('vvvvvvvvv', $this->object->getSpFlag(), $c1, $startOffsetX, $r1, $startOffsetY, $c2, $endOffsetX, $r2, $endOffsetY); + + $length = strlen($clientAnchorData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + $data .= $header . $clientAnchorData; + } + + // the client data, just empty for now + if (!$this->object->getSpgr()) { + $clientDataData = ''; + + $recVer = 0x0; + $recInstance = 0x0; + $recType = 0xF011; + + $length = strlen($clientDataData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + $data .= $header . $clientDataData; + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF004; + $length = strlen($data); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $data; + + break; + } + + return $this->data; + } + + /** + * Gets the shape offsets. + * + * @return array + */ + public function getSpOffsets() + { + return $this->spOffsets; + } + + /** + * Gets the shape types. + * + * @return array + */ + public function getSpTypes() + { + return $this->spTypes; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Font.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Font.php new file mode 100644 index 00000000000..df37dcb56c5 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Font.php @@ -0,0 +1,149 @@ +colorIndex = 0x7FFF; + $this->font = $font; + } + + /** + * Set the color index. + * + * @param int $colorIndex + */ + public function setColorIndex($colorIndex) + { + $this->colorIndex = $colorIndex; + } + + /** + * Get font record data. + * + * @return string + */ + public function writeFont() + { + $font_outline = 0; + $font_shadow = 0; + + $icv = $this->colorIndex; // Index to color palette + if ($this->font->getSuperscript()) { + $sss = 1; + } elseif ($this->font->getSubscript()) { + $sss = 2; + } else { + $sss = 0; + } + $bFamily = 0; // Font family + $bCharSet = \PhpOffice\PhpSpreadsheet\Shared\Font::getCharsetFromFontName($this->font->getName()); // Character set + + $record = 0x31; // Record identifier + $reserved = 0x00; // Reserved + $grbit = 0x00; // Font attributes + if ($this->font->getItalic()) { + $grbit |= 0x02; + } + if ($this->font->getStrikethrough()) { + $grbit |= 0x08; + } + if ($font_outline) { + $grbit |= 0x10; + } + if ($font_shadow) { + $grbit |= 0x20; + } + + $data = pack( + 'vvvvvCCCC', + // Fontsize (in twips) + $this->font->getSize() * 20, + $grbit, + // Colour + $icv, + // Font weight + self::mapBold($this->font->getBold()), + // Superscript/Subscript + $sss, + self::mapUnderline($this->font->getUnderline()), + $bFamily, + $bCharSet, + $reserved + ); + $data .= StringHelper::UTF8toBIFF8UnicodeShort($this->font->getName()); + + $length = strlen($data); + $header = pack('vv', $record, $length); + + return $header . $data; + } + + /** + * Map to BIFF5-BIFF8 codes for bold. + * + * @param bool $bold + * + * @return int + */ + private static function mapBold($bold) + { + if ($bold) { + return 0x2BC; // 700 = Bold font weight + } + + return 0x190; // 400 = Normal font weight + } + + /** + * Map of BIFF2-BIFF8 codes for underline styles. + * + * @var array of int + */ + private static $mapUnderline = [ + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE => 0x00, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE => 0x01, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE => 0x02, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING => 0x21, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING => 0x22, + ]; + + /** + * Map underline. + * + * @param string $underline + * + * @return int + */ + private static function mapUnderline($underline) + { + if (isset(self::$mapUnderline[$underline])) { + return self::$mapUnderline[$underline]; + } + + return 0x00; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Parser.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Parser.php new file mode 100644 index 00000000000..e87d09a2233 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Parser.php @@ -0,0 +1,1447 @@ +=,;#()"{} + const REGEX_SHEET_TITLE_UNQUOTED = '[^\*\:\/\\\\\?\[\]\+\-\% \\\'\^\&\<\>\=\,\;\#\(\)\"\{\}]+'; + + // Sheet title in quoted form (without surrounding quotes) + // Invalid sheet title characters cannot occur in the sheet title: + // *:/\?[] (usual invalid sheet title characters) + // Single quote is represented as a pair '' + const REGEX_SHEET_TITLE_QUOTED = '(([^\*\:\/\\\\\?\[\]\\\'])+|(\\\'\\\')+)+'; + + /** + * The index of the character we are currently looking at. + * + * @var int + */ + public $currentCharacter; + + /** + * The token we are working on. + * + * @var string + */ + public $currentToken; + + /** + * The formula to parse. + * + * @var string + */ + private $formula; + + /** + * The character ahead of the current char. + * + * @var string + */ + public $lookAhead; + + /** + * The parse tree to be generated. + * + * @var string + */ + private $parseTree; + + /** + * Array of external sheets. + * + * @var array + */ + private $externalSheets; + + /** + * Array of sheet references in the form of REF structures. + * + * @var array + */ + public $references; + + /** + * The Excel ptg indices. + * + * @var array + */ + private $ptg = [ + 'ptgExp' => 0x01, + 'ptgTbl' => 0x02, + 'ptgAdd' => 0x03, + 'ptgSub' => 0x04, + 'ptgMul' => 0x05, + 'ptgDiv' => 0x06, + 'ptgPower' => 0x07, + 'ptgConcat' => 0x08, + 'ptgLT' => 0x09, + 'ptgLE' => 0x0A, + 'ptgEQ' => 0x0B, + 'ptgGE' => 0x0C, + 'ptgGT' => 0x0D, + 'ptgNE' => 0x0E, + 'ptgIsect' => 0x0F, + 'ptgUnion' => 0x10, + 'ptgRange' => 0x11, + 'ptgUplus' => 0x12, + 'ptgUminus' => 0x13, + 'ptgPercent' => 0x14, + 'ptgParen' => 0x15, + 'ptgMissArg' => 0x16, + 'ptgStr' => 0x17, + 'ptgAttr' => 0x19, + 'ptgSheet' => 0x1A, + 'ptgEndSheet' => 0x1B, + 'ptgErr' => 0x1C, + 'ptgBool' => 0x1D, + 'ptgInt' => 0x1E, + 'ptgNum' => 0x1F, + 'ptgArray' => 0x20, + 'ptgFunc' => 0x21, + 'ptgFuncVar' => 0x22, + 'ptgName' => 0x23, + 'ptgRef' => 0x24, + 'ptgArea' => 0x25, + 'ptgMemArea' => 0x26, + 'ptgMemErr' => 0x27, + 'ptgMemNoMem' => 0x28, + 'ptgMemFunc' => 0x29, + 'ptgRefErr' => 0x2A, + 'ptgAreaErr' => 0x2B, + 'ptgRefN' => 0x2C, + 'ptgAreaN' => 0x2D, + 'ptgMemAreaN' => 0x2E, + 'ptgMemNoMemN' => 0x2F, + 'ptgNameX' => 0x39, + 'ptgRef3d' => 0x3A, + 'ptgArea3d' => 0x3B, + 'ptgRefErr3d' => 0x3C, + 'ptgAreaErr3d' => 0x3D, + 'ptgArrayV' => 0x40, + 'ptgFuncV' => 0x41, + 'ptgFuncVarV' => 0x42, + 'ptgNameV' => 0x43, + 'ptgRefV' => 0x44, + 'ptgAreaV' => 0x45, + 'ptgMemAreaV' => 0x46, + 'ptgMemErrV' => 0x47, + 'ptgMemNoMemV' => 0x48, + 'ptgMemFuncV' => 0x49, + 'ptgRefErrV' => 0x4A, + 'ptgAreaErrV' => 0x4B, + 'ptgRefNV' => 0x4C, + 'ptgAreaNV' => 0x4D, + 'ptgMemAreaNV' => 0x4E, + 'ptgMemNoMemNV' => 0x4F, + 'ptgFuncCEV' => 0x58, + 'ptgNameXV' => 0x59, + 'ptgRef3dV' => 0x5A, + 'ptgArea3dV' => 0x5B, + 'ptgRefErr3dV' => 0x5C, + 'ptgAreaErr3dV' => 0x5D, + 'ptgArrayA' => 0x60, + 'ptgFuncA' => 0x61, + 'ptgFuncVarA' => 0x62, + 'ptgNameA' => 0x63, + 'ptgRefA' => 0x64, + 'ptgAreaA' => 0x65, + 'ptgMemAreaA' => 0x66, + 'ptgMemErrA' => 0x67, + 'ptgMemNoMemA' => 0x68, + 'ptgMemFuncA' => 0x69, + 'ptgRefErrA' => 0x6A, + 'ptgAreaErrA' => 0x6B, + 'ptgRefNA' => 0x6C, + 'ptgAreaNA' => 0x6D, + 'ptgMemAreaNA' => 0x6E, + 'ptgMemNoMemNA' => 0x6F, + 'ptgFuncCEA' => 0x78, + 'ptgNameXA' => 0x79, + 'ptgRef3dA' => 0x7A, + 'ptgArea3dA' => 0x7B, + 'ptgRefErr3dA' => 0x7C, + 'ptgAreaErr3dA' => 0x7D, + ]; + + /** + * Thanks to Michael Meeks and Gnumeric for the initial arg values. + * + * The following hash was generated by "function_locale.pl" in the distro. + * Refer to function_locale.pl for non-English function names. + * + * The array elements are as follow: + * ptg: The Excel function ptg code. + * args: The number of arguments that the function takes: + * >=0 is a fixed number of arguments. + * -1 is a variable number of arguments. + * class: The reference, value or array class of the function args. + * vol: The function is volatile. + * + * @var array + */ + private $functions = [ + // function ptg args class vol + 'COUNT' => [0, -1, 0, 0], + 'IF' => [1, -1, 1, 0], + 'ISNA' => [2, 1, 1, 0], + 'ISERROR' => [3, 1, 1, 0], + 'SUM' => [4, -1, 0, 0], + 'AVERAGE' => [5, -1, 0, 0], + 'MIN' => [6, -1, 0, 0], + 'MAX' => [7, -1, 0, 0], + 'ROW' => [8, -1, 0, 0], + 'COLUMN' => [9, -1, 0, 0], + 'NA' => [10, 0, 0, 0], + 'NPV' => [11, -1, 1, 0], + 'STDEV' => [12, -1, 0, 0], + 'DOLLAR' => [13, -1, 1, 0], + 'FIXED' => [14, -1, 1, 0], + 'SIN' => [15, 1, 1, 0], + 'COS' => [16, 1, 1, 0], + 'TAN' => [17, 1, 1, 0], + 'ATAN' => [18, 1, 1, 0], + 'PI' => [19, 0, 1, 0], + 'SQRT' => [20, 1, 1, 0], + 'EXP' => [21, 1, 1, 0], + 'LN' => [22, 1, 1, 0], + 'LOG10' => [23, 1, 1, 0], + 'ABS' => [24, 1, 1, 0], + 'INT' => [25, 1, 1, 0], + 'SIGN' => [26, 1, 1, 0], + 'ROUND' => [27, 2, 1, 0], + 'LOOKUP' => [28, -1, 0, 0], + 'INDEX' => [29, -1, 0, 1], + 'REPT' => [30, 2, 1, 0], + 'MID' => [31, 3, 1, 0], + 'LEN' => [32, 1, 1, 0], + 'VALUE' => [33, 1, 1, 0], + 'TRUE' => [34, 0, 1, 0], + 'FALSE' => [35, 0, 1, 0], + 'AND' => [36, -1, 0, 0], + 'OR' => [37, -1, 0, 0], + 'NOT' => [38, 1, 1, 0], + 'MOD' => [39, 2, 1, 0], + 'DCOUNT' => [40, 3, 0, 0], + 'DSUM' => [41, 3, 0, 0], + 'DAVERAGE' => [42, 3, 0, 0], + 'DMIN' => [43, 3, 0, 0], + 'DMAX' => [44, 3, 0, 0], + 'DSTDEV' => [45, 3, 0, 0], + 'VAR' => [46, -1, 0, 0], + 'DVAR' => [47, 3, 0, 0], + 'TEXT' => [48, 2, 1, 0], + 'LINEST' => [49, -1, 0, 0], + 'TREND' => [50, -1, 0, 0], + 'LOGEST' => [51, -1, 0, 0], + 'GROWTH' => [52, -1, 0, 0], + 'PV' => [56, -1, 1, 0], + 'FV' => [57, -1, 1, 0], + 'NPER' => [58, -1, 1, 0], + 'PMT' => [59, -1, 1, 0], + 'RATE' => [60, -1, 1, 0], + 'MIRR' => [61, 3, 0, 0], + 'IRR' => [62, -1, 0, 0], + 'RAND' => [63, 0, 1, 1], + 'MATCH' => [64, -1, 0, 0], + 'DATE' => [65, 3, 1, 0], + 'TIME' => [66, 3, 1, 0], + 'DAY' => [67, 1, 1, 0], + 'MONTH' => [68, 1, 1, 0], + 'YEAR' => [69, 1, 1, 0], + 'WEEKDAY' => [70, -1, 1, 0], + 'HOUR' => [71, 1, 1, 0], + 'MINUTE' => [72, 1, 1, 0], + 'SECOND' => [73, 1, 1, 0], + 'NOW' => [74, 0, 1, 1], + 'AREAS' => [75, 1, 0, 1], + 'ROWS' => [76, 1, 0, 1], + 'COLUMNS' => [77, 1, 0, 1], + 'OFFSET' => [78, -1, 0, 1], + 'SEARCH' => [82, -1, 1, 0], + 'TRANSPOSE' => [83, 1, 1, 0], + 'TYPE' => [86, 1, 1, 0], + 'ATAN2' => [97, 2, 1, 0], + 'ASIN' => [98, 1, 1, 0], + 'ACOS' => [99, 1, 1, 0], + 'CHOOSE' => [100, -1, 1, 0], + 'HLOOKUP' => [101, -1, 0, 0], + 'VLOOKUP' => [102, -1, 0, 0], + 'ISREF' => [105, 1, 0, 0], + 'LOG' => [109, -1, 1, 0], + 'CHAR' => [111, 1, 1, 0], + 'LOWER' => [112, 1, 1, 0], + 'UPPER' => [113, 1, 1, 0], + 'PROPER' => [114, 1, 1, 0], + 'LEFT' => [115, -1, 1, 0], + 'RIGHT' => [116, -1, 1, 0], + 'EXACT' => [117, 2, 1, 0], + 'TRIM' => [118, 1, 1, 0], + 'REPLACE' => [119, 4, 1, 0], + 'SUBSTITUTE' => [120, -1, 1, 0], + 'CODE' => [121, 1, 1, 0], + 'FIND' => [124, -1, 1, 0], + 'CELL' => [125, -1, 0, 1], + 'ISERR' => [126, 1, 1, 0], + 'ISTEXT' => [127, 1, 1, 0], + 'ISNUMBER' => [128, 1, 1, 0], + 'ISBLANK' => [129, 1, 1, 0], + 'T' => [130, 1, 0, 0], + 'N' => [131, 1, 0, 0], + 'DATEVALUE' => [140, 1, 1, 0], + 'TIMEVALUE' => [141, 1, 1, 0], + 'SLN' => [142, 3, 1, 0], + 'SYD' => [143, 4, 1, 0], + 'DDB' => [144, -1, 1, 0], + 'INDIRECT' => [148, -1, 1, 1], + 'CALL' => [150, -1, 1, 0], + 'CLEAN' => [162, 1, 1, 0], + 'MDETERM' => [163, 1, 2, 0], + 'MINVERSE' => [164, 1, 2, 0], + 'MMULT' => [165, 2, 2, 0], + 'IPMT' => [167, -1, 1, 0], + 'PPMT' => [168, -1, 1, 0], + 'COUNTA' => [169, -1, 0, 0], + 'PRODUCT' => [183, -1, 0, 0], + 'FACT' => [184, 1, 1, 0], + 'DPRODUCT' => [189, 3, 0, 0], + 'ISNONTEXT' => [190, 1, 1, 0], + 'STDEVP' => [193, -1, 0, 0], + 'VARP' => [194, -1, 0, 0], + 'DSTDEVP' => [195, 3, 0, 0], + 'DVARP' => [196, 3, 0, 0], + 'TRUNC' => [197, -1, 1, 0], + 'ISLOGICAL' => [198, 1, 1, 0], + 'DCOUNTA' => [199, 3, 0, 0], + 'USDOLLAR' => [204, -1, 1, 0], + 'FINDB' => [205, -1, 1, 0], + 'SEARCHB' => [206, -1, 1, 0], + 'REPLACEB' => [207, 4, 1, 0], + 'LEFTB' => [208, -1, 1, 0], + 'RIGHTB' => [209, -1, 1, 0], + 'MIDB' => [210, 3, 1, 0], + 'LENB' => [211, 1, 1, 0], + 'ROUNDUP' => [212, 2, 1, 0], + 'ROUNDDOWN' => [213, 2, 1, 0], + 'ASC' => [214, 1, 1, 0], + 'DBCS' => [215, 1, 1, 0], + 'RANK' => [216, -1, 0, 0], + 'ADDRESS' => [219, -1, 1, 0], + 'DAYS360' => [220, -1, 1, 0], + 'TODAY' => [221, 0, 1, 1], + 'VDB' => [222, -1, 1, 0], + 'MEDIAN' => [227, -1, 0, 0], + 'SUMPRODUCT' => [228, -1, 2, 0], + 'SINH' => [229, 1, 1, 0], + 'COSH' => [230, 1, 1, 0], + 'TANH' => [231, 1, 1, 0], + 'ASINH' => [232, 1, 1, 0], + 'ACOSH' => [233, 1, 1, 0], + 'ATANH' => [234, 1, 1, 0], + 'DGET' => [235, 3, 0, 0], + 'INFO' => [244, 1, 1, 1], + 'DB' => [247, -1, 1, 0], + 'FREQUENCY' => [252, 2, 0, 0], + 'ERROR.TYPE' => [261, 1, 1, 0], + 'REGISTER.ID' => [267, -1, 1, 0], + 'AVEDEV' => [269, -1, 0, 0], + 'BETADIST' => [270, -1, 1, 0], + 'GAMMALN' => [271, 1, 1, 0], + 'BETAINV' => [272, -1, 1, 0], + 'BINOMDIST' => [273, 4, 1, 0], + 'CHIDIST' => [274, 2, 1, 0], + 'CHIINV' => [275, 2, 1, 0], + 'COMBIN' => [276, 2, 1, 0], + 'CONFIDENCE' => [277, 3, 1, 0], + 'CRITBINOM' => [278, 3, 1, 0], + 'EVEN' => [279, 1, 1, 0], + 'EXPONDIST' => [280, 3, 1, 0], + 'FDIST' => [281, 3, 1, 0], + 'FINV' => [282, 3, 1, 0], + 'FISHER' => [283, 1, 1, 0], + 'FISHERINV' => [284, 1, 1, 0], + 'FLOOR' => [285, 2, 1, 0], + 'GAMMADIST' => [286, 4, 1, 0], + 'GAMMAINV' => [287, 3, 1, 0], + 'CEILING' => [288, 2, 1, 0], + 'HYPGEOMDIST' => [289, 4, 1, 0], + 'LOGNORMDIST' => [290, 3, 1, 0], + 'LOGINV' => [291, 3, 1, 0], + 'NEGBINOMDIST' => [292, 3, 1, 0], + 'NORMDIST' => [293, 4, 1, 0], + 'NORMSDIST' => [294, 1, 1, 0], + 'NORMINV' => [295, 3, 1, 0], + 'NORMSINV' => [296, 1, 1, 0], + 'STANDARDIZE' => [297, 3, 1, 0], + 'ODD' => [298, 1, 1, 0], + 'PERMUT' => [299, 2, 1, 0], + 'POISSON' => [300, 3, 1, 0], + 'TDIST' => [301, 3, 1, 0], + 'WEIBULL' => [302, 4, 1, 0], + 'SUMXMY2' => [303, 2, 2, 0], + 'SUMX2MY2' => [304, 2, 2, 0], + 'SUMX2PY2' => [305, 2, 2, 0], + 'CHITEST' => [306, 2, 2, 0], + 'CORREL' => [307, 2, 2, 0], + 'COVAR' => [308, 2, 2, 0], + 'FORECAST' => [309, 3, 2, 0], + 'FTEST' => [310, 2, 2, 0], + 'INTERCEPT' => [311, 2, 2, 0], + 'PEARSON' => [312, 2, 2, 0], + 'RSQ' => [313, 2, 2, 0], + 'STEYX' => [314, 2, 2, 0], + 'SLOPE' => [315, 2, 2, 0], + 'TTEST' => [316, 4, 2, 0], + 'PROB' => [317, -1, 2, 0], + 'DEVSQ' => [318, -1, 0, 0], + 'GEOMEAN' => [319, -1, 0, 0], + 'HARMEAN' => [320, -1, 0, 0], + 'SUMSQ' => [321, -1, 0, 0], + 'KURT' => [322, -1, 0, 0], + 'SKEW' => [323, -1, 0, 0], + 'ZTEST' => [324, -1, 0, 0], + 'LARGE' => [325, 2, 0, 0], + 'SMALL' => [326, 2, 0, 0], + 'QUARTILE' => [327, 2, 0, 0], + 'PERCENTILE' => [328, 2, 0, 0], + 'PERCENTRANK' => [329, -1, 0, 0], + 'MODE' => [330, -1, 2, 0], + 'TRIMMEAN' => [331, 2, 0, 0], + 'TINV' => [332, 2, 1, 0], + 'CONCATENATE' => [336, -1, 1, 0], + 'POWER' => [337, 2, 1, 0], + 'RADIANS' => [342, 1, 1, 0], + 'DEGREES' => [343, 1, 1, 0], + 'SUBTOTAL' => [344, -1, 0, 0], + 'SUMIF' => [345, -1, 0, 0], + 'COUNTIF' => [346, 2, 0, 0], + 'COUNTBLANK' => [347, 1, 0, 0], + 'ISPMT' => [350, 4, 1, 0], + 'DATEDIF' => [351, 3, 1, 0], + 'DATESTRING' => [352, 1, 1, 0], + 'NUMBERSTRING' => [353, 2, 1, 0], + 'ROMAN' => [354, -1, 1, 0], + 'GETPIVOTDATA' => [358, -1, 0, 0], + 'HYPERLINK' => [359, -1, 1, 0], + 'PHONETIC' => [360, 1, 0, 0], + 'AVERAGEA' => [361, -1, 0, 0], + 'MAXA' => [362, -1, 0, 0], + 'MINA' => [363, -1, 0, 0], + 'STDEVPA' => [364, -1, 0, 0], + 'VARPA' => [365, -1, 0, 0], + 'STDEVA' => [366, -1, 0, 0], + 'VARA' => [367, -1, 0, 0], + 'BAHTTEXT' => [368, 1, 0, 0], + ]; + + /** + * The class constructor. + */ + public function __construct() + { + $this->currentCharacter = 0; + $this->currentToken = ''; // The token we are working on. + $this->formula = ''; // The formula to parse. + $this->lookAhead = ''; // The character ahead of the current char. + $this->parseTree = ''; // The parse tree to be generated. + $this->externalSheets = []; + $this->references = []; + } + + /** + * Convert a token to the proper ptg value. + * + * @param mixed $token the token to convert + * + * @return mixed the converted token on success + */ + private function convert($token) + { + if (preg_match('/"([^"]|""){0,255}"/', $token)) { + return $this->convertString($token); + } elseif (is_numeric($token)) { + return $this->convertNumber($token); + // match references like A1 or $A$1 + } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { + return $this->convertRef2d($token); + // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1 + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) { + return $this->convertRef3d($token); + // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1 + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) { + return $this->convertRef3d($token); + // match ranges like A1:B2 or $A$1:$B$2 + } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { + return $this->convertRange2d($token); + // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) { + return $this->convertRange3d($token); + // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) { + return $this->convertRange3d($token); + // operators (including parentheses) + } elseif (isset($this->ptg[$token])) { + return pack('C', $this->ptg[$token]); + // match error codes + } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) or $token == '#N/A') { + return $this->convertError($token); + // commented so argument number can be processed correctly. See toReversePolish(). + /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) + { + return($this->convertFunction($token, $this->_func_args)); + }*/ + // if it's an argument, ignore the token (the argument remains) + } elseif ($token == 'arg') { + return ''; + } + + // TODO: use real error codes + throw new WriterException("Unknown token $token"); + } + + /** + * Convert a number token to ptgInt or ptgNum. + * + * @param mixed $num an integer or double for conversion to its ptg value + * + * @return string + */ + private function convertNumber($num) + { + // Integer in the range 0..2**16-1 + if ((preg_match('/^\\d+$/', $num)) and ($num <= 65535)) { + return pack('Cv', $this->ptg['ptgInt'], $num); + } + + // A float + if (BIFFwriter::getByteOrder()) { // if it's Big Endian + $num = strrev($num); + } + + return pack('Cd', $this->ptg['ptgNum'], $num); + } + + /** + * Convert a string token to ptgStr. + * + * @param string $string a string for conversion to its ptg value + * + * @return mixed the converted token on success + */ + private function convertString($string) + { + // chop away beggining and ending quotes + $string = substr($string, 1, -1); + if (strlen($string) > 255) { + throw new WriterException('String is too long'); + } + + return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string); + } + + /** + * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of + * args that it takes. + * + * @param string $token the name of the function for convertion to ptg value + * @param int $num_args the number of arguments the function receives + * + * @return string The packed ptg for the function + */ + private function convertFunction($token, $num_args) + { + $args = $this->functions[$token][1]; + + // Fixed number of args eg. TIME($i, $j, $k). + if ($args >= 0) { + return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]); + } + // Variable number of args eg. SUM($i, $j, $k, ..). + if ($args == -1) { + return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]); + } + } + + /** + * Convert an Excel range such as A1:D4 to a ptgRefV. + * + * @param string $range An Excel range in the A1:A2 + * @param int $class + * + * @return string + */ + private function convertRange2d($range, $class = 0) + { + // TODO: possible class value 0,1,2 check Formula.pm + // Split the range into 2 cell refs + if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) { + list($cell1, $cell2) = explode(':', $range); + } else { + // TODO: use real error codes + throw new WriterException('Unknown range separator'); + } + + // Convert the cell references + list($row1, $col1) = $this->cellToPackedRowcol($cell1); + list($row2, $col2) = $this->cellToPackedRowcol($cell2); + + // The ptg value depends on the class of the ptg. + if ($class == 0) { + $ptgArea = pack('C', $this->ptg['ptgArea']); + } elseif ($class == 1) { + $ptgArea = pack('C', $this->ptg['ptgAreaV']); + } elseif ($class == 2) { + $ptgArea = pack('C', $this->ptg['ptgAreaA']); + } else { + // TODO: use real error codes + throw new WriterException("Unknown class $class"); + } + + return $ptgArea . $row1 . $row2 . $col1 . $col2; + } + + /** + * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to + * a ptgArea3d. + * + * @param string $token an Excel range in the Sheet1!A1:A2 format + * + * @return mixed the packed ptgArea3d token on success + */ + private function convertRange3d($token) + { + // Split the ref at the ! symbol + list($ext_ref, $range) = PhpspreadsheetWorksheet::extractSheetTitle($token, true); + + // Convert the external reference part (different for BIFF8) + $ext_ref = $this->getRefIndex($ext_ref); + + // Split the range into 2 cell refs + list($cell1, $cell2) = explode(':', $range); + + // Convert the cell references + if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) { + list($row1, $col1) = $this->cellToPackedRowcol($cell1); + list($row2, $col2) = $this->cellToPackedRowcol($cell2); + } else { // It's a rows range (like 26:27) + list($row1, $col1, $row2, $col2) = $this->rangeToPackedRange($cell1 . ':' . $cell2); + } + + // The ptg value depends on the class of the ptg. + $ptgArea = pack('C', $this->ptg['ptgArea3d']); + + return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2; + } + + /** + * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV. + * + * @param string $cell An Excel cell reference + * + * @return string The cell in packed() format with the corresponding ptg + */ + private function convertRef2d($cell) + { + // Convert the cell reference + $cell_array = $this->cellToPackedRowcol($cell); + list($row, $col) = $cell_array; + + // The ptg value depends on the class of the ptg. + $ptgRef = pack('C', $this->ptg['ptgRefA']); + + return $ptgRef . $row . $col; + } + + /** + * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a + * ptgRef3d. + * + * @param string $cell An Excel cell reference + * + * @return mixed the packed ptgRef3d token on success + */ + private function convertRef3d($cell) + { + // Split the ref at the ! symbol + list($ext_ref, $cell) = PhpspreadsheetWorksheet::extractSheetTitle($cell, true); + + // Convert the external reference part (different for BIFF8) + $ext_ref = $this->getRefIndex($ext_ref); + + // Convert the cell reference part + list($row, $col) = $this->cellToPackedRowcol($cell); + + // The ptg value depends on the class of the ptg. + $ptgRef = pack('C', $this->ptg['ptgRef3dA']); + + return $ptgRef . $ext_ref . $row . $col; + } + + /** + * Convert an error code to a ptgErr. + * + * @param string $errorCode The error code for conversion to its ptg value + * + * @return string The error code ptgErr + */ + private function convertError($errorCode) + { + switch ($errorCode) { + case '#NULL!': + return pack('C', 0x00); + case '#DIV/0!': + return pack('C', 0x07); + case '#VALUE!': + return pack('C', 0x0F); + case '#REF!': + return pack('C', 0x17); + case '#NAME?': + return pack('C', 0x1D); + case '#NUM!': + return pack('C', 0x24); + case '#N/A': + return pack('C', 0x2A); + } + + return pack('C', 0xFF); + } + + /** + * Look up the REF index that corresponds to an external sheet name + * (or range). If it doesn't exist yet add it to the workbook's references + * array. It assumes all sheet names given must exist. + * + * @param string $ext_ref The name of the external reference + * + * @return mixed The reference index in packed() format on success + */ + private function getRefIndex($ext_ref) + { + $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. + $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. + $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' + + // Check if there is a sheet range eg., Sheet1:Sheet2. + if (preg_match('/:/', $ext_ref)) { + list($sheet_name1, $sheet_name2) = explode(':', $ext_ref); + + $sheet1 = $this->getSheetIndex($sheet_name1); + if ($sheet1 == -1) { + throw new WriterException("Unknown sheet name $sheet_name1 in formula"); + } + $sheet2 = $this->getSheetIndex($sheet_name2); + if ($sheet2 == -1) { + throw new WriterException("Unknown sheet name $sheet_name2 in formula"); + } + + // Reverse max and min sheet numbers if necessary + if ($sheet1 > $sheet2) { + list($sheet1, $sheet2) = [$sheet2, $sheet1]; + } + } else { // Single sheet name only. + $sheet1 = $this->getSheetIndex($ext_ref); + if ($sheet1 == -1) { + throw new WriterException("Unknown sheet name $ext_ref in formula"); + } + $sheet2 = $sheet1; + } + + // assume all references belong to this document + $supbook_index = 0x00; + $ref = pack('vvv', $supbook_index, $sheet1, $sheet2); + $totalreferences = count($this->references); + $index = -1; + for ($i = 0; $i < $totalreferences; ++$i) { + if ($ref == $this->references[$i]) { + $index = $i; + + break; + } + } + // if REF was not found add it to references array + if ($index == -1) { + $this->references[$totalreferences] = $ref; + $index = $totalreferences; + } + + return pack('v', $index); + } + + /** + * Look up the index that corresponds to an external sheet name. The hash of + * sheet names is updated by the addworksheet() method of the + * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. + * + * @param string $sheet_name Sheet name + * + * @return int The sheet index, -1 if the sheet was not found + */ + private function getSheetIndex($sheet_name) + { + if (!isset($this->externalSheets[$sheet_name])) { + return -1; + } + + return $this->externalSheets[$sheet_name]; + } + + /** + * This method is used to update the array of sheet names. It is + * called by the addWorksheet() method of the + * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. + * + * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet() + * + * @param string $name The name of the worksheet being added + * @param int $index The index of the worksheet being added + */ + public function setExtSheet($name, $index) + { + $this->externalSheets[$name] = $index; + } + + /** + * pack() row and column into the required 3 or 4 byte format. + * + * @param string $cell The Excel cell reference to be packed + * + * @return array Array containing the row and column in packed() format + */ + private function cellToPackedRowcol($cell) + { + $cell = strtoupper($cell); + list($row, $col, $row_rel, $col_rel) = $this->cellToRowcol($cell); + if ($col >= 256) { + throw new WriterException("Column in: $cell greater than 255"); + } + if ($row >= 65536) { + throw new WriterException("Row in: $cell greater than 65536 "); + } + + // Set the high bits to indicate if row or col are relative. + $col |= $col_rel << 14; + $col |= $row_rel << 15; + $col = pack('v', $col); + + $row = pack('v', $row); + + return [$row, $col]; + } + + /** + * pack() row range into the required 3 or 4 byte format. + * Just using maximum col/rows, which is probably not the correct solution. + * + * @param string $range The Excel range to be packed + * + * @return array Array containing (row1,col1,row2,col2) in packed() format + */ + private function rangeToPackedRange($range) + { + preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match); + // return absolute rows if there is a $ in the ref + $row1_rel = empty($match[1]) ? 1 : 0; + $row1 = $match[2]; + $row2_rel = empty($match[3]) ? 1 : 0; + $row2 = $match[4]; + // Convert 1-index to zero-index + --$row1; + --$row2; + // Trick poor inocent Excel + $col1 = 0; + $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!) + + // FIXME: this changes for BIFF8 + if (($row1 >= 65536) or ($row2 >= 65536)) { + throw new WriterException("Row in: $range greater than 65536 "); + } + + // Set the high bits to indicate if rows are relative. + $col1 |= $row1_rel << 15; + $col2 |= $row2_rel << 15; + $col1 = pack('v', $col1); + $col2 = pack('v', $col2); + + $row1 = pack('v', $row1); + $row2 = pack('v', $row2); + + return [$row1, $col1, $row2, $col2]; + } + + /** + * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero + * indexed row and column number. Also returns two (0,1) values to indicate + * whether the row or column are relative references. + * + * @param string $cell the Excel cell reference in A1 format + * + * @return array + */ + private function cellToRowcol($cell) + { + preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match); + // return absolute column if there is a $ in the ref + $col_rel = empty($match[1]) ? 1 : 0; + $col_ref = $match[2]; + $row_rel = empty($match[3]) ? 1 : 0; + $row = $match[4]; + + // Convert base26 column string to a number. + $expn = strlen($col_ref) - 1; + $col = 0; + $col_ref_length = strlen($col_ref); + for ($i = 0; $i < $col_ref_length; ++$i) { + $col += (ord($col_ref[$i]) - 64) * pow(26, $expn); + --$expn; + } + + // Convert 1-index to zero-index + --$row; + --$col; + + return [$row, $col, $row_rel, $col_rel]; + } + + /** + * Advance to the next valid token. + */ + private function advance() + { + $i = $this->currentCharacter; + $formula_length = strlen($this->formula); + // eat up white spaces + if ($i < $formula_length) { + while ($this->formula[$i] == ' ') { + ++$i; + } + + if ($i < ($formula_length - 1)) { + $this->lookAhead = $this->formula[$i + 1]; + } + $token = ''; + } + + while ($i < $formula_length) { + $token .= $this->formula[$i]; + + if ($i < ($formula_length - 1)) { + $this->lookAhead = $this->formula[$i + 1]; + } else { + $this->lookAhead = ''; + } + + if ($this->match($token) != '') { + $this->currentCharacter = $i + 1; + $this->currentToken = $token; + + return 1; + } + + if ($i < ($formula_length - 2)) { + $this->lookAhead = $this->formula[$i + 2]; + } else { // if we run out of characters lookAhead becomes empty + $this->lookAhead = ''; + } + ++$i; + } + //die("Lexical error ".$this->currentCharacter); + } + + /** + * Checks if it's a valid token. + * + * @param mixed $token the token to check + * + * @return mixed The checked token or false on failure + */ + private function match($token) + { + switch ($token) { + case '+': + case '-': + case '*': + case '/': + case '(': + case ')': + case ',': + case ';': + case '>=': + case '<=': + case '=': + case '<>': + case '^': + case '&': + case '%': + return $token; + + break; + case '>': + if ($this->lookAhead == '=') { // it's a GE token + break; + } + + return $token; + + break; + case '<': + // it's a LE or a NE token + if (($this->lookAhead == '=') or ($this->lookAhead == '>')) { + break; + } + + return $token; + + break; + default: + // if it's a reference A1 or $A$1 or $A1 or A$1 + if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) and !preg_match('/\d/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.') and ($this->lookAhead != '!')) { + return $token; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) and !preg_match('/\d/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.')) { + // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) + return $token; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) and !preg_match('/\d/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.')) { + // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) + return $token; + } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) { + // if it's a range A1:A2 or $A$1:$A$2 + return $token; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) and !preg_match('/\d/', $this->lookAhead)) { + // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 + return $token; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) and !preg_match('/\d/', $this->lookAhead)) { + // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 + return $token; + } elseif (is_numeric($token) and (!is_numeric($token . $this->lookAhead) or ($this->lookAhead == '')) and ($this->lookAhead != '!') and ($this->lookAhead != ':')) { + // If it's a number (check that it's not a sheet name or range) + return $token; + } elseif (preg_match('/"([^"]|""){0,255}"/', $token) and $this->lookAhead != '"' and (substr_count($token, '"') % 2 == 0)) { + // If it's a string (of maximum 255 characters) + return $token; + } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) or $token == '#N/A') { + // If it's an error code + return $token; + } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) and ($this->lookAhead == '(')) { + // if it's a function call + return $token; + } elseif (substr($token, -1) == ')') { + // It's an argument of some description (e.g. a named range), + // precise nature yet to be determined + return $token; + } + + return ''; + } + } + + /** + * The parsing method. It parses a formula. + * + * @param string $formula the formula to parse, without the initial equal + * sign (=) + * + * @return mixed true on success + */ + public function parse($formula) + { + $this->currentCharacter = 0; + $this->formula = $formula; + $this->lookAhead = isset($formula[1]) ? $formula[1] + : ''; + $this->advance(); + $this->parseTree = $this->condition(); + + return true; + } + + /** + * It parses a condition. It assumes the following rule: + * Cond -> Expr [(">" | "<") Expr]. + * + * @return mixed The parsed ptg'd tree on success + */ + private function condition() + { + $result = $this->expression(); + if ($this->currentToken == '<') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgLT', $result, $result2); + } elseif ($this->currentToken == '>') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgGT', $result, $result2); + } elseif ($this->currentToken == '<=') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgLE', $result, $result2); + } elseif ($this->currentToken == '>=') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgGE', $result, $result2); + } elseif ($this->currentToken == '=') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgEQ', $result, $result2); + } elseif ($this->currentToken == '<>') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgNE', $result, $result2); + } elseif ($this->currentToken == '&') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgConcat', $result, $result2); + } + + return $result; + } + + /** + * It parses a expression. It assumes the following rule: + * Expr -> Term [("+" | "-") Term] + * -> "string" + * -> "-" Term : Negative value + * -> "+" Term : Positive value + * -> Error code. + * + * @return mixed The parsed ptg'd tree on success + */ + private function expression() + { + // If it's a string return a string node + if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) { + $tmp = str_replace('""', '"', $this->currentToken); + if (($tmp == '"') || ($tmp == '')) { + // Trap for "" that has been used for an empty string + $tmp = '""'; + } + $result = $this->createTree($tmp, '', ''); + $this->advance(); + + return $result; + // If it's an error code + } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) or $this->currentToken == '#N/A') { + $result = $this->createTree($this->currentToken, 'ptgErr', ''); + $this->advance(); + + return $result; + // If it's a negative value + } elseif ($this->currentToken == '-') { + // catch "-" Term + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgUminus', $result2, ''); + + return $result; + // If it's a positive value + } elseif ($this->currentToken == '+') { + // catch "+" Term + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgUplus', $result2, ''); + + return $result; + } + $result = $this->term(); + while (($this->currentToken == '+') or + ($this->currentToken == '-') or + ($this->currentToken == '^')) { + if ($this->currentToken == '+') { + $this->advance(); + $result2 = $this->term(); + $result = $this->createTree('ptgAdd', $result, $result2); + } elseif ($this->currentToken == '-') { + $this->advance(); + $result2 = $this->term(); + $result = $this->createTree('ptgSub', $result, $result2); + } else { + $this->advance(); + $result2 = $this->term(); + $result = $this->createTree('ptgPower', $result, $result2); + } + } + + return $result; + } + + /** + * This function just introduces a ptgParen element in the tree, so that Excel + * doesn't get confused when working with a parenthesized formula afterwards. + * + * @see fact() + * + * @return array The parsed ptg'd tree + */ + private function parenthesizedExpression() + { + $result = $this->createTree('ptgParen', $this->expression(), ''); + + return $result; + } + + /** + * It parses a term. It assumes the following rule: + * Term -> Fact [("*" | "/") Fact]. + * + * @return mixed The parsed ptg'd tree on success + */ + private function term() + { + $result = $this->fact(); + while (($this->currentToken == '*') or + ($this->currentToken == '/')) { + if ($this->currentToken == '*') { + $this->advance(); + $result2 = $this->fact(); + $result = $this->createTree('ptgMul', $result, $result2); + } else { + $this->advance(); + $result2 = $this->fact(); + $result = $this->createTree('ptgDiv', $result, $result2); + } + } + + return $result; + } + + /** + * It parses a factor. It assumes the following rule: + * Fact -> ( Expr ) + * | CellRef + * | CellRange + * | Number + * | Function. + * + * @return mixed The parsed ptg'd tree on success + */ + private function fact() + { + if ($this->currentToken == '(') { + $this->advance(); // eat the "(" + $result = $this->parenthesizedExpression(); + if ($this->currentToken != ')') { + throw new WriterException("')' token expected."); + } + $this->advance(); // eat the ")" + return $result; + } + // if it's a reference + if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) { + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) { + // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) { + // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) or + preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken)) { + // if it's a range A1:B2 or $A$1:$B$2 + // must be an error? + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) { + // If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2) + // must be an error? + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) { + // If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2) + // must be an error? + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (is_numeric($this->currentToken)) { + // If it's a number or a percent + if ($this->lookAhead == '%') { + $result = $this->createTree('ptgPercent', $this->currentToken, ''); + $this->advance(); // Skip the percentage operator once we've pre-built that tree + } else { + $result = $this->createTree($this->currentToken, '', ''); + } + $this->advance(); + + return $result; + } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken)) { + // if it's a function call + $result = $this->func(); + + return $result; + } + + throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter); + } + + /** + * It parses a function call. It assumes the following rule: + * Func -> ( Expr [,Expr]* ). + * + * @return mixed The parsed ptg'd tree on success + */ + private function func() + { + $num_args = 0; // number of arguments received + $function = strtoupper($this->currentToken); + $result = ''; // initialize result + $this->advance(); + $this->advance(); // eat the "(" + while ($this->currentToken != ')') { + if ($num_args > 0) { + if ($this->currentToken == ',' || $this->currentToken == ';') { + $this->advance(); // eat the "," or ";" + } else { + throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}"); + } + $result2 = $this->condition(); + $result = $this->createTree('arg', $result, $result2); + } else { // first argument + $result2 = $this->condition(); + $result = $this->createTree('arg', '', $result2); + } + ++$num_args; + } + if (!isset($this->functions[$function])) { + throw new WriterException("Function $function() doesn't exist"); + } + $args = $this->functions[$function][1]; + // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid. + if (($args >= 0) and ($args != $num_args)) { + throw new WriterException("Incorrect number of arguments in function $function() "); + } + + $result = $this->createTree($function, $result, $num_args); + $this->advance(); // eat the ")" + return $result; + } + + /** + * Creates a tree. In fact an array which may have one or two arrays (sub-trees) + * as elements. + * + * @param mixed $value the value of this node + * @param mixed $left the left array (sub-tree) or a final node + * @param mixed $right the right array (sub-tree) or a final node + * + * @return array A tree + */ + private function createTree($value, $left, $right) + { + return ['value' => $value, 'left' => $left, 'right' => $right]; + } + + /** + * Builds a string containing the tree in reverse polish notation (What you + * would use in a HP calculator stack). + * The following tree:. + * + * + + * / \ + * 2 3 + * + * produces: "23+" + * + * The following tree: + * + * + + * / \ + * 3 * + * / \ + * 6 A1 + * + * produces: "36A1*+" + * + * In fact all operands, functions, references, etc... are written as ptg's + * + * @param array $tree the optional tree to convert + * + * @return string The tree in reverse polish notation + */ + public function toReversePolish($tree = []) + { + $polish = ''; // the string we are going to return + if (empty($tree)) { // If it's the first call use parseTree + $tree = $this->parseTree; + } + + if (is_array($tree['left'])) { + $converted_tree = $this->toReversePolish($tree['left']); + $polish .= $converted_tree; + } elseif ($tree['left'] != '') { // It's a final node + $converted_tree = $this->convert($tree['left']); + $polish .= $converted_tree; + } + if (is_array($tree['right'])) { + $converted_tree = $this->toReversePolish($tree['right']); + $polish .= $converted_tree; + } elseif ($tree['right'] != '') { // It's a final node + $converted_tree = $this->convert($tree['right']); + $polish .= $converted_tree; + } + // if it's a function convert it here (so we can set it's arguments) + if (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) and + !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) and + !preg_match('/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/', $tree['value']) and + !is_numeric($tree['value']) and + !isset($this->ptg[$tree['value']])) { + // left subtree for a function is always an array. + if ($tree['left'] != '') { + $left_tree = $this->toReversePolish($tree['left']); + } else { + $left_tree = ''; + } + // add it's left subtree and return. + return $left_tree . $this->convertFunction($tree['value'], $tree['right']); + } + $converted_tree = $this->convert($tree['value']); + + $polish .= $converted_tree; + + return $polish; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Workbook.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Workbook.php new file mode 100644 index 00000000000..b463ce4a8f0 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Workbook.php @@ -0,0 +1,1152 @@ + +// * +// * The majority of this is _NOT_ my code. I simply ported it from the +// * PERL Spreadsheet::WriteExcel module. +// * +// * The author of the Spreadsheet::WriteExcel module is John McNamara +// * +// * +// * I _DO_ maintain this code, and John McNamara has nothing to do with the +// * porting of this code to PHP. Any questions directly related to this +// * class library should be directed to me. +// * +// * License Information: +// * +// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets +// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com +// * +// * This library is free software; you can redistribute it and/or +// * modify it under the terms of the GNU Lesser General Public +// * License as published by the Free Software Foundation; either +// * version 2.1 of the License, or (at your option) any later version. +// * +// * This library is distributed in the hope that it will be useful, +// * but WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Lesser General Public License for more details. +// * +// * You should have received a copy of the GNU Lesser General Public +// * License along with this library; if not, write to the Free Software +// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// */ +class Workbook extends BIFFwriter +{ + /** + * Formula parser. + * + * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser + */ + private $parser; + + /** + * The BIFF file size for the workbook. + * + * @var int + * + * @see calcSheetOffsets() + */ + private $biffSize; + + /** + * XF Writers. + * + * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[] + */ + private $xfWriters = []; + + /** + * Array containing the colour palette. + * + * @var array + */ + private $palette; + + /** + * The codepage indicates the text encoding used for strings. + * + * @var int + */ + private $codepage; + + /** + * The country code used for localization. + * + * @var int + */ + private $countryCode; + + /** + * Workbook. + * + * @var Spreadsheet + */ + private $spreadsheet; + + /** + * Fonts writers. + * + * @var Font[] + */ + private $fontWriters = []; + + /** + * Added fonts. Maps from font's hash => index in workbook. + * + * @var array + */ + private $addedFonts = []; + + /** + * Shared number formats. + * + * @var array + */ + private $numberFormats = []; + + /** + * Added number formats. Maps from numberFormat's hash => index in workbook. + * + * @var array + */ + private $addedNumberFormats = []; + + /** + * Sizes of the binary worksheet streams. + * + * @var array + */ + private $worksheetSizes = []; + + /** + * Offsets of the binary worksheet streams relative to the start of the global workbook stream. + * + * @var array + */ + private $worksheetOffsets = []; + + /** + * Total number of shared strings in workbook. + * + * @var int + */ + private $stringTotal; + + /** + * Number of unique shared strings in workbook. + * + * @var int + */ + private $stringUnique; + + /** + * Array of unique shared strings in workbook. + * + * @var array + */ + private $stringTable; + + /** + * Color cache. + */ + private $colors; + + /** + * Escher object corresponding to MSODRAWINGGROUP. + * + * @var \PhpOffice\PhpSpreadsheet\Shared\Escher + */ + private $escher; + + /** + * Class constructor. + * + * @param Spreadsheet $spreadsheet The Workbook + * @param int $str_total Total number of strings + * @param int $str_unique Total number of unique strings + * @param array $str_table String Table + * @param array $colors Colour Table + * @param Parser $parser The formula parser created for the Workbook + */ + public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser) + { + // It needs to call its parent's constructor explicitly + parent::__construct(); + + $this->parser = $parser; + $this->biffSize = 0; + $this->palette = []; + $this->countryCode = -1; + + $this->stringTotal = &$str_total; + $this->stringUnique = &$str_unique; + $this->stringTable = &$str_table; + $this->colors = &$colors; + $this->setPaletteXl97(); + + $this->spreadsheet = $spreadsheet; + + $this->codepage = 0x04B0; + + // Add empty sheets and Build color cache + $countSheets = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $countSheets; ++$i) { + $phpSheet = $spreadsheet->getSheet($i); + + $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser + + $supbook_index = 0x00; + $ref = pack('vvv', $supbook_index, $i, $i); + $this->parser->references[] = $ref; // Register reference with parser + + // Sheet tab colors? + if ($phpSheet->isTabColorSet()) { + $this->addColor($phpSheet->getTabColor()->getRGB()); + } + } + } + + /** + * Add a new XF writer. + * + * @param Style $style + * @param bool $isStyleXf Is it a style XF? + * + * @return int Index to XF record + */ + public function addXfWriter(Style $style, $isStyleXf = false) + { + $xfWriter = new Xf($style); + $xfWriter->setIsStyleXf($isStyleXf); + + // Add the font if not already added + $fontIndex = $this->addFont($style->getFont()); + + // Assign the font index to the xf record + $xfWriter->setFontIndex($fontIndex); + + // Background colors, best to treat these after the font so black will come after white in custom palette + $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB())); + $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB())); + $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB())); + $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB())); + $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB())); + $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB())); + $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB())); + + // Add the number format if it is not a built-in one and not already added + if ($style->getNumberFormat()->getBuiltInFormatCode() === false) { + $numberFormatHashCode = $style->getNumberFormat()->getHashCode(); + + if (isset($this->addedNumberFormats[$numberFormatHashCode])) { + $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode]; + } else { + $numberFormatIndex = 164 + count($this->numberFormats); + $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat(); + $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex; + } + } else { + $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode(); + } + + // Assign the number format index to xf record + $xfWriter->setNumberFormatIndex($numberFormatIndex); + + $this->xfWriters[] = $xfWriter; + + $xfIndex = count($this->xfWriters) - 1; + + return $xfIndex; + } + + /** + * Add a font to added fonts. + * + * @param \PhpOffice\PhpSpreadsheet\Style\Font $font + * + * @return int Index to FONT record + */ + public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font) + { + $fontHashCode = $font->getHashCode(); + if (isset($this->addedFonts[$fontHashCode])) { + $fontIndex = $this->addedFonts[$fontHashCode]; + } else { + $countFonts = count($this->fontWriters); + $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1; + + $fontWriter = new Font($font); + $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB())); + $this->fontWriters[] = $fontWriter; + + $this->addedFonts[$fontHashCode] = $fontIndex; + } + + return $fontIndex; + } + + /** + * Alter color palette adding a custom color. + * + * @param string $rgb E.g. 'FF00AA' + * + * @return int Color index + */ + private function addColor($rgb) + { + if (!isset($this->colors[$rgb])) { + $color = + [ + hexdec(substr($rgb, 0, 2)), + hexdec(substr($rgb, 2, 2)), + hexdec(substr($rgb, 4)), + 0, + ]; + $colorIndex = array_search($color, $this->palette); + if ($colorIndex) { + $this->colors[$rgb] = $colorIndex; + } else { + if (count($this->colors) == 0) { + $lastColor = 7; + } else { + $lastColor = end($this->colors); + } + if ($lastColor < 57) { + // then we add a custom color altering the palette + $colorIndex = $lastColor + 1; + $this->palette[$colorIndex] = $color; + $this->colors[$rgb] = $colorIndex; + } else { + // no room for more custom colors, just map to black + $colorIndex = 0; + } + } + } else { + // fetch already added custom color + $colorIndex = $this->colors[$rgb]; + } + + return $colorIndex; + } + + /** + * Sets the colour palette to the Excel 97+ default. + */ + private function setPaletteXl97() + { + $this->palette = [ + 0x08 => [0x00, 0x00, 0x00, 0x00], + 0x09 => [0xff, 0xff, 0xff, 0x00], + 0x0A => [0xff, 0x00, 0x00, 0x00], + 0x0B => [0x00, 0xff, 0x00, 0x00], + 0x0C => [0x00, 0x00, 0xff, 0x00], + 0x0D => [0xff, 0xff, 0x00, 0x00], + 0x0E => [0xff, 0x00, 0xff, 0x00], + 0x0F => [0x00, 0xff, 0xff, 0x00], + 0x10 => [0x80, 0x00, 0x00, 0x00], + 0x11 => [0x00, 0x80, 0x00, 0x00], + 0x12 => [0x00, 0x00, 0x80, 0x00], + 0x13 => [0x80, 0x80, 0x00, 0x00], + 0x14 => [0x80, 0x00, 0x80, 0x00], + 0x15 => [0x00, 0x80, 0x80, 0x00], + 0x16 => [0xc0, 0xc0, 0xc0, 0x00], + 0x17 => [0x80, 0x80, 0x80, 0x00], + 0x18 => [0x99, 0x99, 0xff, 0x00], + 0x19 => [0x99, 0x33, 0x66, 0x00], + 0x1A => [0xff, 0xff, 0xcc, 0x00], + 0x1B => [0xcc, 0xff, 0xff, 0x00], + 0x1C => [0x66, 0x00, 0x66, 0x00], + 0x1D => [0xff, 0x80, 0x80, 0x00], + 0x1E => [0x00, 0x66, 0xcc, 0x00], + 0x1F => [0xcc, 0xcc, 0xff, 0x00], + 0x20 => [0x00, 0x00, 0x80, 0x00], + 0x21 => [0xff, 0x00, 0xff, 0x00], + 0x22 => [0xff, 0xff, 0x00, 0x00], + 0x23 => [0x00, 0xff, 0xff, 0x00], + 0x24 => [0x80, 0x00, 0x80, 0x00], + 0x25 => [0x80, 0x00, 0x00, 0x00], + 0x26 => [0x00, 0x80, 0x80, 0x00], + 0x27 => [0x00, 0x00, 0xff, 0x00], + 0x28 => [0x00, 0xcc, 0xff, 0x00], + 0x29 => [0xcc, 0xff, 0xff, 0x00], + 0x2A => [0xcc, 0xff, 0xcc, 0x00], + 0x2B => [0xff, 0xff, 0x99, 0x00], + 0x2C => [0x99, 0xcc, 0xff, 0x00], + 0x2D => [0xff, 0x99, 0xcc, 0x00], + 0x2E => [0xcc, 0x99, 0xff, 0x00], + 0x2F => [0xff, 0xcc, 0x99, 0x00], + 0x30 => [0x33, 0x66, 0xff, 0x00], + 0x31 => [0x33, 0xcc, 0xcc, 0x00], + 0x32 => [0x99, 0xcc, 0x00, 0x00], + 0x33 => [0xff, 0xcc, 0x00, 0x00], + 0x34 => [0xff, 0x99, 0x00, 0x00], + 0x35 => [0xff, 0x66, 0x00, 0x00], + 0x36 => [0x66, 0x66, 0x99, 0x00], + 0x37 => [0x96, 0x96, 0x96, 0x00], + 0x38 => [0x00, 0x33, 0x66, 0x00], + 0x39 => [0x33, 0x99, 0x66, 0x00], + 0x3A => [0x00, 0x33, 0x00, 0x00], + 0x3B => [0x33, 0x33, 0x00, 0x00], + 0x3C => [0x99, 0x33, 0x00, 0x00], + 0x3D => [0x99, 0x33, 0x66, 0x00], + 0x3E => [0x33, 0x33, 0x99, 0x00], + 0x3F => [0x33, 0x33, 0x33, 0x00], + ]; + } + + /** + * Assemble worksheets into a workbook and send the BIFF data to an OLE + * storage. + * + * @param array $pWorksheetSizes The sizes in bytes of the binary worksheet streams + * + * @return string Binary data for workbook stream + */ + public function writeWorkbook(array $pWorksheetSizes) + { + $this->worksheetSizes = $pWorksheetSizes; + + // Calculate the number of selected worksheet tabs and call the finalization + // methods for each worksheet + $total_worksheets = $this->spreadsheet->getSheetCount(); + + // Add part 1 of the Workbook globals, what goes before the SHEET records + $this->storeBof(0x0005); + $this->writeCodepage(); + $this->writeWindow1(); + + $this->writeDateMode(); + $this->writeAllFonts(); + $this->writeAllNumberFormats(); + $this->writeAllXfs(); + $this->writeAllStyles(); + $this->writePalette(); + + // Prepare part 3 of the workbook global stream, what goes after the SHEET records + $part3 = ''; + if ($this->countryCode != -1) { + $part3 .= $this->writeCountry(); + } + $part3 .= $this->writeRecalcId(); + + $part3 .= $this->writeSupbookInternal(); + /* TODO: store external SUPBOOK records and XCT and CRN records + in case of external references for BIFF8 */ + $part3 .= $this->writeExternalsheetBiff8(); + $part3 .= $this->writeAllDefinedNamesBiff8(); + $part3 .= $this->writeMsoDrawingGroup(); + $part3 .= $this->writeSharedStringsTable(); + + $part3 .= $this->writeEof(); + + // Add part 2 of the Workbook globals, the SHEET records + $this->calcSheetOffsets(); + for ($i = 0; $i < $total_worksheets; ++$i) { + $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]); + } + + // Add part 3 of the Workbook globals + $this->_data .= $part3; + + return $this->_data; + } + + /** + * Calculate offsets for Worksheet BOF records. + */ + private function calcSheetOffsets() + { + $boundsheet_length = 10; // fixed length for a BOUNDSHEET record + + // size of Workbook globals part 1 + 3 + $offset = $this->_datasize; + + // add size of Workbook globals part 2, the length of the SHEET records + $total_worksheets = count($this->spreadsheet->getAllSheets()); + foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) { + $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle())); + } + + // add the sizes of each of the Sheet substreams, respectively + for ($i = 0; $i < $total_worksheets; ++$i) { + $this->worksheetOffsets[$i] = $offset; + $offset += $this->worksheetSizes[$i]; + } + $this->biffSize = $offset; + } + + /** + * Store the Excel FONT records. + */ + private function writeAllFonts() + { + foreach ($this->fontWriters as $fontWriter) { + $this->append($fontWriter->writeFont()); + } + } + + /** + * Store user defined numerical formats i.e. FORMAT records. + */ + private function writeAllNumberFormats() + { + foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) { + $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex); + } + } + + /** + * Write all XF records. + */ + private function writeAllXfs() + { + foreach ($this->xfWriters as $xfWriter) { + $this->append($xfWriter->writeXf()); + } + } + + /** + * Write all STYLE records. + */ + private function writeAllStyles() + { + $this->writeStyle(); + } + + /** + * Writes all the DEFINEDNAME records (BIFF8). + * So far this is only used for repeating rows/columns (print titles) and print areas. + */ + private function writeAllDefinedNamesBiff8() + { + $chunk = ''; + + // Named ranges + if (count($this->spreadsheet->getNamedRanges()) > 0) { + // Loop named ranges + $namedRanges = $this->spreadsheet->getNamedRanges(); + foreach ($namedRanges as $namedRange) { + // Create absolute coordinate + $range = Coordinate::splitRange($namedRange->getRange()); + $iMax = count($range); + for ($i = 0; $i < $iMax; ++$i) { + $range[$i][0] = '\'' . str_replace("'", "''", $namedRange->getWorksheet()->getTitle()) . '\'!' . Coordinate::absoluteCoordinate($range[$i][0]); + if (isset($range[$i][1])) { + $range[$i][1] = Coordinate::absoluteCoordinate($range[$i][1]); + } + } + $range = Coordinate::buildRange($range); // e.g. Sheet1!$A$1:$B$2 + + // parse formula + try { + $error = $this->parser->parse($range); + $formulaData = $this->parser->toReversePolish(); + + // make sure tRef3d is of type tRef3dR (0x3A) + if (isset($formulaData[0]) and ($formulaData[0] == "\x7A" or $formulaData[0] == "\x5A")) { + $formulaData = "\x3A" . substr($formulaData, 1); + } + + if ($namedRange->getLocalOnly()) { + // local scope + $scope = $this->spreadsheet->getIndex($namedRange->getScope()) + 1; + } else { + // global scope + $scope = 0; + } + $chunk .= $this->writeData($this->writeDefinedNameBiff8($namedRange->getName(), $formulaData, $scope, false)); + } catch (PhpSpreadsheetException $e) { + // do nothing + } + } + } + + // total number of sheets + $total_worksheets = $this->spreadsheet->getSheetCount(); + + // write the print titles (repeating rows, columns), if any + for ($i = 0; $i < $total_worksheets; ++$i) { + $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup(); + // simultaneous repeatColumns repeatRows + if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) { + $repeat = $sheetSetup->getColumnsToRepeatAtLeft(); + $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1; + $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1; + + $repeat = $sheetSetup->getRowsToRepeatAtTop(); + $rowmin = $repeat[0] - 1; + $rowmax = $repeat[1] - 1; + + // construct formula data manually + $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc + $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d + $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d + $formulaData .= pack('C', 0x10); // tList + + // store the DEFINEDNAME record + $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true)); + + // (exclusive) either repeatColumns or repeatRows + } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) { + // Columns to repeat + if ($sheetSetup->isColumnsToRepeatAtLeftSet()) { + $repeat = $sheetSetup->getColumnsToRepeatAtLeft(); + $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1; + $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1; + } else { + $colmin = 0; + $colmax = 255; + } + // Rows to repeat + if ($sheetSetup->isRowsToRepeatAtTopSet()) { + $repeat = $sheetSetup->getRowsToRepeatAtTop(); + $rowmin = $repeat[0] - 1; + $rowmax = $repeat[1] - 1; + } else { + $rowmin = 0; + $rowmax = 65535; + } + + // construct formula data manually because parser does not recognize absolute 3d cell references + $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax); + + // store the DEFINEDNAME record + $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true)); + } + } + + // write the print areas, if any + for ($i = 0; $i < $total_worksheets; ++$i) { + $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup(); + if ($sheetSetup->isPrintAreaSet()) { + // Print area, e.g. A3:J6,H1:X20 + $printArea = Coordinate::splitRange($sheetSetup->getPrintArea()); + $countPrintArea = count($printArea); + + $formulaData = ''; + for ($j = 0; $j < $countPrintArea; ++$j) { + $printAreaRect = $printArea[$j]; // e.g. A3:J6 + $printAreaRect[0] = Coordinate::coordinateFromString($printAreaRect[0]); + $printAreaRect[1] = Coordinate::coordinateFromString($printAreaRect[1]); + + $print_rowmin = $printAreaRect[0][1] - 1; + $print_rowmax = $printAreaRect[1][1] - 1; + $print_colmin = Coordinate::columnIndexFromString($printAreaRect[0][0]) - 1; + $print_colmax = Coordinate::columnIndexFromString($printAreaRect[1][0]) - 1; + + // construct formula data manually because parser does not recognize absolute 3d cell references + $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax); + + if ($j > 0) { + $formulaData .= pack('C', 0x10); // list operator token ',' + } + } + + // store the DEFINEDNAME record + $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true)); + } + } + + // write autofilters, if any + for ($i = 0; $i < $total_worksheets; ++$i) { + $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter(); + $autoFilterRange = $sheetAutoFilter->getRange(); + if (!empty($autoFilterRange)) { + $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange); + + //Autofilter built in name + $name = pack('C', 0x0D); + + $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true)); + } + } + + return $chunk; + } + + /** + * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data. + * + * @param string $name The name in UTF-8 + * @param string $formulaData The binary formula data + * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global + * @param bool $isBuiltIn Built-in name? + * + * @return string Complete binary record data + */ + private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false) + { + $record = 0x0018; + + // option flags + $options = $isBuiltIn ? 0x20 : 0x00; + + // length of the name, character count + $nlen = StringHelper::countCharacters($name); + + // name with stripped length field + $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2); + + // size of the formula (in bytes) + $sz = strlen($formulaData); + + // combine the parts + $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0) + . $name . $formulaData; + $length = strlen($data); + + $header = pack('vv', $record, $length); + + return $header . $data; + } + + /** + * Write a short NAME record. + * + * @param string $name + * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global + * @param integer[][] $rangeBounds range boundaries + * @param bool $isHidden + * + * @return string Complete binary record data + * */ + private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false) + { + $record = 0x0018; + + // option flags + $options = ($isHidden ? 0x21 : 0x00); + + $extra = pack( + 'Cvvvvv', + 0x3B, + $sheetIndex - 1, + $rangeBounds[0][1] - 1, + $rangeBounds[1][1] - 1, + $rangeBounds[0][0] - 1, + $rangeBounds[1][0] - 1 + ); + + // size of the formula (in bytes) + $sz = strlen($extra); + + // combine the parts + $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0) + . $name . $extra; + $length = strlen($data); + + $header = pack('vv', $record, $length); + + return $header . $data; + } + + /** + * Stores the CODEPAGE biff record. + */ + private function writeCodepage() + { + $record = 0x0042; // Record identifier + $length = 0x0002; // Number of bytes to follow + $cv = $this->codepage; // The code page + + $header = pack('vv', $record, $length); + $data = pack('v', $cv); + + $this->append($header . $data); + } + + /** + * Write Excel BIFF WINDOW1 record. + */ + private function writeWindow1() + { + $record = 0x003D; // Record identifier + $length = 0x0012; // Number of bytes to follow + + $xWn = 0x0000; // Horizontal position of window + $yWn = 0x0000; // Vertical position of window + $dxWn = 0x25BC; // Width of window + $dyWn = 0x1572; // Height of window + + $grbit = 0x0038; // Option flags + + // not supported by PhpSpreadsheet, so there is only one selected sheet, the active + $ctabsel = 1; // Number of workbook tabs selected + + $wTabRatio = 0x0258; // Tab to scrollbar ratio + + // not supported by PhpSpreadsheet, set to 0 + $itabFirst = 0; // 1st displayed worksheet + $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet + + $header = pack('vv', $record, $length); + $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio); + $this->append($header . $data); + } + + /** + * Writes Excel BIFF BOUNDSHEET record. + * + * @param Worksheet $sheet Worksheet name + * @param int $offset Location of worksheet BOF + */ + private function writeBoundSheet($sheet, $offset) + { + $sheetname = $sheet->getTitle(); + $record = 0x0085; // Record identifier + + // sheet state + switch ($sheet->getSheetState()) { + case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE: + $ss = 0x00; + + break; + case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN: + $ss = 0x01; + + break; + case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN: + $ss = 0x02; + + break; + default: + $ss = 0x00; + + break; + } + + // sheet type + $st = 0x00; + + $grbit = 0x0000; // Visibility and sheet type + + $data = pack('VCC', $offset, $ss, $st); + $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname); + + $length = strlen($data); + $header = pack('vv', $record, $length); + $this->append($header . $data); + } + + /** + * Write Internal SUPBOOK record. + */ + private function writeSupbookInternal() + { + $record = 0x01AE; // Record identifier + $length = 0x0004; // Bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401); + + return $this->writeData($header . $data); + } + + /** + * Writes the Excel BIFF EXTERNSHEET record. These references are used by + * formulas. + */ + private function writeExternalsheetBiff8() + { + $totalReferences = count($this->parser->references); + $record = 0x0017; // Record identifier + $length = 2 + 6 * $totalReferences; // Number of bytes to follow + + $supbook_index = 0; // FIXME: only using internal SUPBOOK record + $header = pack('vv', $record, $length); + $data = pack('v', $totalReferences); + for ($i = 0; $i < $totalReferences; ++$i) { + $data .= $this->parser->references[$i]; + } + + return $this->writeData($header . $data); + } + + /** + * Write Excel BIFF STYLE records. + */ + private function writeStyle() + { + $record = 0x0293; // Record identifier + $length = 0x0004; // Bytes to follow + + $ixfe = 0x8000; // Index to cell style XF + $BuiltIn = 0x00; // Built-in style + $iLevel = 0xff; // Outline style level + + $header = pack('vv', $record, $length); + $data = pack('vCC', $ixfe, $BuiltIn, $iLevel); + $this->append($header . $data); + } + + /** + * Writes Excel FORMAT record for non "built-in" numerical formats. + * + * @param string $format Custom format string + * @param int $ifmt Format index code + */ + private function writeNumberFormat($format, $ifmt) + { + $record = 0x041E; // Record identifier + + $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format); + $length = 2 + strlen($numberFormatString); // Number of bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('v', $ifmt) . $numberFormatString; + $this->append($header . $data); + } + + /** + * Write DATEMODE record to indicate the date system in use (1904 or 1900). + */ + private function writeDateMode() + { + $record = 0x0022; // Record identifier + $length = 0x0002; // Bytes to follow + + $f1904 = (Date::getExcelCalendar() == Date::CALENDAR_MAC_1904) + ? 1 + : 0; // Flag for 1904 date system + + $header = pack('vv', $record, $length); + $data = pack('v', $f1904); + $this->append($header . $data); + } + + /** + * Stores the COUNTRY record for localization. + * + * @return string + */ + private function writeCountry() + { + $record = 0x008C; // Record identifier + $length = 4; // Number of bytes to follow + + $header = pack('vv', $record, $length); + // using the same country code always for simplicity + $data = pack('vv', $this->countryCode, $this->countryCode); + + return $this->writeData($header . $data); + } + + /** + * Write the RECALCID record. + * + * @return string + */ + private function writeRecalcId() + { + $record = 0x01C1; // Record identifier + $length = 8; // Number of bytes to follow + + $header = pack('vv', $record, $length); + + // by inspection of real Excel files, MS Office Excel 2007 writes this + $data = pack('VV', 0x000001C1, 0x00001E667); + + return $this->writeData($header . $data); + } + + /** + * Stores the PALETTE biff record. + */ + private function writePalette() + { + $aref = $this->palette; + + $record = 0x0092; // Record identifier + $length = 2 + 4 * count($aref); // Number of bytes to follow + $ccv = count($aref); // Number of RGB values to follow + $data = ''; // The RGB data + + // Pack the RGB data + foreach ($aref as $color) { + foreach ($color as $byte) { + $data .= pack('C', $byte); + } + } + + $header = pack('vvv', $record, $length, $ccv); + $this->append($header . $data); + } + + /** + * Handling of the SST continue blocks is complicated by the need to include an + * additional continuation byte depending on whether the string is split between + * blocks or whether it starts at the beginning of the block. (There are also + * additional complications that will arise later when/if Rich Strings are + * supported). + * + * The Excel documentation says that the SST record should be followed by an + * EXTSST record. The EXTSST record is a hash table that is used to optimise + * access to SST. However, despite the documentation it doesn't seem to be + * required so we will ignore it. + * + * @return string Binary data + */ + private function writeSharedStringsTable() + { + // maximum size of record data (excluding record header) + $continue_limit = 8224; + + // initialize array of record data blocks + $recordDatas = []; + + // start SST record data block with total number of strings, total number of unique strings + $recordData = pack('VV', $this->stringTotal, $this->stringUnique); + + // loop through all (unique) strings in shared strings table + foreach (array_keys($this->stringTable) as $string) { + // here $string is a BIFF8 encoded string + + // length = character count + $headerinfo = unpack('vlength/Cencoding', $string); + + // currently, this is always 1 = uncompressed + $encoding = $headerinfo['encoding']; + + // initialize finished writing current $string + $finished = false; + + while ($finished === false) { + // normally, there will be only one cycle, but if string cannot immediately be written as is + // there will be need for more than one cylcle, if string longer than one record data block, there + // may be need for even more cycles + + if (strlen($recordData) + strlen($string) <= $continue_limit) { + // then we can write the string (or remainder of string) without any problems + $recordData .= $string; + + if (strlen($recordData) + strlen($string) == $continue_limit) { + // we close the record data block, and initialize a new one + $recordDatas[] = $recordData; + $recordData = ''; + } + + // we are finished writing this string + $finished = true; + } else { + // special treatment writing the string (or remainder of the string) + // If the string is very long it may need to be written in more than one CONTINUE record. + + // check how many bytes more there is room for in the current record + $space_remaining = $continue_limit - strlen($recordData); + + // minimum space needed + // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character + // compressed: 2 byte string length length field + 1 byte option flags + 1 byte character + $min_space_needed = ($encoding == 1) ? 5 : 4; + + // We have two cases + // 1. space remaining is less than minimum space needed + // here we must waste the space remaining and move to next record data block + // 2. space remaining is greater than or equal to minimum space needed + // here we write as much as we can in the current block, then move to next record data block + + // 1. space remaining is less than minimum space needed + if ($space_remaining < $min_space_needed) { + // we close the block, store the block data + $recordDatas[] = $recordData; + + // and start new record data block where we start writing the string + $recordData = ''; + + // 2. space remaining is greater than or equal to minimum space needed + } else { + // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below + $effective_space_remaining = $space_remaining; + + // for uncompressed strings, sometimes effective space remaining is reduced by 1 + if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) { + --$effective_space_remaining; + } + + // one block fininshed, store the block data + $recordData .= substr($string, 0, $effective_space_remaining); + + $string = substr($string, $effective_space_remaining); // for next cycle in while loop + $recordDatas[] = $recordData; + + // start new record data block with the repeated option flags + $recordData = pack('C', $encoding); + } + } + } + } + + // Store the last record data block unless it is empty + // if there was no need for any continue records, this will be the for SST record data block itself + if (strlen($recordData) > 0) { + $recordDatas[] = $recordData; + } + + // combine into one chunk with all the blocks SST, CONTINUE,... + $chunk = ''; + foreach ($recordDatas as $i => $recordData) { + // first block should have the SST record header, remaing should have CONTINUE header + $record = ($i == 0) ? 0x00FC : 0x003C; + + $header = pack('vv', $record, strlen($recordData)); + $data = $header . $recordData; + + $chunk .= $this->writeData($data); + } + + return $chunk; + } + + /** + * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records. + */ + private function writeMsoDrawingGroup() + { + // write the Escher stream if necessary + if (isset($this->escher)) { + $writer = new Escher($this->escher); + $data = $writer->close(); + + $record = 0x00EB; + $length = strlen($data); + $header = pack('vv', $record, $length); + + return $this->writeData($header . $data); + } + + return ''; + } + + /** + * Get Escher object. + * + * @return \PhpOffice\PhpSpreadsheet\Shared\Escher + */ + public function getEscher() + { + return $this->escher; + } + + /** + * Set Escher object. + * + * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue + */ + public function setEscher(\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null) + { + $this->escher = $pValue; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Worksheet.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Worksheet.php new file mode 100644 index 00000000000..db63fa34423 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -0,0 +1,4447 @@ + +// * +// * The majority of this is _NOT_ my code. I simply ported it from the +// * PERL Spreadsheet::WriteExcel module. +// * +// * The author of the Spreadsheet::WriteExcel module is John McNamara +// * +// * +// * I _DO_ maintain this code, and John McNamara has nothing to do with the +// * porting of this code to PHP. Any questions directly related to this +// * class library should be directed to me. +// * +// * License Information: +// * +// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets +// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com +// * +// * This library is free software; you can redistribute it and/or +// * modify it under the terms of the GNU Lesser General Public +// * License as published by the Free Software Foundation; either +// * version 2.1 of the License, or (at your option) any later version. +// * +// * This library is distributed in the hope that it will be useful, +// * but WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Lesser General Public License for more details. +// * +// * You should have received a copy of the GNU Lesser General Public +// * License along with this library; if not, write to the Free Software +// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// */ +class Worksheet extends BIFFwriter +{ + /** + * Formula parser. + * + * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser + */ + private $parser; + + /** + * Maximum number of characters for a string (LABEL record in BIFF5). + * + * @var int + */ + private $xlsStringMaxLength; + + /** + * Array containing format information for columns. + * + * @var array + */ + private $columnInfo; + + /** + * Array containing the selected area for the worksheet. + * + * @var array + */ + private $selection; + + /** + * The active pane for the worksheet. + * + * @var int + */ + private $activePane; + + /** + * Whether to use outline. + * + * @var int + */ + private $outlineOn; + + /** + * Auto outline styles. + * + * @var bool + */ + private $outlineStyle; + + /** + * Whether to have outline summary below. + * + * @var bool + */ + private $outlineBelow; + + /** + * Whether to have outline summary at the right. + * + * @var bool + */ + private $outlineRight; + + /** + * Reference to the total number of strings in the workbook. + * + * @var int + */ + private $stringTotal; + + /** + * Reference to the number of unique strings in the workbook. + * + * @var int + */ + private $stringUnique; + + /** + * Reference to the array containing all the unique strings in the workbook. + * + * @var array + */ + private $stringTable; + + /** + * Color cache. + */ + private $colors; + + /** + * Index of first used row (at least 0). + * + * @var int + */ + private $firstRowIndex; + + /** + * Index of last used row. (no used rows means -1). + * + * @var int + */ + private $lastRowIndex; + + /** + * Index of first used column (at least 0). + * + * @var int + */ + private $firstColumnIndex; + + /** + * Index of last used column (no used columns means -1). + * + * @var int + */ + private $lastColumnIndex; + + /** + * Sheet object. + * + * @var \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + */ + public $phpSheet; + + /** + * Count cell style Xfs. + * + * @var int + */ + private $countCellStyleXfs; + + /** + * Escher object corresponding to MSODRAWING. + * + * @var \PhpOffice\PhpSpreadsheet\Shared\Escher + */ + private $escher; + + /** + * Array of font hashes associated to FONT records index. + * + * @var array + */ + public $fontHashIndex; + + /** + * @var bool + */ + private $preCalculateFormulas; + + /** + * @var int + */ + private $printHeaders; + + /** + * Constructor. + * + * @param int $str_total Total number of strings + * @param int $str_unique Total number of unique strings + * @param array &$str_table String Table + * @param array &$colors Colour Table + * @param Parser $parser The formula parser created for the Workbook + * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write + */ + public function __construct(&$str_total, &$str_unique, &$str_table, &$colors, Parser $parser, $preCalculateFormulas, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet) + { + // It needs to call its parent's constructor explicitly + parent::__construct(); + + $this->preCalculateFormulas = $preCalculateFormulas; + $this->stringTotal = &$str_total; + $this->stringUnique = &$str_unique; + $this->stringTable = &$str_table; + $this->colors = &$colors; + $this->parser = $parser; + + $this->phpSheet = $phpSheet; + + $this->xlsStringMaxLength = 255; + $this->columnInfo = []; + $this->selection = [0, 0, 0, 0]; + $this->activePane = 3; + + $this->printHeaders = 0; + + $this->outlineStyle = 0; + $this->outlineBelow = 1; + $this->outlineRight = 1; + $this->outlineOn = 1; + + $this->fontHashIndex = []; + + // calculate values for DIMENSIONS record + $minR = 1; + $minC = 'A'; + + $maxR = $this->phpSheet->getHighestRow(); + $maxC = $this->phpSheet->getHighestColumn(); + + // Determine lowest and highest column and row + $this->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR; + + $this->firstColumnIndex = Coordinate::columnIndexFromString($minC); + $this->lastColumnIndex = Coordinate::columnIndexFromString($maxC); + +// if ($this->firstColumnIndex > 255) $this->firstColumnIndex = 255; + if ($this->lastColumnIndex > 255) { + $this->lastColumnIndex = 255; + } + + $this->countCellStyleXfs = count($phpSheet->getParent()->getCellStyleXfCollection()); + } + + /** + * Add data to the beginning of the workbook (note the reverse order) + * and to the end of the workbook. + * + * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::storeWorkbook() + */ + public function close() + { + $phpSheet = $this->phpSheet; + + // Write BOF record + $this->storeBof(0x0010); + + // Write PRINTHEADERS + $this->writePrintHeaders(); + + // Write PRINTGRIDLINES + $this->writePrintGridlines(); + + // Write GRIDSET + $this->writeGridset(); + + // Calculate column widths + $phpSheet->calculateColumnWidths(); + + // Column dimensions + if (($defaultWidth = $phpSheet->getDefaultColumnDimension()->getWidth()) < 0) { + $defaultWidth = \PhpOffice\PhpSpreadsheet\Shared\Font::getDefaultColumnWidthByFont($phpSheet->getParent()->getDefaultStyle()->getFont()); + } + + $columnDimensions = $phpSheet->getColumnDimensions(); + $maxCol = $this->lastColumnIndex - 1; + for ($i = 0; $i <= $maxCol; ++$i) { + $hidden = 0; + $level = 0; + $xfIndex = 15; // there are 15 cell style Xfs + + $width = $defaultWidth; + + $columnLetter = Coordinate::stringFromColumnIndex($i + 1); + if (isset($columnDimensions[$columnLetter])) { + $columnDimension = $columnDimensions[$columnLetter]; + if ($columnDimension->getWidth() >= 0) { + $width = $columnDimension->getWidth(); + } + $hidden = $columnDimension->getVisible() ? 0 : 1; + $level = $columnDimension->getOutlineLevel(); + $xfIndex = $columnDimension->getXfIndex() + 15; // there are 15 cell style Xfs + } + + // Components of columnInfo: + // $firstcol first column on the range + // $lastcol last column on the range + // $width width to set + // $xfIndex The optional cell style Xf index to apply to the columns + // $hidden The optional hidden atribute + // $level The optional outline level + $this->columnInfo[] = [$i, $i, $width, $xfIndex, $hidden, $level]; + } + + // Write GUTS + $this->writeGuts(); + + // Write DEFAULTROWHEIGHT + $this->writeDefaultRowHeight(); + // Write WSBOOL + $this->writeWsbool(); + // Write horizontal and vertical page breaks + $this->writeBreaks(); + // Write page header + $this->writeHeader(); + // Write page footer + $this->writeFooter(); + // Write page horizontal centering + $this->writeHcenter(); + // Write page vertical centering + $this->writeVcenter(); + // Write left margin + $this->writeMarginLeft(); + // Write right margin + $this->writeMarginRight(); + // Write top margin + $this->writeMarginTop(); + // Write bottom margin + $this->writeMarginBottom(); + // Write page setup + $this->writeSetup(); + // Write sheet protection + $this->writeProtect(); + // Write SCENPROTECT + $this->writeScenProtect(); + // Write OBJECTPROTECT + $this->writeObjectProtect(); + // Write sheet password + $this->writePassword(); + // Write DEFCOLWIDTH record + $this->writeDefcol(); + + // Write the COLINFO records if they exist + if (!empty($this->columnInfo)) { + $colcount = count($this->columnInfo); + for ($i = 0; $i < $colcount; ++$i) { + $this->writeColinfo($this->columnInfo[$i]); + } + } + $autoFilterRange = $phpSheet->getAutoFilter()->getRange(); + if (!empty($autoFilterRange)) { + // Write AUTOFILTERINFO + $this->writeAutoFilterInfo(); + } + + // Write sheet dimensions + $this->writeDimensions(); + + // Row dimensions + foreach ($phpSheet->getRowDimensions() as $rowDimension) { + $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs + $this->writeRow($rowDimension->getRowIndex() - 1, $rowDimension->getRowHeight(), $xfIndex, ($rowDimension->getVisible() ? '0' : '1'), $rowDimension->getOutlineLevel()); + } + + // Write Cells + foreach ($phpSheet->getCoordinates() as $coordinate) { + $cell = $phpSheet->getCell($coordinate); + $row = $cell->getRow() - 1; + $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1; + + // Don't break Excel break the code! + if ($row > 65535 || $column > 255) { + throw new WriterException('Rows or columns overflow! Excel5 has limit to 65535 rows and 255 columns. Use XLSX instead.'); + } + + // Write cell value + $xfIndex = $cell->getXfIndex() + 15; // there are 15 cell style Xfs + + $cVal = $cell->getValue(); + if ($cVal instanceof RichText) { + $arrcRun = []; + $str_len = StringHelper::countCharacters($cVal->getPlainText(), 'UTF-8'); + $str_pos = 0; + $elements = $cVal->getRichTextElements(); + foreach ($elements as $element) { + // FONT Index + if ($element instanceof Run) { + $str_fontidx = $this->fontHashIndex[$element->getFont()->getHashCode()]; + } else { + $str_fontidx = 0; + } + $arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx]; + // Position FROM + $str_pos += StringHelper::countCharacters($element->getText(), 'UTF-8'); + } + $this->writeRichTextString($row, $column, $cVal->getPlainText(), $xfIndex, $arrcRun); + } else { + switch ($cell->getDatatype()) { + case DataType::TYPE_STRING: + case DataType::TYPE_NULL: + if ($cVal === '' || $cVal === null) { + $this->writeBlank($row, $column, $xfIndex); + } else { + $this->writeString($row, $column, $cVal, $xfIndex); + } + + break; + case DataType::TYPE_NUMERIC: + $this->writeNumber($row, $column, $cVal, $xfIndex); + + break; + case DataType::TYPE_FORMULA: + $calculatedValue = $this->preCalculateFormulas ? + $cell->getCalculatedValue() : null; + $this->writeFormula($row, $column, $cVal, $xfIndex, $calculatedValue); + + break; + case DataType::TYPE_BOOL: + $this->writeBoolErr($row, $column, $cVal, 0, $xfIndex); + + break; + case DataType::TYPE_ERROR: + $this->writeBoolErr($row, $column, self::mapErrorCode($cVal), 1, $xfIndex); + + break; + } + } + } + + // Append + $this->writeMsoDrawing(); + + // Write WINDOW2 record + $this->writeWindow2(); + + // Write PLV record + $this->writePageLayoutView(); + + // Write ZOOM record + $this->writeZoom(); + if ($phpSheet->getFreezePane()) { + $this->writePanes(); + } + + // Write SELECTION record + $this->writeSelection(); + + // Write MergedCellsTable Record + $this->writeMergedCells(); + + // Hyperlinks + foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) { + list($column, $row) = Coordinate::coordinateFromString($coordinate); + + $url = $hyperlink->getUrl(); + + if (strpos($url, 'sheet://') !== false) { + // internal to current workbook + $url = str_replace('sheet://', 'internal:', $url); + } elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) { + // URL + } else { + // external (local file) + $url = 'external:' . $url; + } + + $this->writeUrl($row - 1, Coordinate::columnIndexFromString($column) - 1, $url); + } + + $this->writeDataValidity(); + $this->writeSheetLayout(); + + // Write SHEETPROTECTION record + $this->writeSheetProtection(); + $this->writeRangeProtection(); + + $arrConditionalStyles = $phpSheet->getConditionalStylesCollection(); + if (!empty($arrConditionalStyles)) { + $arrConditional = []; + // @todo CFRule & CFHeader + // Write CFHEADER record + $this->writeCFHeader(); + // Write ConditionalFormattingTable records + foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) { + foreach ($conditionalStyles as $conditional) { + if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION + || $conditional->getConditionType() == Conditional::CONDITION_CELLIS) { + if (!isset($arrConditional[$conditional->getHashCode()])) { + // This hash code has been handled + $arrConditional[$conditional->getHashCode()] = true; + + // Write CFRULE record + $this->writeCFRule($conditional); + } + } + } + } + } + + $this->storeEof(); + } + + /** + * Write a cell range address in BIFF8 + * always fixed range + * See section 2.5.14 in OpenOffice.org's Documentation of the Microsoft Excel File Format. + * + * @param string $range E.g. 'A1' or 'A1:B6' + * + * @return string Binary data + */ + private function writeBIFF8CellRangeAddressFixed($range) + { + $explodes = explode(':', $range); + + // extract first cell, e.g. 'A1' + $firstCell = $explodes[0]; + + // extract last cell, e.g. 'B6' + if (count($explodes) == 1) { + $lastCell = $firstCell; + } else { + $lastCell = $explodes[1]; + } + + $firstCellCoordinates = Coordinate::coordinateFromString($firstCell); // e.g. [0, 1] + $lastCellCoordinates = Coordinate::coordinateFromString($lastCell); // e.g. [1, 6] + + return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, Coordinate::columnIndexFromString($firstCellCoordinates[0]) - 1, Coordinate::columnIndexFromString($lastCellCoordinates[0]) - 1); + } + + /** + * Retrieves data from memory in one chunk, or from disk in $buffer + * sized chunks. + * + * @return string The data + */ + public function getData() + { + $buffer = 4096; + + // Return data stored in memory + if (isset($this->_data)) { + $tmp = $this->_data; + unset($this->_data); + + return $tmp; + } + // No data to return + return false; + } + + /** + * Set the option to print the row and column headers on the printed page. + * + * @param int $print Whether to print the headers or not. Defaults to 1 (print). + */ + public function printRowColHeaders($print = 1) + { + $this->printHeaders = $print; + } + + /** + * This method sets the properties for outlining and grouping. The defaults + * correspond to Excel's defaults. + * + * @param bool $visible + * @param bool $symbols_below + * @param bool $symbols_right + * @param bool $auto_style + */ + public function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false) + { + $this->outlineOn = $visible; + $this->outlineBelow = $symbols_below; + $this->outlineRight = $symbols_right; + $this->outlineStyle = $auto_style; + + // Ensure this is a boolean vale for Window2 + if ($this->outlineOn) { + $this->outlineOn = 1; + } + } + + /** + * Write a double to the specified row and column (zero indexed). + * An integer can be written as a double. Excel will display an + * integer. $format is optional. + * + * Returns 0 : normal termination + * -2 : row or column out of range + * + * @param int $row Zero indexed row + * @param int $col Zero indexed column + * @param float $num The number to write + * @param mixed $xfIndex The optional XF format + * + * @return int + */ + private function writeNumber($row, $col, $num, $xfIndex) + { + $record = 0x0203; // Record identifier + $length = 0x000E; // Number of bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('vvv', $row, $col, $xfIndex); + $xl_double = pack('d', $num); + if (self::getByteOrder()) { // if it's Big Endian + $xl_double = strrev($xl_double); + } + + $this->append($header . $data . $xl_double); + + return 0; + } + + /** + * Write a LABELSST record or a LABEL record. Which one depends on BIFF version. + * + * @param int $row Row index (0-based) + * @param int $col Column index (0-based) + * @param string $str The string + * @param int $xfIndex Index to XF record + */ + private function writeString($row, $col, $str, $xfIndex) + { + $this->writeLabelSst($row, $col, $str, $xfIndex); + } + + /** + * Write a LABELSST record or a LABEL record. Which one depends on BIFF version + * It differs from writeString by the writing of rich text strings. + * + * @param int $row Row index (0-based) + * @param int $col Column index (0-based) + * @param string $str The string + * @param int $xfIndex The XF format index for the cell + * @param array $arrcRun Index to Font record and characters beginning + */ + private function writeRichTextString($row, $col, $str, $xfIndex, $arrcRun) + { + $record = 0x00FD; // Record identifier + $length = 0x000A; // Bytes to follow + $str = StringHelper::UTF8toBIFF8UnicodeShort($str, $arrcRun); + + // check if string is already present + if (!isset($this->stringTable[$str])) { + $this->stringTable[$str] = $this->stringUnique++; + } + ++$this->stringTotal; + + $header = pack('vv', $record, $length); + $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]); + $this->append($header . $data); + } + + /** + * Write a string to the specified row and column (zero indexed). + * This is the BIFF8 version (no 255 chars limit). + * $format is optional. + * + * @param int $row Zero indexed row + * @param int $col Zero indexed column + * @param string $str The string to write + * @param mixed $xfIndex The XF format index for the cell + */ + private function writeLabelSst($row, $col, $str, $xfIndex) + { + $record = 0x00FD; // Record identifier + $length = 0x000A; // Bytes to follow + + $str = StringHelper::UTF8toBIFF8UnicodeLong($str); + + // check if string is already present + if (!isset($this->stringTable[$str])) { + $this->stringTable[$str] = $this->stringUnique++; + } + ++$this->stringTotal; + + $header = pack('vv', $record, $length); + $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]); + $this->append($header . $data); + } + + /** + * Write a blank cell to the specified row and column (zero indexed). + * A blank cell is used to specify formatting without adding a string + * or a number. + * + * A blank cell without a format serves no purpose. Therefore, we don't write + * a BLANK record unless a format is specified. + * + * Returns 0 : normal termination (including no format) + * -1 : insufficient number of arguments + * -2 : row or column out of range + * + * @param int $row Zero indexed row + * @param int $col Zero indexed column + * @param mixed $xfIndex The XF format index + * + * @return int + */ + public function writeBlank($row, $col, $xfIndex) + { + $record = 0x0201; // Record identifier + $length = 0x0006; // Number of bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('vvv', $row, $col, $xfIndex); + $this->append($header . $data); + + return 0; + } + + /** + * Write a boolean or an error type to the specified row and column (zero indexed). + * + * @param int $row Row index (0-based) + * @param int $col Column index (0-based) + * @param int $value + * @param bool $isError Error or Boolean? + * @param int $xfIndex + * + * @return int + */ + private function writeBoolErr($row, $col, $value, $isError, $xfIndex) + { + $record = 0x0205; + $length = 8; + + $header = pack('vv', $record, $length); + $data = pack('vvvCC', $row, $col, $xfIndex, $value, $isError); + $this->append($header . $data); + + return 0; + } + + /** + * Write a formula to the specified row and column (zero indexed). + * The textual representation of the formula is passed to the parser in + * Parser.php which returns a packed binary string. + * + * Returns 0 : normal termination + * -1 : formula errors (bad formula) + * -2 : row or column out of range + * + * @param int $row Zero indexed row + * @param int $col Zero indexed column + * @param string $formula The formula text string + * @param mixed $xfIndex The XF format index + * @param mixed $calculatedValue Calculated value + * + * @return int + */ + private function writeFormula($row, $col, $formula, $xfIndex, $calculatedValue) + { + $record = 0x0006; // Record identifier + + // Initialize possible additional value for STRING record that should be written after the FORMULA record? + $stringValue = null; + + // calculated value + if (isset($calculatedValue)) { + // Since we can't yet get the data type of the calculated value, + // we use best effort to determine data type + if (is_bool($calculatedValue)) { + // Boolean value + $num = pack('CCCvCv', 0x01, 0x00, (int) $calculatedValue, 0x00, 0x00, 0xFFFF); + } elseif (is_int($calculatedValue) || is_float($calculatedValue)) { + // Numeric value + $num = pack('d', $calculatedValue); + } elseif (is_string($calculatedValue)) { + $errorCodes = DataType::getErrorCodes(); + if (isset($errorCodes[$calculatedValue])) { + // Error value + $num = pack('CCCvCv', 0x02, 0x00, self::mapErrorCode($calculatedValue), 0x00, 0x00, 0xFFFF); + } elseif ($calculatedValue === '') { + // Empty string (and BIFF8) + $num = pack('CCCvCv', 0x03, 0x00, 0x00, 0x00, 0x00, 0xFFFF); + } else { + // Non-empty string value (or empty string BIFF5) + $stringValue = $calculatedValue; + $num = pack('CCCvCv', 0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFF); + } + } else { + // We are really not supposed to reach here + $num = pack('d', 0x00); + } + } else { + $num = pack('d', 0x00); + } + + $grbit = 0x03; // Option flags + $unknown = 0x0000; // Must be zero + + // Strip the '=' or '@' sign at the beginning of the formula string + if ($formula[0] == '=') { + $formula = substr($formula, 1); + } else { + // Error handling + $this->writeString($row, $col, 'Unrecognised character for formula', 0); + + return -1; + } + + // Parse the formula using the parser in Parser.php + try { + $error = $this->parser->parse($formula); + $formula = $this->parser->toReversePolish(); + + $formlen = strlen($formula); // Length of the binary string + $length = 0x16 + $formlen; // Length of the record data + + $header = pack('vv', $record, $length); + + $data = pack('vvv', $row, $col, $xfIndex) + . $num + . pack('vVv', $grbit, $unknown, $formlen); + $this->append($header . $data . $formula); + + // Append also a STRING record if necessary + if ($stringValue !== null) { + $this->writeStringRecord($stringValue); + } + + return 0; + } catch (PhpSpreadsheetException $e) { + // do nothing + } + } + + /** + * Write a STRING record. This. + * + * @param string $stringValue + */ + private function writeStringRecord($stringValue) + { + $record = 0x0207; // Record identifier + $data = StringHelper::UTF8toBIFF8UnicodeLong($stringValue); + + $length = strlen($data); + $header = pack('vv', $record, $length); + + $this->append($header . $data); + } + + /** + * Write a hyperlink. + * This is comprised of two elements: the visible label and + * the invisible link. The visible label is the same as the link unless an + * alternative string is specified. The label is written using the + * writeString() method. Therefore the 255 characters string limit applies. + * $string and $format are optional. + * + * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external + * directory url. + * + * Returns 0 : normal termination + * -2 : row or column out of range + * -3 : long string truncated to 255 chars + * + * @param int $row Row + * @param int $col Column + * @param string $url URL string + * + * @return int + */ + private function writeUrl($row, $col, $url) + { + // Add start row and col to arg list + return $this->writeUrlRange($row, $col, $row, $col, $url); + } + + /** + * This is the more general form of writeUrl(). It allows a hyperlink to be + * written to a range of cells. This function also decides the type of hyperlink + * to be written. These are either, Web (http, ftp, mailto), Internal + * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1'). + * + * @see writeUrl() + * + * @param int $row1 Start row + * @param int $col1 Start column + * @param int $row2 End row + * @param int $col2 End column + * @param string $url URL string + * + * @return int + */ + public function writeUrlRange($row1, $col1, $row2, $col2, $url) + { + // Check for internal/external sheet links or default to web link + if (preg_match('[^internal:]', $url)) { + return $this->writeUrlInternal($row1, $col1, $row2, $col2, $url); + } + if (preg_match('[^external:]', $url)) { + return $this->writeUrlExternal($row1, $col1, $row2, $col2, $url); + } + + return $this->writeUrlWeb($row1, $col1, $row2, $col2, $url); + } + + /** + * Used to write http, ftp and mailto hyperlinks. + * The link type ($options) is 0x03 is the same as absolute dir ref without + * sheet. However it is differentiated by the $unknown2 data stream. + * + * @see writeUrl() + * + * @param int $row1 Start row + * @param int $col1 Start column + * @param int $row2 End row + * @param int $col2 End column + * @param string $url URL string + * + * @return int + */ + public function writeUrlWeb($row1, $col1, $row2, $col2, $url) + { + $record = 0x01B8; // Record identifier + $length = 0x00000; // Bytes to follow + + // Pack the undocumented parts of the hyperlink stream + $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); + $unknown2 = pack('H*', 'E0C9EA79F9BACE118C8200AA004BA90B'); + + // Pack the option flags + $options = pack('V', 0x03); + + // Convert URL to a null terminated wchar string + $url = implode("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY)); + $url = $url . "\0\0\0"; + + // Pack the length of the URL + $url_len = pack('V', strlen($url)); + + // Calculate the data length + $length = 0x34 + strlen($url); + + // Pack the header data + $header = pack('vv', $record, $length); + $data = pack('vvvv', $row1, $row2, $col1, $col2); + + // Write the packed data + $this->append($header . $data . $unknown1 . $options . $unknown2 . $url_len . $url); + + return 0; + } + + /** + * Used to write internal reference hyperlinks such as "Sheet1!A1". + * + * @see writeUrl() + * + * @param int $row1 Start row + * @param int $col1 Start column + * @param int $row2 End row + * @param int $col2 End column + * @param string $url URL string + * + * @return int + */ + public function writeUrlInternal($row1, $col1, $row2, $col2, $url) + { + $record = 0x01B8; // Record identifier + $length = 0x00000; // Bytes to follow + + // Strip URL type + $url = preg_replace('/^internal:/', '', $url); + + // Pack the undocumented parts of the hyperlink stream + $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); + + // Pack the option flags + $options = pack('V', 0x08); + + // Convert the URL type and to a null terminated wchar string + $url .= "\0"; + + // character count + $url_len = StringHelper::countCharacters($url); + $url_len = pack('V', $url_len); + + $url = StringHelper::convertEncoding($url, 'UTF-16LE', 'UTF-8'); + + // Calculate the data length + $length = 0x24 + strlen($url); + + // Pack the header data + $header = pack('vv', $record, $length); + $data = pack('vvvv', $row1, $row2, $col1, $col2); + + // Write the packed data + $this->append($header . $data . $unknown1 . $options . $url_len . $url); + + return 0; + } + + /** + * Write links to external directory names such as 'c:\foo.xls', + * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'. + * + * Note: Excel writes some relative links with the $dir_long string. We ignore + * these cases for the sake of simpler code. + * + * @see writeUrl() + * + * @param int $row1 Start row + * @param int $col1 Start column + * @param int $row2 End row + * @param int $col2 End column + * @param string $url URL string + * + * @return int + */ + public function writeUrlExternal($row1, $col1, $row2, $col2, $url) + { + // Network drives are different. We will handle them separately + // MS/Novell network drives and shares start with \\ + if (preg_match('[^external:\\\\]', $url)) { + return; //($this->writeUrlExternal_net($row1, $col1, $row2, $col2, $url, $str, $format)); + } + + $record = 0x01B8; // Record identifier + $length = 0x00000; // Bytes to follow + + // Strip URL type and change Unix dir separator to Dos style (if needed) + // + $url = preg_replace('/^external:/', '', $url); + $url = preg_replace('/\//', '\\', $url); + + // Determine if the link is relative or absolute: + // relative if link contains no dir separator, "somefile.xls" + // relative if link starts with up-dir, "..\..\somefile.xls" + // otherwise, absolute + + $absolute = 0x00; // relative path + if (preg_match('/^[A-Z]:/', $url)) { + $absolute = 0x02; // absolute path on Windows, e.g. C:\... + } + $link_type = 0x01 | $absolute; + + // Determine if the link contains a sheet reference and change some of the + // parameters accordingly. + // Split the dir name and sheet name (if it exists) + $dir_long = $url; + if (preg_match('/\\#/', $url)) { + $link_type |= 0x08; + } + + // Pack the link type + $link_type = pack('V', $link_type); + + // Calculate the up-level dir count e.g.. (..\..\..\ == 3) + $up_count = preg_match_all('/\\.\\.\\\\/', $dir_long, $useless); + $up_count = pack('v', $up_count); + + // Store the short dos dir name (null terminated) + $dir_short = preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; + + // Store the long dir name as a wchar string (non-null terminated) + $dir_long = $dir_long . "\0"; + + // Pack the lengths of the dir strings + $dir_short_len = pack('V', strlen($dir_short)); + $dir_long_len = pack('V', strlen($dir_long)); + $stream_len = pack('V', 0); //strlen($dir_long) + 0x06); + + // Pack the undocumented parts of the hyperlink stream + $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); + $unknown2 = pack('H*', '0303000000000000C000000000000046'); + $unknown3 = pack('H*', 'FFFFADDE000000000000000000000000000000000000000'); + $unknown4 = pack('v', 0x03); + + // Pack the main data stream + $data = pack('vvvv', $row1, $row2, $col1, $col2) . + $unknown1 . + $link_type . + $unknown2 . + $up_count . + $dir_short_len . + $dir_short . + $unknown3 . + $stream_len; /*. + $dir_long_len . + $unknown4 . + $dir_long . + $sheet_len . + $sheet ;*/ + + // Pack the header data + $length = strlen($data); + $header = pack('vv', $record, $length); + + // Write the packed data + $this->append($header . $data); + + return 0; + } + + /** + * This method is used to set the height and format for a row. + * + * @param int $row The row to set + * @param int $height Height we are giving to the row. + * Use null to set XF without setting height + * @param int $xfIndex The optional cell style Xf index to apply to the columns + * @param bool $hidden The optional hidden attribute + * @param int $level The optional outline level for row, in range [0,7] + */ + private function writeRow($row, $height, $xfIndex, $hidden = false, $level = 0) + { + $record = 0x0208; // Record identifier + $length = 0x0010; // Number of bytes to follow + + $colMic = 0x0000; // First defined column + $colMac = 0x0000; // Last defined column + $irwMac = 0x0000; // Used by Excel to optimise loading + $reserved = 0x0000; // Reserved + $grbit = 0x0000; // Option flags + $ixfe = $xfIndex; + + if ($height < 0) { + $height = null; + } + + // Use writeRow($row, null, $XF) to set XF format without setting height + if ($height != null) { + $miyRw = $height * 20; // row height + } else { + $miyRw = 0xff; // default row height is 256 + } + + // Set the options flags. fUnsynced is used to show that the font and row + // heights are not compatible. This is usually the case for WriteExcel. + // The collapsed flag 0x10 doesn't seem to be used to indicate that a row + // is collapsed. Instead it is used to indicate that the previous row is + // collapsed. The zero height flag, 0x20, is used to collapse a row. + + $grbit |= $level; + if ($hidden) { + $grbit |= 0x0030; + } + if ($height !== null) { + $grbit |= 0x0040; // fUnsynced + } + if ($xfIndex !== 0xF) { + $grbit |= 0x0080; + } + $grbit |= 0x0100; + + $header = pack('vv', $record, $length); + $data = pack('vvvvvvvv', $row, $colMic, $colMac, $miyRw, $irwMac, $reserved, $grbit, $ixfe); + $this->append($header . $data); + } + + /** + * Writes Excel DIMENSIONS to define the area in which there is data. + */ + private function writeDimensions() + { + $record = 0x0200; // Record identifier + + $length = 0x000E; + $data = pack('VVvvv', $this->firstRowIndex, $this->lastRowIndex + 1, $this->firstColumnIndex, $this->lastColumnIndex + 1, 0x0000); // reserved + + $header = pack('vv', $record, $length); + $this->append($header . $data); + } + + /** + * Write BIFF record Window2. + */ + private function writeWindow2() + { + $record = 0x023E; // Record identifier + $length = 0x0012; + + $grbit = 0x00B6; // Option flags + $rwTop = 0x0000; // Top row visible in window + $colLeft = 0x0000; // Leftmost column visible in window + + // The options flags that comprise $grbit + $fDspFmla = 0; // 0 - bit + $fDspGrid = $this->phpSheet->getShowGridlines() ? 1 : 0; // 1 + $fDspRwCol = $this->phpSheet->getShowRowColHeaders() ? 1 : 0; // 2 + $fFrozen = $this->phpSheet->getFreezePane() ? 1 : 0; // 3 + $fDspZeros = 1; // 4 + $fDefaultHdr = 1; // 5 + $fArabic = $this->phpSheet->getRightToLeft() ? 1 : 0; // 6 + $fDspGuts = $this->outlineOn; // 7 + $fFrozenNoSplit = 0; // 0 - bit + // no support in PhpSpreadsheet for selected sheet, therefore sheet is only selected if it is the active sheet + $fSelected = ($this->phpSheet === $this->phpSheet->getParent()->getActiveSheet()) ? 1 : 0; + $fPaged = 1; // 2 + $fPageBreakPreview = $this->phpSheet->getSheetView()->getView() === SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW; + + $grbit = $fDspFmla; + $grbit |= $fDspGrid << 1; + $grbit |= $fDspRwCol << 2; + $grbit |= $fFrozen << 3; + $grbit |= $fDspZeros << 4; + $grbit |= $fDefaultHdr << 5; + $grbit |= $fArabic << 6; + $grbit |= $fDspGuts << 7; + $grbit |= $fFrozenNoSplit << 8; + $grbit |= $fSelected << 9; + $grbit |= $fPaged << 10; + $grbit |= $fPageBreakPreview << 11; + + $header = pack('vv', $record, $length); + $data = pack('vvv', $grbit, $rwTop, $colLeft); + + // FIXME !!! + $rgbHdr = 0x0040; // Row/column heading and gridline color index + $zoom_factor_page_break = ($fPageBreakPreview ? $this->phpSheet->getSheetView()->getZoomScale() : 0x0000); + $zoom_factor_normal = $this->phpSheet->getSheetView()->getZoomScaleNormal(); + + $data .= pack('vvvvV', $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000); + + $this->append($header . $data); + } + + /** + * Write BIFF record DEFAULTROWHEIGHT. + */ + private function writeDefaultRowHeight() + { + $defaultRowHeight = $this->phpSheet->getDefaultRowDimension()->getRowHeight(); + + if ($defaultRowHeight < 0) { + return; + } + + // convert to twips + $defaultRowHeight = (int) 20 * $defaultRowHeight; + + $record = 0x0225; // Record identifier + $length = 0x0004; // Number of bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('vv', 1, $defaultRowHeight); + $this->append($header . $data); + } + + /** + * Write BIFF record DEFCOLWIDTH if COLINFO records are in use. + */ + private function writeDefcol() + { + $defaultColWidth = 8; + + $record = 0x0055; // Record identifier + $length = 0x0002; // Number of bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('v', $defaultColWidth); + $this->append($header . $data); + } + + /** + * Write BIFF record COLINFO to define column widths. + * + * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C + * length record. + * + * @param array $col_array This is the only parameter received and is composed of the following: + * 0 => First formatted column, + * 1 => Last formatted column, + * 2 => Col width (8.43 is Excel default), + * 3 => The optional XF format of the column, + * 4 => Option flags. + * 5 => Optional outline level + */ + private function writeColinfo($col_array) + { + if (isset($col_array[0])) { + $colFirst = $col_array[0]; + } + if (isset($col_array[1])) { + $colLast = $col_array[1]; + } + if (isset($col_array[2])) { + $coldx = $col_array[2]; + } else { + $coldx = 8.43; + } + if (isset($col_array[3])) { + $xfIndex = $col_array[3]; + } else { + $xfIndex = 15; + } + if (isset($col_array[4])) { + $grbit = $col_array[4]; + } else { + $grbit = 0; + } + if (isset($col_array[5])) { + $level = $col_array[5]; + } else { + $level = 0; + } + $record = 0x007D; // Record identifier + $length = 0x000C; // Number of bytes to follow + + $coldx *= 256; // Convert to units of 1/256 of a char + + $ixfe = $xfIndex; + $reserved = 0x0000; // Reserved + + $level = max(0, min($level, 7)); + $grbit |= $level << 8; + + $header = pack('vv', $record, $length); + $data = pack('vvvvvv', $colFirst, $colLast, $coldx, $ixfe, $grbit, $reserved); + $this->append($header . $data); + } + + /** + * Write BIFF record SELECTION. + */ + private function writeSelection() + { + // look up the selected cell range + $selectedCells = Coordinate::splitRange($this->phpSheet->getSelectedCells()); + $selectedCells = $selectedCells[0]; + if (count($selectedCells) == 2) { + list($first, $last) = $selectedCells; + } else { + $first = $selectedCells[0]; + $last = $selectedCells[0]; + } + + list($colFirst, $rwFirst) = Coordinate::coordinateFromString($first); + $colFirst = Coordinate::columnIndexFromString($colFirst) - 1; // base 0 column index + --$rwFirst; // base 0 row index + + list($colLast, $rwLast) = Coordinate::coordinateFromString($last); + $colLast = Coordinate::columnIndexFromString($colLast) - 1; // base 0 column index + --$rwLast; // base 0 row index + + // make sure we are not out of bounds + $colFirst = min($colFirst, 255); + $colLast = min($colLast, 255); + + $rwFirst = min($rwFirst, 65535); + $rwLast = min($rwLast, 65535); + + $record = 0x001D; // Record identifier + $length = 0x000F; // Number of bytes to follow + + $pnn = $this->activePane; // Pane position + $rwAct = $rwFirst; // Active row + $colAct = $colFirst; // Active column + $irefAct = 0; // Active cell ref + $cref = 1; // Number of refs + + if (!isset($rwLast)) { + $rwLast = $rwFirst; // Last row in reference + } + if (!isset($colLast)) { + $colLast = $colFirst; // Last col in reference + } + + // Swap last row/col for first row/col as necessary + if ($rwFirst > $rwLast) { + list($rwFirst, $rwLast) = [$rwLast, $rwFirst]; + } + + if ($colFirst > $colLast) { + list($colFirst, $colLast) = [$colLast, $colFirst]; + } + + $header = pack('vv', $record, $length); + $data = pack('CvvvvvvCC', $pnn, $rwAct, $colAct, $irefAct, $cref, $rwFirst, $rwLast, $colFirst, $colLast); + $this->append($header . $data); + } + + /** + * Store the MERGEDCELLS records for all ranges of merged cells. + */ + private function writeMergedCells() + { + $mergeCells = $this->phpSheet->getMergeCells(); + $countMergeCells = count($mergeCells); + + if ($countMergeCells == 0) { + return; + } + + // maximum allowed number of merged cells per record + $maxCountMergeCellsPerRecord = 1027; + + // record identifier + $record = 0x00E5; + + // counter for total number of merged cells treated so far by the writer + $i = 0; + + // counter for number of merged cells written in record currently being written + $j = 0; + + // initialize record data + $recordData = ''; + + // loop through the merged cells + foreach ($mergeCells as $mergeCell) { + ++$i; + ++$j; + + // extract the row and column indexes + $range = Coordinate::splitRange($mergeCell); + list($first, $last) = $range[0]; + list($firstColumn, $firstRow) = Coordinate::coordinateFromString($first); + list($lastColumn, $lastRow) = Coordinate::coordinateFromString($last); + + $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, Coordinate::columnIndexFromString($firstColumn) - 1, Coordinate::columnIndexFromString($lastColumn) - 1); + + // flush record if we have reached limit for number of merged cells, or reached final merged cell + if ($j == $maxCountMergeCellsPerRecord or $i == $countMergeCells) { + $recordData = pack('v', $j) . $recordData; + $length = strlen($recordData); + $header = pack('vv', $record, $length); + $this->append($header . $recordData); + + // initialize for next record, if any + $recordData = ''; + $j = 0; + } + } + } + + /** + * Write SHEETLAYOUT record. + */ + private function writeSheetLayout() + { + if (!$this->phpSheet->isTabColorSet()) { + return; + } + + $recordData = pack( + 'vvVVVvv', + 0x0862, + 0x0000, // unused + 0x00000000, // unused + 0x00000000, // unused + 0x00000014, // size of record data + $this->colors[$this->phpSheet->getTabColor()->getRGB()], // color index + 0x0000 // unused + ); + + $length = strlen($recordData); + + $record = 0x0862; // Record identifier + $header = pack('vv', $record, $length); + $this->append($header . $recordData); + } + + /** + * Write SHEETPROTECTION. + */ + private function writeSheetProtection() + { + // record identifier + $record = 0x0867; + + // prepare options + $options = (int) !$this->phpSheet->getProtection()->getObjects() + | (int) !$this->phpSheet->getProtection()->getScenarios() << 1 + | (int) !$this->phpSheet->getProtection()->getFormatCells() << 2 + | (int) !$this->phpSheet->getProtection()->getFormatColumns() << 3 + | (int) !$this->phpSheet->getProtection()->getFormatRows() << 4 + | (int) !$this->phpSheet->getProtection()->getInsertColumns() << 5 + | (int) !$this->phpSheet->getProtection()->getInsertRows() << 6 + | (int) !$this->phpSheet->getProtection()->getInsertHyperlinks() << 7 + | (int) !$this->phpSheet->getProtection()->getDeleteColumns() << 8 + | (int) !$this->phpSheet->getProtection()->getDeleteRows() << 9 + | (int) !$this->phpSheet->getProtection()->getSelectLockedCells() << 10 + | (int) !$this->phpSheet->getProtection()->getSort() << 11 + | (int) !$this->phpSheet->getProtection()->getAutoFilter() << 12 + | (int) !$this->phpSheet->getProtection()->getPivotTables() << 13 + | (int) !$this->phpSheet->getProtection()->getSelectUnlockedCells() << 14; + + // record data + $recordData = pack( + 'vVVCVVvv', + 0x0867, // repeated record identifier + 0x0000, // not used + 0x0000, // not used + 0x00, // not used + 0x01000200, // unknown data + 0xFFFFFFFF, // unknown data + $options, // options + 0x0000 // not used + ); + + $length = strlen($recordData); + $header = pack('vv', $record, $length); + + $this->append($header . $recordData); + } + + /** + * Write BIFF record RANGEPROTECTION. + * + * Openoffice.org's Documentaion of the Microsoft Excel File Format uses term RANGEPROTECTION for these records + * Microsoft Office Excel 97-2007 Binary File Format Specification uses term FEAT for these records + */ + private function writeRangeProtection() + { + foreach ($this->phpSheet->getProtectedCells() as $range => $password) { + // number of ranges, e.g. 'A1:B3 C20:D25' + $cellRanges = explode(' ', $range); + $cref = count($cellRanges); + + $recordData = pack( + 'vvVVvCVvVv', + 0x0868, + 0x00, + 0x0000, + 0x0000, + 0x02, + 0x0, + 0x0000, + $cref, + 0x0000, + 0x00 + ); + + foreach ($cellRanges as $cellRange) { + $recordData .= $this->writeBIFF8CellRangeAddressFixed($cellRange); + } + + // the rgbFeat structure + $recordData .= pack( + 'VV', + 0x0000, + hexdec($password) + ); + + $recordData .= StringHelper::UTF8toBIFF8UnicodeLong('p' . md5($recordData)); + + $length = strlen($recordData); + + $record = 0x0868; // Record identifier + $header = pack('vv', $record, $length); + $this->append($header . $recordData); + } + } + + /** + * Writes the Excel BIFF PANE record. + * The panes can either be frozen or thawed (unfrozen). + * Frozen panes are specified in terms of an integer number of rows and columns. + * Thawed panes are specified in terms of Excel's units for rows and columns. + */ + private function writePanes() + { + $panes = []; + if ($this->phpSheet->getFreezePane()) { + list($column, $row) = Coordinate::coordinateFromString($this->phpSheet->getFreezePane()); + $panes[0] = Coordinate::columnIndexFromString($column) - 1; + $panes[1] = $row - 1; + + list($leftMostColumn, $topRow) = Coordinate::coordinateFromString($this->phpSheet->getTopLeftCell()); + //Coordinates are zero-based in xls files + $panes[2] = $topRow - 1; + $panes[3] = Coordinate::columnIndexFromString($leftMostColumn) - 1; + } else { + // thaw panes + return; + } + + $x = isset($panes[0]) ? $panes[0] : null; + $y = isset($panes[1]) ? $panes[1] : null; + $rwTop = isset($panes[2]) ? $panes[2] : null; + $colLeft = isset($panes[3]) ? $panes[3] : null; + if (count($panes) > 4) { // if Active pane was received + $pnnAct = $panes[4]; + } else { + $pnnAct = null; + } + $record = 0x0041; // Record identifier + $length = 0x000A; // Number of bytes to follow + + // Code specific to frozen or thawed panes. + if ($this->phpSheet->getFreezePane()) { + // Set default values for $rwTop and $colLeft + if (!isset($rwTop)) { + $rwTop = $y; + } + if (!isset($colLeft)) { + $colLeft = $x; + } + } else { + // Set default values for $rwTop and $colLeft + if (!isset($rwTop)) { + $rwTop = 0; + } + if (!isset($colLeft)) { + $colLeft = 0; + } + + // Convert Excel's row and column units to the internal units. + // The default row height is 12.75 + // The default column width is 8.43 + // The following slope and intersection values were interpolated. + // + $y = 20 * $y + 255; + $x = 113.879 * $x + 390; + } + + // Determine which pane should be active. There is also the undocumented + // option to override this should it be necessary: may be removed later. + // + if (!isset($pnnAct)) { + if ($x != 0 && $y != 0) { + $pnnAct = 0; // Bottom right + } + if ($x != 0 && $y == 0) { + $pnnAct = 1; // Top right + } + if ($x == 0 && $y != 0) { + $pnnAct = 2; // Bottom left + } + if ($x == 0 && $y == 0) { + $pnnAct = 3; // Top left + } + } + + $this->activePane = $pnnAct; // Used in writeSelection + + $header = pack('vv', $record, $length); + $data = pack('vvvvv', $x, $y, $rwTop, $colLeft, $pnnAct); + $this->append($header . $data); + } + + /** + * Store the page setup SETUP BIFF record. + */ + private function writeSetup() + { + $record = 0x00A1; // Record identifier + $length = 0x0022; // Number of bytes to follow + + $iPaperSize = $this->phpSheet->getPageSetup()->getPaperSize(); // Paper size + + $iScale = $this->phpSheet->getPageSetup()->getScale() ? + $this->phpSheet->getPageSetup()->getScale() : 100; // Print scaling factor + + $iPageStart = 0x01; // Starting page number + $iFitWidth = (int) $this->phpSheet->getPageSetup()->getFitToWidth(); // Fit to number of pages wide + $iFitHeight = (int) $this->phpSheet->getPageSetup()->getFitToHeight(); // Fit to number of pages high + $grbit = 0x00; // Option flags + $iRes = 0x0258; // Print resolution + $iVRes = 0x0258; // Vertical print resolution + + $numHdr = $this->phpSheet->getPageMargins()->getHeader(); // Header Margin + + $numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin + $iCopies = 0x01; // Number of copies + + $fLeftToRight = 0x0; // Print over then down + + // Page orientation + $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) ? + 0x0 : 0x1; + + $fNoPls = 0x0; // Setup not read from printer + $fNoColor = 0x0; // Print black and white + $fDraft = 0x0; // Print draft quality + $fNotes = 0x0; // Print notes + $fNoOrient = 0x0; // Orientation not set + $fUsePage = 0x0; // Use custom starting page + + $grbit = $fLeftToRight; + $grbit |= $fLandscape << 1; + $grbit |= $fNoPls << 2; + $grbit |= $fNoColor << 3; + $grbit |= $fDraft << 4; + $grbit |= $fNotes << 5; + $grbit |= $fNoOrient << 6; + $grbit |= $fUsePage << 7; + + $numHdr = pack('d', $numHdr); + $numFtr = pack('d', $numFtr); + if (self::getByteOrder()) { // if it's Big Endian + $numHdr = strrev($numHdr); + $numFtr = strrev($numFtr); + } + + $header = pack('vv', $record, $length); + $data1 = pack('vvvvvvvv', $iPaperSize, $iScale, $iPageStart, $iFitWidth, $iFitHeight, $grbit, $iRes, $iVRes); + $data2 = $numHdr . $numFtr; + $data3 = pack('v', $iCopies); + $this->append($header . $data1 . $data2 . $data3); + } + + /** + * Store the header caption BIFF record. + */ + private function writeHeader() + { + $record = 0x0014; // Record identifier + + /* removing for now + // need to fix character count (multibyte!) + if (strlen($this->phpSheet->getHeaderFooter()->getOddHeader()) <= 255) { + $str = $this->phpSheet->getHeaderFooter()->getOddHeader(); // header string + } else { + $str = ''; + } + */ + + $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddHeader()); + $length = strlen($recordData); + + $header = pack('vv', $record, $length); + + $this->append($header . $recordData); + } + + /** + * Store the footer caption BIFF record. + */ + private function writeFooter() + { + $record = 0x0015; // Record identifier + + /* removing for now + // need to fix character count (multibyte!) + if (strlen($this->phpSheet->getHeaderFooter()->getOddFooter()) <= 255) { + $str = $this->phpSheet->getHeaderFooter()->getOddFooter(); + } else { + $str = ''; + } + */ + + $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddFooter()); + $length = strlen($recordData); + + $header = pack('vv', $record, $length); + + $this->append($header . $recordData); + } + + /** + * Store the horizontal centering HCENTER BIFF record. + */ + private function writeHcenter() + { + $record = 0x0083; // Record identifier + $length = 0x0002; // Bytes to follow + + $fHCenter = $this->phpSheet->getPageSetup()->getHorizontalCentered() ? 1 : 0; // Horizontal centering + + $header = pack('vv', $record, $length); + $data = pack('v', $fHCenter); + + $this->append($header . $data); + } + + /** + * Store the vertical centering VCENTER BIFF record. + */ + private function writeVcenter() + { + $record = 0x0084; // Record identifier + $length = 0x0002; // Bytes to follow + + $fVCenter = $this->phpSheet->getPageSetup()->getVerticalCentered() ? 1 : 0; // Horizontal centering + + $header = pack('vv', $record, $length); + $data = pack('v', $fVCenter); + $this->append($header . $data); + } + + /** + * Store the LEFTMARGIN BIFF record. + */ + private function writeMarginLeft() + { + $record = 0x0026; // Record identifier + $length = 0x0008; // Bytes to follow + + $margin = $this->phpSheet->getPageMargins()->getLeft(); // Margin in inches + + $header = pack('vv', $record, $length); + $data = pack('d', $margin); + if (self::getByteOrder()) { // if it's Big Endian + $data = strrev($data); + } + + $this->append($header . $data); + } + + /** + * Store the RIGHTMARGIN BIFF record. + */ + private function writeMarginRight() + { + $record = 0x0027; // Record identifier + $length = 0x0008; // Bytes to follow + + $margin = $this->phpSheet->getPageMargins()->getRight(); // Margin in inches + + $header = pack('vv', $record, $length); + $data = pack('d', $margin); + if (self::getByteOrder()) { // if it's Big Endian + $data = strrev($data); + } + + $this->append($header . $data); + } + + /** + * Store the TOPMARGIN BIFF record. + */ + private function writeMarginTop() + { + $record = 0x0028; // Record identifier + $length = 0x0008; // Bytes to follow + + $margin = $this->phpSheet->getPageMargins()->getTop(); // Margin in inches + + $header = pack('vv', $record, $length); + $data = pack('d', $margin); + if (self::getByteOrder()) { // if it's Big Endian + $data = strrev($data); + } + + $this->append($header . $data); + } + + /** + * Store the BOTTOMMARGIN BIFF record. + */ + private function writeMarginBottom() + { + $record = 0x0029; // Record identifier + $length = 0x0008; // Bytes to follow + + $margin = $this->phpSheet->getPageMargins()->getBottom(); // Margin in inches + + $header = pack('vv', $record, $length); + $data = pack('d', $margin); + if (self::getByteOrder()) { // if it's Big Endian + $data = strrev($data); + } + + $this->append($header . $data); + } + + /** + * Write the PRINTHEADERS BIFF record. + */ + private function writePrintHeaders() + { + $record = 0x002a; // Record identifier + $length = 0x0002; // Bytes to follow + + $fPrintRwCol = $this->printHeaders; // Boolean flag + + $header = pack('vv', $record, $length); + $data = pack('v', $fPrintRwCol); + $this->append($header . $data); + } + + /** + * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the + * GRIDSET record. + */ + private function writePrintGridlines() + { + $record = 0x002b; // Record identifier + $length = 0x0002; // Bytes to follow + + $fPrintGrid = $this->phpSheet->getPrintGridlines() ? 1 : 0; // Boolean flag + + $header = pack('vv', $record, $length); + $data = pack('v', $fPrintGrid); + $this->append($header . $data); + } + + /** + * Write the GRIDSET BIFF record. Must be used in conjunction with the + * PRINTGRIDLINES record. + */ + private function writeGridset() + { + $record = 0x0082; // Record identifier + $length = 0x0002; // Bytes to follow + + $fGridSet = !$this->phpSheet->getPrintGridlines(); // Boolean flag + + $header = pack('vv', $record, $length); + $data = pack('v', $fGridSet); + $this->append($header . $data); + } + + /** + * Write the AUTOFILTERINFO BIFF record. This is used to configure the number of autofilter select used in the sheet. + */ + private function writeAutoFilterInfo() + { + $record = 0x009D; // Record identifier + $length = 0x0002; // Bytes to follow + + $rangeBounds = Coordinate::rangeBoundaries($this->phpSheet->getAutoFilter()->getRange()); + $iNumFilters = 1 + $rangeBounds[1][0] - $rangeBounds[0][0]; + + $header = pack('vv', $record, $length); + $data = pack('v', $iNumFilters); + $this->append($header . $data); + } + + /** + * Write the GUTS BIFF record. This is used to configure the gutter margins + * where Excel outline symbols are displayed. The visibility of the gutters is + * controlled by a flag in WSBOOL. + * + * @see writeWsbool() + */ + private function writeGuts() + { + $record = 0x0080; // Record identifier + $length = 0x0008; // Bytes to follow + + $dxRwGut = 0x0000; // Size of row gutter + $dxColGut = 0x0000; // Size of col gutter + + // determine maximum row outline level + $maxRowOutlineLevel = 0; + foreach ($this->phpSheet->getRowDimensions() as $rowDimension) { + $maxRowOutlineLevel = max($maxRowOutlineLevel, $rowDimension->getOutlineLevel()); + } + + $col_level = 0; + + // Calculate the maximum column outline level. The equivalent calculation + // for the row outline level is carried out in writeRow(). + $colcount = count($this->columnInfo); + for ($i = 0; $i < $colcount; ++$i) { + $col_level = max($this->columnInfo[$i][5], $col_level); + } + + // Set the limits for the outline levels (0 <= x <= 7). + $col_level = max(0, min($col_level, 7)); + + // The displayed level is one greater than the max outline levels + if ($maxRowOutlineLevel) { + ++$maxRowOutlineLevel; + } + if ($col_level) { + ++$col_level; + } + + $header = pack('vv', $record, $length); + $data = pack('vvvv', $dxRwGut, $dxColGut, $maxRowOutlineLevel, $col_level); + + $this->append($header . $data); + } + + /** + * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction + * with the SETUP record. + */ + private function writeWsbool() + { + $record = 0x0081; // Record identifier + $length = 0x0002; // Bytes to follow + $grbit = 0x0000; + + // The only option that is of interest is the flag for fit to page. So we + // set all the options in one go. + // + // Set the option flags + $grbit |= 0x0001; // Auto page breaks visible + if ($this->outlineStyle) { + $grbit |= 0x0020; // Auto outline styles + } + if ($this->phpSheet->getShowSummaryBelow()) { + $grbit |= 0x0040; // Outline summary below + } + if ($this->phpSheet->getShowSummaryRight()) { + $grbit |= 0x0080; // Outline summary right + } + if ($this->phpSheet->getPageSetup()->getFitToPage()) { + $grbit |= 0x0100; // Page setup fit to page + } + if ($this->outlineOn) { + $grbit |= 0x0400; // Outline symbols displayed + } + + $header = pack('vv', $record, $length); + $data = pack('v', $grbit); + $this->append($header . $data); + } + + /** + * Write the HORIZONTALPAGEBREAKS and VERTICALPAGEBREAKS BIFF records. + */ + private function writeBreaks() + { + // initialize + $vbreaks = []; + $hbreaks = []; + + foreach ($this->phpSheet->getBreaks() as $cell => $breakType) { + // Fetch coordinates + $coordinates = Coordinate::coordinateFromString($cell); + + // Decide what to do by the type of break + switch ($breakType) { + case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_COLUMN: + // Add to list of vertical breaks + $vbreaks[] = Coordinate::columnIndexFromString($coordinates[0]) - 1; + + break; + case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_ROW: + // Add to list of horizontal breaks + $hbreaks[] = $coordinates[1]; + + break; + case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_NONE: + default: + // Nothing to do + break; + } + } + + //horizontal page breaks + if (!empty($hbreaks)) { + // Sort and filter array of page breaks + sort($hbreaks, SORT_NUMERIC); + if ($hbreaks[0] == 0) { // don't use first break if it's 0 + array_shift($hbreaks); + } + + $record = 0x001b; // Record identifier + $cbrk = count($hbreaks); // Number of page breaks + $length = 2 + 6 * $cbrk; // Bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('v', $cbrk); + + // Append each page break + foreach ($hbreaks as $hbreak) { + $data .= pack('vvv', $hbreak, 0x0000, 0x00ff); + } + + $this->append($header . $data); + } + + // vertical page breaks + if (!empty($vbreaks)) { + // 1000 vertical pagebreaks appears to be an internal Excel 5 limit. + // It is slightly higher in Excel 97/200, approx. 1026 + $vbreaks = array_slice($vbreaks, 0, 1000); + + // Sort and filter array of page breaks + sort($vbreaks, SORT_NUMERIC); + if ($vbreaks[0] == 0) { // don't use first break if it's 0 + array_shift($vbreaks); + } + + $record = 0x001a; // Record identifier + $cbrk = count($vbreaks); // Number of page breaks + $length = 2 + 6 * $cbrk; // Bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('v', $cbrk); + + // Append each page break + foreach ($vbreaks as $vbreak) { + $data .= pack('vvv', $vbreak, 0x0000, 0xffff); + } + + $this->append($header . $data); + } + } + + /** + * Set the Biff PROTECT record to indicate that the worksheet is protected. + */ + private function writeProtect() + { + // Exit unless sheet protection has been specified + if (!$this->phpSheet->getProtection()->getSheet()) { + return; + } + + $record = 0x0012; // Record identifier + $length = 0x0002; // Bytes to follow + + $fLock = 1; // Worksheet is protected + + $header = pack('vv', $record, $length); + $data = pack('v', $fLock); + + $this->append($header . $data); + } + + /** + * Write SCENPROTECT. + */ + private function writeScenProtect() + { + // Exit if sheet protection is not active + if (!$this->phpSheet->getProtection()->getSheet()) { + return; + } + + // Exit if scenarios are not protected + if (!$this->phpSheet->getProtection()->getScenarios()) { + return; + } + + $record = 0x00DD; // Record identifier + $length = 0x0002; // Bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('v', 1); + + $this->append($header . $data); + } + + /** + * Write OBJECTPROTECT. + */ + private function writeObjectProtect() + { + // Exit if sheet protection is not active + if (!$this->phpSheet->getProtection()->getSheet()) { + return; + } + + // Exit if objects are not protected + if (!$this->phpSheet->getProtection()->getObjects()) { + return; + } + + $record = 0x0063; // Record identifier + $length = 0x0002; // Bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('v', 1); + + $this->append($header . $data); + } + + /** + * Write the worksheet PASSWORD record. + */ + private function writePassword() + { + // Exit unless sheet protection and password have been specified + if (!$this->phpSheet->getProtection()->getSheet() || !$this->phpSheet->getProtection()->getPassword()) { + return; + } + + $record = 0x0013; // Record identifier + $length = 0x0002; // Bytes to follow + + $wPassword = hexdec($this->phpSheet->getProtection()->getPassword()); // Encoded password + + $header = pack('vv', $record, $length); + $data = pack('v', $wPassword); + + $this->append($header . $data); + } + + /** + * Insert a 24bit bitmap image in a worksheet. + * + * @param int $row The row we are going to insert the bitmap into + * @param int $col The column we are going to insert the bitmap into + * @param mixed $bitmap The bitmap filename or GD-image resource + * @param int $x the horizontal position (offset) of the image inside the cell + * @param int $y the vertical position (offset) of the image inside the cell + * @param float $scale_x The horizontal scale + * @param float $scale_y The vertical scale + */ + public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1) + { + $bitmap_array = (is_resource($bitmap) ? $this->processBitmapGd($bitmap) : $this->processBitmap($bitmap)); + list($width, $height, $size, $data) = $bitmap_array; + + // Scale the frame of the image. + $width *= $scale_x; + $height *= $scale_y; + + // Calculate the vertices of the image and write the OBJ record + $this->positionImage($col, $row, $x, $y, $width, $height); + + // Write the IMDATA record to store the bitmap data + $record = 0x007f; + $length = 8 + $size; + $cf = 0x09; + $env = 0x01; + $lcb = $size; + + $header = pack('vvvvV', $record, $length, $cf, $env, $lcb); + $this->append($header . $data); + } + + /** + * Calculate the vertices that define the position of the image as required by + * the OBJ record. + * + * +------------+------------+ + * | A | B | + * +-----+------------+------------+ + * | |(x1,y1) | | + * | 1 |(A1)._______|______ | + * | | | | | + * | | | | | + * +-----+----| BITMAP |-----+ + * | | | | | + * | 2 | |______________. | + * | | | (B2)| + * | | | (x2,y2)| + * +---- +------------+------------+ + * + * Example of a bitmap that covers some of the area from cell A1 to cell B2. + * + * Based on the width and height of the bitmap we need to calculate 8 vars: + * $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2. + * The width and height of the cells are also variable and have to be taken into + * account. + * The values of $col_start and $row_start are passed in from the calling + * function. The values of $col_end and $row_end are calculated by subtracting + * the width and height of the bitmap from the width and height of the + * underlying cells. + * The vertices are expressed as a percentage of the underlying cell width as + * follows (rhs values are in pixels): + * + * x1 = X / W *1024 + * y1 = Y / H *256 + * x2 = (X-1) / W *1024 + * y2 = (Y-1) / H *256 + * + * Where: X is distance from the left side of the underlying cell + * Y is distance from the top of the underlying cell + * W is the width of the cell + * H is the height of the cell + * The SDK incorrectly states that the height should be expressed as a + * percentage of 1024. + * + * @param int $col_start Col containing upper left corner of object + * @param int $row_start Row containing top left corner of object + * @param int $x1 Distance to left side of object + * @param int $y1 Distance to top of object + * @param int $width Width of image frame + * @param int $height Height of image frame + */ + public function positionImage($col_start, $row_start, $x1, $y1, $width, $height) + { + // Initialise end cell to the same as the start cell + $col_end = $col_start; // Col containing lower right corner of object + $row_end = $row_start; // Row containing bottom right corner of object + + // Zero the specified offset if greater than the cell dimensions + if ($x1 >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1))) { + $x1 = 0; + } + if ($y1 >= Xls::sizeRow($this->phpSheet, $row_start + 1)) { + $y1 = 0; + } + + $width = $width + $x1 - 1; + $height = $height + $y1 - 1; + + // Subtract the underlying cell widths to find the end cell of the image + while ($width >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1))) { + $width -= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)); + ++$col_end; + } + + // Subtract the underlying cell heights to find the end cell of the image + while ($height >= Xls::sizeRow($this->phpSheet, $row_end + 1)) { + $height -= Xls::sizeRow($this->phpSheet, $row_end + 1); + ++$row_end; + } + + // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell + // with zero eight or width. + // + if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) == 0) { + return; + } + if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) == 0) { + return; + } + if (Xls::sizeRow($this->phpSheet, $row_start + 1) == 0) { + return; + } + if (Xls::sizeRow($this->phpSheet, $row_end + 1) == 0) { + return; + } + + // Convert the pixel values to the percentage value expected by Excel + $x1 = $x1 / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) * 1024; + $y1 = $y1 / Xls::sizeRow($this->phpSheet, $row_start + 1) * 256; + $x2 = $width / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) * 1024; // Distance to right side of object + $y2 = $height / Xls::sizeRow($this->phpSheet, $row_end + 1) * 256; // Distance to bottom of object + + $this->writeObjPicture($col_start, $x1, $row_start, $y1, $col_end, $x2, $row_end, $y2); + } + + /** + * Store the OBJ record that precedes an IMDATA record. This could be generalise + * to support other Excel objects. + * + * @param int $colL Column containing upper left corner of object + * @param int $dxL Distance from left side of cell + * @param int $rwT Row containing top left corner of object + * @param int $dyT Distance from top of cell + * @param int $colR Column containing lower right corner of object + * @param int $dxR Distance from right of cell + * @param int $rwB Row containing bottom right corner of object + * @param int $dyB Distance from bottom of cell + */ + private function writeObjPicture($colL, $dxL, $rwT, $dyT, $colR, $dxR, $rwB, $dyB) + { + $record = 0x005d; // Record identifier + $length = 0x003c; // Bytes to follow + + $cObj = 0x0001; // Count of objects in file (set to 1) + $OT = 0x0008; // Object type. 8 = Picture + $id = 0x0001; // Object ID + $grbit = 0x0614; // Option flags + + $cbMacro = 0x0000; // Length of FMLA structure + $Reserved1 = 0x0000; // Reserved + $Reserved2 = 0x0000; // Reserved + + $icvBack = 0x09; // Background colour + $icvFore = 0x09; // Foreground colour + $fls = 0x00; // Fill pattern + $fAuto = 0x00; // Automatic fill + $icv = 0x08; // Line colour + $lns = 0xff; // Line style + $lnw = 0x01; // Line weight + $fAutoB = 0x00; // Automatic border + $frs = 0x0000; // Frame style + $cf = 0x0009; // Image format, 9 = bitmap + $Reserved3 = 0x0000; // Reserved + $cbPictFmla = 0x0000; // Length of FMLA structure + $Reserved4 = 0x0000; // Reserved + $grbit2 = 0x0001; // Option flags + $Reserved5 = 0x0000; // Reserved + + $header = pack('vv', $record, $length); + $data = pack('V', $cObj); + $data .= pack('v', $OT); + $data .= pack('v', $id); + $data .= pack('v', $grbit); + $data .= pack('v', $colL); + $data .= pack('v', $dxL); + $data .= pack('v', $rwT); + $data .= pack('v', $dyT); + $data .= pack('v', $colR); + $data .= pack('v', $dxR); + $data .= pack('v', $rwB); + $data .= pack('v', $dyB); + $data .= pack('v', $cbMacro); + $data .= pack('V', $Reserved1); + $data .= pack('v', $Reserved2); + $data .= pack('C', $icvBack); + $data .= pack('C', $icvFore); + $data .= pack('C', $fls); + $data .= pack('C', $fAuto); + $data .= pack('C', $icv); + $data .= pack('C', $lns); + $data .= pack('C', $lnw); + $data .= pack('C', $fAutoB); + $data .= pack('v', $frs); + $data .= pack('V', $cf); + $data .= pack('v', $Reserved3); + $data .= pack('v', $cbPictFmla); + $data .= pack('v', $Reserved4); + $data .= pack('v', $grbit2); + $data .= pack('V', $Reserved5); + + $this->append($header . $data); + } + + /** + * Convert a GD-image into the internal format. + * + * @param resource $image The image to process + * + * @return array Array with data and properties of the bitmap + */ + public function processBitmapGd($image) + { + $width = imagesx($image); + $height = imagesy($image); + + $data = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); + for ($j = $height; --$j;) { + for ($i = 0; $i < $width; ++$i) { + $color = imagecolorsforindex($image, imagecolorat($image, $i, $j)); + foreach (['red', 'green', 'blue'] as $key) { + $color[$key] = $color[$key] + round((255 - $color[$key]) * $color['alpha'] / 127); + } + $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); + } + if (3 * $width % 4) { + $data .= str_repeat("\x00", 4 - 3 * $width % 4); + } + } + + return [$width, $height, strlen($data), $data]; + } + + /** + * Convert a 24 bit bitmap into the modified internal format used by Windows. + * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the + * MSDN library. + * + * @param string $bitmap The bitmap to process + * + * @return array Array with data and properties of the bitmap + */ + public function processBitmap($bitmap) + { + // Open file. + $bmp_fd = @fopen($bitmap, 'rb'); + if (!$bmp_fd) { + throw new WriterException("Couldn't import $bitmap"); + } + + // Slurp the file into a string. + $data = fread($bmp_fd, filesize($bitmap)); + + // Check that the file is big enough to be a bitmap. + if (strlen($data) <= 0x36) { + throw new WriterException("$bitmap doesn't contain enough data.\n"); + } + + // The first 2 bytes are used to identify the bitmap. + $identity = unpack('A2ident', $data); + if ($identity['ident'] != 'BM') { + throw new WriterException("$bitmap doesn't appear to be a valid bitmap image.\n"); + } + + // Remove bitmap data: ID. + $data = substr($data, 2); + + // Read and remove the bitmap size. This is more reliable than reading + // the data size at offset 0x22. + // + $size_array = unpack('Vsa', substr($data, 0, 4)); + $size = $size_array['sa']; + $data = substr($data, 4); + $size -= 0x36; // Subtract size of bitmap header. + $size += 0x0C; // Add size of BIFF header. + + // Remove bitmap data: reserved, offset, header length. + $data = substr($data, 12); + + // Read and remove the bitmap width and height. Verify the sizes. + $width_and_height = unpack('V2', substr($data, 0, 8)); + $width = $width_and_height[1]; + $height = $width_and_height[2]; + $data = substr($data, 8); + if ($width > 0xFFFF) { + throw new WriterException("$bitmap: largest image width supported is 65k.\n"); + } + if ($height > 0xFFFF) { + throw new WriterException("$bitmap: largest image height supported is 65k.\n"); + } + + // Read and remove the bitmap planes and bpp data. Verify them. + $planes_and_bitcount = unpack('v2', substr($data, 0, 4)); + $data = substr($data, 4); + if ($planes_and_bitcount[2] != 24) { // Bitcount + throw new WriterException("$bitmap isn't a 24bit true color bitmap.\n"); + } + if ($planes_and_bitcount[1] != 1) { + throw new WriterException("$bitmap: only 1 plane supported in bitmap image.\n"); + } + + // Read and remove the bitmap compression. Verify compression. + $compression = unpack('Vcomp', substr($data, 0, 4)); + $data = substr($data, 4); + + if ($compression['comp'] != 0) { + throw new WriterException("$bitmap: compression not supported in bitmap image.\n"); + } + + // Remove bitmap data: data size, hres, vres, colours, imp. colours. + $data = substr($data, 20); + + // Add the BITMAPCOREHEADER data + $header = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); + $data = $header . $data; + + return [$width, $height, $size, $data]; + } + + /** + * Store the window zoom factor. This should be a reduced fraction but for + * simplicity we will store all fractions with a numerator of 100. + */ + private function writeZoom() + { + // If scale is 100 we don't need to write a record + if ($this->phpSheet->getSheetView()->getZoomScale() == 100) { + return; + } + + $record = 0x00A0; // Record identifier + $length = 0x0004; // Bytes to follow + + $header = pack('vv', $record, $length); + $data = pack('vv', $this->phpSheet->getSheetView()->getZoomScale(), 100); + $this->append($header . $data); + } + + /** + * Get Escher object. + * + * @return \PhpOffice\PhpSpreadsheet\Shared\Escher + */ + public function getEscher() + { + return $this->escher; + } + + /** + * Set Escher object. + * + * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue + */ + public function setEscher(\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null) + { + $this->escher = $pValue; + } + + /** + * Write MSODRAWING record. + */ + private function writeMsoDrawing() + { + // write the Escher stream if necessary + if (isset($this->escher)) { + $writer = new Escher($this->escher); + $data = $writer->close(); + $spOffsets = $writer->getSpOffsets(); + $spTypes = $writer->getSpTypes(); + // write the neccesary MSODRAWING, OBJ records + + // split the Escher stream + $spOffsets[0] = 0; + $nm = count($spOffsets) - 1; // number of shapes excluding first shape + for ($i = 1; $i <= $nm; ++$i) { + // MSODRAWING record + $record = 0x00EC; // Record identifier + + // chunk of Escher stream for one shape + $dataChunk = substr($data, $spOffsets[$i - 1], $spOffsets[$i] - $spOffsets[$i - 1]); + + $length = strlen($dataChunk); + $header = pack('vv', $record, $length); + + $this->append($header . $dataChunk); + + // OBJ record + $record = 0x005D; // record identifier + $objData = ''; + + // ftCmo + if ($spTypes[$i] == 0x00C9) { + // Add ftCmo (common object data) subobject + $objData .= + pack( + 'vvvvvVVV', + 0x0015, // 0x0015 = ftCmo + 0x0012, // length of ftCmo data + 0x0014, // object type, 0x0014 = filter + $i, // object id number, Excel seems to use 1-based index, local for the sheet + 0x2101, // option flags, 0x2001 is what OpenOffice.org uses + 0, // reserved + 0, // reserved + 0 // reserved + ); + + // Add ftSbs Scroll bar subobject + $objData .= pack('vv', 0x00C, 0x0014); + $objData .= pack('H*', '0000000000000000640001000A00000010000100'); + // Add ftLbsData (List box data) subobject + $objData .= pack('vv', 0x0013, 0x1FEE); + $objData .= pack('H*', '00000000010001030000020008005700'); + } else { + // Add ftCmo (common object data) subobject + $objData .= + pack( + 'vvvvvVVV', + 0x0015, // 0x0015 = ftCmo + 0x0012, // length of ftCmo data + 0x0008, // object type, 0x0008 = picture + $i, // object id number, Excel seems to use 1-based index, local for the sheet + 0x6011, // option flags, 0x6011 is what OpenOffice.org uses + 0, // reserved + 0, // reserved + 0 // reserved + ); + } + + // ftEnd + $objData .= + pack( + 'vv', + 0x0000, // 0x0000 = ftEnd + 0x0000 // length of ftEnd data + ); + + $length = strlen($objData); + $header = pack('vv', $record, $length); + $this->append($header . $objData); + } + } + } + + /** + * Store the DATAVALIDATIONS and DATAVALIDATION records. + */ + private function writeDataValidity() + { + // Datavalidation collection + $dataValidationCollection = $this->phpSheet->getDataValidationCollection(); + + // Write data validations? + if (!empty($dataValidationCollection)) { + // DATAVALIDATIONS record + $record = 0x01B2; // Record identifier + $length = 0x0012; // Bytes to follow + + $grbit = 0x0000; // Prompt box at cell, no cached validity data at DV records + $horPos = 0x00000000; // Horizontal position of prompt box, if fixed position + $verPos = 0x00000000; // Vertical position of prompt box, if fixed position + $objId = 0xFFFFFFFF; // Object identifier of drop down arrow object, or -1 if not visible + + $header = pack('vv', $record, $length); + $data = pack('vVVVV', $grbit, $horPos, $verPos, $objId, count($dataValidationCollection)); + $this->append($header . $data); + + // DATAVALIDATION records + $record = 0x01BE; // Record identifier + + foreach ($dataValidationCollection as $cellCoordinate => $dataValidation) { + // initialize record data + $data = ''; + + // options + $options = 0x00000000; + + // data type + $type = 0x00; + switch ($dataValidation->getType()) { + case DataValidation::TYPE_NONE: + $type = 0x00; + + break; + case DataValidation::TYPE_WHOLE: + $type = 0x01; + + break; + case DataValidation::TYPE_DECIMAL: + $type = 0x02; + + break; + case DataValidation::TYPE_LIST: + $type = 0x03; + + break; + case DataValidation::TYPE_DATE: + $type = 0x04; + + break; + case DataValidation::TYPE_TIME: + $type = 0x05; + + break; + case DataValidation::TYPE_TEXTLENGTH: + $type = 0x06; + + break; + case DataValidation::TYPE_CUSTOM: + $type = 0x07; + + break; + } + + $options |= $type << 0; + + // error style + $errorStyle = 0x00; + switch ($dataValidation->getErrorStyle()) { + case DataValidation::STYLE_STOP: + $errorStyle = 0x00; + + break; + case DataValidation::STYLE_WARNING: + $errorStyle = 0x01; + + break; + case DataValidation::STYLE_INFORMATION: + $errorStyle = 0x02; + + break; + } + + $options |= $errorStyle << 4; + + // explicit formula? + if ($type == 0x03 && preg_match('/^\".*\"$/', $dataValidation->getFormula1())) { + $options |= 0x01 << 7; + } + + // empty cells allowed + $options |= $dataValidation->getAllowBlank() << 8; + + // show drop down + $options |= (!$dataValidation->getShowDropDown()) << 9; + + // show input message + $options |= $dataValidation->getShowInputMessage() << 18; + + // show error message + $options |= $dataValidation->getShowErrorMessage() << 19; + + // condition operator + $operator = 0x00; + switch ($dataValidation->getOperator()) { + case DataValidation::OPERATOR_BETWEEN: + $operator = 0x00; + + break; + case DataValidation::OPERATOR_NOTBETWEEN: + $operator = 0x01; + + break; + case DataValidation::OPERATOR_EQUAL: + $operator = 0x02; + + break; + case DataValidation::OPERATOR_NOTEQUAL: + $operator = 0x03; + + break; + case DataValidation::OPERATOR_GREATERTHAN: + $operator = 0x04; + + break; + case DataValidation::OPERATOR_LESSTHAN: + $operator = 0x05; + + break; + case DataValidation::OPERATOR_GREATERTHANOREQUAL: + $operator = 0x06; + + break; + case DataValidation::OPERATOR_LESSTHANOREQUAL: + $operator = 0x07; + + break; + } + + $options |= $operator << 20; + + $data = pack('V', $options); + + // prompt title + $promptTitle = $dataValidation->getPromptTitle() !== '' ? + $dataValidation->getPromptTitle() : chr(0); + $data .= StringHelper::UTF8toBIFF8UnicodeLong($promptTitle); + + // error title + $errorTitle = $dataValidation->getErrorTitle() !== '' ? + $dataValidation->getErrorTitle() : chr(0); + $data .= StringHelper::UTF8toBIFF8UnicodeLong($errorTitle); + + // prompt text + $prompt = $dataValidation->getPrompt() !== '' ? + $dataValidation->getPrompt() : chr(0); + $data .= StringHelper::UTF8toBIFF8UnicodeLong($prompt); + + // error text + $error = $dataValidation->getError() !== '' ? + $dataValidation->getError() : chr(0); + $data .= StringHelper::UTF8toBIFF8UnicodeLong($error); + + // formula 1 + try { + $formula1 = $dataValidation->getFormula1(); + if ($type == 0x03) { // list type + $formula1 = str_replace(',', chr(0), $formula1); + } + $this->parser->parse($formula1); + $formula1 = $this->parser->toReversePolish(); + $sz1 = strlen($formula1); + } catch (PhpSpreadsheetException $e) { + $sz1 = 0; + $formula1 = ''; + } + $data .= pack('vv', $sz1, 0x0000); + $data .= $formula1; + + // formula 2 + try { + $formula2 = $dataValidation->getFormula2(); + if ($formula2 === '') { + throw new WriterException('No formula2'); + } + $this->parser->parse($formula2); + $formula2 = $this->parser->toReversePolish(); + $sz2 = strlen($formula2); + } catch (PhpSpreadsheetException $e) { + $sz2 = 0; + $formula2 = ''; + } + $data .= pack('vv', $sz2, 0x0000); + $data .= $formula2; + + // cell range address list + $data .= pack('v', 0x0001); + $data .= $this->writeBIFF8CellRangeAddressFixed($cellCoordinate); + + $length = strlen($data); + $header = pack('vv', $record, $length); + + $this->append($header . $data); + } + } + } + + /** + * Map Error code. + * + * @param string $errorCode + * + * @return int + */ + private static function mapErrorCode($errorCode) + { + switch ($errorCode) { + case '#NULL!': + return 0x00; + case '#DIV/0!': + return 0x07; + case '#VALUE!': + return 0x0F; + case '#REF!': + return 0x17; + case '#NAME?': + return 0x1D; + case '#NUM!': + return 0x24; + case '#N/A': + return 0x2A; + } + + return 0; + } + + /** + * Write PLV Record. + */ + private function writePageLayoutView() + { + $record = 0x088B; // Record identifier + $length = 0x0010; // Bytes to follow + + $rt = 0x088B; // 2 + $grbitFrt = 0x0000; // 2 + $reserved = 0x0000000000000000; // 8 + $wScalvePLV = $this->phpSheet->getSheetView()->getZoomScale(); // 2 + + // The options flags that comprise $grbit + if ($this->phpSheet->getSheetView()->getView() == SheetView::SHEETVIEW_PAGE_LAYOUT) { + $fPageLayoutView = 1; + } else { + $fPageLayoutView = 0; + } + $fRulerVisible = 0; + $fWhitespaceHidden = 0; + + $grbit = $fPageLayoutView; // 2 + $grbit |= $fRulerVisible << 1; + $grbit |= $fWhitespaceHidden << 3; + + $header = pack('vv', $record, $length); + $data = pack('vvVVvv', $rt, $grbitFrt, 0x00000000, 0x00000000, $wScalvePLV, $grbit); + $this->append($header . $data); + } + + /** + * Write CFRule Record. + * + * @param Conditional $conditional + */ + private function writeCFRule(Conditional $conditional) + { + $record = 0x01B1; // Record identifier + + // $type : Type of the CF + // $operatorType : Comparison operator + if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { + $type = 0x02; + $operatorType = 0x00; + } elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS) { + $type = 0x01; + + switch ($conditional->getOperatorType()) { + case Conditional::OPERATOR_NONE: + $operatorType = 0x00; + + break; + case Conditional::OPERATOR_EQUAL: + $operatorType = 0x03; + + break; + case Conditional::OPERATOR_GREATERTHAN: + $operatorType = 0x05; + + break; + case Conditional::OPERATOR_GREATERTHANOREQUAL: + $operatorType = 0x07; + + break; + case Conditional::OPERATOR_LESSTHAN: + $operatorType = 0x06; + + break; + case Conditional::OPERATOR_LESSTHANOREQUAL: + $operatorType = 0x08; + + break; + case Conditional::OPERATOR_NOTEQUAL: + $operatorType = 0x04; + + break; + case Conditional::OPERATOR_BETWEEN: + $operatorType = 0x01; + + break; + // not OPERATOR_NOTBETWEEN 0x02 + } + } + + // $szValue1 : size of the formula data for first value or formula + // $szValue2 : size of the formula data for second value or formula + $arrConditions = $conditional->getConditions(); + $numConditions = count($arrConditions); + if ($numConditions == 1) { + $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); + $szValue2 = 0x0000; + $operand1 = pack('Cv', 0x1E, $arrConditions[0]); + $operand2 = null; + } elseif ($numConditions == 2 && ($conditional->getOperatorType() == Conditional::OPERATOR_BETWEEN)) { + $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); + $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000); + $operand1 = pack('Cv', 0x1E, $arrConditions[0]); + $operand2 = pack('Cv', 0x1E, $arrConditions[1]); + } else { + $szValue1 = 0x0000; + $szValue2 = 0x0000; + $operand1 = null; + $operand2 = null; + } + + // $flags : Option flags + // Alignment + $bAlignHz = ($conditional->getStyle()->getAlignment()->getHorizontal() == null ? 1 : 0); + $bAlignVt = ($conditional->getStyle()->getAlignment()->getVertical() == null ? 1 : 0); + $bAlignWrapTx = ($conditional->getStyle()->getAlignment()->getWrapText() == false ? 1 : 0); + $bTxRotation = ($conditional->getStyle()->getAlignment()->getTextRotation() == null ? 1 : 0); + $bIndent = ($conditional->getStyle()->getAlignment()->getIndent() == 0 ? 1 : 0); + $bShrinkToFit = ($conditional->getStyle()->getAlignment()->getShrinkToFit() == false ? 1 : 0); + if ($bAlignHz == 0 || $bAlignVt == 0 || $bAlignWrapTx == 0 || $bTxRotation == 0 || $bIndent == 0 || $bShrinkToFit == 0) { + $bFormatAlign = 1; + } else { + $bFormatAlign = 0; + } + // Protection + $bProtLocked = ($conditional->getStyle()->getProtection()->getLocked() == null ? 1 : 0); + $bProtHidden = ($conditional->getStyle()->getProtection()->getHidden() == null ? 1 : 0); + if ($bProtLocked == 0 || $bProtHidden == 0) { + $bFormatProt = 1; + } else { + $bFormatProt = 0; + } + // Border + $bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getColor()->getARGB() == Color::COLOR_BLACK + && $conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); + $bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getColor()->getARGB() == Color::COLOR_BLACK + && $conditional->getStyle()->getBorders()->getRight()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); + $bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getColor()->getARGB() == Color::COLOR_BLACK + && $conditional->getStyle()->getBorders()->getTop()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); + $bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getColor()->getARGB() == Color::COLOR_BLACK + && $conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0); + if ($bBorderLeft == 0 || $bBorderRight == 0 || $bBorderTop == 0 || $bBorderBottom == 0) { + $bFormatBorder = 1; + } else { + $bFormatBorder = 0; + } + // Pattern + $bFillStyle = ($conditional->getStyle()->getFill()->getFillType() == null ? 0 : 1); + $bFillColor = ($conditional->getStyle()->getFill()->getStartColor()->getARGB() == null ? 0 : 1); + $bFillColorBg = ($conditional->getStyle()->getFill()->getEndColor()->getARGB() == null ? 0 : 1); + if ($bFillStyle == 0 || $bFillColor == 0 || $bFillColorBg == 0) { + $bFormatFill = 1; + } else { + $bFormatFill = 0; + } + // Font + if ($conditional->getStyle()->getFont()->getName() != null + || $conditional->getStyle()->getFont()->getSize() != null + || $conditional->getStyle()->getFont()->getBold() != null + || $conditional->getStyle()->getFont()->getItalic() != null + || $conditional->getStyle()->getFont()->getSuperscript() != null + || $conditional->getStyle()->getFont()->getSubscript() != null + || $conditional->getStyle()->getFont()->getUnderline() != null + || $conditional->getStyle()->getFont()->getStrikethrough() != null + || $conditional->getStyle()->getFont()->getColor()->getARGB() != null) { + $bFormatFont = 1; + } else { + $bFormatFont = 0; + } + // Alignment + $flags = 0; + $flags |= (1 == $bAlignHz ? 0x00000001 : 0); + $flags |= (1 == $bAlignVt ? 0x00000002 : 0); + $flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0); + $flags |= (1 == $bTxRotation ? 0x00000008 : 0); + // Justify last line flag + $flags |= (1 == 1 ? 0x00000010 : 0); + $flags |= (1 == $bIndent ? 0x00000020 : 0); + $flags |= (1 == $bShrinkToFit ? 0x00000040 : 0); + // Default + $flags |= (1 == 1 ? 0x00000080 : 0); + // Protection + $flags |= (1 == $bProtLocked ? 0x00000100 : 0); + $flags |= (1 == $bProtHidden ? 0x00000200 : 0); + // Border + $flags |= (1 == $bBorderLeft ? 0x00000400 : 0); + $flags |= (1 == $bBorderRight ? 0x00000800 : 0); + $flags |= (1 == $bBorderTop ? 0x00001000 : 0); + $flags |= (1 == $bBorderBottom ? 0x00002000 : 0); + $flags |= (1 == 1 ? 0x00004000 : 0); // Top left to Bottom right border + $flags |= (1 == 1 ? 0x00008000 : 0); // Bottom left to Top right border + // Pattern + $flags |= (1 == $bFillStyle ? 0x00010000 : 0); + $flags |= (1 == $bFillColor ? 0x00020000 : 0); + $flags |= (1 == $bFillColorBg ? 0x00040000 : 0); + $flags |= (1 == 1 ? 0x00380000 : 0); + // Font + $flags |= (1 == $bFormatFont ? 0x04000000 : 0); + // Alignment: + $flags |= (1 == $bFormatAlign ? 0x08000000 : 0); + // Border + $flags |= (1 == $bFormatBorder ? 0x10000000 : 0); + // Pattern + $flags |= (1 == $bFormatFill ? 0x20000000 : 0); + // Protection + $flags |= (1 == $bFormatProt ? 0x40000000 : 0); + // Text direction + $flags |= (1 == 0 ? 0x80000000 : 0); + + // Data Blocks + if ($bFormatFont == 1) { + // Font Name + if ($conditional->getStyle()->getFont()->getName() == null) { + $dataBlockFont = pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000); + $dataBlockFont .= pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000); + } else { + $dataBlockFont = StringHelper::UTF8toBIFF8UnicodeLong($conditional->getStyle()->getFont()->getName()); + } + // Font Size + if ($conditional->getStyle()->getFont()->getSize() == null) { + $dataBlockFont .= pack('V', 20 * 11); + } else { + $dataBlockFont .= pack('V', 20 * $conditional->getStyle()->getFont()->getSize()); + } + // Font Options + $dataBlockFont .= pack('V', 0); + // Font weight + if ($conditional->getStyle()->getFont()->getBold() == true) { + $dataBlockFont .= pack('v', 0x02BC); + } else { + $dataBlockFont .= pack('v', 0x0190); + } + // Escapement type + if ($conditional->getStyle()->getFont()->getSubscript() == true) { + $dataBlockFont .= pack('v', 0x02); + $fontEscapement = 0; + } elseif ($conditional->getStyle()->getFont()->getSuperscript() == true) { + $dataBlockFont .= pack('v', 0x01); + $fontEscapement = 0; + } else { + $dataBlockFont .= pack('v', 0x00); + $fontEscapement = 1; + } + // Underline type + switch ($conditional->getStyle()->getFont()->getUnderline()) { + case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE: + $dataBlockFont .= pack('C', 0x00); + $fontUnderline = 0; + + break; + case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE: + $dataBlockFont .= pack('C', 0x02); + $fontUnderline = 0; + + break; + case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING: + $dataBlockFont .= pack('C', 0x22); + $fontUnderline = 0; + + break; + case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE: + $dataBlockFont .= pack('C', 0x01); + $fontUnderline = 0; + + break; + case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING: + $dataBlockFont .= pack('C', 0x21); + $fontUnderline = 0; + + break; + default: + $dataBlockFont .= pack('C', 0x00); + $fontUnderline = 1; + + break; + } + // Not used (3) + $dataBlockFont .= pack('vC', 0x0000, 0x00); + // Font color index + switch ($conditional->getStyle()->getFont()->getColor()->getRGB()) { + case '000000': + $colorIdx = 0x08; + + break; + case 'FFFFFF': + $colorIdx = 0x09; + + break; + case 'FF0000': + $colorIdx = 0x0A; + + break; + case '00FF00': + $colorIdx = 0x0B; + + break; + case '0000FF': + $colorIdx = 0x0C; + + break; + case 'FFFF00': + $colorIdx = 0x0D; + + break; + case 'FF00FF': + $colorIdx = 0x0E; + + break; + case '00FFFF': + $colorIdx = 0x0F; + + break; + case '800000': + $colorIdx = 0x10; + + break; + case '008000': + $colorIdx = 0x11; + + break; + case '000080': + $colorIdx = 0x12; + + break; + case '808000': + $colorIdx = 0x13; + + break; + case '800080': + $colorIdx = 0x14; + + break; + case '008080': + $colorIdx = 0x15; + + break; + case 'C0C0C0': + $colorIdx = 0x16; + + break; + case '808080': + $colorIdx = 0x17; + + break; + case '9999FF': + $colorIdx = 0x18; + + break; + case '993366': + $colorIdx = 0x19; + + break; + case 'FFFFCC': + $colorIdx = 0x1A; + + break; + case 'CCFFFF': + $colorIdx = 0x1B; + + break; + case '660066': + $colorIdx = 0x1C; + + break; + case 'FF8080': + $colorIdx = 0x1D; + + break; + case '0066CC': + $colorIdx = 0x1E; + + break; + case 'CCCCFF': + $colorIdx = 0x1F; + + break; + case '000080': + $colorIdx = 0x20; + + break; + case 'FF00FF': + $colorIdx = 0x21; + + break; + case 'FFFF00': + $colorIdx = 0x22; + + break; + case '00FFFF': + $colorIdx = 0x23; + + break; + case '800080': + $colorIdx = 0x24; + + break; + case '800000': + $colorIdx = 0x25; + + break; + case '008080': + $colorIdx = 0x26; + + break; + case '0000FF': + $colorIdx = 0x27; + + break; + case '00CCFF': + $colorIdx = 0x28; + + break; + case 'CCFFFF': + $colorIdx = 0x29; + + break; + case 'CCFFCC': + $colorIdx = 0x2A; + + break; + case 'FFFF99': + $colorIdx = 0x2B; + + break; + case '99CCFF': + $colorIdx = 0x2C; + + break; + case 'FF99CC': + $colorIdx = 0x2D; + + break; + case 'CC99FF': + $colorIdx = 0x2E; + + break; + case 'FFCC99': + $colorIdx = 0x2F; + + break; + case '3366FF': + $colorIdx = 0x30; + + break; + case '33CCCC': + $colorIdx = 0x31; + + break; + case '99CC00': + $colorIdx = 0x32; + + break; + case 'FFCC00': + $colorIdx = 0x33; + + break; + case 'FF9900': + $colorIdx = 0x34; + + break; + case 'FF6600': + $colorIdx = 0x35; + + break; + case '666699': + $colorIdx = 0x36; + + break; + case '969696': + $colorIdx = 0x37; + + break; + case '003366': + $colorIdx = 0x38; + + break; + case '339966': + $colorIdx = 0x39; + + break; + case '003300': + $colorIdx = 0x3A; + + break; + case '333300': + $colorIdx = 0x3B; + + break; + case '993300': + $colorIdx = 0x3C; + + break; + case '993366': + $colorIdx = 0x3D; + + break; + case '333399': + $colorIdx = 0x3E; + + break; + case '333333': + $colorIdx = 0x3F; + + break; + default: + $colorIdx = 0x00; + + break; + } + $dataBlockFont .= pack('V', $colorIdx); + // Not used (4) + $dataBlockFont .= pack('V', 0x00000000); + // Options flags for modified font attributes + $optionsFlags = 0; + $optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() == null ? 1 : 0); + $optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0); + $optionsFlags |= (1 == 1 ? 0x00000008 : 0); + $optionsFlags |= (1 == 1 ? 0x00000010 : 0); + $optionsFlags |= (1 == 0 ? 0x00000020 : 0); + $optionsFlags |= (1 == 1 ? 0x00000080 : 0); + $dataBlockFont .= pack('V', $optionsFlags); + // Escapement type + $dataBlockFont .= pack('V', $fontEscapement); + // Underline type + $dataBlockFont .= pack('V', $fontUnderline); + // Always + $dataBlockFont .= pack('V', 0x00000000); + // Always + $dataBlockFont .= pack('V', 0x00000000); + // Not used (8) + $dataBlockFont .= pack('VV', 0x00000000, 0x00000000); + // Always + $dataBlockFont .= pack('v', 0x0001); + } + if ($bFormatAlign == 1) { + $blockAlign = 0; + // Alignment and text break + switch ($conditional->getStyle()->getAlignment()->getHorizontal()) { + case Alignment::HORIZONTAL_GENERAL: + $blockAlign = 0; + + break; + case Alignment::HORIZONTAL_LEFT: + $blockAlign = 1; + + break; + case Alignment::HORIZONTAL_RIGHT: + $blockAlign = 3; + + break; + case Alignment::HORIZONTAL_CENTER: + $blockAlign = 2; + + break; + case Alignment::HORIZONTAL_CENTER_CONTINUOUS: + $blockAlign = 6; + + break; + case Alignment::HORIZONTAL_JUSTIFY: + $blockAlign = 5; + + break; + } + if ($conditional->getStyle()->getAlignment()->getWrapText() == true) { + $blockAlign |= 1 << 3; + } else { + $blockAlign |= 0 << 3; + } + switch ($conditional->getStyle()->getAlignment()->getVertical()) { + case Alignment::VERTICAL_BOTTOM: + $blockAlign = 2 << 4; + + break; + case Alignment::VERTICAL_TOP: + $blockAlign = 0 << 4; + + break; + case Alignment::VERTICAL_CENTER: + $blockAlign = 1 << 4; + + break; + case Alignment::VERTICAL_JUSTIFY: + $blockAlign = 3 << 4; + + break; + } + $blockAlign |= 0 << 7; + + // Text rotation angle + $blockRotation = $conditional->getStyle()->getAlignment()->getTextRotation(); + + // Indentation + $blockIndent = $conditional->getStyle()->getAlignment()->getIndent(); + if ($conditional->getStyle()->getAlignment()->getShrinkToFit() == true) { + $blockIndent |= 1 << 4; + } else { + $blockIndent |= 0 << 4; + } + $blockIndent |= 0 << 6; + + // Relative indentation + $blockIndentRelative = 255; + + $dataBlockAlign = pack('CCvvv', $blockAlign, $blockRotation, $blockIndent, $blockIndentRelative, 0x0000); + } + if ($bFormatBorder == 1) { + $blockLineStyle = 0; + switch ($conditional->getStyle()->getBorders()->getLeft()->getBorderStyle()) { + case Border::BORDER_NONE: + $blockLineStyle |= 0x00; + + break; + case Border::BORDER_THIN: + $blockLineStyle |= 0x01; + + break; + case Border::BORDER_MEDIUM: + $blockLineStyle |= 0x02; + + break; + case Border::BORDER_DASHED: + $blockLineStyle |= 0x03; + + break; + case Border::BORDER_DOTTED: + $blockLineStyle |= 0x04; + + break; + case Border::BORDER_THICK: + $blockLineStyle |= 0x05; + + break; + case Border::BORDER_DOUBLE: + $blockLineStyle |= 0x06; + + break; + case Border::BORDER_HAIR: + $blockLineStyle |= 0x07; + + break; + case Border::BORDER_MEDIUMDASHED: + $blockLineStyle |= 0x08; + + break; + case Border::BORDER_DASHDOT: + $blockLineStyle |= 0x09; + + break; + case Border::BORDER_MEDIUMDASHDOT: + $blockLineStyle |= 0x0A; + + break; + case Border::BORDER_DASHDOTDOT: + $blockLineStyle |= 0x0B; + + break; + case Border::BORDER_MEDIUMDASHDOTDOT: + $blockLineStyle |= 0x0C; + + break; + case Border::BORDER_SLANTDASHDOT: + $blockLineStyle |= 0x0D; + + break; + } + switch ($conditional->getStyle()->getBorders()->getRight()->getBorderStyle()) { + case Border::BORDER_NONE: + $blockLineStyle |= 0x00 << 4; + + break; + case Border::BORDER_THIN: + $blockLineStyle |= 0x01 << 4; + + break; + case Border::BORDER_MEDIUM: + $blockLineStyle |= 0x02 << 4; + + break; + case Border::BORDER_DASHED: + $blockLineStyle |= 0x03 << 4; + + break; + case Border::BORDER_DOTTED: + $blockLineStyle |= 0x04 << 4; + + break; + case Border::BORDER_THICK: + $blockLineStyle |= 0x05 << 4; + + break; + case Border::BORDER_DOUBLE: + $blockLineStyle |= 0x06 << 4; + + break; + case Border::BORDER_HAIR: + $blockLineStyle |= 0x07 << 4; + + break; + case Border::BORDER_MEDIUMDASHED: + $blockLineStyle |= 0x08 << 4; + + break; + case Border::BORDER_DASHDOT: + $blockLineStyle |= 0x09 << 4; + + break; + case Border::BORDER_MEDIUMDASHDOT: + $blockLineStyle |= 0x0A << 4; + + break; + case Border::BORDER_DASHDOTDOT: + $blockLineStyle |= 0x0B << 4; + + break; + case Border::BORDER_MEDIUMDASHDOTDOT: + $blockLineStyle |= 0x0C << 4; + + break; + case Border::BORDER_SLANTDASHDOT: + $blockLineStyle |= 0x0D << 4; + + break; + } + switch ($conditional->getStyle()->getBorders()->getTop()->getBorderStyle()) { + case Border::BORDER_NONE: + $blockLineStyle |= 0x00 << 8; + + break; + case Border::BORDER_THIN: + $blockLineStyle |= 0x01 << 8; + + break; + case Border::BORDER_MEDIUM: + $blockLineStyle |= 0x02 << 8; + + break; + case Border::BORDER_DASHED: + $blockLineStyle |= 0x03 << 8; + + break; + case Border::BORDER_DOTTED: + $blockLineStyle |= 0x04 << 8; + + break; + case Border::BORDER_THICK: + $blockLineStyle |= 0x05 << 8; + + break; + case Border::BORDER_DOUBLE: + $blockLineStyle |= 0x06 << 8; + + break; + case Border::BORDER_HAIR: + $blockLineStyle |= 0x07 << 8; + + break; + case Border::BORDER_MEDIUMDASHED: + $blockLineStyle |= 0x08 << 8; + + break; + case Border::BORDER_DASHDOT: + $blockLineStyle |= 0x09 << 8; + + break; + case Border::BORDER_MEDIUMDASHDOT: + $blockLineStyle |= 0x0A << 8; + + break; + case Border::BORDER_DASHDOTDOT: + $blockLineStyle |= 0x0B << 8; + + break; + case Border::BORDER_MEDIUMDASHDOTDOT: + $blockLineStyle |= 0x0C << 8; + + break; + case Border::BORDER_SLANTDASHDOT: + $blockLineStyle |= 0x0D << 8; + + break; + } + switch ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle()) { + case Border::BORDER_NONE: + $blockLineStyle |= 0x00 << 12; + + break; + case Border::BORDER_THIN: + $blockLineStyle |= 0x01 << 12; + + break; + case Border::BORDER_MEDIUM: + $blockLineStyle |= 0x02 << 12; + + break; + case Border::BORDER_DASHED: + $blockLineStyle |= 0x03 << 12; + + break; + case Border::BORDER_DOTTED: + $blockLineStyle |= 0x04 << 12; + + break; + case Border::BORDER_THICK: + $blockLineStyle |= 0x05 << 12; + + break; + case Border::BORDER_DOUBLE: + $blockLineStyle |= 0x06 << 12; + + break; + case Border::BORDER_HAIR: + $blockLineStyle |= 0x07 << 12; + + break; + case Border::BORDER_MEDIUMDASHED: + $blockLineStyle |= 0x08 << 12; + + break; + case Border::BORDER_DASHDOT: + $blockLineStyle |= 0x09 << 12; + + break; + case Border::BORDER_MEDIUMDASHDOT: + $blockLineStyle |= 0x0A << 12; + + break; + case Border::BORDER_DASHDOTDOT: + $blockLineStyle |= 0x0B << 12; + + break; + case Border::BORDER_MEDIUMDASHDOTDOT: + $blockLineStyle |= 0x0C << 12; + + break; + case Border::BORDER_SLANTDASHDOT: + $blockLineStyle |= 0x0D << 12; + + break; + } + //@todo writeCFRule() => $blockLineStyle => Index Color for left line + //@todo writeCFRule() => $blockLineStyle => Index Color for right line + //@todo writeCFRule() => $blockLineStyle => Top-left to bottom-right on/off + //@todo writeCFRule() => $blockLineStyle => Bottom-left to top-right on/off + $blockColor = 0; + //@todo writeCFRule() => $blockColor => Index Color for top line + //@todo writeCFRule() => $blockColor => Index Color for bottom line + //@todo writeCFRule() => $blockColor => Index Color for diagonal line + switch ($conditional->getStyle()->getBorders()->getDiagonal()->getBorderStyle()) { + case Border::BORDER_NONE: + $blockColor |= 0x00 << 21; + + break; + case Border::BORDER_THIN: + $blockColor |= 0x01 << 21; + + break; + case Border::BORDER_MEDIUM: + $blockColor |= 0x02 << 21; + + break; + case Border::BORDER_DASHED: + $blockColor |= 0x03 << 21; + + break; + case Border::BORDER_DOTTED: + $blockColor |= 0x04 << 21; + + break; + case Border::BORDER_THICK: + $blockColor |= 0x05 << 21; + + break; + case Border::BORDER_DOUBLE: + $blockColor |= 0x06 << 21; + + break; + case Border::BORDER_HAIR: + $blockColor |= 0x07 << 21; + + break; + case Border::BORDER_MEDIUMDASHED: + $blockColor |= 0x08 << 21; + + break; + case Border::BORDER_DASHDOT: + $blockColor |= 0x09 << 21; + + break; + case Border::BORDER_MEDIUMDASHDOT: + $blockColor |= 0x0A << 21; + + break; + case Border::BORDER_DASHDOTDOT: + $blockColor |= 0x0B << 21; + + break; + case Border::BORDER_MEDIUMDASHDOTDOT: + $blockColor |= 0x0C << 21; + + break; + case Border::BORDER_SLANTDASHDOT: + $blockColor |= 0x0D << 21; + + break; + } + $dataBlockBorder = pack('vv', $blockLineStyle, $blockColor); + } + if ($bFormatFill == 1) { + // Fill Patern Style + $blockFillPatternStyle = 0; + switch ($conditional->getStyle()->getFill()->getFillType()) { + case Fill::FILL_NONE: + $blockFillPatternStyle = 0x00; + + break; + case Fill::FILL_SOLID: + $blockFillPatternStyle = 0x01; + + break; + case Fill::FILL_PATTERN_MEDIUMGRAY: + $blockFillPatternStyle = 0x02; + + break; + case Fill::FILL_PATTERN_DARKGRAY: + $blockFillPatternStyle = 0x03; + + break; + case Fill::FILL_PATTERN_LIGHTGRAY: + $blockFillPatternStyle = 0x04; + + break; + case Fill::FILL_PATTERN_DARKHORIZONTAL: + $blockFillPatternStyle = 0x05; + + break; + case Fill::FILL_PATTERN_DARKVERTICAL: + $blockFillPatternStyle = 0x06; + + break; + case Fill::FILL_PATTERN_DARKDOWN: + $blockFillPatternStyle = 0x07; + + break; + case Fill::FILL_PATTERN_DARKUP: + $blockFillPatternStyle = 0x08; + + break; + case Fill::FILL_PATTERN_DARKGRID: + $blockFillPatternStyle = 0x09; + + break; + case Fill::FILL_PATTERN_DARKTRELLIS: + $blockFillPatternStyle = 0x0A; + + break; + case Fill::FILL_PATTERN_LIGHTHORIZONTAL: + $blockFillPatternStyle = 0x0B; + + break; + case Fill::FILL_PATTERN_LIGHTVERTICAL: + $blockFillPatternStyle = 0x0C; + + break; + case Fill::FILL_PATTERN_LIGHTDOWN: + $blockFillPatternStyle = 0x0D; + + break; + case Fill::FILL_PATTERN_LIGHTUP: + $blockFillPatternStyle = 0x0E; + + break; + case Fill::FILL_PATTERN_LIGHTGRID: + $blockFillPatternStyle = 0x0F; + + break; + case Fill::FILL_PATTERN_LIGHTTRELLIS: + $blockFillPatternStyle = 0x10; + + break; + case Fill::FILL_PATTERN_GRAY125: + $blockFillPatternStyle = 0x11; + + break; + case Fill::FILL_PATTERN_GRAY0625: + $blockFillPatternStyle = 0x12; + + break; + case Fill::FILL_GRADIENT_LINEAR: + $blockFillPatternStyle = 0x00; + + break; // does not exist in BIFF8 + case Fill::FILL_GRADIENT_PATH: + $blockFillPatternStyle = 0x00; + + break; // does not exist in BIFF8 + default: + $blockFillPatternStyle = 0x00; + + break; + } + // Color + switch ($conditional->getStyle()->getFill()->getStartColor()->getRGB()) { + case '000000': + $colorIdxBg = 0x08; + + break; + case 'FFFFFF': + $colorIdxBg = 0x09; + + break; + case 'FF0000': + $colorIdxBg = 0x0A; + + break; + case '00FF00': + $colorIdxBg = 0x0B; + + break; + case '0000FF': + $colorIdxBg = 0x0C; + + break; + case 'FFFF00': + $colorIdxBg = 0x0D; + + break; + case 'FF00FF': + $colorIdxBg = 0x0E; + + break; + case '00FFFF': + $colorIdxBg = 0x0F; + + break; + case '800000': + $colorIdxBg = 0x10; + + break; + case '008000': + $colorIdxBg = 0x11; + + break; + case '000080': + $colorIdxBg = 0x12; + + break; + case '808000': + $colorIdxBg = 0x13; + + break; + case '800080': + $colorIdxBg = 0x14; + + break; + case '008080': + $colorIdxBg = 0x15; + + break; + case 'C0C0C0': + $colorIdxBg = 0x16; + + break; + case '808080': + $colorIdxBg = 0x17; + + break; + case '9999FF': + $colorIdxBg = 0x18; + + break; + case '993366': + $colorIdxBg = 0x19; + + break; + case 'FFFFCC': + $colorIdxBg = 0x1A; + + break; + case 'CCFFFF': + $colorIdxBg = 0x1B; + + break; + case '660066': + $colorIdxBg = 0x1C; + + break; + case 'FF8080': + $colorIdxBg = 0x1D; + + break; + case '0066CC': + $colorIdxBg = 0x1E; + + break; + case 'CCCCFF': + $colorIdxBg = 0x1F; + + break; + case '000080': + $colorIdxBg = 0x20; + + break; + case 'FF00FF': + $colorIdxBg = 0x21; + + break; + case 'FFFF00': + $colorIdxBg = 0x22; + + break; + case '00FFFF': + $colorIdxBg = 0x23; + + break; + case '800080': + $colorIdxBg = 0x24; + + break; + case '800000': + $colorIdxBg = 0x25; + + break; + case '008080': + $colorIdxBg = 0x26; + + break; + case '0000FF': + $colorIdxBg = 0x27; + + break; + case '00CCFF': + $colorIdxBg = 0x28; + + break; + case 'CCFFFF': + $colorIdxBg = 0x29; + + break; + case 'CCFFCC': + $colorIdxBg = 0x2A; + + break; + case 'FFFF99': + $colorIdxBg = 0x2B; + + break; + case '99CCFF': + $colorIdxBg = 0x2C; + + break; + case 'FF99CC': + $colorIdxBg = 0x2D; + + break; + case 'CC99FF': + $colorIdxBg = 0x2E; + + break; + case 'FFCC99': + $colorIdxBg = 0x2F; + + break; + case '3366FF': + $colorIdxBg = 0x30; + + break; + case '33CCCC': + $colorIdxBg = 0x31; + + break; + case '99CC00': + $colorIdxBg = 0x32; + + break; + case 'FFCC00': + $colorIdxBg = 0x33; + + break; + case 'FF9900': + $colorIdxBg = 0x34; + + break; + case 'FF6600': + $colorIdxBg = 0x35; + + break; + case '666699': + $colorIdxBg = 0x36; + + break; + case '969696': + $colorIdxBg = 0x37; + + break; + case '003366': + $colorIdxBg = 0x38; + + break; + case '339966': + $colorIdxBg = 0x39; + + break; + case '003300': + $colorIdxBg = 0x3A; + + break; + case '333300': + $colorIdxBg = 0x3B; + + break; + case '993300': + $colorIdxBg = 0x3C; + + break; + case '993366': + $colorIdxBg = 0x3D; + + break; + case '333399': + $colorIdxBg = 0x3E; + + break; + case '333333': + $colorIdxBg = 0x3F; + + break; + default: + $colorIdxBg = 0x41; + + break; + } + // Fg Color + switch ($conditional->getStyle()->getFill()->getEndColor()->getRGB()) { + case '000000': + $colorIdxFg = 0x08; + + break; + case 'FFFFFF': + $colorIdxFg = 0x09; + + break; + case 'FF0000': + $colorIdxFg = 0x0A; + + break; + case '00FF00': + $colorIdxFg = 0x0B; + + break; + case '0000FF': + $colorIdxFg = 0x0C; + + break; + case 'FFFF00': + $colorIdxFg = 0x0D; + + break; + case 'FF00FF': + $colorIdxFg = 0x0E; + + break; + case '00FFFF': + $colorIdxFg = 0x0F; + + break; + case '800000': + $colorIdxFg = 0x10; + + break; + case '008000': + $colorIdxFg = 0x11; + + break; + case '000080': + $colorIdxFg = 0x12; + + break; + case '808000': + $colorIdxFg = 0x13; + + break; + case '800080': + $colorIdxFg = 0x14; + + break; + case '008080': + $colorIdxFg = 0x15; + + break; + case 'C0C0C0': + $colorIdxFg = 0x16; + + break; + case '808080': + $colorIdxFg = 0x17; + + break; + case '9999FF': + $colorIdxFg = 0x18; + + break; + case '993366': + $colorIdxFg = 0x19; + + break; + case 'FFFFCC': + $colorIdxFg = 0x1A; + + break; + case 'CCFFFF': + $colorIdxFg = 0x1B; + + break; + case '660066': + $colorIdxFg = 0x1C; + + break; + case 'FF8080': + $colorIdxFg = 0x1D; + + break; + case '0066CC': + $colorIdxFg = 0x1E; + + break; + case 'CCCCFF': + $colorIdxFg = 0x1F; + + break; + case '000080': + $colorIdxFg = 0x20; + + break; + case 'FF00FF': + $colorIdxFg = 0x21; + + break; + case 'FFFF00': + $colorIdxFg = 0x22; + + break; + case '00FFFF': + $colorIdxFg = 0x23; + + break; + case '800080': + $colorIdxFg = 0x24; + + break; + case '800000': + $colorIdxFg = 0x25; + + break; + case '008080': + $colorIdxFg = 0x26; + + break; + case '0000FF': + $colorIdxFg = 0x27; + + break; + case '00CCFF': + $colorIdxFg = 0x28; + + break; + case 'CCFFFF': + $colorIdxFg = 0x29; + + break; + case 'CCFFCC': + $colorIdxFg = 0x2A; + + break; + case 'FFFF99': + $colorIdxFg = 0x2B; + + break; + case '99CCFF': + $colorIdxFg = 0x2C; + + break; + case 'FF99CC': + $colorIdxFg = 0x2D; + + break; + case 'CC99FF': + $colorIdxFg = 0x2E; + + break; + case 'FFCC99': + $colorIdxFg = 0x2F; + + break; + case '3366FF': + $colorIdxFg = 0x30; + + break; + case '33CCCC': + $colorIdxFg = 0x31; + + break; + case '99CC00': + $colorIdxFg = 0x32; + + break; + case 'FFCC00': + $colorIdxFg = 0x33; + + break; + case 'FF9900': + $colorIdxFg = 0x34; + + break; + case 'FF6600': + $colorIdxFg = 0x35; + + break; + case '666699': + $colorIdxFg = 0x36; + + break; + case '969696': + $colorIdxFg = 0x37; + + break; + case '003366': + $colorIdxFg = 0x38; + + break; + case '339966': + $colorIdxFg = 0x39; + + break; + case '003300': + $colorIdxFg = 0x3A; + + break; + case '333300': + $colorIdxFg = 0x3B; + + break; + case '993300': + $colorIdxFg = 0x3C; + + break; + case '993366': + $colorIdxFg = 0x3D; + + break; + case '333399': + $colorIdxFg = 0x3E; + + break; + case '333333': + $colorIdxFg = 0x3F; + + break; + default: + $colorIdxFg = 0x40; + + break; + } + $dataBlockFill = pack('v', $blockFillPatternStyle); + $dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7)); + } + if ($bFormatProt == 1) { + $dataBlockProtection = 0; + if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) { + $dataBlockProtection = 1; + } + if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) { + $dataBlockProtection = 1 << 1; + } + } + + $data = pack('CCvvVv', $type, $operatorType, $szValue1, $szValue2, $flags, 0x0000); + if ($bFormatFont == 1) { // Block Formatting : OK + $data .= $dataBlockFont; + } + if ($bFormatAlign == 1) { + $data .= $dataBlockAlign; + } + if ($bFormatBorder == 1) { + $data .= $dataBlockBorder; + } + if ($bFormatFill == 1) { // Block Formatting : OK + $data .= $dataBlockFill; + } + if ($bFormatProt == 1) { + $data .= $dataBlockProtection; + } + if ($operand1 !== null) { + $data .= $operand1; + } + if ($operand2 !== null) { + $data .= $operand2; + } + $header = pack('vv', $record, strlen($data)); + $this->append($header . $data); + } + + /** + * Write CFHeader record. + */ + private function writeCFHeader() + { + $record = 0x01B0; // Record identifier + $length = 0x0016; // Bytes to follow + + $numColumnMin = null; + $numColumnMax = null; + $numRowMin = null; + $numRowMax = null; + $arrConditional = []; + foreach ($this->phpSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { + foreach ($conditionalStyles as $conditional) { + if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION + || $conditional->getConditionType() == Conditional::CONDITION_CELLIS) { + if (!in_array($conditional->getHashCode(), $arrConditional)) { + $arrConditional[] = $conditional->getHashCode(); + } + // Cells + $arrCoord = Coordinate::coordinateFromString($cellCoordinate); + if (!is_numeric($arrCoord[0])) { + $arrCoord[0] = Coordinate::columnIndexFromString($arrCoord[0]); + } + if ($numColumnMin === null || ($numColumnMin > $arrCoord[0])) { + $numColumnMin = $arrCoord[0]; + } + if ($numColumnMax === null || ($numColumnMax < $arrCoord[0])) { + $numColumnMax = $arrCoord[0]; + } + if ($numRowMin === null || ($numRowMin > $arrCoord[1])) { + $numRowMin = $arrCoord[1]; + } + if ($numRowMax === null || ($numRowMax < $arrCoord[1])) { + $numRowMax = $arrCoord[1]; + } + } + } + } + $needRedraw = 1; + $cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1); + + $header = pack('vv', $record, $length); + $data = pack('vv', count($arrConditional), $needRedraw); + $data .= $cellRange; + $data .= pack('v', 0x0001); + $data .= $cellRange; + $this->append($header . $data); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Xf.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Xf.php new file mode 100644 index 00000000000..238fb34c683 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xls/Xf.php @@ -0,0 +1,548 @@ + +// * +// * The majority of this is _NOT_ my code. I simply ported it from the +// * PERL Spreadsheet::WriteExcel module. +// * +// * The author of the Spreadsheet::WriteExcel module is John McNamara +// * +// * +// * I _DO_ maintain this code, and John McNamara has nothing to do with the +// * porting of this code to PHP. Any questions directly related to this +// * class library should be directed to me. +// * +// * License Information: +// * +// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets +// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com +// * +// * This library is free software; you can redistribute it and/or +// * modify it under the terms of the GNU Lesser General Public +// * License as published by the Free Software Foundation; either +// * version 2.1 of the License, or (at your option) any later version. +// * +// * This library is distributed in the hope that it will be useful, +// * but WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Lesser General Public License for more details. +// * +// * You should have received a copy of the GNU Lesser General Public +// * License along with this library; if not, write to the Free Software +// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// */ +class Xf +{ + /** + * Style XF or a cell XF ? + * + * @var bool + */ + private $isStyleXf; + + /** + * Index to the FONT record. Index 4 does not exist. + * + * @var int + */ + private $fontIndex; + + /** + * An index (2 bytes) to a FORMAT record (number format). + * + * @var int + */ + private $numberFormatIndex; + + /** + * 1 bit, apparently not used. + * + * @var int + */ + private $textJustLast; + + /** + * The cell's foreground color. + * + * @var int + */ + private $foregroundColor; + + /** + * The cell's background color. + * + * @var int + */ + private $backgroundColor; + + /** + * Color of the bottom border of the cell. + * + * @var int + */ + private $bottomBorderColor; + + /** + * Color of the top border of the cell. + * + * @var int + */ + private $topBorderColor; + + /** + * Color of the left border of the cell. + * + * @var int + */ + private $leftBorderColor; + + /** + * Color of the right border of the cell. + * + * @var int + */ + private $rightBorderColor; + + /** + * Constructor. + * + * @param Style $style The XF format + */ + public function __construct(Style $style) + { + $this->isStyleXf = false; + $this->fontIndex = 0; + + $this->numberFormatIndex = 0; + + $this->textJustLast = 0; + + $this->foregroundColor = 0x40; + $this->backgroundColor = 0x41; + + $this->_diag = 0; + + $this->bottomBorderColor = 0x40; + $this->topBorderColor = 0x40; + $this->leftBorderColor = 0x40; + $this->rightBorderColor = 0x40; + $this->_diag_color = 0x40; + $this->_style = $style; + } + + /** + * Generate an Excel BIFF XF record (style or cell). + * + * @return string The XF record + */ + public function writeXf() + { + // Set the type of the XF record and some of the attributes. + if ($this->isStyleXf) { + $style = 0xFFF5; + } else { + $style = self::mapLocked($this->_style->getProtection()->getLocked()); + $style |= self::mapHidden($this->_style->getProtection()->getHidden()) << 1; + } + + // Flags to indicate if attributes have been set. + $atr_num = ($this->numberFormatIndex != 0) ? 1 : 0; + $atr_fnt = ($this->fontIndex != 0) ? 1 : 0; + $atr_alc = ((int) $this->_style->getAlignment()->getWrapText()) ? 1 : 0; + $atr_bdr = (self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) || + self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) || + self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()) || + self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle())) ? 1 : 0; + $atr_pat = (($this->foregroundColor != 0x40) || + ($this->backgroundColor != 0x41) || + self::mapFillType($this->_style->getFill()->getFillType())) ? 1 : 0; + $atr_prot = self::mapLocked($this->_style->getProtection()->getLocked()) + | self::mapHidden($this->_style->getProtection()->getHidden()); + + // Zero the default border colour if the border has not been set. + if (self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) == 0) { + $this->bottomBorderColor = 0; + } + if (self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) == 0) { + $this->topBorderColor = 0; + } + if (self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle()) == 0) { + $this->rightBorderColor = 0; + } + if (self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()) == 0) { + $this->leftBorderColor = 0; + } + if (self::mapBorderStyle($this->_style->getBorders()->getDiagonal()->getBorderStyle()) == 0) { + $this->_diag_color = 0; + } + + $record = 0x00E0; // Record identifier + $length = 0x0014; // Number of bytes to follow + + $ifnt = $this->fontIndex; // Index to FONT record + $ifmt = $this->numberFormatIndex; // Index to FORMAT record + + $align = $this->mapHAlign($this->_style->getAlignment()->getHorizontal()); // Alignment + $align |= (int) $this->_style->getAlignment()->getWrapText() << 3; + $align |= self::mapVAlign($this->_style->getAlignment()->getVertical()) << 4; + $align |= $this->textJustLast << 7; + + $used_attrib = $atr_num << 2; + $used_attrib |= $atr_fnt << 3; + $used_attrib |= $atr_alc << 4; + $used_attrib |= $atr_bdr << 5; + $used_attrib |= $atr_pat << 6; + $used_attrib |= $atr_prot << 7; + + $icv = $this->foregroundColor; // fg and bg pattern colors + $icv |= $this->backgroundColor << 7; + + $border1 = self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()); // Border line style and color + $border1 |= self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle()) << 4; + $border1 |= self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) << 8; + $border1 |= self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) << 12; + $border1 |= $this->leftBorderColor << 16; + $border1 |= $this->rightBorderColor << 23; + + $diagonalDirection = $this->_style->getBorders()->getDiagonalDirection(); + $diag_tl_to_rb = $diagonalDirection == Borders::DIAGONAL_BOTH + || $diagonalDirection == Borders::DIAGONAL_DOWN; + $diag_tr_to_lb = $diagonalDirection == Borders::DIAGONAL_BOTH + || $diagonalDirection == Borders::DIAGONAL_UP; + $border1 |= $diag_tl_to_rb << 30; + $border1 |= $diag_tr_to_lb << 31; + + $border2 = $this->topBorderColor; // Border color + $border2 |= $this->bottomBorderColor << 7; + $border2 |= $this->_diag_color << 14; + $border2 |= self::mapBorderStyle($this->_style->getBorders()->getDiagonal()->getBorderStyle()) << 21; + $border2 |= self::mapFillType($this->_style->getFill()->getFillType()) << 26; + + $header = pack('vv', $record, $length); + + //BIFF8 options: identation, shrinkToFit and text direction + $biff8_options = $this->_style->getAlignment()->getIndent(); + $biff8_options |= (int) $this->_style->getAlignment()->getShrinkToFit() << 4; + + $data = pack('vvvC', $ifnt, $ifmt, $style, $align); + $data .= pack('CCC', self::mapTextRotation($this->_style->getAlignment()->getTextRotation()), $biff8_options, $used_attrib); + $data .= pack('VVv', $border1, $border2, $icv); + + return $header . $data; + } + + /** + * Is this a style XF ? + * + * @param bool $value + */ + public function setIsStyleXf($value) + { + $this->isStyleXf = $value; + } + + /** + * Sets the cell's bottom border color. + * + * @param int $colorIndex Color index + */ + public function setBottomColor($colorIndex) + { + $this->bottomBorderColor = $colorIndex; + } + + /** + * Sets the cell's top border color. + * + * @param int $colorIndex Color index + */ + public function setTopColor($colorIndex) + { + $this->topBorderColor = $colorIndex; + } + + /** + * Sets the cell's left border color. + * + * @param int $colorIndex Color index + */ + public function setLeftColor($colorIndex) + { + $this->leftBorderColor = $colorIndex; + } + + /** + * Sets the cell's right border color. + * + * @param int $colorIndex Color index + */ + public function setRightColor($colorIndex) + { + $this->rightBorderColor = $colorIndex; + } + + /** + * Sets the cell's diagonal border color. + * + * @param int $colorIndex Color index + */ + public function setDiagColor($colorIndex) + { + $this->_diag_color = $colorIndex; + } + + /** + * Sets the cell's foreground color. + * + * @param int $colorIndex Color index + */ + public function setFgColor($colorIndex) + { + $this->foregroundColor = $colorIndex; + } + + /** + * Sets the cell's background color. + * + * @param int $colorIndex Color index + */ + public function setBgColor($colorIndex) + { + $this->backgroundColor = $colorIndex; + } + + /** + * Sets the index to the number format record + * It can be date, time, currency, etc... + * + * @param int $numberFormatIndex Index to format record + */ + public function setNumberFormatIndex($numberFormatIndex) + { + $this->numberFormatIndex = $numberFormatIndex; + } + + /** + * Set the font index. + * + * @param int $value Font index, note that value 4 does not exist + */ + public function setFontIndex($value) + { + $this->fontIndex = $value; + } + + /** + * Map of BIFF2-BIFF8 codes for border styles. + * + * @var array of int + */ + private static $mapBorderStyles = [ + Border::BORDER_NONE => 0x00, + Border::BORDER_THIN => 0x01, + Border::BORDER_MEDIUM => 0x02, + Border::BORDER_DASHED => 0x03, + Border::BORDER_DOTTED => 0x04, + Border::BORDER_THICK => 0x05, + Border::BORDER_DOUBLE => 0x06, + Border::BORDER_HAIR => 0x07, + Border::BORDER_MEDIUMDASHED => 0x08, + Border::BORDER_DASHDOT => 0x09, + Border::BORDER_MEDIUMDASHDOT => 0x0A, + Border::BORDER_DASHDOTDOT => 0x0B, + Border::BORDER_MEDIUMDASHDOTDOT => 0x0C, + Border::BORDER_SLANTDASHDOT => 0x0D, + ]; + + /** + * Map border style. + * + * @param string $borderStyle + * + * @return int + */ + private static function mapBorderStyle($borderStyle) + { + if (isset(self::$mapBorderStyles[$borderStyle])) { + return self::$mapBorderStyles[$borderStyle]; + } + + return 0x00; + } + + /** + * Map of BIFF2-BIFF8 codes for fill types. + * + * @var array of int + */ + private static $mapFillTypes = [ + Fill::FILL_NONE => 0x00, + Fill::FILL_SOLID => 0x01, + Fill::FILL_PATTERN_MEDIUMGRAY => 0x02, + Fill::FILL_PATTERN_DARKGRAY => 0x03, + Fill::FILL_PATTERN_LIGHTGRAY => 0x04, + Fill::FILL_PATTERN_DARKHORIZONTAL => 0x05, + Fill::FILL_PATTERN_DARKVERTICAL => 0x06, + Fill::FILL_PATTERN_DARKDOWN => 0x07, + Fill::FILL_PATTERN_DARKUP => 0x08, + Fill::FILL_PATTERN_DARKGRID => 0x09, + Fill::FILL_PATTERN_DARKTRELLIS => 0x0A, + Fill::FILL_PATTERN_LIGHTHORIZONTAL => 0x0B, + Fill::FILL_PATTERN_LIGHTVERTICAL => 0x0C, + Fill::FILL_PATTERN_LIGHTDOWN => 0x0D, + Fill::FILL_PATTERN_LIGHTUP => 0x0E, + Fill::FILL_PATTERN_LIGHTGRID => 0x0F, + Fill::FILL_PATTERN_LIGHTTRELLIS => 0x10, + Fill::FILL_PATTERN_GRAY125 => 0x11, + Fill::FILL_PATTERN_GRAY0625 => 0x12, + Fill::FILL_GRADIENT_LINEAR => 0x00, // does not exist in BIFF8 + Fill::FILL_GRADIENT_PATH => 0x00, // does not exist in BIFF8 + ]; + + /** + * Map fill type. + * + * @param string $fillType + * + * @return int + */ + private static function mapFillType($fillType) + { + if (isset(self::$mapFillTypes[$fillType])) { + return self::$mapFillTypes[$fillType]; + } + + return 0x00; + } + + /** + * Map of BIFF2-BIFF8 codes for horizontal alignment. + * + * @var array of int + */ + private static $mapHAlignments = [ + Alignment::HORIZONTAL_GENERAL => 0, + Alignment::HORIZONTAL_LEFT => 1, + Alignment::HORIZONTAL_CENTER => 2, + Alignment::HORIZONTAL_RIGHT => 3, + Alignment::HORIZONTAL_FILL => 4, + Alignment::HORIZONTAL_JUSTIFY => 5, + Alignment::HORIZONTAL_CENTER_CONTINUOUS => 6, + ]; + + /** + * Map to BIFF2-BIFF8 codes for horizontal alignment. + * + * @param string $hAlign + * + * @return int + */ + private function mapHAlign($hAlign) + { + if (isset(self::$mapHAlignments[$hAlign])) { + return self::$mapHAlignments[$hAlign]; + } + + return 0; + } + + /** + * Map of BIFF2-BIFF8 codes for vertical alignment. + * + * @var array of int + */ + private static $mapVAlignments = [ + Alignment::VERTICAL_TOP => 0, + Alignment::VERTICAL_CENTER => 1, + Alignment::VERTICAL_BOTTOM => 2, + Alignment::VERTICAL_JUSTIFY => 3, + ]; + + /** + * Map to BIFF2-BIFF8 codes for vertical alignment. + * + * @param string $vAlign + * + * @return int + */ + private static function mapVAlign($vAlign) + { + if (isset(self::$mapVAlignments[$vAlign])) { + return self::$mapVAlignments[$vAlign]; + } + + return 2; + } + + /** + * Map to BIFF8 codes for text rotation angle. + * + * @param int $textRotation + * + * @return int + */ + private static function mapTextRotation($textRotation) + { + if ($textRotation >= 0) { + return $textRotation; + } elseif ($textRotation == -165) { + return 255; + } elseif ($textRotation < 0) { + return 90 - $textRotation; + } + } + + /** + * Map locked. + * + * @param string $locked + * + * @return int + */ + private static function mapLocked($locked) + { + switch ($locked) { + case Protection::PROTECTION_INHERIT: + return 1; + case Protection::PROTECTION_PROTECTED: + return 1; + case Protection::PROTECTION_UNPROTECTED: + return 0; + default: + return 1; + } + } + + /** + * Map hidden. + * + * @param string $hidden + * + * @return int + */ + private static function mapHidden($hidden) + { + switch ($hidden) { + case Protection::PROTECTION_INHERIT: + return 0; + case Protection::PROTECTION_PROTECTED: + return 1; + case Protection::PROTECTION_UNPROTECTED: + return 0; + default: + return 0; + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx.php new file mode 100644 index 00000000000..dd19021e46e --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx.php @@ -0,0 +1,547 @@ +setSpreadsheet($spreadsheet); + + $writerPartsArray = [ + 'stringtable' => StringTable::class, + 'contenttypes' => ContentTypes::class, + 'docprops' => DocProps::class, + 'rels' => Rels::class, + 'theme' => Theme::class, + 'style' => Style::class, + 'workbook' => Workbook::class, + 'worksheet' => Worksheet::class, + 'drawing' => Drawing::class, + 'comments' => Comments::class, + 'chart' => Chart::class, + 'relsvba' => RelsVBA::class, + 'relsribbonobjects' => RelsRibbon::class, + ]; + + // Initialise writer parts + // and Assign their parent IWriters + foreach ($writerPartsArray as $writer => $class) { + $this->writerParts[$writer] = new $class($this); + } + + $hashTablesArray = ['stylesConditionalHashTable', 'fillHashTable', 'fontHashTable', + 'bordersHashTable', 'numFmtHashTable', 'drawingHashTable', + 'styleHashTable', + ]; + + // Set HashTable variables + foreach ($hashTablesArray as $tableName) { + $this->$tableName = new HashTable(); + } + } + + /** + * Get writer part. + * + * @param string $pPartName Writer part name + * + * @return \PhpOffice\PhpSpreadsheet\Writer\Xlsx\WriterPart + */ + public function getWriterPart($pPartName) + { + if ($pPartName != '' && isset($this->writerParts[strtolower($pPartName)])) { + return $this->writerParts[strtolower($pPartName)]; + } + + return null; + } + + /** + * Save PhpSpreadsheet to file. + * + * @param string $pFilename + * + * @throws WriterException + */ + public function save($pFilename) + { + if ($this->spreadSheet !== null) { + // garbage collect + $this->spreadSheet->garbageCollect(); + + // If $pFilename is php://output or php://stdout, make it a temporary file... + $originalFilename = $pFilename; + if (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { + $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); + if ($pFilename == '') { + $pFilename = $originalFilename; + } + } + + $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog(); + Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog(false); + $saveDateReturnType = Functions::getReturnDateType(); + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + + // Create string lookup table + $this->stringTable = []; + for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { + $this->stringTable = $this->getWriterPart('StringTable')->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable); + } + + // Create styles dictionaries + $this->styleHashTable->addFromSource($this->getWriterPart('Style')->allStyles($this->spreadSheet)); + $this->stylesConditionalHashTable->addFromSource($this->getWriterPart('Style')->allConditionalStyles($this->spreadSheet)); + $this->fillHashTable->addFromSource($this->getWriterPart('Style')->allFills($this->spreadSheet)); + $this->fontHashTable->addFromSource($this->getWriterPart('Style')->allFonts($this->spreadSheet)); + $this->bordersHashTable->addFromSource($this->getWriterPart('Style')->allBorders($this->spreadSheet)); + $this->numFmtHashTable->addFromSource($this->getWriterPart('Style')->allNumberFormats($this->spreadSheet)); + + // Create drawing dictionary + $this->drawingHashTable->addFromSource($this->getWriterPart('Drawing')->allDrawings($this->spreadSheet)); + + $zip = new ZipArchive(); + + if (file_exists($pFilename)) { + unlink($pFilename); + } + // Try opening the ZIP file + if ($zip->open($pFilename, ZipArchive::OVERWRITE) !== true) { + if ($zip->open($pFilename, ZipArchive::CREATE) !== true) { + throw new WriterException('Could not open ' . $pFilename . ' for writing.'); + } + } + + // Add [Content_Types].xml to ZIP file + $zip->addFromString('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts)); + + //if hasMacros, add the vbaProject.bin file, Certificate file(if exists) + if ($this->spreadSheet->hasMacros()) { + $macrosCode = $this->spreadSheet->getMacrosCode(); + if ($macrosCode !== null) { + // we have the code ? + $zip->addFromString('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin + if ($this->spreadSheet->hasMacrosCertificate()) { + //signed macros ? + // Yes : add the certificate file and the related rels file + $zip->addFromString('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate()); + $zip->addFromString('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet)); + } + } + } + //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels) + if ($this->spreadSheet->hasRibbon()) { + $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target'); + $zip->addFromString($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data')); + if ($this->spreadSheet->hasRibbonBinObjects()) { + $tmpRootPath = dirname($tmpRibbonTarget) . '/'; + $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write + foreach ($ribbonBinObjects as $aPath => $aContent) { + $zip->addFromString($tmpRootPath . $aPath, $aContent); + } + //the rels for files + $zip->addFromString($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet)); + } + } + + // Add relationships to ZIP file + $zip->addFromString('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet)); + $zip->addFromString('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet)); + + // Add document properties to ZIP file + $zip->addFromString('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet)); + $zip->addFromString('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet)); + $customPropertiesPart = $this->getWriterPart('DocProps')->writeDocPropsCustom($this->spreadSheet); + if ($customPropertiesPart !== null) { + $zip->addFromString('docProps/custom.xml', $customPropertiesPart); + } + + // Add theme to ZIP file + $zip->addFromString('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet)); + + // Add string table to ZIP file + $zip->addFromString('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable)); + + // Add styles to ZIP file + $zip->addFromString('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet)); + + // Add workbook to ZIP file + $zip->addFromString('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); + + $chartCount = 0; + // Add worksheets + for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { + $zip->addFromString('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); + if ($this->includeCharts) { + $charts = $this->spreadSheet->getSheet($i)->getChartCollection(); + if (count($charts) > 0) { + foreach ($charts as $chart) { + $zip->addFromString('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas)); + ++$chartCount; + } + } + } + } + + $chartRef1 = 0; + // Add worksheet relationships (drawings, ...) + for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { + // Add relationships + $zip->addFromString('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); + + // Add unparsedLoadedData + $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); + $unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData(); + if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) { + foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) { + $zip->addFromString($ctrlProp['filePath'], $ctrlProp['content']); + } + } + if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) { + foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) { + $zip->addFromString($ctrlProp['filePath'], $ctrlProp['content']); + } + } + + $drawings = $this->spreadSheet->getSheet($i)->getDrawingCollection(); + $drawingCount = count($drawings); + if ($this->includeCharts) { + $chartCount = $this->spreadSheet->getSheet($i)->getChartCount(); + } + + // Add drawing and image relationship parts + if (($drawingCount > 0) || ($chartCount > 0)) { + // Drawing relationships + $zip->addFromString('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); + + // Drawings + $zip->addFromString('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) { + // Drawings + $zip->addFromString('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + } + + // Add comment relationship parts + if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) { + // VML Comments + $zip->addFromString('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i))); + + // Comments + $zip->addFromString('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i))); + } + + // Add unparsed relationship parts + if (isset($unparsedLoadedData['sheets'][$this->spreadSheet->getSheet($i)->getCodeName()]['vmlDrawings'])) { + foreach ($unparsedLoadedData['sheets'][$this->spreadSheet->getSheet($i)->getCodeName()]['vmlDrawings'] as $vmlDrawing) { + $zip->addFromString($vmlDrawing['filePath'], $vmlDrawing['content']); + } + } + + // Add header/footer relationship parts + if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) { + // VML Drawings + $zip->addFromString('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); + + // VML Drawing relationships + $zip->addFromString('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); + + // Media + foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) { + $zip->addFromString('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath())); + } + } + } + + // Add media + for ($i = 0; $i < $this->getDrawingHashTable()->count(); ++$i) { + if ($this->getDrawingHashTable()->getByIndex($i) instanceof WorksheetDrawing) { + $imageContents = null; + $imagePath = $this->getDrawingHashTable()->getByIndex($i)->getPath(); + if (strpos($imagePath, 'zip://') !== false) { + $imagePath = substr($imagePath, 6); + $imagePathSplitted = explode('#', $imagePath); + + $imageZip = new ZipArchive(); + $imageZip->open($imagePathSplitted[0]); + $imageContents = $imageZip->getFromName($imagePathSplitted[1]); + $imageZip->close(); + unset($imageZip); + } else { + $imageContents = file_get_contents($imagePath); + } + + $zip->addFromString('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) { + ob_start(); + call_user_func( + $this->getDrawingHashTable()->getByIndex($i)->getRenderingFunction(), + $this->getDrawingHashTable()->getByIndex($i)->getImageResource() + ); + $imageContents = ob_get_contents(); + ob_end_clean(); + + $zip->addFromString('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + } + } + + Functions::setReturnDateType($saveDateReturnType); + Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); + + // Close file + if ($zip->close() === false) { + throw new WriterException("Could not close zip file $pFilename."); + } + + // If a temporary file was used, copy it to the correct file stream + if ($originalFilename != $pFilename) { + if (copy($pFilename, $originalFilename) === false) { + throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); + } + @unlink($pFilename); + } + } else { + throw new WriterException('PhpSpreadsheet object unassigned.'); + } + } + + /** + * Get Spreadsheet object. + * + * @throws WriterException + * + * @return Spreadsheet + */ + public function getSpreadsheet() + { + if ($this->spreadSheet !== null) { + return $this->spreadSheet; + } + + throw new WriterException('No Spreadsheet object assigned.'); + } + + /** + * Set Spreadsheet object. + * + * @param Spreadsheet $spreadsheet PhpSpreadsheet object + * + * @return Xlsx + */ + public function setSpreadsheet(Spreadsheet $spreadsheet) + { + $this->spreadSheet = $spreadsheet; + + return $this; + } + + /** + * Get string table. + * + * @return string[] + */ + public function getStringTable() + { + return $this->stringTable; + } + + /** + * Get Style HashTable. + * + * @return HashTable + */ + public function getStyleHashTable() + { + return $this->styleHashTable; + } + + /** + * Get Conditional HashTable. + * + * @return HashTable + */ + public function getStylesConditionalHashTable() + { + return $this->stylesConditionalHashTable; + } + + /** + * Get Fill HashTable. + * + * @return HashTable + */ + public function getFillHashTable() + { + return $this->fillHashTable; + } + + /** + * Get \PhpOffice\PhpSpreadsheet\Style\Font HashTable. + * + * @return HashTable + */ + public function getFontHashTable() + { + return $this->fontHashTable; + } + + /** + * Get Borders HashTable. + * + * @return HashTable + */ + public function getBordersHashTable() + { + return $this->bordersHashTable; + } + + /** + * Get NumberFormat HashTable. + * + * @return HashTable + */ + public function getNumFmtHashTable() + { + return $this->numFmtHashTable; + } + + /** + * Get \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable. + * + * @return HashTable + */ + public function getDrawingHashTable() + { + return $this->drawingHashTable; + } + + /** + * Get Office2003 compatibility. + * + * @return bool + */ + public function getOffice2003Compatibility() + { + return $this->office2003compatibility; + } + + /** + * Set Office2003 compatibility. + * + * @param bool $pValue Office2003 compatibility? + * + * @return Xlsx + */ + public function setOffice2003Compatibility($pValue) + { + $this->office2003compatibility = $pValue; + + return $this; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Chart.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Chart.php new file mode 100644 index 00000000000..625fd16de21 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -0,0 +1,1541 @@ +calculateCellValues = $calculateCellValues; + + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + // Ensure that data series values are up-to-date before we save + if ($this->calculateCellValues) { + $pChart->refresh(); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // c:chartSpace + $objWriter->startElement('c:chartSpace'); + $objWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + $objWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + $objWriter->startElement('c:date1904'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + $objWriter->startElement('c:lang'); + $objWriter->writeAttribute('val', 'en-GB'); + $objWriter->endElement(); + $objWriter->startElement('c:roundedCorners'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $this->writeAlternateContent($objWriter); + + $objWriter->startElement('c:chart'); + + $this->writeTitle($objWriter, $pChart->getTitle()); + + $objWriter->startElement('c:autoTitleDeleted'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $this->writePlotArea($objWriter, $pChart->getWorksheet(), $pChart->getPlotArea(), $pChart->getXAxisLabel(), $pChart->getYAxisLabel(), $pChart->getChartAxisX(), $pChart->getChartAxisY(), $pChart->getMajorGridlines(), $pChart->getMinorGridlines()); + + $this->writeLegend($objWriter, $pChart->getLegend()); + + $objWriter->startElement('c:plotVisOnly'); + $objWriter->writeAttribute('val', 1); + $objWriter->endElement(); + + $objWriter->startElement('c:dispBlanksAs'); + $objWriter->writeAttribute('val', 'gap'); + $objWriter->endElement(); + + $objWriter->startElement('c:showDLblsOverMax'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $objWriter->endElement(); + + $this->writePrintSettings($objWriter); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write Chart Title. + * + * @param XMLWriter $objWriter XML Writer + * @param Title $title + * + * @throws WriterException + */ + private function writeTitle(XMLWriter $objWriter, Title $title = null) + { + if ($title === null) { + return; + } + + $objWriter->startElement('c:title'); + $objWriter->startElement('c:tx'); + $objWriter->startElement('c:rich'); + + $objWriter->startElement('a:bodyPr'); + $objWriter->endElement(); + + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); + + $objWriter->startElement('a:p'); + + $caption = $title->getCaption(); + if ((is_array($caption)) && (count($caption) > 0)) { + $caption = $caption[0]; + } + $this->getParentWriter()->getWriterPart('stringtable')->writeRichTextForCharts($objWriter, $caption, 'a'); + + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + $this->writeLayout($objWriter, $title->getLayout()); + + $objWriter->startElement('c:overlay'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write Chart Legend. + * + * @param XMLWriter $objWriter XML Writer + * @param Legend $legend + * + * @throws WriterException + */ + private function writeLegend(XMLWriter $objWriter, Legend $legend = null) + { + if ($legend === null) { + return; + } + + $objWriter->startElement('c:legend'); + + $objWriter->startElement('c:legendPos'); + $objWriter->writeAttribute('val', $legend->getPosition()); + $objWriter->endElement(); + + $this->writeLayout($objWriter, $legend->getLayout()); + + $objWriter->startElement('c:overlay'); + $objWriter->writeAttribute('val', ($legend->getOverlay()) ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->endElement(); + + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); + + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->writeAttribute('rtl', 0); + + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('a:endParaRPr'); + $objWriter->writeAttribute('lang', 'en-US'); + $objWriter->endElement(); + + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write Chart Plot Area. + * + * @param XMLWriter $objWriter XML Writer + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pSheet + * @param PlotArea $plotArea + * @param Title $xAxisLabel + * @param Title $yAxisLabel + * @param Axis $xAxis + * @param Axis $yAxis + * @param null|GridLines $majorGridlines + * @param null|GridLines $minorGridlines + * + * @throws WriterException + */ + private function writePlotArea(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pSheet, PlotArea $plotArea, Title $xAxisLabel = null, Title $yAxisLabel = null, Axis $xAxis = null, Axis $yAxis = null, GridLines $majorGridlines = null, GridLines $minorGridlines = null) + { + if ($plotArea === null) { + return; + } + + $id1 = $id2 = 0; + $this->seriesIndex = 0; + $objWriter->startElement('c:plotArea'); + + $layout = $plotArea->getLayout(); + + $this->writeLayout($objWriter, $layout); + + $chartTypes = self::getChartType($plotArea); + $catIsMultiLevelSeries = $valIsMultiLevelSeries = false; + $plotGroupingType = ''; + foreach ($chartTypes as $chartType) { + $objWriter->startElement('c:' . $chartType); + + $groupCount = $plotArea->getPlotGroupCount(); + for ($i = 0; $i < $groupCount; ++$i) { + $plotGroup = $plotArea->getPlotGroupByIndex($i); + $groupType = $plotGroup->getPlotType(); + if ($groupType == $chartType) { + $plotStyle = $plotGroup->getPlotStyle(); + if ($groupType === DataSeries::TYPE_RADARCHART) { + $objWriter->startElement('c:radarStyle'); + $objWriter->writeAttribute('val', $plotStyle); + $objWriter->endElement(); + } elseif ($groupType === DataSeries::TYPE_SCATTERCHART) { + $objWriter->startElement('c:scatterStyle'); + $objWriter->writeAttribute('val', $plotStyle); + $objWriter->endElement(); + } + + $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType); + } + } + + $this->writeDataLabels($objWriter, $layout); + + if ($chartType === DataSeries::TYPE_LINECHART) { + // Line only, Line3D can't be smoothed + $objWriter->startElement('c:smooth'); + $objWriter->writeAttribute('val', (int) $plotGroup->getSmoothLine()); + $objWriter->endElement(); + } elseif (($chartType === DataSeries::TYPE_BARCHART) || ($chartType === DataSeries::TYPE_BARCHART_3D)) { + $objWriter->startElement('c:gapWidth'); + $objWriter->writeAttribute('val', 150); + $objWriter->endElement(); + + if ($plotGroupingType == 'percentStacked' || $plotGroupingType == 'stacked') { + $objWriter->startElement('c:overlap'); + $objWriter->writeAttribute('val', 100); + $objWriter->endElement(); + } + } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) { + $objWriter->startElement('c:bubbleScale'); + $objWriter->writeAttribute('val', 25); + $objWriter->endElement(); + + $objWriter->startElement('c:showNegBubbles'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + } elseif ($chartType === DataSeries::TYPE_STOCKCHART) { + $objWriter->startElement('c:hiLowLines'); + $objWriter->endElement(); + + $objWriter->startElement('c:upDownBars'); + + $objWriter->startElement('c:gapWidth'); + $objWriter->writeAttribute('val', 300); + $objWriter->endElement(); + + $objWriter->startElement('c:upBars'); + $objWriter->endElement(); + + $objWriter->startElement('c:downBars'); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + // Generate 2 unique numbers to use for axId values + $id1 = '75091328'; + $id2 = '75089408'; + + if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id1); + $objWriter->endElement(); + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); + } else { + $objWriter->startElement('c:firstSliceAng'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + if ($chartType === DataSeries::TYPE_DONUTCHART) { + $objWriter->startElement('c:holeSize'); + $objWriter->writeAttribute('val', 50); + $objWriter->endElement(); + } + } + + $objWriter->endElement(); + } + + if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { + if ($chartType === DataSeries::TYPE_BUBBLECHART) { + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + } else { + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $yAxis); + } + + $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + } + + $objWriter->endElement(); + } + + /** + * Write Data Labels. + * + * @param XMLWriter $objWriter XML Writer + * @param \PhpOffice\PhpSpreadsheet\Chart\Layout $chartLayout Chart layout + */ + private function writeDataLabels(XMLWriter $objWriter, Layout $chartLayout = null) + { + $objWriter->startElement('c:dLbls'); + + $objWriter->startElement('c:showLegendKey'); + $showLegendKey = (empty($chartLayout)) ? 0 : $chartLayout->getShowLegendKey(); + $objWriter->writeAttribute('val', ((empty($showLegendKey)) ? 0 : 1)); + $objWriter->endElement(); + + $objWriter->startElement('c:showVal'); + $showVal = (empty($chartLayout)) ? 0 : $chartLayout->getShowVal(); + $objWriter->writeAttribute('val', ((empty($showVal)) ? 0 : 1)); + $objWriter->endElement(); + + $objWriter->startElement('c:showCatName'); + $showCatName = (empty($chartLayout)) ? 0 : $chartLayout->getShowCatName(); + $objWriter->writeAttribute('val', ((empty($showCatName)) ? 0 : 1)); + $objWriter->endElement(); + + $objWriter->startElement('c:showSerName'); + $showSerName = (empty($chartLayout)) ? 0 : $chartLayout->getShowSerName(); + $objWriter->writeAttribute('val', ((empty($showSerName)) ? 0 : 1)); + $objWriter->endElement(); + + $objWriter->startElement('c:showPercent'); + $showPercent = (empty($chartLayout)) ? 0 : $chartLayout->getShowPercent(); + $objWriter->writeAttribute('val', ((empty($showPercent)) ? 0 : 1)); + $objWriter->endElement(); + + $objWriter->startElement('c:showBubbleSize'); + $showBubbleSize = (empty($chartLayout)) ? 0 : $chartLayout->getShowBubbleSize(); + $objWriter->writeAttribute('val', ((empty($showBubbleSize)) ? 0 : 1)); + $objWriter->endElement(); + + $objWriter->startElement('c:showLeaderLines'); + $showLeaderLines = (empty($chartLayout)) ? 1 : $chartLayout->getShowLeaderLines(); + $objWriter->writeAttribute('val', ((empty($showLeaderLines)) ? 0 : 1)); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write Category Axis. + * + * @param XMLWriter $objWriter XML Writer + * @param Title $xAxisLabel + * @param string $id1 + * @param string $id2 + * @param bool $isMultiLevelSeries + * @param Axis $yAxis + * + * @throws WriterException + */ + private function writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis) + { + $objWriter->startElement('c:catAx'); + + if ($id1 > 0) { + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id1); + $objWriter->endElement(); + } + + $objWriter->startElement('c:scaling'); + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('c:delete'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $objWriter->startElement('c:axPos'); + $objWriter->writeAttribute('val', 'b'); + $objWriter->endElement(); + + if ($xAxisLabel !== null) { + $objWriter->startElement('c:title'); + $objWriter->startElement('c:tx'); + $objWriter->startElement('c:rich'); + + $objWriter->startElement('a:bodyPr'); + $objWriter->endElement(); + + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); + + $objWriter->startElement('a:p'); + $objWriter->startElement('a:r'); + + $caption = $xAxisLabel->getCaption(); + if (is_array($caption)) { + $caption = $caption[0]; + } + $objWriter->startElement('a:t'); + $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); + $objWriter->endElement(); + + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + $layout = $xAxisLabel->getLayout(); + $this->writeLayout($objWriter, $layout); + + $objWriter->startElement('c:overlay'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', $yAxis->getAxisNumberFormat()); + $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked()); + $objWriter->endElement(); + + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + + if ($id2 > 0) { + $objWriter->startElement('c:crossAx'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); + + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); + $objWriter->endElement(); + } + + $objWriter->startElement('c:auto'); + $objWriter->writeAttribute('val', 1); + $objWriter->endElement(); + + $objWriter->startElement('c:lblAlgn'); + $objWriter->writeAttribute('val', 'ctr'); + $objWriter->endElement(); + + $objWriter->startElement('c:lblOffset'); + $objWriter->writeAttribute('val', 100); + $objWriter->endElement(); + + if ($isMultiLevelSeries) { + $objWriter->startElement('c:noMultiLvlLbl'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + } + $objWriter->endElement(); + } + + /** + * Write Value Axis. + * + * @param XMLWriter $objWriter XML Writer + * @param Title $yAxisLabel + * @param string $groupType Chart type + * @param string $id1 + * @param string $id2 + * @param bool $isMultiLevelSeries + * @param Axis $xAxis + * @param GridLines $majorGridlines + * @param GridLines $minorGridlines + * + * @throws WriterException + */ + private function writeValueAxis($objWriter, $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis, GridLines $majorGridlines, GridLines $minorGridlines) + { + $objWriter->startElement('c:valAx'); + + if ($id2 > 0) { + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); + } + + $objWriter->startElement('c:scaling'); + + if ($xAxis->getAxisOptionsProperty('maximum') !== null) { + $objWriter->startElement('c:max'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('maximum')); + $objWriter->endElement(); + } + + if ($xAxis->getAxisOptionsProperty('minimum') !== null) { + $objWriter->startElement('c:min'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minimum')); + $objWriter->endElement(); + } + + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('c:delete'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $objWriter->startElement('c:axPos'); + $objWriter->writeAttribute('val', 'l'); + $objWriter->endElement(); + + $objWriter->startElement('c:majorGridlines'); + $objWriter->startElement('c:spPr'); + + if ($majorGridlines->getLineColorProperty('value') !== null) { + $objWriter->startElement('a:ln'); + $objWriter->writeAttribute('w', $majorGridlines->getLineStyleProperty('width')); + $objWriter->startElement('a:solidFill'); + $objWriter->startElement("a:{$majorGridlines->getLineColorProperty('type')}"); + $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('value')); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('alpha')); + $objWriter->endElement(); //end alpha + $objWriter->endElement(); //end srgbClr + $objWriter->endElement(); //end solidFill + + $objWriter->startElement('a:prstDash'); + $objWriter->writeAttribute('val', $majorGridlines->getLineStyleProperty('dash')); + $objWriter->endElement(); + + if ($majorGridlines->getLineStyleProperty('join') == 'miter') { + $objWriter->startElement('a:miter'); + $objWriter->writeAttribute('lim', '800000'); + $objWriter->endElement(); + } else { + $objWriter->startElement('a:bevel'); + $objWriter->endElement(); + } + + if ($majorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { + $objWriter->startElement('a:headEnd'); + $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('head', 'w')); + $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('head', 'len')); + $objWriter->endElement(); + } + + if ($majorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { + $objWriter->startElement('a:tailEnd'); + $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('end', 'w')); + $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('end', 'len')); + $objWriter->endElement(); + } + $objWriter->endElement(); //end ln + } + $objWriter->startElement('a:effectLst'); + + if ($majorGridlines->getGlowSize() !== null) { + $objWriter->startElement('a:glow'); + $objWriter->writeAttribute('rad', $majorGridlines->getGlowSize()); + $objWriter->startElement("a:{$majorGridlines->getGlowColor('type')}"); + $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('value')); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('alpha')); + $objWriter->endElement(); //end alpha + $objWriter->endElement(); //end schemeClr + $objWriter->endElement(); //end glow + } + + if ($majorGridlines->getShadowProperty('presets') !== null) { + $objWriter->startElement("a:{$majorGridlines->getShadowProperty('effect')}"); + if ($majorGridlines->getShadowProperty('blur') !== null) { + $objWriter->writeAttribute('blurRad', $majorGridlines->getShadowProperty('blur')); + } + if ($majorGridlines->getShadowProperty('distance') !== null) { + $objWriter->writeAttribute('dist', $majorGridlines->getShadowProperty('distance')); + } + if ($majorGridlines->getShadowProperty('direction') !== null) { + $objWriter->writeAttribute('dir', $majorGridlines->getShadowProperty('direction')); + } + if ($majorGridlines->getShadowProperty('algn') !== null) { + $objWriter->writeAttribute('algn', $majorGridlines->getShadowProperty('algn')); + } + if ($majorGridlines->getShadowProperty(['size', 'sx']) !== null) { + $objWriter->writeAttribute('sx', $majorGridlines->getShadowProperty(['size', 'sx'])); + } + if ($majorGridlines->getShadowProperty(['size', 'sy']) !== null) { + $objWriter->writeAttribute('sy', $majorGridlines->getShadowProperty(['size', 'sy'])); + } + if ($majorGridlines->getShadowProperty(['size', 'kx']) !== null) { + $objWriter->writeAttribute('kx', $majorGridlines->getShadowProperty(['size', 'kx'])); + } + if ($majorGridlines->getShadowProperty('rotWithShape') !== null) { + $objWriter->writeAttribute('rotWithShape', $majorGridlines->getShadowProperty('rotWithShape')); + } + $objWriter->startElement("a:{$majorGridlines->getShadowProperty(['color', 'type'])}"); + $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'value'])); + + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'alpha'])); + $objWriter->endElement(); //end alpha + + $objWriter->endElement(); //end color:type + $objWriter->endElement(); //end shadow + } + + if ($majorGridlines->getSoftEdgesSize() !== null) { + $objWriter->startElement('a:softEdge'); + $objWriter->writeAttribute('rad', $majorGridlines->getSoftEdgesSize()); + $objWriter->endElement(); //end softEdge + } + + $objWriter->endElement(); //end effectLst + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end majorGridLines + + if ($minorGridlines->getObjectState()) { + $objWriter->startElement('c:minorGridlines'); + $objWriter->startElement('c:spPr'); + + if ($minorGridlines->getLineColorProperty('value') !== null) { + $objWriter->startElement('a:ln'); + $objWriter->writeAttribute('w', $minorGridlines->getLineStyleProperty('width')); + $objWriter->startElement('a:solidFill'); + $objWriter->startElement("a:{$minorGridlines->getLineColorProperty('type')}"); + $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('value')); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('alpha')); + $objWriter->endElement(); //end alpha + $objWriter->endElement(); //end srgbClr + $objWriter->endElement(); //end solidFill + + $objWriter->startElement('a:prstDash'); + $objWriter->writeAttribute('val', $minorGridlines->getLineStyleProperty('dash')); + $objWriter->endElement(); + + if ($minorGridlines->getLineStyleProperty('join') == 'miter') { + $objWriter->startElement('a:miter'); + $objWriter->writeAttribute('lim', '800000'); + $objWriter->endElement(); + } else { + $objWriter->startElement('a:bevel'); + $objWriter->endElement(); + } + + if ($minorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { + $objWriter->startElement('a:headEnd'); + $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('head', 'w')); + $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('head', 'len')); + $objWriter->endElement(); + } + + if ($minorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { + $objWriter->startElement('a:tailEnd'); + $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('end', 'w')); + $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('end', 'len')); + $objWriter->endElement(); + } + $objWriter->endElement(); //end ln + } + + $objWriter->startElement('a:effectLst'); + + if ($minorGridlines->getGlowSize() !== null) { + $objWriter->startElement('a:glow'); + $objWriter->writeAttribute('rad', $minorGridlines->getGlowSize()); + $objWriter->startElement("a:{$minorGridlines->getGlowColor('type')}"); + $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('value')); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('alpha')); + $objWriter->endElement(); //end alpha + $objWriter->endElement(); //end schemeClr + $objWriter->endElement(); //end glow + } + + if ($minorGridlines->getShadowProperty('presets') !== null) { + $objWriter->startElement("a:{$minorGridlines->getShadowProperty('effect')}"); + if ($minorGridlines->getShadowProperty('blur') !== null) { + $objWriter->writeAttribute('blurRad', $minorGridlines->getShadowProperty('blur')); + } + if ($minorGridlines->getShadowProperty('distance') !== null) { + $objWriter->writeAttribute('dist', $minorGridlines->getShadowProperty('distance')); + } + if ($minorGridlines->getShadowProperty('direction') !== null) { + $objWriter->writeAttribute('dir', $minorGridlines->getShadowProperty('direction')); + } + if ($minorGridlines->getShadowProperty('algn') !== null) { + $objWriter->writeAttribute('algn', $minorGridlines->getShadowProperty('algn')); + } + if ($minorGridlines->getShadowProperty(['size', 'sx']) !== null) { + $objWriter->writeAttribute('sx', $minorGridlines->getShadowProperty(['size', 'sx'])); + } + if ($minorGridlines->getShadowProperty(['size', 'sy']) !== null) { + $objWriter->writeAttribute('sy', $minorGridlines->getShadowProperty(['size', 'sy'])); + } + if ($minorGridlines->getShadowProperty(['size', 'kx']) !== null) { + $objWriter->writeAttribute('kx', $minorGridlines->getShadowProperty(['size', 'kx'])); + } + if ($minorGridlines->getShadowProperty('rotWithShape') !== null) { + $objWriter->writeAttribute('rotWithShape', $minorGridlines->getShadowProperty('rotWithShape')); + } + $objWriter->startElement("a:{$minorGridlines->getShadowProperty(['color', 'type'])}"); + $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'value'])); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'alpha'])); + $objWriter->endElement(); //end alpha + $objWriter->endElement(); //end color:type + $objWriter->endElement(); //end shadow + } + + if ($minorGridlines->getSoftEdgesSize() !== null) { + $objWriter->startElement('a:softEdge'); + $objWriter->writeAttribute('rad', $minorGridlines->getSoftEdgesSize()); + $objWriter->endElement(); //end softEdge + } + + $objWriter->endElement(); //end effectLst + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end minorGridLines + } + + if ($yAxisLabel !== null) { + $objWriter->startElement('c:title'); + $objWriter->startElement('c:tx'); + $objWriter->startElement('c:rich'); + + $objWriter->startElement('a:bodyPr'); + $objWriter->endElement(); + + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); + + $objWriter->startElement('a:p'); + $objWriter->startElement('a:r'); + + $caption = $yAxisLabel->getCaption(); + if (is_array($caption)) { + $caption = $caption[0]; + } + + $objWriter->startElement('a:t'); + $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); + $objWriter->endElement(); + + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + if ($groupType !== DataSeries::TYPE_BUBBLECHART) { + $layout = $yAxisLabel->getLayout(); + $this->writeLayout($objWriter, $layout); + } + + $objWriter->startElement('c:overlay'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', $xAxis->getAxisNumberFormat()); + $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked()); + $objWriter->endElement(); + + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + + $objWriter->startElement('c:spPr'); + + if ($xAxis->getFillProperty('value') !== null) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:' . $xAxis->getFillProperty('type')); + $objWriter->writeAttribute('val', $xAxis->getFillProperty('value')); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $xAxis->getFillProperty('alpha')); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + } + + $objWriter->startElement('a:ln'); + + $objWriter->writeAttribute('w', $xAxis->getLineStyleProperty('width')); + $objWriter->writeAttribute('cap', $xAxis->getLineStyleProperty('cap')); + $objWriter->writeAttribute('cmpd', $xAxis->getLineStyleProperty('compound')); + + if ($xAxis->getLineProperty('value') !== null) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:' . $xAxis->getLineProperty('type')); + $objWriter->writeAttribute('val', $xAxis->getLineProperty('value')); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $xAxis->getLineProperty('alpha')); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + } + + $objWriter->startElement('a:prstDash'); + $objWriter->writeAttribute('val', $xAxis->getLineStyleProperty('dash')); + $objWriter->endElement(); + + if ($xAxis->getLineStyleProperty('join') == 'miter') { + $objWriter->startElement('a:miter'); + $objWriter->writeAttribute('lim', '800000'); + $objWriter->endElement(); + } else { + $objWriter->startElement('a:bevel'); + $objWriter->endElement(); + } + + if ($xAxis->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { + $objWriter->startElement('a:headEnd'); + $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'head', 'type'])); + $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('head')); + $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('head')); + $objWriter->endElement(); + } + + if ($xAxis->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { + $objWriter->startElement('a:tailEnd'); + $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'end', 'type'])); + $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('end')); + $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('end')); + $objWriter->endElement(); + } + + $objWriter->endElement(); + + $objWriter->startElement('a:effectLst'); + + if ($xAxis->getGlowProperty('size') !== null) { + $objWriter->startElement('a:glow'); + $objWriter->writeAttribute('rad', $xAxis->getGlowProperty('size')); + $objWriter->startElement("a:{$xAxis->getGlowProperty(['color', 'type'])}"); + $objWriter->writeAttribute('val', $xAxis->getGlowProperty(['color', 'value'])); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $xAxis->getGlowProperty(['color', 'alpha'])); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + } + + if ($xAxis->getShadowProperty('presets') !== null) { + $objWriter->startElement("a:{$xAxis->getShadowProperty('effect')}"); + + if ($xAxis->getShadowProperty('blur') !== null) { + $objWriter->writeAttribute('blurRad', $xAxis->getShadowProperty('blur')); + } + if ($xAxis->getShadowProperty('distance') !== null) { + $objWriter->writeAttribute('dist', $xAxis->getShadowProperty('distance')); + } + if ($xAxis->getShadowProperty('direction') !== null) { + $objWriter->writeAttribute('dir', $xAxis->getShadowProperty('direction')); + } + if ($xAxis->getShadowProperty('algn') !== null) { + $objWriter->writeAttribute('algn', $xAxis->getShadowProperty('algn')); + } + if ($xAxis->getShadowProperty(['size', 'sx']) !== null) { + $objWriter->writeAttribute('sx', $xAxis->getShadowProperty(['size', 'sx'])); + } + if ($xAxis->getShadowProperty(['size', 'sy']) !== null) { + $objWriter->writeAttribute('sy', $xAxis->getShadowProperty(['size', 'sy'])); + } + if ($xAxis->getShadowProperty(['size', 'kx']) !== null) { + $objWriter->writeAttribute('kx', $xAxis->getShadowProperty(['size', 'kx'])); + } + if ($xAxis->getShadowProperty('rotWithShape') !== null) { + $objWriter->writeAttribute('rotWithShape', $xAxis->getShadowProperty('rotWithShape')); + } + + $objWriter->startElement("a:{$xAxis->getShadowProperty(['color', 'type'])}"); + $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'value'])); + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'alpha'])); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + if ($xAxis->getSoftEdgesSize() !== null) { + $objWriter->startElement('a:softEdge'); + $objWriter->writeAttribute('rad', $xAxis->getSoftEdgesSize()); + $objWriter->endElement(); + } + + $objWriter->endElement(); //effectList + $objWriter->endElement(); //end spPr + + if ($id1 > 0) { + $objWriter->startElement('c:crossAx'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); + + if ($xAxis->getAxisOptionsProperty('horizontal_crosses_value') !== null) { + $objWriter->startElement('c:crossesAt'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value')); + $objWriter->endElement(); + } else { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses')); + $objWriter->endElement(); + } + + $objWriter->startElement('c:crossBetween'); + $objWriter->writeAttribute('val', 'midCat'); + $objWriter->endElement(); + + if ($xAxis->getAxisOptionsProperty('major_unit') !== null) { + $objWriter->startElement('c:majorUnit'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_unit')); + $objWriter->endElement(); + } + + if ($xAxis->getAxisOptionsProperty('minor_unit') !== null) { + $objWriter->startElement('c:minorUnit'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_unit')); + $objWriter->endElement(); + } + } + + if ($isMultiLevelSeries) { + if ($groupType !== DataSeries::TYPE_BUBBLECHART) { + $objWriter->startElement('c:noMultiLvlLbl'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + } + } + + $objWriter->endElement(); + } + + /** + * Get the data series type(s) for a chart plot series. + * + * @param PlotArea $plotArea + * + * @throws WriterException + * + * @return array|string + */ + private static function getChartType($plotArea) + { + $groupCount = $plotArea->getPlotGroupCount(); + + if ($groupCount == 1) { + $chartType = [$plotArea->getPlotGroupByIndex(0)->getPlotType()]; + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $plotArea->getPlotGroupByIndex($i)->getPlotType(); + } + $chartType = array_unique($chartTypes); + if (count($chartTypes) == 0) { + throw new WriterException('Chart is not yet implemented'); + } + } + + return $chartType; + } + + /** + * Method writing plot series values. + * + * @param XMLWriter $objWriter XML Writer + * @param int $val value for idx (default: 3) + * @param string $fillColor hex color (default: FF9900) + * + * @return XMLWriter XML Writer + */ + private function writePlotSeriesValuesElement($objWriter, $val = 3, $fillColor = 'FF9900') + { + $objWriter->startElement('c:dPt'); + $objWriter->startElement('c:idx'); + $objWriter->writeAttribute('val', $val); + $objWriter->endElement(); + + $objWriter->startElement('c:bubble3D'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $fillColor); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + return $objWriter; + } + + /** + * Write Plot Group (series of related plots). + * + * @param DataSeries $plotGroup + * @param string $groupType Type of plot for dataseries + * @param XMLWriter $objWriter XML Writer + * @param bool &$catIsMultiLevelSeries Is category a multi-series category + * @param bool &$valIsMultiLevelSeries Is value set a multi-series set + * @param string &$plotGroupingType Type of grouping for multi-series values + * + * @throws WriterException + */ + private function writePlotGroup($plotGroup, $groupType, $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType) + { + if ($plotGroup === null) { + return; + } + + if (($groupType == DataSeries::TYPE_BARCHART) || ($groupType == DataSeries::TYPE_BARCHART_3D)) { + $objWriter->startElement('c:barDir'); + $objWriter->writeAttribute('val', $plotGroup->getPlotDirection()); + $objWriter->endElement(); + } + + if ($plotGroup->getPlotGrouping() !== null) { + $plotGroupingType = $plotGroup->getPlotGrouping(); + $objWriter->startElement('c:grouping'); + $objWriter->writeAttribute('val', $plotGroupingType); + $objWriter->endElement(); + } + + // Get these details before the loop, because we can use the count to check for varyColors + $plotSeriesOrder = $plotGroup->getPlotOrder(); + $plotSeriesCount = count($plotSeriesOrder); + + if (($groupType !== DataSeries::TYPE_RADARCHART) && ($groupType !== DataSeries::TYPE_STOCKCHART)) { + if ($groupType !== DataSeries::TYPE_LINECHART) { + if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART) || ($plotSeriesCount > 1)) { + $objWriter->startElement('c:varyColors'); + $objWriter->writeAttribute('val', 1); + $objWriter->endElement(); + } else { + $objWriter->startElement('c:varyColors'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + } + } + } + + foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) { + $objWriter->startElement('c:ser'); + + $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); + if ($plotLabel) { + $fillColor = $plotLabel->getFillColor(); + if ($fillColor !== null && !is_array($fillColor)) { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $fillColor); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + + $objWriter->startElement('c:idx'); + $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesIdx); + $objWriter->endElement(); + + $objWriter->startElement('c:order'); + $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesRef); + $objWriter->endElement(); + + // Values + $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesRef); + + if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { + $fillColorValues = $plotSeriesValues->getFillColor(); + if ($fillColorValues !== null && is_array($fillColorValues)) { + foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { + $this->writePlotSeriesValuesElement($objWriter, $dataKey, (isset($fillColorValues[$dataKey]) ? $fillColorValues[$dataKey] : 'FF9900')); + } + } else { + $this->writePlotSeriesValuesElement($objWriter); + } + } + + // Labels + $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesRef); + if ($plotSeriesLabel && ($plotSeriesLabel->getPointCount() > 0)) { + $objWriter->startElement('c:tx'); + $objWriter->startElement('c:strRef'); + $this->writePlotSeriesLabel($plotSeriesLabel, $objWriter); + $objWriter->endElement(); + $objWriter->endElement(); + } + + // Formatting for the points + if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART)) { + $plotLineWidth = 12700; + if ($plotSeriesValues) { + $plotLineWidth = $plotSeriesValues->getLineWidth(); + } + + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:ln'); + $objWriter->writeAttribute('w', $plotLineWidth); + if ($groupType == DataSeries::TYPE_STOCKCHART) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); + } + $objWriter->endElement(); + $objWriter->endElement(); + } + + if ($plotSeriesValues) { + $plotSeriesMarker = $plotSeriesValues->getPointMarker(); + if ($plotSeriesMarker) { + $objWriter->startElement('c:marker'); + $objWriter->startElement('c:symbol'); + $objWriter->writeAttribute('val', $plotSeriesMarker); + $objWriter->endElement(); + + if ($plotSeriesMarker !== 'none') { + $objWriter->startElement('c:size'); + $objWriter->writeAttribute('val', 3); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + } + + if (($groupType === DataSeries::TYPE_BARCHART) || ($groupType === DataSeries::TYPE_BARCHART_3D) || ($groupType === DataSeries::TYPE_BUBBLECHART)) { + $objWriter->startElement('c:invertIfNegative'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + } + + // Category Labels + $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesRef); + if ($plotSeriesCategory && ($plotSeriesCategory->getPointCount() > 0)) { + $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries(); + + if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { + if ($plotGroup->getPlotStyle() !== null) { + $plotStyle = $plotGroup->getPlotStyle(); + if ($plotStyle) { + $objWriter->startElement('c:explosion'); + $objWriter->writeAttribute('val', 25); + $objWriter->endElement(); + } + } + } + + if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) { + $objWriter->startElement('c:xVal'); + } else { + $objWriter->startElement('c:cat'); + } + + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + $objWriter->endElement(); + } + + // Values + if ($plotSeriesValues) { + $valIsMultiLevelSeries = $valIsMultiLevelSeries || $plotSeriesValues->isMultiLevelSeries(); + + if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) { + $objWriter->startElement('c:yVal'); + } else { + $objWriter->startElement('c:val'); + } + + $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num'); + $objWriter->endElement(); + } + + if ($groupType === DataSeries::TYPE_BUBBLECHART) { + $this->writeBubbles($plotSeriesValues, $objWriter); + } + + $objWriter->endElement(); + } + + $this->seriesIndex += $plotSeriesIdx + 1; + } + + /** + * Write Plot Series Label. + * + * @param DataSeriesValues $plotSeriesLabel + * @param XMLWriter $objWriter XML Writer + */ + private function writePlotSeriesLabel($plotSeriesLabel, $objWriter) + { + if ($plotSeriesLabel === null) { + return; + } + + $objWriter->startElement('c:f'); + $objWriter->writeRawData($plotSeriesLabel->getDataSource()); + $objWriter->endElement(); + + $objWriter->startElement('c:strCache'); + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', $plotSeriesLabel->getPointCount()); + $objWriter->endElement(); + + foreach ($plotSeriesLabel->getDataValues() as $plotLabelKey => $plotLabelValue) { + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', $plotLabelKey); + + $objWriter->startElement('c:v'); + $objWriter->writeRawData($plotLabelValue); + $objWriter->endElement(); + $objWriter->endElement(); + } + $objWriter->endElement(); + } + + /** + * Write Plot Series Values. + * + * @param DataSeriesValues $plotSeriesValues + * @param XMLWriter $objWriter XML Writer + * @param string $groupType Type of plot for dataseries + * @param string $dataType Datatype of series values + */ + private function writePlotSeriesValues($plotSeriesValues, XMLWriter $objWriter, $groupType, $dataType = 'str') + { + if ($plotSeriesValues === null) { + return; + } + + if ($plotSeriesValues->isMultiLevelSeries()) { + $levelCount = $plotSeriesValues->multiLevelCount(); + + $objWriter->startElement('c:multiLvlStrRef'); + + $objWriter->startElement('c:f'); + $objWriter->writeRawData($plotSeriesValues->getDataSource()); + $objWriter->endElement(); + + $objWriter->startElement('c:multiLvlStrCache'); + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->endElement(); + + for ($level = 0; $level < $levelCount; ++$level) { + $objWriter->startElement('c:lvl'); + + foreach ($plotSeriesValues->getDataValues() as $plotSeriesKey => $plotSeriesValue) { + if (isset($plotSeriesValue[$level])) { + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', $plotSeriesKey); + + $objWriter->startElement('c:v'); + $objWriter->writeRawData($plotSeriesValue[$level]); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + + $objWriter->endElement(); + } + + $objWriter->endElement(); + + $objWriter->endElement(); + } else { + $objWriter->startElement('c:' . $dataType . 'Ref'); + + $objWriter->startElement('c:f'); + $objWriter->writeRawData($plotSeriesValues->getDataSource()); + $objWriter->endElement(); + + $objWriter->startElement('c:' . $dataType . 'Cache'); + + if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) { + if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) { + $objWriter->startElement('c:formatCode'); + $objWriter->writeRawData($plotSeriesValues->getFormatCode()); + $objWriter->endElement(); + } + } + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->endElement(); + + $dataValues = $plotSeriesValues->getDataValues(); + if (!empty($dataValues)) { + if (is_array($dataValues)) { + foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', $plotSeriesKey); + + $objWriter->startElement('c:v'); + $objWriter->writeRawData($plotSeriesValue); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + } + + $objWriter->endElement(); + + $objWriter->endElement(); + } + } + + /** + * Write Bubble Chart Details. + * + * @param DataSeriesValues $plotSeriesValues + * @param XMLWriter $objWriter XML Writer + */ + private function writeBubbles($plotSeriesValues, $objWriter) + { + if ($plotSeriesValues === null) { + return; + } + + $objWriter->startElement('c:bubbleSize'); + $objWriter->startElement('c:numLit'); + + $objWriter->startElement('c:formatCode'); + $objWriter->writeRawData('General'); + $objWriter->endElement(); + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->endElement(); + + $dataValues = $plotSeriesValues->getDataValues(); + if (!empty($dataValues)) { + if (is_array($dataValues)) { + foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', $plotSeriesKey); + $objWriter->startElement('c:v'); + $objWriter->writeRawData(1); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + } + + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('c:bubble3D'); + $objWriter->writeAttribute('val', 0); + $objWriter->endElement(); + } + + /** + * Write Layout. + * + * @param XMLWriter $objWriter XML Writer + * @param Layout $layout + */ + private function writeLayout(XMLWriter $objWriter, Layout $layout = null) + { + $objWriter->startElement('c:layout'); + + if ($layout !== null) { + $objWriter->startElement('c:manualLayout'); + + $layoutTarget = $layout->getLayoutTarget(); + if ($layoutTarget !== null) { + $objWriter->startElement('c:layoutTarget'); + $objWriter->writeAttribute('val', $layoutTarget); + $objWriter->endElement(); + } + + $xMode = $layout->getXMode(); + if ($xMode !== null) { + $objWriter->startElement('c:xMode'); + $objWriter->writeAttribute('val', $xMode); + $objWriter->endElement(); + } + + $yMode = $layout->getYMode(); + if ($yMode !== null) { + $objWriter->startElement('c:yMode'); + $objWriter->writeAttribute('val', $yMode); + $objWriter->endElement(); + } + + $x = $layout->getXPosition(); + if ($x !== null) { + $objWriter->startElement('c:x'); + $objWriter->writeAttribute('val', $x); + $objWriter->endElement(); + } + + $y = $layout->getYPosition(); + if ($y !== null) { + $objWriter->startElement('c:y'); + $objWriter->writeAttribute('val', $y); + $objWriter->endElement(); + } + + $w = $layout->getWidth(); + if ($w !== null) { + $objWriter->startElement('c:w'); + $objWriter->writeAttribute('val', $w); + $objWriter->endElement(); + } + + $h = $layout->getHeight(); + if ($h !== null) { + $objWriter->startElement('c:h'); + $objWriter->writeAttribute('val', $h); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + + /** + * Write Alternate Content block. + * + * @param XMLWriter $objWriter XML Writer + */ + private function writeAlternateContent($objWriter) + { + $objWriter->startElement('mc:AlternateContent'); + $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); + + $objWriter->startElement('mc:Choice'); + $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'); + $objWriter->writeAttribute('Requires', 'c14'); + + $objWriter->startElement('c14:style'); + $objWriter->writeAttribute('val', '102'); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('mc:Fallback'); + $objWriter->startElement('c:style'); + $objWriter->writeAttribute('val', '2'); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write Printer Settings. + * + * @param XMLWriter $objWriter XML Writer + */ + private function writePrintSettings($objWriter) + { + $objWriter->startElement('c:printSettings'); + + $objWriter->startElement('c:headerFooter'); + $objWriter->endElement(); + + $objWriter->startElement('c:pageMargins'); + $objWriter->writeAttribute('footer', 0.3); + $objWriter->writeAttribute('header', 0.3); + $objWriter->writeAttribute('r', 0.7); + $objWriter->writeAttribute('l', 0.7); + $objWriter->writeAttribute('t', 0.75); + $objWriter->writeAttribute('b', 0.75); + $objWriter->endElement(); + + $objWriter->startElement('c:pageSetup'); + $objWriter->writeAttribute('orientation', 'portrait'); + $objWriter->endElement(); + + $objWriter->endElement(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Comments.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Comments.php new file mode 100644 index 00000000000..8b08f31f3f8 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Comments.php @@ -0,0 +1,242 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Comments cache + $comments = $pWorksheet->getComments(); + + // Authors cache + $authors = []; + $authorId = 0; + foreach ($comments as $comment) { + if (!isset($authors[$comment->getAuthor()])) { + $authors[$comment->getAuthor()] = $authorId++; + } + } + + // comments + $objWriter->startElement('comments'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + + // Loop through authors + $objWriter->startElement('authors'); + foreach ($authors as $author => $index) { + $objWriter->writeElement('author', $author); + } + $objWriter->endElement(); + + // Loop through comments + $objWriter->startElement('commentList'); + foreach ($comments as $key => $value) { + $this->writeComment($objWriter, $key, $value, $authors); + } + $objWriter->endElement(); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write comment to XML format. + * + * @param XMLWriter $objWriter XML Writer + * @param string $pCellReference Cell reference + * @param Comment $pComment Comment + * @param array $pAuthors Array of authors + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + */ + private function writeComment(XMLWriter $objWriter, $pCellReference, Comment $pComment, array $pAuthors) + { + // comment + $objWriter->startElement('comment'); + $objWriter->writeAttribute('ref', $pCellReference); + $objWriter->writeAttribute('authorId', $pAuthors[$pComment->getAuthor()]); + + // text + $objWriter->startElement('text'); + $this->getParentWriter()->getWriterPart('stringtable')->writeRichText($objWriter, $pComment->getText()); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write VML comments to XML format. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @return string XML Output + */ + public function writeVMLComments(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Comments cache + $comments = $pWorksheet->getComments(); + + // xml + $objWriter->startElement('xml'); + $objWriter->writeAttribute('xmlns:v', 'urn:schemas-microsoft-com:vml'); + $objWriter->writeAttribute('xmlns:o', 'urn:schemas-microsoft-com:office:office'); + $objWriter->writeAttribute('xmlns:x', 'urn:schemas-microsoft-com:office:excel'); + + // o:shapelayout + $objWriter->startElement('o:shapelayout'); + $objWriter->writeAttribute('v:ext', 'edit'); + + // o:idmap + $objWriter->startElement('o:idmap'); + $objWriter->writeAttribute('v:ext', 'edit'); + $objWriter->writeAttribute('data', '1'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // v:shapetype + $objWriter->startElement('v:shapetype'); + $objWriter->writeAttribute('id', '_x0000_t202'); + $objWriter->writeAttribute('coordsize', '21600,21600'); + $objWriter->writeAttribute('o:spt', '202'); + $objWriter->writeAttribute('path', 'm,l,21600r21600,l21600,xe'); + + // v:stroke + $objWriter->startElement('v:stroke'); + $objWriter->writeAttribute('joinstyle', 'miter'); + $objWriter->endElement(); + + // v:path + $objWriter->startElement('v:path'); + $objWriter->writeAttribute('gradientshapeok', 't'); + $objWriter->writeAttribute('o:connecttype', 'rect'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // Loop through comments + foreach ($comments as $key => $value) { + $this->writeVMLComment($objWriter, $key, $value); + } + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write VML comment to XML format. + * + * @param XMLWriter $objWriter XML Writer + * @param string $pCellReference Cell reference, eg: 'A1' + * @param Comment $pComment Comment + */ + private function writeVMLComment(XMLWriter $objWriter, $pCellReference, Comment $pComment) + { + // Metadata + list($column, $row) = Coordinate::coordinateFromString($pCellReference); + $column = Coordinate::columnIndexFromString($column); + $id = 1024 + $column + $row; + $id = substr($id, 0, 4); + + // v:shape + $objWriter->startElement('v:shape'); + $objWriter->writeAttribute('id', '_x0000_s' . $id); + $objWriter->writeAttribute('type', '#_x0000_t202'); + $objWriter->writeAttribute('style', 'position:absolute;margin-left:' . $pComment->getMarginLeft() . ';margin-top:' . $pComment->getMarginTop() . ';width:' . $pComment->getWidth() . ';height:' . $pComment->getHeight() . ';z-index:1;visibility:' . ($pComment->getVisible() ? 'visible' : 'hidden')); + $objWriter->writeAttribute('fillcolor', '#' . $pComment->getFillColor()->getRGB()); + $objWriter->writeAttribute('o:insetmode', 'auto'); + + // v:fill + $objWriter->startElement('v:fill'); + $objWriter->writeAttribute('color2', '#' . $pComment->getFillColor()->getRGB()); + $objWriter->endElement(); + + // v:shadow + $objWriter->startElement('v:shadow'); + $objWriter->writeAttribute('on', 't'); + $objWriter->writeAttribute('color', 'black'); + $objWriter->writeAttribute('obscured', 't'); + $objWriter->endElement(); + + // v:path + $objWriter->startElement('v:path'); + $objWriter->writeAttribute('o:connecttype', 'none'); + $objWriter->endElement(); + + // v:textbox + $objWriter->startElement('v:textbox'); + $objWriter->writeAttribute('style', 'mso-direction-alt:auto'); + + // div + $objWriter->startElement('div'); + $objWriter->writeAttribute('style', 'text-align:left'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // x:ClientData + $objWriter->startElement('x:ClientData'); + $objWriter->writeAttribute('ObjectType', 'Note'); + + // x:MoveWithCells + $objWriter->writeElement('x:MoveWithCells', ''); + + // x:SizeWithCells + $objWriter->writeElement('x:SizeWithCells', ''); + + // x:AutoFill + $objWriter->writeElement('x:AutoFill', 'False'); + + // x:Row + $objWriter->writeElement('x:Row', ($row - 1)); + + // x:Column + $objWriter->writeElement('x:Column', ($column - 1)); + + $objWriter->endElement(); + + $objWriter->endElement(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php new file mode 100644 index 00000000000..6b22d7134f9 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -0,0 +1,249 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Types + $objWriter->startElement('Types'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/content-types'); + + // Theme + $this->writeOverrideContentType($objWriter, '/xl/theme/theme1.xml', 'application/vnd.openxmlformats-officedocument.theme+xml'); + + // Styles + $this->writeOverrideContentType($objWriter, '/xl/styles.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'); + + // Rels + $this->writeDefaultContentType($objWriter, 'rels', 'application/vnd.openxmlformats-package.relationships+xml'); + + // XML + $this->writeDefaultContentType($objWriter, 'xml', 'application/xml'); + + // VML + $this->writeDefaultContentType($objWriter, 'vml', 'application/vnd.openxmlformats-officedocument.vmlDrawing'); + + // Workbook + if ($spreadsheet->hasMacros()) { //Macros in workbook ? + // Yes : not standard content but "macroEnabled" + $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.ms-excel.sheet.macroEnabled.main+xml'); + //... and define a new type for the VBA project + // Better use Override, because we can use 'bin' also for xl\printerSettings\printerSettings1.bin + $this->writeOverrideContentType($objWriter, '/xl/vbaProject.bin', 'application/vnd.ms-office.vbaProject'); + if ($spreadsheet->hasMacrosCertificate()) { + // signed macros ? + // Yes : add needed information + $this->writeOverrideContentType($objWriter, '/xl/vbaProjectSignature.bin', 'application/vnd.ms-office.vbaProjectSignature'); + } + } else { + // no macros in workbook, so standard type + $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'); + } + + // DocProps + $this->writeOverrideContentType($objWriter, '/docProps/app.xml', 'application/vnd.openxmlformats-officedocument.extended-properties+xml'); + + $this->writeOverrideContentType($objWriter, '/docProps/core.xml', 'application/vnd.openxmlformats-package.core-properties+xml'); + + $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); + if (!empty($customPropertyList)) { + $this->writeOverrideContentType($objWriter, '/docProps/custom.xml', 'application/vnd.openxmlformats-officedocument.custom-properties+xml'); + } + + // Worksheets + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $this->writeOverrideContentType($objWriter, '/xl/worksheets/sheet' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'); + } + + // Shared strings + $this->writeOverrideContentType($objWriter, '/xl/sharedStrings.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'); + + // Add worksheet relationship content types + $unparsedLoadedData = $spreadsheet->getUnparsedLoadedData(); + $chart = 1; + for ($i = 0; $i < $sheetCount; ++$i) { + $drawings = $spreadsheet->getSheet($i)->getDrawingCollection(); + $drawingCount = count($drawings); + $chartCount = ($includeCharts) ? $spreadsheet->getSheet($i)->getChartCount() : 0; + $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$spreadsheet->getSheet($i)->getCodeName()]['drawingOriginalIds']); + + // We need a drawing relationship for the worksheet if we have either drawings or charts + if (($drawingCount > 0) || ($chartCount > 0) || $hasUnparsedDrawing) { + $this->writeOverrideContentType($objWriter, '/xl/drawings/drawing' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.drawing+xml'); + } + + // If we have charts, then we need a chart relationship for every individual chart + if ($chartCount > 0) { + for ($c = 0; $c < $chartCount; ++$c) { + $this->writeOverrideContentType($objWriter, '/xl/charts/chart' . $chart++ . '.xml', 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'); + } + } + } + + // Comments + for ($i = 0; $i < $sheetCount; ++$i) { + if (count($spreadsheet->getSheet($i)->getComments()) > 0) { + $this->writeOverrideContentType($objWriter, '/xl/comments' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml'); + } + } + + // Add media content-types + $aMediaContentTypes = []; + $mediaCount = $this->getParentWriter()->getDrawingHashTable()->count(); + for ($i = 0; $i < $mediaCount; ++$i) { + $extension = ''; + $mimeType = ''; + + if ($this->getParentWriter()->getDrawingHashTable()->getByIndex($i) instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing) { + $extension = strtolower($this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getExtension()); + $mimeType = $this->getImageMimeType($this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getPath()); + } elseif ($this->getParentWriter()->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) { + $extension = strtolower($this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getMimeType()); + $extension = explode('/', $extension); + $extension = $extension[1]; + + $mimeType = $this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getMimeType(); + } + + if (!isset($aMediaContentTypes[$extension])) { + $aMediaContentTypes[$extension] = $mimeType; + + $this->writeDefaultContentType($objWriter, $extension, $mimeType); + } + } + if ($spreadsheet->hasRibbonBinObjects()) { + // Some additional objects in the ribbon ? + // we need to write "Extension" but not already write for media content + $tabRibbonTypes = array_diff($spreadsheet->getRibbonBinObjects('types'), array_keys($aMediaContentTypes)); + foreach ($tabRibbonTypes as $aRibbonType) { + $mimeType = 'image/.' . $aRibbonType; //we wrote $mimeType like customUI Editor + $this->writeDefaultContentType($objWriter, $aRibbonType, $mimeType); + } + } + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + if (count($spreadsheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) { + foreach ($spreadsheet->getSheet($i)->getHeaderFooter()->getImages() as $image) { + if (!isset($aMediaContentTypes[strtolower($image->getExtension())])) { + $aMediaContentTypes[strtolower($image->getExtension())] = $this->getImageMimeType($image->getPath()); + + $this->writeDefaultContentType($objWriter, strtolower($image->getExtension()), $aMediaContentTypes[strtolower($image->getExtension())]); + } + } + } + } + + // unparsed defaults + if (isset($unparsedLoadedData['default_content_types'])) { + foreach ($unparsedLoadedData['default_content_types'] as $extName => $contentType) { + $this->writeDefaultContentType($objWriter, $extName, $contentType); + } + } + + // unparsed overrides + if (isset($unparsedLoadedData['override_content_types'])) { + foreach ($unparsedLoadedData['override_content_types'] as $partName => $overrideType) { + $this->writeOverrideContentType($objWriter, $partName, $overrideType); + } + } + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Get image mime type. + * + * @param string $pFile Filename + * + * @throws WriterException + * + * @return string Mime Type + */ + private function getImageMimeType($pFile) + { + if (File::fileExists($pFile)) { + $image = getimagesize($pFile); + + return image_type_to_mime_type($image[2]); + } + + throw new WriterException("File $pFile does not exist"); + } + + /** + * Write Default content type. + * + * @param XMLWriter $objWriter XML Writer + * @param string $pPartname Part name + * @param string $pContentType Content type + * + * @throws WriterException + */ + private function writeDefaultContentType(XMLWriter $objWriter, $pPartname, $pContentType) + { + if ($pPartname != '' && $pContentType != '') { + // Write content type + $objWriter->startElement('Default'); + $objWriter->writeAttribute('Extension', $pPartname); + $objWriter->writeAttribute('ContentType', $pContentType); + $objWriter->endElement(); + } else { + throw new WriterException('Invalid parameters passed.'); + } + } + + /** + * Write Override content type. + * + * @param XMLWriter $objWriter XML Writer + * @param string $pPartname Part name + * @param string $pContentType Content type + * + * @throws WriterException + */ + private function writeOverrideContentType(XMLWriter $objWriter, $pPartname, $pContentType) + { + if ($pPartname != '' && $pContentType != '') { + // Write content type + $objWriter->startElement('Override'); + $objWriter->writeAttribute('PartName', $pPartname); + $objWriter->writeAttribute('ContentType', $pContentType); + $objWriter->endElement(); + } else { + throw new WriterException('Invalid parameters passed.'); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/DocProps.php new file mode 100644 index 00000000000..2a18d5c7e3d --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -0,0 +1,251 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Properties + $objWriter->startElement('Properties'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties'); + $objWriter->writeAttribute('xmlns:vt', 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); + + // Application + $objWriter->writeElement('Application', 'Microsoft Excel'); + + // DocSecurity + $objWriter->writeElement('DocSecurity', '0'); + + // ScaleCrop + $objWriter->writeElement('ScaleCrop', 'false'); + + // HeadingPairs + $objWriter->startElement('HeadingPairs'); + + // Vector + $objWriter->startElement('vt:vector'); + $objWriter->writeAttribute('size', '2'); + $objWriter->writeAttribute('baseType', 'variant'); + + // Variant + $objWriter->startElement('vt:variant'); + $objWriter->writeElement('vt:lpstr', 'Worksheets'); + $objWriter->endElement(); + + // Variant + $objWriter->startElement('vt:variant'); + $objWriter->writeElement('vt:i4', $spreadsheet->getSheetCount()); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // TitlesOfParts + $objWriter->startElement('TitlesOfParts'); + + // Vector + $objWriter->startElement('vt:vector'); + $objWriter->writeAttribute('size', $spreadsheet->getSheetCount()); + $objWriter->writeAttribute('baseType', 'lpstr'); + + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $objWriter->writeElement('vt:lpstr', $spreadsheet->getSheet($i)->getTitle()); + } + + $objWriter->endElement(); + + $objWriter->endElement(); + + // Company + $objWriter->writeElement('Company', $spreadsheet->getProperties()->getCompany()); + + // Company + $objWriter->writeElement('Manager', $spreadsheet->getProperties()->getManager()); + + // LinksUpToDate + $objWriter->writeElement('LinksUpToDate', 'false'); + + // SharedDoc + $objWriter->writeElement('SharedDoc', 'false'); + + // HyperlinksChanged + $objWriter->writeElement('HyperlinksChanged', 'false'); + + // AppVersion + $objWriter->writeElement('AppVersion', '12.0000'); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write docProps/core.xml to XML format. + * + * @param Spreadsheet $spreadsheet + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @return string XML Output + */ + public function writeDocPropsCore(Spreadsheet $spreadsheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // cp:coreProperties + $objWriter->startElement('cp:coreProperties'); + $objWriter->writeAttribute('xmlns:cp', 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties'); + $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + $objWriter->writeAttribute('xmlns:dcterms', 'http://purl.org/dc/terms/'); + $objWriter->writeAttribute('xmlns:dcmitype', 'http://purl.org/dc/dcmitype/'); + $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + + // dc:creator + $objWriter->writeElement('dc:creator', $spreadsheet->getProperties()->getCreator()); + + // cp:lastModifiedBy + $objWriter->writeElement('cp:lastModifiedBy', $spreadsheet->getProperties()->getLastModifiedBy()); + + // dcterms:created + $objWriter->startElement('dcterms:created'); + $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF'); + $objWriter->writeRawData(date(DATE_W3C, $spreadsheet->getProperties()->getCreated())); + $objWriter->endElement(); + + // dcterms:modified + $objWriter->startElement('dcterms:modified'); + $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF'); + $objWriter->writeRawData(date(DATE_W3C, $spreadsheet->getProperties()->getModified())); + $objWriter->endElement(); + + // dc:title + $objWriter->writeElement('dc:title', $spreadsheet->getProperties()->getTitle()); + + // dc:description + $objWriter->writeElement('dc:description', $spreadsheet->getProperties()->getDescription()); + + // dc:subject + $objWriter->writeElement('dc:subject', $spreadsheet->getProperties()->getSubject()); + + // cp:keywords + $objWriter->writeElement('cp:keywords', $spreadsheet->getProperties()->getKeywords()); + + // cp:category + $objWriter->writeElement('cp:category', $spreadsheet->getProperties()->getCategory()); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write docProps/custom.xml to XML format. + * + * @param Spreadsheet $spreadsheet + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @return string XML Output + */ + public function writeDocPropsCustom(Spreadsheet $spreadsheet) + { + $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); + if (empty($customPropertyList)) { + return; + } + + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // cp:coreProperties + $objWriter->startElement('Properties'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties'); + $objWriter->writeAttribute('xmlns:vt', 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); + + foreach ($customPropertyList as $key => $customProperty) { + $propertyValue = $spreadsheet->getProperties()->getCustomPropertyValue($customProperty); + $propertyType = $spreadsheet->getProperties()->getCustomPropertyType($customProperty); + + $objWriter->startElement('property'); + $objWriter->writeAttribute('fmtid', '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}'); + $objWriter->writeAttribute('pid', $key + 2); + $objWriter->writeAttribute('name', $customProperty); + + switch ($propertyType) { + case 'i': + $objWriter->writeElement('vt:i4', $propertyValue); + + break; + case 'f': + $objWriter->writeElement('vt:r8', $propertyValue); + + break; + case 'b': + $objWriter->writeElement('vt:bool', ($propertyValue) ? 'true' : 'false'); + + break; + case 'd': + $objWriter->startElement('vt:filetime'); + $objWriter->writeRawData(date(DATE_W3C, $propertyValue)); + $objWriter->endElement(); + + break; + default: + $objWriter->writeElement('vt:lpwstr', $propertyValue); + + break; + } + + $objWriter->endElement(); + } + + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Drawing.php new file mode 100644 index 00000000000..08256a1d5b7 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -0,0 +1,519 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // xdr:wsDr + $objWriter->startElement('xdr:wsDr'); + $objWriter->writeAttribute('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing'); + $objWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + + // Loop through images and write drawings + $i = 1; + $iterator = $pWorksheet->getDrawingCollection()->getIterator(); + while ($iterator->valid()) { + /** @var BaseDrawing $pDrawing */ + $pDrawing = $iterator->current(); + $pRelationId = $i; + $hlinkClickId = $pDrawing->getHyperlink() === null ? null : ++$i; + + $this->writeDrawing($objWriter, $pDrawing, $pRelationId, $hlinkClickId); + + $iterator->next(); + ++$i; + } + + if ($includeCharts) { + $chartCount = $pWorksheet->getChartCount(); + // Loop through charts and write the chart position + if ($chartCount > 0) { + for ($c = 0; $c < $chartCount; ++$c) { + $this->writeChart($objWriter, $pWorksheet->getChartByIndex($c), $c + $i); + } + } + } + + // unparsed AlternateContent + $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData(); + if (isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingAlternateContents'])) { + foreach ($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingAlternateContents'] as $drawingAlternateContent) { + $objWriter->writeRaw($drawingAlternateContent); + } + } + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write drawings to XML format. + * + * @param XMLWriter $objWriter XML Writer + * @param \PhpOffice\PhpSpreadsheet\Chart\Chart $pChart + * @param int $pRelationId + */ + public function writeChart(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Chart\Chart $pChart, $pRelationId = -1) + { + $tl = $pChart->getTopLeftPosition(); + $tl['colRow'] = Coordinate::coordinateFromString($tl['cell']); + $br = $pChart->getBottomRightPosition(); + $br['colRow'] = Coordinate::coordinateFromString($br['cell']); + + $objWriter->startElement('xdr:twoCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', Coordinate::columnIndexFromString($tl['colRow'][0]) - 1); + $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset'])); + $objWriter->writeElement('xdr:row', $tl['colRow'][1] - 1); + $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:to'); + $objWriter->writeElement('xdr:col', Coordinate::columnIndexFromString($br['colRow'][0]) - 1); + $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset'])); + $objWriter->writeElement('xdr:row', $br['colRow'][1] - 1); + $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset'])); + $objWriter->endElement(); + + $objWriter->startElement('xdr:graphicFrame'); + $objWriter->writeAttribute('macro', ''); + $objWriter->startElement('xdr:nvGraphicFramePr'); + $objWriter->startElement('xdr:cNvPr'); + $objWriter->writeAttribute('name', 'Chart ' . $pRelationId); + $objWriter->writeAttribute('id', 1025 * $pRelationId); + $objWriter->endElement(); + $objWriter->startElement('xdr:cNvGraphicFramePr'); + $objWriter->startElement('a:graphicFrameLocks'); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('xdr:xfrm'); + $objWriter->startElement('a:off'); + $objWriter->writeAttribute('x', '0'); + $objWriter->writeAttribute('y', '0'); + $objWriter->endElement(); + $objWriter->startElement('a:ext'); + $objWriter->writeAttribute('cx', '0'); + $objWriter->writeAttribute('cy', '0'); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('a:graphic'); + $objWriter->startElement('a:graphicData'); + $objWriter->writeAttribute('uri', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + $objWriter->startElement('c:chart'); + $objWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $objWriter->writeAttribute('r:id', 'rId' . $pRelationId); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('xdr:clientData'); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write drawings to XML format. + * + * @param XMLWriter $objWriter XML Writer + * @param BaseDrawing $pDrawing + * @param int $pRelationId + * @param null|int $hlinkClickId + * + * @throws WriterException + */ + public function writeDrawing(XMLWriter $objWriter, BaseDrawing $pDrawing, $pRelationId = -1, $hlinkClickId = null) + { + if ($pRelationId >= 0) { + // xdr:oneCellAnchor + $objWriter->startElement('xdr:oneCellAnchor'); + // Image location + $aCoordinates = Coordinate::coordinateFromString($pDrawing->getCoordinates()); + $aCoordinates[0] = Coordinate::columnIndexFromString($aCoordinates[0]); + + // xdr:from + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1); + $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getOffsetX())); + $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1); + $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getOffsetY())); + $objWriter->endElement(); + + // xdr:ext + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getWidth())); + $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getHeight())); + $objWriter->endElement(); + + // xdr:pic + $objWriter->startElement('xdr:pic'); + + // xdr:nvPicPr + $objWriter->startElement('xdr:nvPicPr'); + + // xdr:cNvPr + $objWriter->startElement('xdr:cNvPr'); + $objWriter->writeAttribute('id', $pRelationId); + $objWriter->writeAttribute('name', $pDrawing->getName()); + $objWriter->writeAttribute('descr', $pDrawing->getDescription()); + + //a:hlinkClick + $this->writeHyperLinkDrawing($objWriter, $hlinkClickId); + + $objWriter->endElement(); + + // xdr:cNvPicPr + $objWriter->startElement('xdr:cNvPicPr'); + + // a:picLocks + $objWriter->startElement('a:picLocks'); + $objWriter->writeAttribute('noChangeAspect', '1'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // xdr:blipFill + $objWriter->startElement('xdr:blipFill'); + + // a:blip + $objWriter->startElement('a:blip'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $objWriter->writeAttribute('r:embed', 'rId' . $pRelationId); + $objWriter->endElement(); + + // a:stretch + $objWriter->startElement('a:stretch'); + $objWriter->writeElement('a:fillRect', null); + $objWriter->endElement(); + + $objWriter->endElement(); + + // xdr:spPr + $objWriter->startElement('xdr:spPr'); + + // a:xfrm + $objWriter->startElement('a:xfrm'); + $objWriter->writeAttribute('rot', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($pDrawing->getRotation())); + $objWriter->endElement(); + + // a:prstGeom + $objWriter->startElement('a:prstGeom'); + $objWriter->writeAttribute('prst', 'rect'); + + // a:avLst + $objWriter->writeElement('a:avLst', null); + + $objWriter->endElement(); + + if ($pDrawing->getShadow()->getVisible()) { + // a:effectLst + $objWriter->startElement('a:effectLst'); + + // a:outerShdw + $objWriter->startElement('a:outerShdw'); + $objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getShadow()->getBlurRadius())); + $objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getShadow()->getDistance())); + $objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($pDrawing->getShadow()->getDirection())); + $objWriter->writeAttribute('algn', $pDrawing->getShadow()->getAlignment()); + $objWriter->writeAttribute('rotWithShape', '0'); + + // a:srgbClr + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $pDrawing->getShadow()->getColor()->getRGB()); + + // a:alpha + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $pDrawing->getShadow()->getAlpha() * 1000); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + } + $objWriter->endElement(); + + $objWriter->endElement(); + + // xdr:clientData + $objWriter->writeElement('xdr:clientData', null); + + $objWriter->endElement(); + } else { + throw new WriterException('Invalid parameters passed.'); + } + } + + /** + * Write VML header/footer images to XML format. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet + * + * @throws WriterException + * + * @return string XML Output + */ + public function writeVMLHeaderFooterImages(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Header/footer images + $images = $pWorksheet->getHeaderFooter()->getImages(); + + // xml + $objWriter->startElement('xml'); + $objWriter->writeAttribute('xmlns:v', 'urn:schemas-microsoft-com:vml'); + $objWriter->writeAttribute('xmlns:o', 'urn:schemas-microsoft-com:office:office'); + $objWriter->writeAttribute('xmlns:x', 'urn:schemas-microsoft-com:office:excel'); + + // o:shapelayout + $objWriter->startElement('o:shapelayout'); + $objWriter->writeAttribute('v:ext', 'edit'); + + // o:idmap + $objWriter->startElement('o:idmap'); + $objWriter->writeAttribute('v:ext', 'edit'); + $objWriter->writeAttribute('data', '1'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // v:shapetype + $objWriter->startElement('v:shapetype'); + $objWriter->writeAttribute('id', '_x0000_t75'); + $objWriter->writeAttribute('coordsize', '21600,21600'); + $objWriter->writeAttribute('o:spt', '75'); + $objWriter->writeAttribute('o:preferrelative', 't'); + $objWriter->writeAttribute('path', 'm@4@5l@4@11@9@11@9@5xe'); + $objWriter->writeAttribute('filled', 'f'); + $objWriter->writeAttribute('stroked', 'f'); + + // v:stroke + $objWriter->startElement('v:stroke'); + $objWriter->writeAttribute('joinstyle', 'miter'); + $objWriter->endElement(); + + // v:formulas + $objWriter->startElement('v:formulas'); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'if lineDrawn pixelLineWidth 0'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @0 1 0'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum 0 0 @1'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @2 1 2'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelWidth'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelHeight'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @0 0 1'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @6 1 2'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelWidth'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @8 21600 0'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelHeight'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @10 21600 0'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // v:path + $objWriter->startElement('v:path'); + $objWriter->writeAttribute('o:extrusionok', 'f'); + $objWriter->writeAttribute('gradientshapeok', 't'); + $objWriter->writeAttribute('o:connecttype', 'rect'); + $objWriter->endElement(); + + // o:lock + $objWriter->startElement('o:lock'); + $objWriter->writeAttribute('v:ext', 'edit'); + $objWriter->writeAttribute('aspectratio', 't'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // Loop through images + foreach ($images as $key => $value) { + $this->writeVMLHeaderFooterImage($objWriter, $key, $value); + } + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write VML comment to XML format. + * + * @param XMLWriter $objWriter XML Writer + * @param string $pReference Reference + * @param HeaderFooterDrawing $pImage Image + */ + private function writeVMLHeaderFooterImage(XMLWriter $objWriter, $pReference, HeaderFooterDrawing $pImage) + { + // Calculate object id + preg_match('{(\d+)}', md5($pReference), $m); + $id = 1500 + (substr($m[1], 0, 2) * 1); + + // Calculate offset + $width = $pImage->getWidth(); + $height = $pImage->getHeight(); + $marginLeft = $pImage->getOffsetX(); + $marginTop = $pImage->getOffsetY(); + + // v:shape + $objWriter->startElement('v:shape'); + $objWriter->writeAttribute('id', $pReference); + $objWriter->writeAttribute('o:spid', '_x0000_s' . $id); + $objWriter->writeAttribute('type', '#_x0000_t75'); + $objWriter->writeAttribute('style', "position:absolute;margin-left:{$marginLeft}px;margin-top:{$marginTop}px;width:{$width}px;height:{$height}px;z-index:1"); + + // v:imagedata + $objWriter->startElement('v:imagedata'); + $objWriter->writeAttribute('o:relid', 'rId' . $pReference); + $objWriter->writeAttribute('o:title', $pImage->getName()); + $objWriter->endElement(); + + // o:lock + $objWriter->startElement('o:lock'); + $objWriter->writeAttribute('v:ext', 'edit'); + $objWriter->writeAttribute('textRotation', 't'); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Get an array of all drawings. + * + * @param Spreadsheet $spreadsheet + * + * @return \PhpOffice\PhpSpreadsheet\Worksheet\Drawing[] All drawings in PhpSpreadsheet + */ + public function allDrawings(Spreadsheet $spreadsheet) + { + // Get an array of all drawings + $aDrawings = []; + + // Loop through PhpSpreadsheet + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + // Loop through images and add to array + $iterator = $spreadsheet->getSheet($i)->getDrawingCollection()->getIterator(); + while ($iterator->valid()) { + $aDrawings[] = $iterator->current(); + + $iterator->next(); + } + } + + return $aDrawings; + } + + /** + * @param XMLWriter $objWriter + * @param null|int $hlinkClickId + */ + private function writeHyperLinkDrawing(XMLWriter $objWriter, $hlinkClickId) + { + if ($hlinkClickId === null) { + return; + } + + $objWriter->startElement('a:hlinkClick'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId); + $objWriter->endElement(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Rels.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Rels.php new file mode 100644 index 00000000000..76c196b4495 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -0,0 +1,466 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Relationships + $objWriter->startElement('Relationships'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); + if (!empty($customPropertyList)) { + // Relationship docProps/app.xml + $this->writeRelationship( + $objWriter, + 4, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties', + 'docProps/custom.xml' + ); + } + + // Relationship docProps/app.xml + $this->writeRelationship( + $objWriter, + 3, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties', + 'docProps/app.xml' + ); + + // Relationship docProps/core.xml + $this->writeRelationship( + $objWriter, + 2, + 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', + 'docProps/core.xml' + ); + + // Relationship xl/workbook.xml + $this->writeRelationship( + $objWriter, + 1, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument', + 'xl/workbook.xml' + ); + // a custom UI in workbook ? + if ($spreadsheet->hasRibbon()) { + $this->writeRelationShip( + $objWriter, + 5, + 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility', + $spreadsheet->getRibbonXMLData('target') + ); + } + + $objWriter->endElement(); + + return $objWriter->getData(); + } + + /** + * Write workbook relationships to XML format. + * + * @param Spreadsheet $spreadsheet + * + * @throws WriterException + * + * @return string XML Output + */ + public function writeWorkbookRelationships(Spreadsheet $spreadsheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Relationships + $objWriter->startElement('Relationships'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + // Relationship styles.xml + $this->writeRelationship( + $objWriter, + 1, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles', + 'styles.xml' + ); + + // Relationship theme/theme1.xml + $this->writeRelationship( + $objWriter, + 2, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme', + 'theme/theme1.xml' + ); + + // Relationship sharedStrings.xml + $this->writeRelationship( + $objWriter, + 3, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings', + 'sharedStrings.xml' + ); + + // Relationships with sheets + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $this->writeRelationship( + $objWriter, + ($i + 1 + 3), + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet', + 'worksheets/sheet' . ($i + 1) . '.xml' + ); + } + // Relationships for vbaProject if needed + // id : just after the last sheet + if ($spreadsheet->hasMacros()) { + $this->writeRelationShip( + $objWriter, + ($i + 1 + 3), + 'http://schemas.microsoft.com/office/2006/relationships/vbaProject', + 'vbaProject.bin' + ); + ++$i; //increment i if needed for an another relation + } + + $objWriter->endElement(); + + return $objWriter->getData(); + } + + /** + * Write worksheet relationships to XML format. + * + * Numbering is as follows: + * rId1 - Drawings + * rId_hyperlink_x - Hyperlinks + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet + * @param int $pWorksheetId + * @param bool $includeCharts Flag indicating if we should write charts + * + * @throws WriterException + * + * @return string XML Output + */ + public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, $pWorksheetId = 1, $includeCharts = false) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Relationships + $objWriter->startElement('Relationships'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + // Write drawing relationships? + $d = 0; + $drawingOriginalIds = []; + $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData(); + if (isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingOriginalIds'])) { + $drawingOriginalIds = $unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingOriginalIds']; + } + + if ($includeCharts) { + $charts = $pWorksheet->getChartCollection(); + } else { + $charts = []; + } + + if (($pWorksheet->getDrawingCollection()->count() > 0) || (count($charts) > 0) || $drawingOriginalIds) { + $relPath = '../drawings/drawing' . $pWorksheetId . '.xml'; + $rId = ++$d; + + if (isset($drawingOriginalIds[$relPath])) { + $rId = (int) (substr($drawingOriginalIds[$relPath], 3)); + } + + $this->writeRelationship( + $objWriter, + $rId, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing', + $relPath + ); + } + + // Write hyperlink relationships? + $i = 1; + foreach ($pWorksheet->getHyperlinkCollection() as $hyperlink) { + if (!$hyperlink->isInternal()) { + $this->writeRelationship( + $objWriter, + '_hyperlink_' . $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', + $hyperlink->getUrl(), + 'External' + ); + + ++$i; + } + } + + // Write comments relationship? + $i = 1; + if (count($pWorksheet->getComments()) > 0) { + $this->writeRelationship( + $objWriter, + '_comments_vml' . $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing', + '../drawings/vmlDrawing' . $pWorksheetId . '.vml' + ); + + $this->writeRelationship( + $objWriter, + '_comments' . $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments', + '../comments' . $pWorksheetId . '.xml' + ); + } + + // Write header/footer relationship? + $i = 1; + if (count($pWorksheet->getHeaderFooter()->getImages()) > 0) { + $this->writeRelationship( + $objWriter, + '_headerfooter_vml' . $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing', + '../drawings/vmlDrawingHF' . $pWorksheetId . '.vml' + ); + } + + $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'ctrlProps', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp'); + $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'vmlDrawings', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing'); + $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'printerSettings', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings'); + + $objWriter->endElement(); + + return $objWriter->getData(); + } + + private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, XMLWriter $objWriter, $relationship, $type) + { + $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData(); + if (!isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()][$relationship])) { + return; + } + + foreach ($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()][$relationship] as $rId => $value) { + $this->writeRelationship( + $objWriter, + $rId, + $type, + $value['relFilePath'] + ); + } + } + + /** + * Write drawing relationships to XML format. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet + * @param int &$chartRef Chart ID + * @param bool $includeCharts Flag indicating if we should write charts + * + * @throws WriterException + * + * @return string XML Output + */ + public function writeDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, &$chartRef, $includeCharts = false) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Relationships + $objWriter->startElement('Relationships'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + // Loop through images and write relationships + $i = 1; + $iterator = $pWorksheet->getDrawingCollection()->getIterator(); + while ($iterator->valid()) { + if ($iterator->current() instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing + || $iterator->current() instanceof MemoryDrawing) { + // Write relationship for image drawing + /** @var \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing */ + $drawing = $iterator->current(); + $this->writeRelationship( + $objWriter, + $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image', + '../media/' . str_replace(' ', '', $drawing->getIndexedFilename()) + ); + + $i = $this->writeDrawingHyperLink($objWriter, $drawing, $i); + } + + $iterator->next(); + ++$i; + } + + if ($includeCharts) { + // Loop through charts and write relationships + $chartCount = $pWorksheet->getChartCount(); + if ($chartCount > 0) { + for ($c = 0; $c < $chartCount; ++$c) { + $this->writeRelationship( + $objWriter, + $i++, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart', + '../charts/chart' . ++$chartRef . '.xml' + ); + } + } + } + + $objWriter->endElement(); + + return $objWriter->getData(); + } + + /** + * Write header/footer drawing relationships to XML format. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet + * + * @throws WriterException + * + * @return string XML Output + */ + public function writeHeaderFooterDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Relationships + $objWriter->startElement('Relationships'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + + // Loop through images and write relationships + foreach ($pWorksheet->getHeaderFooter()->getImages() as $key => $value) { + // Write relationship for image drawing + $this->writeRelationship( + $objWriter, + $key, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image', + '../media/' . $value->getIndexedFilename() + ); + } + + $objWriter->endElement(); + + return $objWriter->getData(); + } + + /** + * Write Override content type. + * + * @param XMLWriter $objWriter XML Writer + * @param int $pId Relationship ID. rId will be prepended! + * @param string $pType Relationship type + * @param string $pTarget Relationship target + * @param string $pTargetMode Relationship target mode + * + * @throws WriterException + */ + private function writeRelationship(XMLWriter $objWriter, $pId, $pType, $pTarget, $pTargetMode = '') + { + if ($pType != '' && $pTarget != '') { + // Write relationship + $objWriter->startElement('Relationship'); + $objWriter->writeAttribute('Id', 'rId' . $pId); + $objWriter->writeAttribute('Type', $pType); + $objWriter->writeAttribute('Target', $pTarget); + + if ($pTargetMode != '') { + $objWriter->writeAttribute('TargetMode', $pTargetMode); + } + + $objWriter->endElement(); + } else { + throw new WriterException('Invalid parameters passed.'); + } + } + + /** + * @param $objWriter + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing + * @param $i + * + * @throws WriterException + * + * @return int + */ + private function writeDrawingHyperLink($objWriter, $drawing, $i) + { + if ($drawing->getHyperlink() === null) { + return $i; + } + + ++$i; + $this->writeRelationship( + $objWriter, + $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', + $drawing->getHyperlink()->getUrl(), + $drawing->getHyperlink()->getTypeHyperlink() + ); + + return $i; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php new file mode 100644 index 00000000000..8a0cfe34508 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php @@ -0,0 +1,49 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Relationships + $objWriter->startElement('Relationships'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + $localRels = $spreadsheet->getRibbonBinObjects('names'); + if (is_array($localRels)) { + foreach ($localRels as $aId => $aTarget) { + $objWriter->startElement('Relationship'); + $objWriter->writeAttribute('Id', $aId); + $objWriter->writeAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'); + $objWriter->writeAttribute('Target', $aTarget); + $objWriter->endElement(); + } + } + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php new file mode 100644 index 00000000000..01ad38de67b --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php @@ -0,0 +1,44 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Relationships + $objWriter->startElement('Relationships'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships'); + $objWriter->startElement('Relationship'); + $objWriter->writeAttribute('Id', 'rId1'); + $objWriter->writeAttribute('Type', 'http://schemas.microsoft.com/office/2006/relationships/vbaProjectSignature'); + $objWriter->writeAttribute('Target', 'vbaProjectSignature.bin'); + $objWriter->endElement(); + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/StringTable.php new file mode 100644 index 00000000000..19604e44867 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -0,0 +1,281 @@ +flipStringTable($aStringTable); + + // Loop through cells + foreach ($pSheet->getCoordinates() as $coordinate) { + $cell = $pSheet->getCell($coordinate); + $cellValue = $cell->getValue(); + if (!is_object($cellValue) && + ($cellValue !== null) && + $cellValue !== '' && + !isset($aFlippedStringTable[$cellValue]) && + ($cell->getDataType() == DataType::TYPE_STRING || $cell->getDataType() == DataType::TYPE_STRING2 || $cell->getDataType() == DataType::TYPE_NULL)) { + $aStringTable[] = $cellValue; + $aFlippedStringTable[$cellValue] = true; + } elseif ($cellValue instanceof RichText && + ($cellValue !== null) && + !isset($aFlippedStringTable[$cellValue->getHashCode()])) { + $aStringTable[] = $cellValue; + $aFlippedStringTable[$cellValue->getHashCode()] = true; + } + } + + return $aStringTable; + } + + /** + * Write string table to XML format. + * + * @param string[] $pStringTable + * + * @throws WriterException + * + * @return string XML Output + */ + public function writeStringTable(array $pStringTable) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // String table + $objWriter->startElement('sst'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + $objWriter->writeAttribute('uniqueCount', count($pStringTable)); + + // Loop through string table + foreach ($pStringTable as $textElement) { + $objWriter->startElement('si'); + + if (!$textElement instanceof RichText) { + $textToWrite = StringHelper::controlCharacterPHP2OOXML($textElement); + $objWriter->startElement('t'); + if ($textToWrite !== trim($textToWrite)) { + $objWriter->writeAttribute('xml:space', 'preserve'); + } + $objWriter->writeRawData($textToWrite); + $objWriter->endElement(); + } elseif ($textElement instanceof RichText) { + $this->writeRichText($objWriter, $textElement); + } + + $objWriter->endElement(); + } + + $objWriter->endElement(); + + return $objWriter->getData(); + } + + /** + * Write Rich Text. + * + * @param XMLWriter $objWriter XML Writer + * @param RichText $pRichText Rich text + * @param string $prefix Optional Namespace prefix + */ + public function writeRichText(XMLWriter $objWriter, RichText $pRichText, $prefix = null) + { + if ($prefix !== null) { + $prefix .= ':'; + } + + // Loop through rich text elements + $elements = $pRichText->getRichTextElements(); + foreach ($elements as $element) { + // r + $objWriter->startElement($prefix . 'r'); + + // rPr + if ($element instanceof Run) { + // rPr + $objWriter->startElement($prefix . 'rPr'); + + // rFont + $objWriter->startElement($prefix . 'rFont'); + $objWriter->writeAttribute('val', $element->getFont()->getName()); + $objWriter->endElement(); + + // Bold + $objWriter->startElement($prefix . 'b'); + $objWriter->writeAttribute('val', ($element->getFont()->getBold() ? 'true' : 'false')); + $objWriter->endElement(); + + // Italic + $objWriter->startElement($prefix . 'i'); + $objWriter->writeAttribute('val', ($element->getFont()->getItalic() ? 'true' : 'false')); + $objWriter->endElement(); + + // Superscript / subscript + if ($element->getFont()->getSuperscript() || $element->getFont()->getSubscript()) { + $objWriter->startElement($prefix . 'vertAlign'); + if ($element->getFont()->getSuperscript()) { + $objWriter->writeAttribute('val', 'superscript'); + } elseif ($element->getFont()->getSubscript()) { + $objWriter->writeAttribute('val', 'subscript'); + } + $objWriter->endElement(); + } + + // Strikethrough + $objWriter->startElement($prefix . 'strike'); + $objWriter->writeAttribute('val', ($element->getFont()->getStrikethrough() ? 'true' : 'false')); + $objWriter->endElement(); + + // Color + $objWriter->startElement($prefix . 'color'); + $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); + $objWriter->endElement(); + + // Size + $objWriter->startElement($prefix . 'sz'); + $objWriter->writeAttribute('val', $element->getFont()->getSize()); + $objWriter->endElement(); + + // Underline + $objWriter->startElement($prefix . 'u'); + $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + // t + $objWriter->startElement($prefix . 't'); + $objWriter->writeAttribute('xml:space', 'preserve'); + $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($element->getText())); + $objWriter->endElement(); + + $objWriter->endElement(); + } + } + + /** + * Write Rich Text. + * + * @param XMLWriter $objWriter XML Writer + * @param RichText|string $pRichText text string or Rich text + * @param string $prefix Optional Namespace prefix + */ + public function writeRichTextForCharts(XMLWriter $objWriter, $pRichText = null, $prefix = null) + { + if (!$pRichText instanceof RichText) { + $textRun = $pRichText; + $pRichText = new RichText(); + $pRichText->createTextRun($textRun); + } + + if ($prefix !== null) { + $prefix .= ':'; + } + + // Loop through rich text elements + $elements = $pRichText->getRichTextElements(); + foreach ($elements as $element) { + // r + $objWriter->startElement($prefix . 'r'); + + // rPr + $objWriter->startElement($prefix . 'rPr'); + + // Bold + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); + // Italic + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); + // Underline + $underlineType = $element->getFont()->getUnderline(); + switch ($underlineType) { + case 'single': + $underlineType = 'sng'; + + break; + case 'double': + $underlineType = 'dbl'; + + break; + } + $objWriter->writeAttribute('u', $underlineType); + // Strikethrough + $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); + + // rFont + $objWriter->startElement($prefix . 'latin'); + $objWriter->writeAttribute('typeface', $element->getFont()->getName()); + $objWriter->endElement(); + + $objWriter->endElement(); + + // t + $objWriter->startElement($prefix . 't'); + $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($element->getText())); + $objWriter->endElement(); + + $objWriter->endElement(); + } + } + + /** + * Flip string table (for index searching). + * + * @param array $stringTable Stringtable + * + * @return array + */ + public function flipStringTable(array $stringTable) + { + // Return value + $returnValue = []; + + // Loop through stringtable and add flipped items to $returnValue + foreach ($stringTable as $key => $value) { + if (!$value instanceof RichText) { + $returnValue[$value] = $key; + } elseif ($value instanceof RichText) { + $returnValue[$value->getHashCode()] = $key; + } + } + + return $returnValue; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Style.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Style.php new file mode 100644 index 00000000000..16e800e01f0 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -0,0 +1,686 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // styleSheet + $objWriter->startElement('styleSheet'); + $objWriter->writeAttribute('xml:space', 'preserve'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + + // numFmts + $objWriter->startElement('numFmts'); + $objWriter->writeAttribute('count', $this->getParentWriter()->getNumFmtHashTable()->count()); + + // numFmt + for ($i = 0; $i < $this->getParentWriter()->getNumFmtHashTable()->count(); ++$i) { + $this->writeNumFmt($objWriter, $this->getParentWriter()->getNumFmtHashTable()->getByIndex($i), $i); + } + + $objWriter->endElement(); + + // fonts + $objWriter->startElement('fonts'); + $objWriter->writeAttribute('count', $this->getParentWriter()->getFontHashTable()->count()); + + // font + for ($i = 0; $i < $this->getParentWriter()->getFontHashTable()->count(); ++$i) { + $this->writeFont($objWriter, $this->getParentWriter()->getFontHashTable()->getByIndex($i)); + } + + $objWriter->endElement(); + + // fills + $objWriter->startElement('fills'); + $objWriter->writeAttribute('count', $this->getParentWriter()->getFillHashTable()->count()); + + // fill + for ($i = 0; $i < $this->getParentWriter()->getFillHashTable()->count(); ++$i) { + $this->writeFill($objWriter, $this->getParentWriter()->getFillHashTable()->getByIndex($i)); + } + + $objWriter->endElement(); + + // borders + $objWriter->startElement('borders'); + $objWriter->writeAttribute('count', $this->getParentWriter()->getBordersHashTable()->count()); + + // border + for ($i = 0; $i < $this->getParentWriter()->getBordersHashTable()->count(); ++$i) { + $this->writeBorder($objWriter, $this->getParentWriter()->getBordersHashTable()->getByIndex($i)); + } + + $objWriter->endElement(); + + // cellStyleXfs + $objWriter->startElement('cellStyleXfs'); + $objWriter->writeAttribute('count', 1); + + // xf + $objWriter->startElement('xf'); + $objWriter->writeAttribute('numFmtId', 0); + $objWriter->writeAttribute('fontId', 0); + $objWriter->writeAttribute('fillId', 0); + $objWriter->writeAttribute('borderId', 0); + $objWriter->endElement(); + + $objWriter->endElement(); + + // cellXfs + $objWriter->startElement('cellXfs'); + $objWriter->writeAttribute('count', count($spreadsheet->getCellXfCollection())); + + // xf + foreach ($spreadsheet->getCellXfCollection() as $cellXf) { + $this->writeCellStyleXf($objWriter, $cellXf, $spreadsheet); + } + + $objWriter->endElement(); + + // cellStyles + $objWriter->startElement('cellStyles'); + $objWriter->writeAttribute('count', 1); + + // cellStyle + $objWriter->startElement('cellStyle'); + $objWriter->writeAttribute('name', 'Normal'); + $objWriter->writeAttribute('xfId', 0); + $objWriter->writeAttribute('builtinId', 0); + $objWriter->endElement(); + + $objWriter->endElement(); + + // dxfs + $objWriter->startElement('dxfs'); + $objWriter->writeAttribute('count', $this->getParentWriter()->getStylesConditionalHashTable()->count()); + + // dxf + for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) { + $this->writeCellStyleDxf($objWriter, $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i)->getStyle()); + } + + $objWriter->endElement(); + + // tableStyles + $objWriter->startElement('tableStyles'); + $objWriter->writeAttribute('defaultTableStyle', 'TableStyleMedium9'); + $objWriter->writeAttribute('defaultPivotStyle', 'PivotTableStyle1'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write Fill. + * + * @param XMLWriter $objWriter XML Writer + * @param Fill $pFill Fill style + */ + private function writeFill(XMLWriter $objWriter, Fill $pFill) + { + // Check if this is a pattern type or gradient type + if ($pFill->getFillType() === Fill::FILL_GRADIENT_LINEAR || + $pFill->getFillType() === Fill::FILL_GRADIENT_PATH) { + // Gradient fill + $this->writeGradientFill($objWriter, $pFill); + } elseif ($pFill->getFillType() !== null) { + // Pattern fill + $this->writePatternFill($objWriter, $pFill); + } + } + + /** + * Write Gradient Fill. + * + * @param XMLWriter $objWriter XML Writer + * @param Fill $pFill Fill style + */ + private function writeGradientFill(XMLWriter $objWriter, Fill $pFill) + { + // fill + $objWriter->startElement('fill'); + + // gradientFill + $objWriter->startElement('gradientFill'); + $objWriter->writeAttribute('type', $pFill->getFillType()); + $objWriter->writeAttribute('degree', $pFill->getRotation()); + + // stop + $objWriter->startElement('stop'); + $objWriter->writeAttribute('position', '0'); + + // color + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $pFill->getStartColor()->getARGB()); + $objWriter->endElement(); + + $objWriter->endElement(); + + // stop + $objWriter->startElement('stop'); + $objWriter->writeAttribute('position', '1'); + + // color + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $pFill->getEndColor()->getARGB()); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write Pattern Fill. + * + * @param XMLWriter $objWriter XML Writer + * @param Fill $pFill Fill style + */ + private function writePatternFill(XMLWriter $objWriter, Fill $pFill) + { + // fill + $objWriter->startElement('fill'); + + // patternFill + $objWriter->startElement('patternFill'); + $objWriter->writeAttribute('patternType', $pFill->getFillType()); + + if ($pFill->getFillType() !== Fill::FILL_NONE) { + // fgColor + if ($pFill->getStartColor()->getARGB()) { + $objWriter->startElement('fgColor'); + $objWriter->writeAttribute('rgb', $pFill->getStartColor()->getARGB()); + $objWriter->endElement(); + } + } + if ($pFill->getFillType() !== Fill::FILL_NONE) { + // bgColor + if ($pFill->getEndColor()->getARGB()) { + $objWriter->startElement('bgColor'); + $objWriter->writeAttribute('rgb', $pFill->getEndColor()->getARGB()); + $objWriter->endElement(); + } + } + + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write Font. + * + * @param XMLWriter $objWriter XML Writer + * @param Font $pFont Font style + */ + private function writeFont(XMLWriter $objWriter, Font $pFont) + { + // font + $objWriter->startElement('font'); + // Weird! The order of these elements actually makes a difference when opening Xlsx + // files in Excel2003 with the compatibility pack. It's not documented behaviour, + // and makes for a real WTF! + + // Bold. We explicitly write this element also when false (like MS Office Excel 2007 does + // for conditional formatting). Otherwise it will apparently not be picked up in conditional + // formatting style dialog + if ($pFont->getBold() !== null) { + $objWriter->startElement('b'); + $objWriter->writeAttribute('val', $pFont->getBold() ? '1' : '0'); + $objWriter->endElement(); + } + + // Italic + if ($pFont->getItalic() !== null) { + $objWriter->startElement('i'); + $objWriter->writeAttribute('val', $pFont->getItalic() ? '1' : '0'); + $objWriter->endElement(); + } + + // Strikethrough + if ($pFont->getStrikethrough() !== null) { + $objWriter->startElement('strike'); + $objWriter->writeAttribute('val', $pFont->getStrikethrough() ? '1' : '0'); + $objWriter->endElement(); + } + + // Underline + if ($pFont->getUnderline() !== null) { + $objWriter->startElement('u'); + $objWriter->writeAttribute('val', $pFont->getUnderline()); + $objWriter->endElement(); + } + + // Superscript / subscript + if ($pFont->getSuperscript() === true || $pFont->getSubscript() === true) { + $objWriter->startElement('vertAlign'); + if ($pFont->getSuperscript() === true) { + $objWriter->writeAttribute('val', 'superscript'); + } elseif ($pFont->getSubscript() === true) { + $objWriter->writeAttribute('val', 'subscript'); + } + $objWriter->endElement(); + } + + // Size + if ($pFont->getSize() !== null) { + $objWriter->startElement('sz'); + $objWriter->writeAttribute('val', StringHelper::formatNumber($pFont->getSize())); + $objWriter->endElement(); + } + + // Foreground color + if ($pFont->getColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $pFont->getColor()->getARGB()); + $objWriter->endElement(); + } + + // Name + if ($pFont->getName() !== null) { + $objWriter->startElement('name'); + $objWriter->writeAttribute('val', $pFont->getName()); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + + /** + * Write Border. + * + * @param XMLWriter $objWriter XML Writer + * @param Borders $pBorders Borders style + */ + private function writeBorder(XMLWriter $objWriter, Borders $pBorders) + { + // Write border + $objWriter->startElement('border'); + // Diagonal? + switch ($pBorders->getDiagonalDirection()) { + case Borders::DIAGONAL_UP: + $objWriter->writeAttribute('diagonalUp', 'true'); + $objWriter->writeAttribute('diagonalDown', 'false'); + + break; + case Borders::DIAGONAL_DOWN: + $objWriter->writeAttribute('diagonalUp', 'false'); + $objWriter->writeAttribute('diagonalDown', 'true'); + + break; + case Borders::DIAGONAL_BOTH: + $objWriter->writeAttribute('diagonalUp', 'true'); + $objWriter->writeAttribute('diagonalDown', 'true'); + + break; + } + + // BorderPr + $this->writeBorderPr($objWriter, 'left', $pBorders->getLeft()); + $this->writeBorderPr($objWriter, 'right', $pBorders->getRight()); + $this->writeBorderPr($objWriter, 'top', $pBorders->getTop()); + $this->writeBorderPr($objWriter, 'bottom', $pBorders->getBottom()); + $this->writeBorderPr($objWriter, 'diagonal', $pBorders->getDiagonal()); + $objWriter->endElement(); + } + + /** + * Write Cell Style Xf. + * + * @param XMLWriter $objWriter XML Writer + * @param \PhpOffice\PhpSpreadsheet\Style\Style $pStyle Style + * @param Spreadsheet $spreadsheet Workbook + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + */ + private function writeCellStyleXf(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Style\Style $pStyle, Spreadsheet $spreadsheet) + { + // xf + $objWriter->startElement('xf'); + $objWriter->writeAttribute('xfId', 0); + $objWriter->writeAttribute('fontId', (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($pStyle->getFont()->getHashCode())); + if ($pStyle->getQuotePrefix()) { + $objWriter->writeAttribute('quotePrefix', 1); + } + + if ($pStyle->getNumberFormat()->getBuiltInFormatCode() === false) { + $objWriter->writeAttribute('numFmtId', (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($pStyle->getNumberFormat()->getHashCode()) + 164)); + } else { + $objWriter->writeAttribute('numFmtId', (int) $pStyle->getNumberFormat()->getBuiltInFormatCode()); + } + + $objWriter->writeAttribute('fillId', (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($pStyle->getFill()->getHashCode())); + $objWriter->writeAttribute('borderId', (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($pStyle->getBorders()->getHashCode())); + + // Apply styles? + $objWriter->writeAttribute('applyFont', ($spreadsheet->getDefaultStyle()->getFont()->getHashCode() != $pStyle->getFont()->getHashCode()) ? '1' : '0'); + $objWriter->writeAttribute('applyNumberFormat', ($spreadsheet->getDefaultStyle()->getNumberFormat()->getHashCode() != $pStyle->getNumberFormat()->getHashCode()) ? '1' : '0'); + $objWriter->writeAttribute('applyFill', ($spreadsheet->getDefaultStyle()->getFill()->getHashCode() != $pStyle->getFill()->getHashCode()) ? '1' : '0'); + $objWriter->writeAttribute('applyBorder', ($spreadsheet->getDefaultStyle()->getBorders()->getHashCode() != $pStyle->getBorders()->getHashCode()) ? '1' : '0'); + $objWriter->writeAttribute('applyAlignment', ($spreadsheet->getDefaultStyle()->getAlignment()->getHashCode() != $pStyle->getAlignment()->getHashCode()) ? '1' : '0'); + if ($pStyle->getProtection()->getLocked() != Protection::PROTECTION_INHERIT || $pStyle->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) { + $objWriter->writeAttribute('applyProtection', 'true'); + } + + // alignment + $objWriter->startElement('alignment'); + $objWriter->writeAttribute('horizontal', $pStyle->getAlignment()->getHorizontal()); + $objWriter->writeAttribute('vertical', $pStyle->getAlignment()->getVertical()); + + $textRotation = 0; + if ($pStyle->getAlignment()->getTextRotation() >= 0) { + $textRotation = $pStyle->getAlignment()->getTextRotation(); + } elseif ($pStyle->getAlignment()->getTextRotation() < 0) { + $textRotation = 90 - $pStyle->getAlignment()->getTextRotation(); + } + $objWriter->writeAttribute('textRotation', $textRotation); + + $objWriter->writeAttribute('wrapText', ($pStyle->getAlignment()->getWrapText() ? 'true' : 'false')); + $objWriter->writeAttribute('shrinkToFit', ($pStyle->getAlignment()->getShrinkToFit() ? 'true' : 'false')); + + if ($pStyle->getAlignment()->getIndent() > 0) { + $objWriter->writeAttribute('indent', $pStyle->getAlignment()->getIndent()); + } + if ($pStyle->getAlignment()->getReadOrder() > 0) { + $objWriter->writeAttribute('readingOrder', $pStyle->getAlignment()->getReadOrder()); + } + $objWriter->endElement(); + + // protection + if ($pStyle->getProtection()->getLocked() != Protection::PROTECTION_INHERIT || $pStyle->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) { + $objWriter->startElement('protection'); + if ($pStyle->getProtection()->getLocked() != Protection::PROTECTION_INHERIT) { + $objWriter->writeAttribute('locked', ($pStyle->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED ? 'true' : 'false')); + } + if ($pStyle->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) { + $objWriter->writeAttribute('hidden', ($pStyle->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED ? 'true' : 'false')); + } + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + + /** + * Write Cell Style Dxf. + * + * @param XMLWriter $objWriter XML Writer + * @param \PhpOffice\PhpSpreadsheet\Style\Style $pStyle Style + */ + private function writeCellStyleDxf(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Style\Style $pStyle) + { + // dxf + $objWriter->startElement('dxf'); + + // font + $this->writeFont($objWriter, $pStyle->getFont()); + + // numFmt + $this->writeNumFmt($objWriter, $pStyle->getNumberFormat()); + + // fill + $this->writeFill($objWriter, $pStyle->getFill()); + + // alignment + $objWriter->startElement('alignment'); + if ($pStyle->getAlignment()->getHorizontal() !== null) { + $objWriter->writeAttribute('horizontal', $pStyle->getAlignment()->getHorizontal()); + } + if ($pStyle->getAlignment()->getVertical() !== null) { + $objWriter->writeAttribute('vertical', $pStyle->getAlignment()->getVertical()); + } + + if ($pStyle->getAlignment()->getTextRotation() !== null) { + $textRotation = 0; + if ($pStyle->getAlignment()->getTextRotation() >= 0) { + $textRotation = $pStyle->getAlignment()->getTextRotation(); + } elseif ($pStyle->getAlignment()->getTextRotation() < 0) { + $textRotation = 90 - $pStyle->getAlignment()->getTextRotation(); + } + $objWriter->writeAttribute('textRotation', $textRotation); + } + $objWriter->endElement(); + + // border + $this->writeBorder($objWriter, $pStyle->getBorders()); + + // protection + if (($pStyle->getProtection()->getLocked() !== null) || ($pStyle->getProtection()->getHidden() !== null)) { + if ($pStyle->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT || + $pStyle->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT) { + $objWriter->startElement('protection'); + if (($pStyle->getProtection()->getLocked() !== null) && + ($pStyle->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT)) { + $objWriter->writeAttribute('locked', ($pStyle->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED ? 'true' : 'false')); + } + if (($pStyle->getProtection()->getHidden() !== null) && + ($pStyle->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT)) { + $objWriter->writeAttribute('hidden', ($pStyle->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED ? 'true' : 'false')); + } + $objWriter->endElement(); + } + } + + $objWriter->endElement(); + } + + /** + * Write BorderPr. + * + * @param XMLWriter $objWriter XML Writer + * @param string $pName Element name + * @param Border $pBorder Border style + */ + private function writeBorderPr(XMLWriter $objWriter, $pName, Border $pBorder) + { + // Write BorderPr + if ($pBorder->getBorderStyle() != Border::BORDER_NONE) { + $objWriter->startElement($pName); + $objWriter->writeAttribute('style', $pBorder->getBorderStyle()); + + // color + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $pBorder->getColor()->getARGB()); + $objWriter->endElement(); + + $objWriter->endElement(); + } + } + + /** + * Write NumberFormat. + * + * @param XMLWriter $objWriter XML Writer + * @param NumberFormat $pNumberFormat Number Format + * @param int $pId Number Format identifier + */ + private function writeNumFmt(XMLWriter $objWriter, NumberFormat $pNumberFormat, $pId = 0) + { + // Translate formatcode + $formatCode = $pNumberFormat->getFormatCode(); + + // numFmt + if ($formatCode !== null) { + $objWriter->startElement('numFmt'); + $objWriter->writeAttribute('numFmtId', ($pId + 164)); + $objWriter->writeAttribute('formatCode', $formatCode); + $objWriter->endElement(); + } + } + + /** + * Get an array of all styles. + * + * @param Spreadsheet $spreadsheet + * + * @return \PhpOffice\PhpSpreadsheet\Style\Style[] All styles in PhpSpreadsheet + */ + public function allStyles(Spreadsheet $spreadsheet) + { + return $spreadsheet->getCellXfCollection(); + } + + /** + * Get an array of all conditional styles. + * + * @param Spreadsheet $spreadsheet + * + * @return Conditional[] All conditional styles in PhpSpreadsheet + */ + public function allConditionalStyles(Spreadsheet $spreadsheet) + { + // Get an array of all styles + $aStyles = []; + + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + foreach ($spreadsheet->getSheet($i)->getConditionalStylesCollection() as $conditionalStyles) { + foreach ($conditionalStyles as $conditionalStyle) { + $aStyles[] = $conditionalStyle; + } + } + } + + return $aStyles; + } + + /** + * Get an array of all fills. + * + * @param Spreadsheet $spreadsheet + * + * @return Fill[] All fills in PhpSpreadsheet + */ + public function allFills(Spreadsheet $spreadsheet) + { + // Get an array of unique fills + $aFills = []; + + // Two first fills are predefined + $fill0 = new Fill(); + $fill0->setFillType(Fill::FILL_NONE); + $aFills[] = $fill0; + + $fill1 = new Fill(); + $fill1->setFillType(Fill::FILL_PATTERN_GRAY125); + $aFills[] = $fill1; + // The remaining fills + $aStyles = $this->allStyles($spreadsheet); + /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */ + foreach ($aStyles as $style) { + if (!isset($aFills[$style->getFill()->getHashCode()])) { + $aFills[$style->getFill()->getHashCode()] = $style->getFill(); + } + } + + return $aFills; + } + + /** + * Get an array of all fonts. + * + * @param Spreadsheet $spreadsheet + * + * @return Font[] All fonts in PhpSpreadsheet + */ + public function allFonts(Spreadsheet $spreadsheet) + { + // Get an array of unique fonts + $aFonts = []; + $aStyles = $this->allStyles($spreadsheet); + + /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */ + foreach ($aStyles as $style) { + if (!isset($aFonts[$style->getFont()->getHashCode()])) { + $aFonts[$style->getFont()->getHashCode()] = $style->getFont(); + } + } + + return $aFonts; + } + + /** + * Get an array of all borders. + * + * @param Spreadsheet $spreadsheet + * + * @return Borders[] All borders in PhpSpreadsheet + */ + public function allBorders(Spreadsheet $spreadsheet) + { + // Get an array of unique borders + $aBorders = []; + $aStyles = $this->allStyles($spreadsheet); + + /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */ + foreach ($aStyles as $style) { + if (!isset($aBorders[$style->getBorders()->getHashCode()])) { + $aBorders[$style->getBorders()->getHashCode()] = $style->getBorders(); + } + } + + return $aBorders; + } + + /** + * Get an array of all number formats. + * + * @param Spreadsheet $spreadsheet + * + * @return NumberFormat[] All number formats in PhpSpreadsheet + */ + public function allNumberFormats(Spreadsheet $spreadsheet) + { + // Get an array of unique number formats + $aNumFmts = []; + $aStyles = $this->allStyles($spreadsheet); + + /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */ + foreach ($aStyles as $style) { + if ($style->getNumberFormat()->getBuiltInFormatCode() === false && !isset($aNumFmts[$style->getNumberFormat()->getHashCode()])) { + $aNumFmts[$style->getNumberFormat()->getHashCode()] = $style->getNumberFormat(); + } + } + + return $aNumFmts; + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Theme.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Theme.php new file mode 100644 index 00000000000..f5f8dc0752d --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Theme.php @@ -0,0 +1,846 @@ + 'MS Pゴシック', + 'Hang' => '맑은 고딕', + 'Hans' => '宋体', + 'Hant' => '新細明體', + 'Arab' => 'Times New Roman', + 'Hebr' => 'Times New Roman', + 'Thai' => 'Tahoma', + 'Ethi' => 'Nyala', + 'Beng' => 'Vrinda', + 'Gujr' => 'Shruti', + 'Khmr' => 'MoolBoran', + 'Knda' => 'Tunga', + 'Guru' => 'Raavi', + 'Cans' => 'Euphemia', + 'Cher' => 'Plantagenet Cherokee', + 'Yiii' => 'Microsoft Yi Baiti', + 'Tibt' => 'Microsoft Himalaya', + 'Thaa' => 'MV Boli', + 'Deva' => 'Mangal', + 'Telu' => 'Gautami', + 'Taml' => 'Latha', + 'Syrc' => 'Estrangelo Edessa', + 'Orya' => 'Kalinga', + 'Mlym' => 'Kartika', + 'Laoo' => 'DokChampa', + 'Sinh' => 'Iskoola Pota', + 'Mong' => 'Mongolian Baiti', + 'Viet' => 'Times New Roman', + 'Uigh' => 'Microsoft Uighur', + 'Geor' => 'Sylfaen', + ]; + + /** + * Map of Minor fonts to write. + * + * @var array of string + */ + private static $minorFonts = [ + 'Jpan' => 'MS Pゴシック', + 'Hang' => '맑은 고딕', + 'Hans' => '宋体', + 'Hant' => '新細明體', + 'Arab' => 'Arial', + 'Hebr' => 'Arial', + 'Thai' => 'Tahoma', + 'Ethi' => 'Nyala', + 'Beng' => 'Vrinda', + 'Gujr' => 'Shruti', + 'Khmr' => 'DaunPenh', + 'Knda' => 'Tunga', + 'Guru' => 'Raavi', + 'Cans' => 'Euphemia', + 'Cher' => 'Plantagenet Cherokee', + 'Yiii' => 'Microsoft Yi Baiti', + 'Tibt' => 'Microsoft Himalaya', + 'Thaa' => 'MV Boli', + 'Deva' => 'Mangal', + 'Telu' => 'Gautami', + 'Taml' => 'Latha', + 'Syrc' => 'Estrangelo Edessa', + 'Orya' => 'Kalinga', + 'Mlym' => 'Kartika', + 'Laoo' => 'DokChampa', + 'Sinh' => 'Iskoola Pota', + 'Mong' => 'Mongolian Baiti', + 'Viet' => 'Arial', + 'Uigh' => 'Microsoft Uighur', + 'Geor' => 'Sylfaen', + ]; + + /** + * Map of core colours. + * + * @var array of string + */ + private static $colourScheme = [ + 'dk2' => '1F497D', + 'lt2' => 'EEECE1', + 'accent1' => '4F81BD', + 'accent2' => 'C0504D', + 'accent3' => '9BBB59', + 'accent4' => '8064A2', + 'accent5' => '4BACC6', + 'accent6' => 'F79646', + 'hlink' => '0000FF', + 'folHlink' => '800080', + ]; + + /** + * Write theme to XML format. + * + * @param Spreadsheet $spreadsheet + * + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @return string XML Output + */ + public function writeTheme(Spreadsheet $spreadsheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // a:theme + $objWriter->startElement('a:theme'); + $objWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + $objWriter->writeAttribute('name', 'Office Theme'); + + // a:themeElements + $objWriter->startElement('a:themeElements'); + + // a:clrScheme + $objWriter->startElement('a:clrScheme'); + $objWriter->writeAttribute('name', 'Office'); + + // a:dk1 + $objWriter->startElement('a:dk1'); + + // a:sysClr + $objWriter->startElement('a:sysClr'); + $objWriter->writeAttribute('val', 'windowText'); + $objWriter->writeAttribute('lastClr', '000000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:lt1 + $objWriter->startElement('a:lt1'); + + // a:sysClr + $objWriter->startElement('a:sysClr'); + $objWriter->writeAttribute('val', 'window'); + $objWriter->writeAttribute('lastClr', 'FFFFFF'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:dk2 + $this->writeColourScheme($objWriter); + + $objWriter->endElement(); + + // a:fontScheme + $objWriter->startElement('a:fontScheme'); + $objWriter->writeAttribute('name', 'Office'); + + // a:majorFont + $objWriter->startElement('a:majorFont'); + $this->writeFonts($objWriter, 'Cambria', self::$majorFonts); + $objWriter->endElement(); + + // a:minorFont + $objWriter->startElement('a:minorFont'); + $this->writeFonts($objWriter, 'Calibri', self::$minorFonts); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:fmtScheme + $objWriter->startElement('a:fmtScheme'); + $objWriter->writeAttribute('name', 'Office'); + + // a:fillStyleLst + $objWriter->startElement('a:fillStyleLst'); + + // a:solidFill + $objWriter->startElement('a:solidFill'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gradFill + $objWriter->startElement('a:gradFill'); + $objWriter->writeAttribute('rotWithShape', '1'); + + // a:gsLst + $objWriter->startElement('a:gsLst'); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '0'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:tint + $objWriter->startElement('a:tint'); + $objWriter->writeAttribute('val', '50000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '300000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '35000'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:tint + $objWriter->startElement('a:tint'); + $objWriter->writeAttribute('val', '37000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '300000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '100000'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:tint + $objWriter->startElement('a:tint'); + $objWriter->writeAttribute('val', '15000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '350000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:lin + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', '16200000'); + $objWriter->writeAttribute('scaled', '1'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gradFill + $objWriter->startElement('a:gradFill'); + $objWriter->writeAttribute('rotWithShape', '1'); + + // a:gsLst + $objWriter->startElement('a:gsLst'); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '0'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:shade + $objWriter->startElement('a:shade'); + $objWriter->writeAttribute('val', '51000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '130000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '80000'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:shade + $objWriter->startElement('a:shade'); + $objWriter->writeAttribute('val', '93000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '130000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '100000'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:shade + $objWriter->startElement('a:shade'); + $objWriter->writeAttribute('val', '94000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '135000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:lin + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', '16200000'); + $objWriter->writeAttribute('scaled', '0'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:lnStyleLst + $objWriter->startElement('a:lnStyleLst'); + + // a:ln + $objWriter->startElement('a:ln'); + $objWriter->writeAttribute('w', '9525'); + $objWriter->writeAttribute('cap', 'flat'); + $objWriter->writeAttribute('cmpd', 'sng'); + $objWriter->writeAttribute('algn', 'ctr'); + + // a:solidFill + $objWriter->startElement('a:solidFill'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:shade + $objWriter->startElement('a:shade'); + $objWriter->writeAttribute('val', '95000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '105000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:prstDash + $objWriter->startElement('a:prstDash'); + $objWriter->writeAttribute('val', 'solid'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:ln + $objWriter->startElement('a:ln'); + $objWriter->writeAttribute('w', '25400'); + $objWriter->writeAttribute('cap', 'flat'); + $objWriter->writeAttribute('cmpd', 'sng'); + $objWriter->writeAttribute('algn', 'ctr'); + + // a:solidFill + $objWriter->startElement('a:solidFill'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:prstDash + $objWriter->startElement('a:prstDash'); + $objWriter->writeAttribute('val', 'solid'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:ln + $objWriter->startElement('a:ln'); + $objWriter->writeAttribute('w', '38100'); + $objWriter->writeAttribute('cap', 'flat'); + $objWriter->writeAttribute('cmpd', 'sng'); + $objWriter->writeAttribute('algn', 'ctr'); + + // a:solidFill + $objWriter->startElement('a:solidFill'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:prstDash + $objWriter->startElement('a:prstDash'); + $objWriter->writeAttribute('val', 'solid'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:effectStyleLst + $objWriter->startElement('a:effectStyleLst'); + + // a:effectStyle + $objWriter->startElement('a:effectStyle'); + + // a:effectLst + $objWriter->startElement('a:effectLst'); + + // a:outerShdw + $objWriter->startElement('a:outerShdw'); + $objWriter->writeAttribute('blurRad', '40000'); + $objWriter->writeAttribute('dist', '20000'); + $objWriter->writeAttribute('dir', '5400000'); + $objWriter->writeAttribute('rotWithShape', '0'); + + // a:srgbClr + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', '000000'); + + // a:alpha + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', '38000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:effectStyle + $objWriter->startElement('a:effectStyle'); + + // a:effectLst + $objWriter->startElement('a:effectLst'); + + // a:outerShdw + $objWriter->startElement('a:outerShdw'); + $objWriter->writeAttribute('blurRad', '40000'); + $objWriter->writeAttribute('dist', '23000'); + $objWriter->writeAttribute('dir', '5400000'); + $objWriter->writeAttribute('rotWithShape', '0'); + + // a:srgbClr + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', '000000'); + + // a:alpha + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', '35000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:effectStyle + $objWriter->startElement('a:effectStyle'); + + // a:effectLst + $objWriter->startElement('a:effectLst'); + + // a:outerShdw + $objWriter->startElement('a:outerShdw'); + $objWriter->writeAttribute('blurRad', '40000'); + $objWriter->writeAttribute('dist', '23000'); + $objWriter->writeAttribute('dir', '5400000'); + $objWriter->writeAttribute('rotWithShape', '0'); + + // a:srgbClr + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', '000000'); + + // a:alpha + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', '35000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:scene3d + $objWriter->startElement('a:scene3d'); + + // a:camera + $objWriter->startElement('a:camera'); + $objWriter->writeAttribute('prst', 'orthographicFront'); + + // a:rot + $objWriter->startElement('a:rot'); + $objWriter->writeAttribute('lat', '0'); + $objWriter->writeAttribute('lon', '0'); + $objWriter->writeAttribute('rev', '0'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:lightRig + $objWriter->startElement('a:lightRig'); + $objWriter->writeAttribute('rig', 'threePt'); + $objWriter->writeAttribute('dir', 't'); + + // a:rot + $objWriter->startElement('a:rot'); + $objWriter->writeAttribute('lat', '0'); + $objWriter->writeAttribute('lon', '0'); + $objWriter->writeAttribute('rev', '1200000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:sp3d + $objWriter->startElement('a:sp3d'); + + // a:bevelT + $objWriter->startElement('a:bevelT'); + $objWriter->writeAttribute('w', '63500'); + $objWriter->writeAttribute('h', '25400'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:bgFillStyleLst + $objWriter->startElement('a:bgFillStyleLst'); + + // a:solidFill + $objWriter->startElement('a:solidFill'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gradFill + $objWriter->startElement('a:gradFill'); + $objWriter->writeAttribute('rotWithShape', '1'); + + // a:gsLst + $objWriter->startElement('a:gsLst'); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '0'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:tint + $objWriter->startElement('a:tint'); + $objWriter->writeAttribute('val', '40000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '350000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '40000'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:tint + $objWriter->startElement('a:tint'); + $objWriter->writeAttribute('val', '45000'); + $objWriter->endElement(); + + // a:shade + $objWriter->startElement('a:shade'); + $objWriter->writeAttribute('val', '99000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '350000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '100000'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:shade + $objWriter->startElement('a:shade'); + $objWriter->writeAttribute('val', '20000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '255000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:path + $objWriter->startElement('a:path'); + $objWriter->writeAttribute('path', 'circle'); + + // a:fillToRect + $objWriter->startElement('a:fillToRect'); + $objWriter->writeAttribute('l', '50000'); + $objWriter->writeAttribute('t', '-80000'); + $objWriter->writeAttribute('r', '50000'); + $objWriter->writeAttribute('b', '180000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gradFill + $objWriter->startElement('a:gradFill'); + $objWriter->writeAttribute('rotWithShape', '1'); + + // a:gsLst + $objWriter->startElement('a:gsLst'); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '0'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:tint + $objWriter->startElement('a:tint'); + $objWriter->writeAttribute('val', '80000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '300000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:gs + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', '100000'); + + // a:schemeClr + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', 'phClr'); + + // a:shade + $objWriter->startElement('a:shade'); + $objWriter->writeAttribute('val', '30000'); + $objWriter->endElement(); + + // a:satMod + $objWriter->startElement('a:satMod'); + $objWriter->writeAttribute('val', '200000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:path + $objWriter->startElement('a:path'); + $objWriter->writeAttribute('path', 'circle'); + + // a:fillToRect + $objWriter->startElement('a:fillToRect'); + $objWriter->writeAttribute('l', '50000'); + $objWriter->writeAttribute('t', '50000'); + $objWriter->writeAttribute('r', '50000'); + $objWriter->writeAttribute('b', '50000'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // a:objectDefaults + $objWriter->writeElement('a:objectDefaults', null); + + // a:extraClrSchemeLst + $objWriter->writeElement('a:extraClrSchemeLst', null); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write fonts to XML format. + * + * @param XMLWriter $objWriter + * @param string $latinFont + * @param array of string $fontSet + * + * @return string XML Output + */ + private function writeFonts($objWriter, $latinFont, $fontSet) + { + // a:latin + $objWriter->startElement('a:latin'); + $objWriter->writeAttribute('typeface', $latinFont); + $objWriter->endElement(); + + // a:ea + $objWriter->startElement('a:ea'); + $objWriter->writeAttribute('typeface', ''); + $objWriter->endElement(); + + // a:cs + $objWriter->startElement('a:cs'); + $objWriter->writeAttribute('typeface', ''); + $objWriter->endElement(); + + foreach ($fontSet as $fontScript => $typeface) { + $objWriter->startElement('a:font'); + $objWriter->writeAttribute('script', $fontScript); + $objWriter->writeAttribute('typeface', $typeface); + $objWriter->endElement(); + } + } + + /** + * Write colour scheme to XML format. + * + * @param XMLWriter $objWriter + * + * @return string XML Output + */ + private function writeColourScheme($objWriter) + { + foreach (self::$colourScheme as $colourName => $colourValue) { + $objWriter->startElement('a:' . $colourName); + + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $colourValue); + $objWriter->endElement(); + + $objWriter->endElement(); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Workbook.php new file mode 100644 index 00000000000..e3ddb03c143 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -0,0 +1,426 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // workbook + $objWriter->startElement('workbook'); + $objWriter->writeAttribute('xml:space', 'preserve'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + // fileVersion + $this->writeFileVersion($objWriter); + + // workbookPr + $this->writeWorkbookPr($objWriter); + + // workbookProtection + $this->writeWorkbookProtection($objWriter, $spreadsheet); + + // bookViews + if ($this->getParentWriter()->getOffice2003Compatibility() === false) { + $this->writeBookViews($objWriter, $spreadsheet); + } + + // sheets + $this->writeSheets($objWriter, $spreadsheet); + + // definedNames + $this->writeDefinedNames($objWriter, $spreadsheet); + + // calcPr + $this->writeCalcPr($objWriter, $recalcRequired); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write file version. + * + * @param XMLWriter $objWriter XML Writer + */ + private function writeFileVersion(XMLWriter $objWriter) + { + $objWriter->startElement('fileVersion'); + $objWriter->writeAttribute('appName', 'xl'); + $objWriter->writeAttribute('lastEdited', '4'); + $objWriter->writeAttribute('lowestEdited', '4'); + $objWriter->writeAttribute('rupBuild', '4505'); + $objWriter->endElement(); + } + + /** + * Write WorkbookPr. + * + * @param XMLWriter $objWriter XML Writer + */ + private function writeWorkbookPr(XMLWriter $objWriter) + { + $objWriter->startElement('workbookPr'); + + if (Date::getExcelCalendar() == Date::CALENDAR_MAC_1904) { + $objWriter->writeAttribute('date1904', '1'); + } + + $objWriter->writeAttribute('codeName', 'ThisWorkbook'); + + $objWriter->endElement(); + } + + /** + * Write BookViews. + * + * @param XMLWriter $objWriter XML Writer + * @param Spreadsheet $spreadsheet + */ + private function writeBookViews(XMLWriter $objWriter, Spreadsheet $spreadsheet) + { + // bookViews + $objWriter->startElement('bookViews'); + + // workbookView + $objWriter->startElement('workbookView'); + + $objWriter->writeAttribute('activeTab', $spreadsheet->getActiveSheetIndex()); + $objWriter->writeAttribute('autoFilterDateGrouping', ($spreadsheet->getAutoFilterDateGrouping() ? 'true' : 'false')); + $objWriter->writeAttribute('firstSheet', $spreadsheet->getFirstSheetIndex()); + $objWriter->writeAttribute('minimized', ($spreadsheet->getMinimized() ? 'true' : 'false')); + $objWriter->writeAttribute('showHorizontalScroll', ($spreadsheet->getShowHorizontalScroll() ? 'true' : 'false')); + $objWriter->writeAttribute('showSheetTabs', ($spreadsheet->getShowSheetTabs() ? 'true' : 'false')); + $objWriter->writeAttribute('showVerticalScroll', ($spreadsheet->getShowVerticalScroll() ? 'true' : 'false')); + $objWriter->writeAttribute('tabRatio', $spreadsheet->getTabRatio()); + $objWriter->writeAttribute('visibility', $spreadsheet->getVisibility()); + + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write WorkbookProtection. + * + * @param XMLWriter $objWriter XML Writer + * @param Spreadsheet $spreadsheet + */ + private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spreadsheet) + { + if ($spreadsheet->getSecurity()->isSecurityEnabled()) { + $objWriter->startElement('workbookProtection'); + $objWriter->writeAttribute('lockRevision', ($spreadsheet->getSecurity()->getLockRevision() ? 'true' : 'false')); + $objWriter->writeAttribute('lockStructure', ($spreadsheet->getSecurity()->getLockStructure() ? 'true' : 'false')); + $objWriter->writeAttribute('lockWindows', ($spreadsheet->getSecurity()->getLockWindows() ? 'true' : 'false')); + + if ($spreadsheet->getSecurity()->getRevisionsPassword() != '') { + $objWriter->writeAttribute('revisionsPassword', $spreadsheet->getSecurity()->getRevisionsPassword()); + } + + if ($spreadsheet->getSecurity()->getWorkbookPassword() != '') { + $objWriter->writeAttribute('workbookPassword', $spreadsheet->getSecurity()->getWorkbookPassword()); + } + + $objWriter->endElement(); + } + } + + /** + * Write calcPr. + * + * @param XMLWriter $objWriter XML Writer + * @param bool $recalcRequired Indicate whether formulas should be recalculated before writing + */ + private function writeCalcPr(XMLWriter $objWriter, $recalcRequired = true) + { + $objWriter->startElement('calcPr'); + + // Set the calcid to a higher value than Excel itself will use, otherwise Excel will always recalc + // If MS Excel does do a recalc, then users opening a file in MS Excel will be prompted to save on exit + // because the file has changed + $objWriter->writeAttribute('calcId', '999999'); + $objWriter->writeAttribute('calcMode', 'auto'); + // fullCalcOnLoad isn't needed if we've recalculating for the save + $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? 1 : 0); + $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? 0 : 1); + $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? 0 : 1); + + $objWriter->endElement(); + } + + /** + * Write sheets. + * + * @param XMLWriter $objWriter XML Writer + * @param Spreadsheet $spreadsheet + * + * @throws WriterException + */ + private function writeSheets(XMLWriter $objWriter, Spreadsheet $spreadsheet) + { + // Write sheets + $objWriter->startElement('sheets'); + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + // sheet + $this->writeSheet( + $objWriter, + $spreadsheet->getSheet($i)->getTitle(), + ($i + 1), + ($i + 1 + 3), + $spreadsheet->getSheet($i)->getSheetState() + ); + } + + $objWriter->endElement(); + } + + /** + * Write sheet. + * + * @param XMLWriter $objWriter XML Writer + * @param string $pSheetname Sheet name + * @param int $pSheetId Sheet id + * @param int $pRelId Relationship ID + * @param string $sheetState Sheet state (visible, hidden, veryHidden) + * + * @throws WriterException + */ + private function writeSheet(XMLWriter $objWriter, $pSheetname, $pSheetId = 1, $pRelId = 1, $sheetState = 'visible') + { + if ($pSheetname != '') { + // Write sheet + $objWriter->startElement('sheet'); + $objWriter->writeAttribute('name', $pSheetname); + $objWriter->writeAttribute('sheetId', $pSheetId); + if ($sheetState != 'visible' && $sheetState != '') { + $objWriter->writeAttribute('state', $sheetState); + } + $objWriter->writeAttribute('r:id', 'rId' . $pRelId); + $objWriter->endElement(); + } else { + throw new WriterException('Invalid parameters passed.'); + } + } + + /** + * Write Defined Names. + * + * @param XMLWriter $objWriter XML Writer + * @param Spreadsheet $spreadsheet + * + * @throws WriterException + */ + private function writeDefinedNames(XMLWriter $objWriter, Spreadsheet $spreadsheet) + { + // Write defined names + $objWriter->startElement('definedNames'); + + // Named ranges + if (count($spreadsheet->getNamedRanges()) > 0) { + // Named ranges + $this->writeNamedRanges($objWriter, $spreadsheet); + } + + // Other defined names + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + // definedName for autoFilter + $this->writeDefinedNameForAutofilter($objWriter, $spreadsheet->getSheet($i), $i); + + // definedName for Print_Titles + $this->writeDefinedNameForPrintTitles($objWriter, $spreadsheet->getSheet($i), $i); + + // definedName for Print_Area + $this->writeDefinedNameForPrintArea($objWriter, $spreadsheet->getSheet($i), $i); + } + + $objWriter->endElement(); + } + + /** + * Write named ranges. + * + * @param XMLWriter $objWriter XML Writer + * @param Spreadsheet $spreadsheet + * + * @throws WriterException + */ + private function writeNamedRanges(XMLWriter $objWriter, Spreadsheet $spreadsheet) + { + // Loop named ranges + $namedRanges = $spreadsheet->getNamedRanges(); + foreach ($namedRanges as $namedRange) { + $this->writeDefinedNameForNamedRange($objWriter, $namedRange); + } + } + + /** + * Write Defined Name for named range. + * + * @param XMLWriter $objWriter XML Writer + * @param NamedRange $pNamedRange + */ + private function writeDefinedNameForNamedRange(XMLWriter $objWriter, NamedRange $pNamedRange) + { + // definedName for named range + $objWriter->startElement('definedName'); + $objWriter->writeAttribute('name', $pNamedRange->getName()); + if ($pNamedRange->getLocalOnly()) { + $objWriter->writeAttribute('localSheetId', $pNamedRange->getScope()->getParent()->getIndex($pNamedRange->getScope())); + } + + // Create absolute coordinate and write as raw text + $range = Coordinate::splitRange($pNamedRange->getRange()); + $iMax = count($range); + for ($i = 0; $i < $iMax; ++$i) { + $range[$i][0] = '\'' . str_replace("'", "''", $pNamedRange->getWorksheet()->getTitle()) . '\'!' . Coordinate::absoluteReference($range[$i][0]); + if (isset($range[$i][1])) { + $range[$i][1] = Coordinate::absoluteReference($range[$i][1]); + } + } + $range = Coordinate::buildRange($range); + + $objWriter->writeRawData($range); + + $objWriter->endElement(); + } + + /** + * Write Defined Name for autoFilter. + * + * @param XMLWriter $objWriter XML Writer + * @param Worksheet $pSheet + * @param int $pSheetId + */ + private function writeDefinedNameForAutofilter(XMLWriter $objWriter, Worksheet $pSheet, $pSheetId = 0) + { + // definedName for autoFilter + $autoFilterRange = $pSheet->getAutoFilter()->getRange(); + if (!empty($autoFilterRange)) { + $objWriter->startElement('definedName'); + $objWriter->writeAttribute('name', '_xlnm._FilterDatabase'); + $objWriter->writeAttribute('localSheetId', $pSheetId); + $objWriter->writeAttribute('hidden', '1'); + + // Create absolute coordinate and write as raw text + $range = Coordinate::splitRange($autoFilterRange); + $range = $range[0]; + // Strip any worksheet ref so we can make the cell ref absolute + list($ws, $range[0]) = Worksheet::extractSheetTitle($range[0], true); + + $range[0] = Coordinate::absoluteCoordinate($range[0]); + $range[1] = Coordinate::absoluteCoordinate($range[1]); + $range = implode(':', $range); + + $objWriter->writeRawData('\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!' . $range); + + $objWriter->endElement(); + } + } + + /** + * Write Defined Name for PrintTitles. + * + * @param XMLWriter $objWriter XML Writer + * @param Worksheet $pSheet + * @param int $pSheetId + */ + private function writeDefinedNameForPrintTitles(XMLWriter $objWriter, Worksheet $pSheet, $pSheetId = 0) + { + // definedName for PrintTitles + if ($pSheet->getPageSetup()->isColumnsToRepeatAtLeftSet() || $pSheet->getPageSetup()->isRowsToRepeatAtTopSet()) { + $objWriter->startElement('definedName'); + $objWriter->writeAttribute('name', '_xlnm.Print_Titles'); + $objWriter->writeAttribute('localSheetId', $pSheetId); + + // Setting string + $settingString = ''; + + // Columns to repeat + if ($pSheet->getPageSetup()->isColumnsToRepeatAtLeftSet()) { + $repeat = $pSheet->getPageSetup()->getColumnsToRepeatAtLeft(); + + $settingString .= '\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!$' . $repeat[0] . ':$' . $repeat[1]; + } + + // Rows to repeat + if ($pSheet->getPageSetup()->isRowsToRepeatAtTopSet()) { + if ($pSheet->getPageSetup()->isColumnsToRepeatAtLeftSet()) { + $settingString .= ','; + } + + $repeat = $pSheet->getPageSetup()->getRowsToRepeatAtTop(); + + $settingString .= '\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!$' . $repeat[0] . ':$' . $repeat[1]; + } + + $objWriter->writeRawData($settingString); + + $objWriter->endElement(); + } + } + + /** + * Write Defined Name for PrintTitles. + * + * @param XMLWriter $objWriter XML Writer + * @param Worksheet $pSheet + * @param int $pSheetId + */ + private function writeDefinedNameForPrintArea(XMLWriter $objWriter, Worksheet $pSheet, $pSheetId = 0) + { + // definedName for PrintArea + if ($pSheet->getPageSetup()->isPrintAreaSet()) { + $objWriter->startElement('definedName'); + $objWriter->writeAttribute('name', '_xlnm.Print_Area'); + $objWriter->writeAttribute('localSheetId', $pSheetId); + + // Print area + $printArea = Coordinate::splitRange($pSheet->getPageSetup()->getPrintArea()); + + $chunks = []; + foreach ($printArea as $printAreaRect) { + $printAreaRect[0] = Coordinate::absoluteReference($printAreaRect[0]); + $printAreaRect[1] = Coordinate::absoluteReference($printAreaRect[1]); + $chunks[] = '\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!' . implode(':', $printAreaRect); + } + + $objWriter->writeRawData(implode(',', $chunks)); + + $objWriter->endElement(); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Worksheet.php new file mode 100644 index 00000000000..78a62e9eca4 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -0,0 +1,1224 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Worksheet + $objWriter->startElement('worksheet'); + $objWriter->writeAttribute('xml:space', 'preserve'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + $objWriter->writeAttribute('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing'); + $objWriter->writeAttribute('xmlns:x14', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'); + $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); + $objWriter->writeAttribute('mc:Ignorable', 'x14ac'); + $objWriter->writeAttribute('xmlns:x14ac', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac'); + + // sheetPr + $this->writeSheetPr($objWriter, $pSheet); + + // Dimension + $this->writeDimension($objWriter, $pSheet); + + // sheetViews + $this->writeSheetViews($objWriter, $pSheet); + + // sheetFormatPr + $this->writeSheetFormatPr($objWriter, $pSheet); + + // cols + $this->writeCols($objWriter, $pSheet); + + // sheetData + $this->writeSheetData($objWriter, $pSheet, $pStringTable); + + // sheetProtection + $this->writeSheetProtection($objWriter, $pSheet); + + // protectedRanges + $this->writeProtectedRanges($objWriter, $pSheet); + + // autoFilter + $this->writeAutoFilter($objWriter, $pSheet); + + // mergeCells + $this->writeMergeCells($objWriter, $pSheet); + + // conditionalFormatting + $this->writeConditionalFormatting($objWriter, $pSheet); + + // dataValidations + $this->writeDataValidations($objWriter, $pSheet); + + // hyperlinks + $this->writeHyperlinks($objWriter, $pSheet); + + // Print options + $this->writePrintOptions($objWriter, $pSheet); + + // Page margins + $this->writePageMargins($objWriter, $pSheet); + + // Page setup + $this->writePageSetup($objWriter, $pSheet); + + // Header / footer + $this->writeHeaderFooter($objWriter, $pSheet); + + // Breaks + $this->writeBreaks($objWriter, $pSheet); + + // Drawings and/or Charts + $this->writeDrawings($objWriter, $pSheet, $includeCharts); + + // LegacyDrawing + $this->writeLegacyDrawing($objWriter, $pSheet); + + // LegacyDrawingHF + $this->writeLegacyDrawingHF($objWriter, $pSheet); + + // AlternateContent + $this->writeAlternateContent($objWriter, $pSheet); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write SheetPr. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // sheetPr + $objWriter->startElement('sheetPr'); + if ($pSheet->getParent()->hasMacros()) { + //if the workbook have macros, we need to have codeName for the sheet + if ($pSheet->hasCodeName() == false) { + $pSheet->setCodeName($pSheet->getTitle()); + } + $objWriter->writeAttribute('codeName', $pSheet->getCodeName()); + } + $autoFilterRange = $pSheet->getAutoFilter()->getRange(); + if (!empty($autoFilterRange)) { + $objWriter->writeAttribute('filterMode', 1); + $pSheet->getAutoFilter()->showHideRows(); + } + + // tabColor + if ($pSheet->isTabColorSet()) { + $objWriter->startElement('tabColor'); + $objWriter->writeAttribute('rgb', $pSheet->getTabColor()->getARGB()); + $objWriter->endElement(); + } + + // outlinePr + $objWriter->startElement('outlinePr'); + $objWriter->writeAttribute('summaryBelow', ($pSheet->getShowSummaryBelow() ? '1' : '0')); + $objWriter->writeAttribute('summaryRight', ($pSheet->getShowSummaryRight() ? '1' : '0')); + $objWriter->endElement(); + + // pageSetUpPr + if ($pSheet->getPageSetup()->getFitToPage()) { + $objWriter->startElement('pageSetUpPr'); + $objWriter->writeAttribute('fitToPage', '1'); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + + /** + * Write Dimension. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeDimension(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // dimension + $objWriter->startElement('dimension'); + $objWriter->writeAttribute('ref', $pSheet->calculateWorksheetDimension()); + $objWriter->endElement(); + } + + /** + * Write SheetViews. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + * + * @throws WriterException + */ + private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // sheetViews + $objWriter->startElement('sheetViews'); + + // Sheet selected? + $sheetSelected = false; + if ($this->getParentWriter()->getSpreadsheet()->getIndex($pSheet) == $this->getParentWriter()->getSpreadsheet()->getActiveSheetIndex()) { + $sheetSelected = true; + } + + // sheetView + $objWriter->startElement('sheetView'); + $objWriter->writeAttribute('tabSelected', $sheetSelected ? '1' : '0'); + $objWriter->writeAttribute('workbookViewId', '0'); + + // Zoom scales + if ($pSheet->getSheetView()->getZoomScale() != 100) { + $objWriter->writeAttribute('zoomScale', $pSheet->getSheetView()->getZoomScale()); + } + if ($pSheet->getSheetView()->getZoomScaleNormal() != 100) { + $objWriter->writeAttribute('zoomScaleNormal', $pSheet->getSheetView()->getZoomScaleNormal()); + } + + // View Layout Type + if ($pSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_NORMAL) { + $objWriter->writeAttribute('view', $pSheet->getSheetView()->getView()); + } + + // Gridlines + if ($pSheet->getShowGridlines()) { + $objWriter->writeAttribute('showGridLines', 'true'); + } else { + $objWriter->writeAttribute('showGridLines', 'false'); + } + + // Row and column headers + if ($pSheet->getShowRowColHeaders()) { + $objWriter->writeAttribute('showRowColHeaders', '1'); + } else { + $objWriter->writeAttribute('showRowColHeaders', '0'); + } + + // Right-to-left + if ($pSheet->getRightToLeft()) { + $objWriter->writeAttribute('rightToLeft', 'true'); + } + + $activeCell = $pSheet->getActiveCell(); + $sqref = $pSheet->getSelectedCells(); + + // Pane + $pane = ''; + if ($pSheet->getFreezePane()) { + list($xSplit, $ySplit) = Coordinate::coordinateFromString($pSheet->getFreezePane()); + $xSplit = Coordinate::columnIndexFromString($xSplit); + --$xSplit; + --$ySplit; + + $topLeftCell = $pSheet->getTopLeftCell(); + $activeCell = $topLeftCell; + $sqref = $topLeftCell; + + // pane + $pane = 'topRight'; + $objWriter->startElement('pane'); + if ($xSplit > 0) { + $objWriter->writeAttribute('xSplit', $xSplit); + } + if ($ySplit > 0) { + $objWriter->writeAttribute('ySplit', $ySplit); + $pane = ($xSplit > 0) ? 'bottomRight' : 'bottomLeft'; + } + $objWriter->writeAttribute('topLeftCell', $topLeftCell); + $objWriter->writeAttribute('activePane', $pane); + $objWriter->writeAttribute('state', 'frozen'); + $objWriter->endElement(); + + if (($xSplit > 0) && ($ySplit > 0)) { + // Write additional selections if more than two panes (ie both an X and a Y split) + $objWriter->startElement('selection'); + $objWriter->writeAttribute('pane', 'topRight'); + $objWriter->endElement(); + $objWriter->startElement('selection'); + $objWriter->writeAttribute('pane', 'bottomLeft'); + $objWriter->endElement(); + } + } + + // Selection + // Only need to write selection element if we have a split pane + // We cheat a little by over-riding the active cell selection, setting it to the split cell + $objWriter->startElement('selection'); + if ($pane != '') { + $objWriter->writeAttribute('pane', $pane); + } + $objWriter->writeAttribute('activeCell', $activeCell); + $objWriter->writeAttribute('sqref', $sqref); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write SheetFormatPr. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeSheetFormatPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // sheetFormatPr + $objWriter->startElement('sheetFormatPr'); + + // Default row height + if ($pSheet->getDefaultRowDimension()->getRowHeight() >= 0) { + $objWriter->writeAttribute('customHeight', 'true'); + $objWriter->writeAttribute('defaultRowHeight', StringHelper::formatNumber($pSheet->getDefaultRowDimension()->getRowHeight())); + } else { + $objWriter->writeAttribute('defaultRowHeight', '14.4'); + } + + // Set Zero Height row + if ((string) $pSheet->getDefaultRowDimension()->getZeroHeight() == '1' || + strtolower((string) $pSheet->getDefaultRowDimension()->getZeroHeight()) == 'true') { + $objWriter->writeAttribute('zeroHeight', '1'); + } + + // Default column width + if ($pSheet->getDefaultColumnDimension()->getWidth() >= 0) { + $objWriter->writeAttribute('defaultColWidth', StringHelper::formatNumber($pSheet->getDefaultColumnDimension()->getWidth())); + } + + // Outline level - row + $outlineLevelRow = 0; + foreach ($pSheet->getRowDimensions() as $dimension) { + if ($dimension->getOutlineLevel() > $outlineLevelRow) { + $outlineLevelRow = $dimension->getOutlineLevel(); + } + } + $objWriter->writeAttribute('outlineLevelRow', (int) $outlineLevelRow); + + // Outline level - column + $outlineLevelCol = 0; + foreach ($pSheet->getColumnDimensions() as $dimension) { + if ($dimension->getOutlineLevel() > $outlineLevelCol) { + $outlineLevelCol = $dimension->getOutlineLevel(); + } + } + $objWriter->writeAttribute('outlineLevelCol', (int) $outlineLevelCol); + + $objWriter->endElement(); + } + + /** + * Write Cols. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeCols(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // cols + if (count($pSheet->getColumnDimensions()) > 0) { + $objWriter->startElement('cols'); + + $pSheet->calculateColumnWidths(); + + // Loop through column dimensions + foreach ($pSheet->getColumnDimensions() as $colDimension) { + // col + $objWriter->startElement('col'); + $objWriter->writeAttribute('min', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('max', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + + if ($colDimension->getWidth() < 0) { + // No width set, apply default of 10 + $objWriter->writeAttribute('width', '9.10'); + } else { + // Width set + $objWriter->writeAttribute('width', StringHelper::formatNumber($colDimension->getWidth())); + } + + // Column visibility + if ($colDimension->getVisible() == false) { + $objWriter->writeAttribute('hidden', 'true'); + } + + // Auto size? + if ($colDimension->getAutoSize()) { + $objWriter->writeAttribute('bestFit', 'true'); + } + + // Custom width? + if ($colDimension->getWidth() != $pSheet->getDefaultColumnDimension()->getWidth()) { + $objWriter->writeAttribute('customWidth', 'true'); + } + + // Collapsed + if ($colDimension->getCollapsed() == true) { + $objWriter->writeAttribute('collapsed', 'true'); + } + + // Outline level + if ($colDimension->getOutlineLevel() > 0) { + $objWriter->writeAttribute('outlineLevel', $colDimension->getOutlineLevel()); + } + + // Style + $objWriter->writeAttribute('style', $colDimension->getXfIndex()); + + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + } + + /** + * Write SheetProtection. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeSheetProtection(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // sheetProtection + $objWriter->startElement('sheetProtection'); + + if ($pSheet->getProtection()->getPassword() != '') { + $objWriter->writeAttribute('password', $pSheet->getProtection()->getPassword()); + } + + $objWriter->writeAttribute('sheet', ($pSheet->getProtection()->getSheet() ? 'true' : 'false')); + $objWriter->writeAttribute('objects', ($pSheet->getProtection()->getObjects() ? 'true' : 'false')); + $objWriter->writeAttribute('scenarios', ($pSheet->getProtection()->getScenarios() ? 'true' : 'false')); + $objWriter->writeAttribute('formatCells', ($pSheet->getProtection()->getFormatCells() ? 'true' : 'false')); + $objWriter->writeAttribute('formatColumns', ($pSheet->getProtection()->getFormatColumns() ? 'true' : 'false')); + $objWriter->writeAttribute('formatRows', ($pSheet->getProtection()->getFormatRows() ? 'true' : 'false')); + $objWriter->writeAttribute('insertColumns', ($pSheet->getProtection()->getInsertColumns() ? 'true' : 'false')); + $objWriter->writeAttribute('insertRows', ($pSheet->getProtection()->getInsertRows() ? 'true' : 'false')); + $objWriter->writeAttribute('insertHyperlinks', ($pSheet->getProtection()->getInsertHyperlinks() ? 'true' : 'false')); + $objWriter->writeAttribute('deleteColumns', ($pSheet->getProtection()->getDeleteColumns() ? 'true' : 'false')); + $objWriter->writeAttribute('deleteRows', ($pSheet->getProtection()->getDeleteRows() ? 'true' : 'false')); + $objWriter->writeAttribute('selectLockedCells', ($pSheet->getProtection()->getSelectLockedCells() ? 'true' : 'false')); + $objWriter->writeAttribute('sort', ($pSheet->getProtection()->getSort() ? 'true' : 'false')); + $objWriter->writeAttribute('autoFilter', ($pSheet->getProtection()->getAutoFilter() ? 'true' : 'false')); + $objWriter->writeAttribute('pivotTables', ($pSheet->getProtection()->getPivotTables() ? 'true' : 'false')); + $objWriter->writeAttribute('selectUnlockedCells', ($pSheet->getProtection()->getSelectUnlockedCells() ? 'true' : 'false')); + $objWriter->endElement(); + } + + /** + * Write ConditionalFormatting. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + * + * @throws WriterException + */ + private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // Conditional id + $id = 1; + + // Loop through styles in the current worksheet + foreach ($pSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { + foreach ($conditionalStyles as $conditional) { + // WHY was this again? + // if ($this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) == '') { + // continue; + // } + if ($conditional->getConditionType() != Conditional::CONDITION_NONE) { + // conditionalFormatting + $objWriter->startElement('conditionalFormatting'); + $objWriter->writeAttribute('sqref', $cellCoordinate); + + // cfRule + $objWriter->startElement('cfRule'); + $objWriter->writeAttribute('type', $conditional->getConditionType()); + $objWriter->writeAttribute('dxfId', $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())); + $objWriter->writeAttribute('priority', $id++); + + if (($conditional->getConditionType() == Conditional::CONDITION_CELLIS || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT) + && $conditional->getOperatorType() != Conditional::OPERATOR_NONE) { + $objWriter->writeAttribute('operator', $conditional->getOperatorType()); + } + + if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT + && $conditional->getText() !== null) { + $objWriter->writeAttribute('text', $conditional->getText()); + } + + if ($conditional->getStopIfTrue()) { + $objWriter->writeAttribute('stopIfTrue', '1'); + } + + if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT + && $conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT + && $conditional->getText() !== null) { + $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $conditional->getText() . '",' . $cellCoordinate . ')))'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT + && $conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH + && $conditional->getText() !== null) { + $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',' . strlen($conditional->getText()) . ')="' . $conditional->getText() . '"'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT + && $conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH + && $conditional->getText() !== null) { + $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',' . strlen($conditional->getText()) . ')="' . $conditional->getText() . '"'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT + && $conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS + && $conditional->getText() !== null) { + $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $conditional->getText() . '",' . $cellCoordinate . '))'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS + || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT + || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { + foreach ($conditional->getConditions() as $formula) { + // Formula + $objWriter->writeElement('formula', $formula); + } + } + + $objWriter->endElement(); + + $objWriter->endElement(); + } + } + } + } + + /** + * Write DataValidations. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // Datavalidation collection + $dataValidationCollection = $pSheet->getDataValidationCollection(); + + // Write data validations? + if (!empty($dataValidationCollection)) { + $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection); + $objWriter->startElement('dataValidations'); + $objWriter->writeAttribute('count', count($dataValidationCollection)); + + foreach ($dataValidationCollection as $coordinate => $dv) { + $objWriter->startElement('dataValidation'); + + if ($dv->getType() != '') { + $objWriter->writeAttribute('type', $dv->getType()); + } + + if ($dv->getErrorStyle() != '') { + $objWriter->writeAttribute('errorStyle', $dv->getErrorStyle()); + } + + if ($dv->getOperator() != '') { + $objWriter->writeAttribute('operator', $dv->getOperator()); + } + + $objWriter->writeAttribute('allowBlank', ($dv->getAllowBlank() ? '1' : '0')); + $objWriter->writeAttribute('showDropDown', (!$dv->getShowDropDown() ? '1' : '0')); + $objWriter->writeAttribute('showInputMessage', ($dv->getShowInputMessage() ? '1' : '0')); + $objWriter->writeAttribute('showErrorMessage', ($dv->getShowErrorMessage() ? '1' : '0')); + + if ($dv->getErrorTitle() !== '') { + $objWriter->writeAttribute('errorTitle', $dv->getErrorTitle()); + } + if ($dv->getError() !== '') { + $objWriter->writeAttribute('error', $dv->getError()); + } + if ($dv->getPromptTitle() !== '') { + $objWriter->writeAttribute('promptTitle', $dv->getPromptTitle()); + } + if ($dv->getPrompt() !== '') { + $objWriter->writeAttribute('prompt', $dv->getPrompt()); + } + + $objWriter->writeAttribute('sqref', $coordinate); + + if ($dv->getFormula1() !== '') { + $objWriter->writeElement('formula1', $dv->getFormula1()); + } + if ($dv->getFormula2() !== '') { + $objWriter->writeElement('formula2', $dv->getFormula2()); + } + + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + } + + /** + * Write Hyperlinks. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeHyperlinks(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // Hyperlink collection + $hyperlinkCollection = $pSheet->getHyperlinkCollection(); + + // Relation ID + $relationId = 1; + + // Write hyperlinks? + if (!empty($hyperlinkCollection)) { + $objWriter->startElement('hyperlinks'); + + foreach ($hyperlinkCollection as $coordinate => $hyperlink) { + $objWriter->startElement('hyperlink'); + + $objWriter->writeAttribute('ref', $coordinate); + if (!$hyperlink->isInternal()) { + $objWriter->writeAttribute('r:id', 'rId_hyperlink_' . $relationId); + ++$relationId; + } else { + $objWriter->writeAttribute('location', str_replace('sheet://', '', $hyperlink->getUrl())); + } + + if ($hyperlink->getTooltip() != '') { + $objWriter->writeAttribute('tooltip', $hyperlink->getTooltip()); + } + + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + } + + /** + * Write ProtectedRanges. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeProtectedRanges(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + if (count($pSheet->getProtectedCells()) > 0) { + // protectedRanges + $objWriter->startElement('protectedRanges'); + + // Loop protectedRanges + foreach ($pSheet->getProtectedCells() as $protectedCell => $passwordHash) { + // protectedRange + $objWriter->startElement('protectedRange'); + $objWriter->writeAttribute('name', 'p' . md5($protectedCell)); + $objWriter->writeAttribute('sqref', $protectedCell); + if (!empty($passwordHash)) { + $objWriter->writeAttribute('password', $passwordHash); + } + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + } + + /** + * Write MergeCells. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeMergeCells(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + if (count($pSheet->getMergeCells()) > 0) { + // mergeCells + $objWriter->startElement('mergeCells'); + + // Loop mergeCells + foreach ($pSheet->getMergeCells() as $mergeCell) { + // mergeCell + $objWriter->startElement('mergeCell'); + $objWriter->writeAttribute('ref', $mergeCell); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + } + + /** + * Write PrintOptions. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writePrintOptions(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // printOptions + $objWriter->startElement('printOptions'); + + $objWriter->writeAttribute('gridLines', ($pSheet->getPrintGridlines() ? 'true' : 'false')); + $objWriter->writeAttribute('gridLinesSet', 'true'); + + if ($pSheet->getPageSetup()->getHorizontalCentered()) { + $objWriter->writeAttribute('horizontalCentered', 'true'); + } + + if ($pSheet->getPageSetup()->getVerticalCentered()) { + $objWriter->writeAttribute('verticalCentered', 'true'); + } + + $objWriter->endElement(); + } + + /** + * Write PageMargins. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writePageMargins(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // pageMargins + $objWriter->startElement('pageMargins'); + $objWriter->writeAttribute('left', StringHelper::formatNumber($pSheet->getPageMargins()->getLeft())); + $objWriter->writeAttribute('right', StringHelper::formatNumber($pSheet->getPageMargins()->getRight())); + $objWriter->writeAttribute('top', StringHelper::formatNumber($pSheet->getPageMargins()->getTop())); + $objWriter->writeAttribute('bottom', StringHelper::formatNumber($pSheet->getPageMargins()->getBottom())); + $objWriter->writeAttribute('header', StringHelper::formatNumber($pSheet->getPageMargins()->getHeader())); + $objWriter->writeAttribute('footer', StringHelper::formatNumber($pSheet->getPageMargins()->getFooter())); + $objWriter->endElement(); + } + + /** + * Write AutoFilter. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + $autoFilterRange = $pSheet->getAutoFilter()->getRange(); + if (!empty($autoFilterRange)) { + // autoFilter + $objWriter->startElement('autoFilter'); + + // Strip any worksheet reference from the filter coordinates + $range = Coordinate::splitRange($autoFilterRange); + $range = $range[0]; + // Strip any worksheet ref + list($ws, $range[0]) = PhpspreadsheetWorksheet::extractSheetTitle($range[0], true); + $range = implode(':', $range); + + $objWriter->writeAttribute('ref', str_replace('$', '', $range)); + + $columns = $pSheet->getAutoFilter()->getColumns(); + if (count($columns) > 0) { + foreach ($columns as $columnID => $column) { + $rules = $column->getRules(); + if (count($rules) > 0) { + $objWriter->startElement('filterColumn'); + $objWriter->writeAttribute('colId', $pSheet->getAutoFilter()->getColumnOffset($columnID)); + + $objWriter->startElement($column->getFilterType()); + if ($column->getJoin() == Column::AUTOFILTER_COLUMN_JOIN_AND) { + $objWriter->writeAttribute('and', 1); + } + + foreach ($rules as $rule) { + if (($column->getFilterType() === Column::AUTOFILTER_FILTERTYPE_FILTER) && + ($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_EQUAL) && + ($rule->getValue() === '')) { + // Filter rule for Blanks + $objWriter->writeAttribute('blank', 1); + } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER) { + // Dynamic Filter Rule + $objWriter->writeAttribute('type', $rule->getGrouping()); + $val = $column->getAttribute('val'); + if ($val !== null) { + $objWriter->writeAttribute('val', $val); + } + $maxVal = $column->getAttribute('maxVal'); + if ($maxVal !== null) { + $objWriter->writeAttribute('maxVal', $maxVal); + } + } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_TOPTENFILTER) { + // Top 10 Filter Rule + $objWriter->writeAttribute('val', $rule->getValue()); + $objWriter->writeAttribute('percent', (($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) ? '1' : '0')); + $objWriter->writeAttribute('top', (($rule->getGrouping() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) ? '1' : '0')); + } else { + // Filter, DateGroupItem or CustomFilter + $objWriter->startElement($rule->getRuleType()); + + if ($rule->getOperator() !== Rule::AUTOFILTER_COLUMN_RULE_EQUAL) { + $objWriter->writeAttribute('operator', $rule->getOperator()); + } + if ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DATEGROUP) { + // Date Group filters + foreach ($rule->getValue() as $key => $value) { + if ($value > '') { + $objWriter->writeAttribute($key, $value); + } + } + $objWriter->writeAttribute('dateTimeGrouping', $rule->getGrouping()); + } else { + $objWriter->writeAttribute('val', $rule->getValue()); + } + + $objWriter->endElement(); + } + } + + $objWriter->endElement(); + + $objWriter->endElement(); + } + } + } + $objWriter->endElement(); + } + } + + /** + * Write PageSetup. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // pageSetup + $objWriter->startElement('pageSetup'); + $objWriter->writeAttribute('paperSize', $pSheet->getPageSetup()->getPaperSize()); + $objWriter->writeAttribute('orientation', $pSheet->getPageSetup()->getOrientation()); + + if ($pSheet->getPageSetup()->getScale() !== null) { + $objWriter->writeAttribute('scale', $pSheet->getPageSetup()->getScale()); + } + if ($pSheet->getPageSetup()->getFitToHeight() !== null) { + $objWriter->writeAttribute('fitToHeight', $pSheet->getPageSetup()->getFitToHeight()); + } else { + $objWriter->writeAttribute('fitToHeight', '0'); + } + if ($pSheet->getPageSetup()->getFitToWidth() !== null) { + $objWriter->writeAttribute('fitToWidth', $pSheet->getPageSetup()->getFitToWidth()); + } else { + $objWriter->writeAttribute('fitToWidth', '0'); + } + if ($pSheet->getPageSetup()->getFirstPageNumber() !== null) { + $objWriter->writeAttribute('firstPageNumber', $pSheet->getPageSetup()->getFirstPageNumber()); + $objWriter->writeAttribute('useFirstPageNumber', '1'); + } + + $getUnparsedLoadedData = $pSheet->getParent()->getUnparsedLoadedData(); + if (isset($getUnparsedLoadedData['sheets'][$pSheet->getCodeName()]['pageSetupRelId'])) { + $objWriter->writeAttribute('r:id', $getUnparsedLoadedData['sheets'][$pSheet->getCodeName()]['pageSetupRelId']); + } + + $objWriter->endElement(); + } + + /** + * Write Header / Footer. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // headerFooter + $objWriter->startElement('headerFooter'); + $objWriter->writeAttribute('differentOddEven', ($pSheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false')); + $objWriter->writeAttribute('differentFirst', ($pSheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false')); + $objWriter->writeAttribute('scaleWithDoc', ($pSheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false')); + $objWriter->writeAttribute('alignWithMargins', ($pSheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false')); + + $objWriter->writeElement('oddHeader', $pSheet->getHeaderFooter()->getOddHeader()); + $objWriter->writeElement('oddFooter', $pSheet->getHeaderFooter()->getOddFooter()); + $objWriter->writeElement('evenHeader', $pSheet->getHeaderFooter()->getEvenHeader()); + $objWriter->writeElement('evenFooter', $pSheet->getHeaderFooter()->getEvenFooter()); + $objWriter->writeElement('firstHeader', $pSheet->getHeaderFooter()->getFirstHeader()); + $objWriter->writeElement('firstFooter', $pSheet->getHeaderFooter()->getFirstFooter()); + $objWriter->endElement(); + } + + /** + * Write Breaks. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeBreaks(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // Get row and column breaks + $aRowBreaks = []; + $aColumnBreaks = []; + foreach ($pSheet->getBreaks() as $cell => $breakType) { + if ($breakType == PhpspreadsheetWorksheet::BREAK_ROW) { + $aRowBreaks[] = $cell; + } elseif ($breakType == PhpspreadsheetWorksheet::BREAK_COLUMN) { + $aColumnBreaks[] = $cell; + } + } + + // rowBreaks + if (!empty($aRowBreaks)) { + $objWriter->startElement('rowBreaks'); + $objWriter->writeAttribute('count', count($aRowBreaks)); + $objWriter->writeAttribute('manualBreakCount', count($aRowBreaks)); + + foreach ($aRowBreaks as $cell) { + $coords = Coordinate::coordinateFromString($cell); + + $objWriter->startElement('brk'); + $objWriter->writeAttribute('id', $coords[1]); + $objWriter->writeAttribute('man', '1'); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + + // Second, write column breaks + if (!empty($aColumnBreaks)) { + $objWriter->startElement('colBreaks'); + $objWriter->writeAttribute('count', count($aColumnBreaks)); + $objWriter->writeAttribute('manualBreakCount', count($aColumnBreaks)); + + foreach ($aColumnBreaks as $cell) { + $coords = Coordinate::coordinateFromString($cell); + + $objWriter->startElement('brk'); + $objWriter->writeAttribute('id', Coordinate::columnIndexFromString($coords[0]) - 1); + $objWriter->writeAttribute('man', '1'); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + } + + /** + * Write SheetData. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + * @param string[] $pStringTable String table + * + * @throws WriterException + */ + private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, array $pStringTable) + { + // Flipped stringtable, for faster index searching + $aFlippedStringTable = $this->getParentWriter()->getWriterPart('stringtable')->flipStringTable($pStringTable); + + // sheetData + $objWriter->startElement('sheetData'); + + // Get column count + $colCount = Coordinate::columnIndexFromString($pSheet->getHighestColumn()); + + // Highest row number + $highestRow = $pSheet->getHighestRow(); + + // Loop through cells + $cellsByRow = []; + foreach ($pSheet->getCoordinates() as $coordinate) { + $cellAddress = Coordinate::coordinateFromString($coordinate); + $cellsByRow[$cellAddress[1]][] = $coordinate; + } + + $currentRow = 0; + while ($currentRow++ < $highestRow) { + // Get row dimension + $rowDimension = $pSheet->getRowDimension($currentRow); + + // Write current row? + $writeCurrentRow = isset($cellsByRow[$currentRow]) || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() == false || $rowDimension->getCollapsed() == true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null; + + if ($writeCurrentRow) { + // Start a new row + $objWriter->startElement('row'); + $objWriter->writeAttribute('r', $currentRow); + $objWriter->writeAttribute('spans', '1:' . $colCount); + + // Row dimensions + if ($rowDimension->getRowHeight() >= 0) { + $objWriter->writeAttribute('customHeight', '1'); + $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight())); + } + + // Row visibility + if ($rowDimension->getVisible() == false) { + $objWriter->writeAttribute('hidden', 'true'); + } + + // Collapsed + if ($rowDimension->getCollapsed() == true) { + $objWriter->writeAttribute('collapsed', 'true'); + } + + // Outline level + if ($rowDimension->getOutlineLevel() > 0) { + $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel()); + } + + // Style + if ($rowDimension->getXfIndex() !== null) { + $objWriter->writeAttribute('s', $rowDimension->getXfIndex()); + $objWriter->writeAttribute('customFormat', '1'); + } + + // Write cells + if (isset($cellsByRow[$currentRow])) { + foreach ($cellsByRow[$currentRow] as $cellAddress) { + // Write cell + $this->writeCell($objWriter, $pSheet, $cellAddress, $aFlippedStringTable); + } + } + + // End row + $objWriter->endElement(); + } + } + + $objWriter->endElement(); + } + + /** + * Write Cell. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + * @param Cell $pCellAddress Cell Address + * @param string[] $pFlippedStringTable String table (flipped), for faster index searching + * + * @throws WriterException + */ + private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, $pCellAddress, array $pFlippedStringTable) + { + // Cell + $pCell = $pSheet->getCell($pCellAddress); + $objWriter->startElement('c'); + $objWriter->writeAttribute('r', $pCellAddress); + + // Sheet styles + if ($pCell->getXfIndex() != '') { + $objWriter->writeAttribute('s', $pCell->getXfIndex()); + } + + // If cell value is supplied, write cell value + $cellValue = $pCell->getValue(); + if (is_object($cellValue) || $cellValue !== '') { + // Map type + $mappedType = $pCell->getDataType(); + + // Write data type depending on its type + switch (strtolower($mappedType)) { + case 'inlinestr': // Inline string + case 's': // String + case 'b': // Boolean + $objWriter->writeAttribute('t', $mappedType); + + break; + case 'f': // Formula + $calculatedValue = ($this->getParentWriter()->getPreCalculateFormulas()) ? + $pCell->getCalculatedValue() : $cellValue; + if (is_string($calculatedValue)) { + $objWriter->writeAttribute('t', 'str'); + } elseif (is_bool($calculatedValue)) { + $objWriter->writeAttribute('t', 'b'); + } + + break; + case 'e': // Error + $objWriter->writeAttribute('t', $mappedType); + } + + // Write data depending on its type + switch (strtolower($mappedType)) { + case 'inlinestr': // Inline string + if (!$cellValue instanceof RichText) { + $objWriter->writeElement('t', StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue))); + } elseif ($cellValue instanceof RichText) { + $objWriter->startElement('is'); + $this->getParentWriter()->getWriterPart('stringtable')->writeRichText($objWriter, $cellValue); + $objWriter->endElement(); + } + + break; + case 's': // String + if (!$cellValue instanceof RichText) { + if (isset($pFlippedStringTable[$cellValue])) { + $objWriter->writeElement('v', $pFlippedStringTable[$cellValue]); + } + } elseif ($cellValue instanceof RichText) { + $objWriter->writeElement('v', $pFlippedStringTable[$cellValue->getHashCode()]); + } + + break; + case 'f': // Formula + $attributes = $pCell->getFormulaAttributes(); + if ($attributes['t'] == 'array') { + $objWriter->startElement('f'); + $objWriter->writeAttribute('t', 'array'); + $objWriter->writeAttribute('ref', $pCellAddress); + $objWriter->writeAttribute('aca', '1'); + $objWriter->writeAttribute('ca', '1'); + $objWriter->text(substr($cellValue, 1)); + $objWriter->endElement(); + } else { + $objWriter->writeElement('f', substr($cellValue, 1)); + } + if ($this->getParentWriter()->getOffice2003Compatibility() === false) { + if ($this->getParentWriter()->getPreCalculateFormulas()) { + if (!is_array($calculatedValue) && substr($calculatedValue, 0, 1) != '#') { + $objWriter->writeElement('v', StringHelper::formatNumber($calculatedValue)); + } else { + $objWriter->writeElement('v', '0'); + } + } else { + $objWriter->writeElement('v', '0'); + } + } + + break; + case 'n': // Numeric + // force point as decimal separator in case current locale uses comma + $objWriter->writeElement('v', str_replace(',', '.', $cellValue)); + + break; + case 'b': // Boolean + $objWriter->writeElement('v', ($cellValue ? '1' : '0')); + + break; + case 'e': // Error + if (substr($cellValue, 0, 1) == '=') { + $objWriter->writeElement('f', substr($cellValue, 1)); + $objWriter->writeElement('v', substr($cellValue, 1)); + } else { + $objWriter->writeElement('v', $cellValue); + } + + break; + } + } + + $objWriter->endElement(); + } + + /** + * Write Drawings. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + * @param bool $includeCharts Flag indicating if we should include drawing details for charts + */ + private function writeDrawings(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, $includeCharts = false) + { + $unparsedLoadedData = $pSheet->getParent()->getUnparsedLoadedData(); + $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$pSheet->getCodeName()]['drawingOriginalIds']); + $chartCount = ($includeCharts) ? $pSheet->getChartCollection()->count() : 0; + if ($chartCount == 0 && $pSheet->getDrawingCollection()->count() == 0 && !$hasUnparsedDrawing) { + return; + } + + // If sheet contains drawings, add the relationships + $objWriter->startElement('drawing'); + + $rId = 'rId1'; + if (isset($unparsedLoadedData['sheets'][$pSheet->getCodeName()]['drawingOriginalIds'])) { + $drawingOriginalIds = $unparsedLoadedData['sheets'][$pSheet->getCodeName()]['drawingOriginalIds']; + // take first. In future can be overriten + $rId = reset($drawingOriginalIds); + } + + $objWriter->writeAttribute('r:id', $rId); + $objWriter->endElement(); + } + + /** + * Write LegacyDrawing. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeLegacyDrawing(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // If sheet contains comments, add the relationships + if (count($pSheet->getComments()) > 0) { + $objWriter->startElement('legacyDrawing'); + $objWriter->writeAttribute('r:id', 'rId_comments_vml1'); + $objWriter->endElement(); + } + } + + /** + * Write LegacyDrawingHF. + * + * @param XMLWriter $objWriter XML Writer + * @param PhpspreadsheetWorksheet $pSheet Worksheet + */ + private function writeLegacyDrawingHF(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + // If sheet contains images, add the relationships + if (count($pSheet->getHeaderFooter()->getImages()) > 0) { + $objWriter->startElement('legacyDrawingHF'); + $objWriter->writeAttribute('r:id', 'rId_headerfooter_vml1'); + $objWriter->endElement(); + } + } + + private function writeAlternateContent(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet) + { + if (empty($pSheet->getParent()->getUnparsedLoadedData()['sheets'][$pSheet->getCodeName()]['AlternateContents'])) { + return; + } + + foreach ($pSheet->getParent()->getUnparsedLoadedData()['sheets'][$pSheet->getCodeName()]['AlternateContents'] as $alternateContent) { + $objWriter->writeRaw($alternateContent); + } + } +} diff --git a/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/WriterPart.php b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/WriterPart.php new file mode 100644 index 00000000000..7119512ce41 --- /dev/null +++ b/htdocs/includes/phpoffice/PhpSpreadsheet/Writer/Xlsx/WriterPart.php @@ -0,0 +1,35 @@ +parentWriter; + } + + /** + * Set parent Xlsx object. + * + * @param Xlsx $pWriter + */ + public function __construct(Xlsx $pWriter) + { + $this->parentWriter = $pWriter; + } +} diff --git a/htdocs/includes/phpoffice/autoloader.php b/htdocs/includes/phpoffice/autoloader.php new file mode 100644 index 00000000000..71e94e9b738 --- /dev/null +++ b/htdocs/includes/phpoffice/autoloader.php @@ -0,0 +1,10 @@ +%s ExampleAnyCodeOrIdFoundIntoDictionary=Tout code (ou id) trouvée dans le dictionnaire %s CSVFormatDesc=Fichier au format Comma Separated Value (.csv).
C'est un fichier au format texte dans lequel les champs sont séparés par le caractère [ %s ]. Si le séparateur est trouvé dans le contenu d'un champ, le champ doit être entouré du caractère [ %s ]. Le caractère d'échappement pour inclure un caractère de contour dans une donnée est [ %s ]. Excel95FormatDesc=Format de fichier Excel (.xls).
Format Excel 95 (BIFF5). -Excel2007FormatDesc=Format de fichier Excel (.xls).
Format standard Excel 2007 (SpreadsheetML). +Excel2007FormatDesc=Format de fichier Excel (.xlsx).
Format standard Excel 2007 (SpreadsheetML). TsvFormatDesc=Format de fichier à Valeurs Séparées par des Tabulations (.tsv).
C'est un fichier texte dont les champs sont séparés par des tabulations [tab]. ExportFieldAutomaticallyAdded=Le champ %s a été ajouté automatiquement car il évitera que des lignes identiques ne soient considérées comme des doublons (avec ce champ, aucune ligne ne sera identique mais aura un identifiant propre). CsvOptions=Options du fichier CSV From d5e94c1689ee77a504d599e07e474ca1379b8b32 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Mon, 21 Jan 2019 14:18:31 +0100 Subject: [PATCH 59/63] Add phpmin of driver --- htdocs/core/modules/export/export_excelnew.modules.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/core/modules/export/export_excelnew.modules.php b/htdocs/core/modules/export/export_excelnew.modules.php index bffbe0436c7..7d2f7b8adf2 100644 --- a/htdocs/core/modules/export/export_excelnew.modules.php +++ b/htdocs/core/modules/export/export_excelnew.modules.php @@ -83,6 +83,7 @@ class ExportExcelnew extends ModeleExports $this->extension='xlsx'; // Extension for generated file by this driver $this->picto='mime/xls'; // Picto $this->version='1.30'; // Driver version + $this->phpmin = array(5,6); // Minimum version of PHP required by module $this->disabled = (in_array(constant('PHPEXCEL_PATH'),array('disabled','disabled/'))?1:0); // A condition to disable module (used for native debian packages) From 495b04a3c74716dadbe71ce4020cf40c9b2be8a0 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Mon, 21 Jan 2019 14:30:50 +0100 Subject: [PATCH 60/63] FIX Hide path into exported filename --- htdocs/core/modules/export/export_excel.modules.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/htdocs/core/modules/export/export_excel.modules.php b/htdocs/core/modules/export/export_excel.modules.php index 1982fffb07e..5dc574a150f 100644 --- a/htdocs/core/modules/export/export_excel.modules.php +++ b/htdocs/core/modules/export/export_excel.modules.php @@ -236,11 +236,11 @@ class ExportExcel extends ModeleExports } $this->workbook = new PHPExcel(); - $this->workbook->getProperties()->setCreator($user->getFullName($outputlangs).' - Dolibarr '.DOL_VERSION); + $this->workbook->getProperties()->setCreator($user->getFullName($outputlangs).' - '.DOL_APPLICATION_TITLE.' '.DOL_VERSION); //$this->workbook->getProperties()->setLastModifiedBy('Dolibarr '.DOL_VERSION); - $this->workbook->getProperties()->setTitle($outputlangs->trans("Export").' - '.$file); - $this->workbook->getProperties()->setSubject($outputlangs->trans("Export").' - '.$file); - $this->workbook->getProperties()->setDescription($outputlangs->trans("Export").' - '.$file); + $this->workbook->getProperties()->setTitle(basename($file)); + $this->workbook->getProperties()->setSubject(basename($file)); + $this->workbook->getProperties()->setDescription(DOL_APPLICATION_TITLE.' '.DOL_VERSION); $this->workbook->setActiveSheetIndex(0); $this->workbook->getActiveSheet()->setTitle($outputlangs->trans("Sheet")); From 0b199f4d2887ddec79fc1930ec1ad810681acb9c Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Mon, 21 Jan 2019 14:31:03 +0100 Subject: [PATCH 61/63] FIX Hide path into exported filename --- .../export/export_excelnew.modules.php | 122 +++++------------- 1 file changed, 29 insertions(+), 93 deletions(-) diff --git a/htdocs/core/modules/export/export_excelnew.modules.php b/htdocs/core/modules/export/export_excelnew.modules.php index 7d2f7b8adf2..dfb9e4381f7 100644 --- a/htdocs/core/modules/export/export_excelnew.modules.php +++ b/htdocs/core/modules/export/export_excelnew.modules.php @@ -77,7 +77,7 @@ class ExportExcelnew extends ModeleExports global $conf, $langs; $this->db = $db; - $this->id='excelnew'; // Same value then xxx in file name export_xxx.modules.php + $this->id='excel2007new'; // Same value then xxx in file name export_xxx.modules.php $this->label='Excel 2007 by PHPSpreadSheet'; // Label of driver $this->desc = $langs->trans('Excel2007FormatDesc'); $this->extension='xlsx'; // Extension for generated file by this driver @@ -195,46 +195,35 @@ class ExportExcelnew extends ModeleExports $ret=1; $outputlangs->load("exports"); - if (! empty($conf->global->MAIN_USE_PHP_WRITEEXCEL)) - { - require_once PHP_WRITEEXCEL_PATH.'class.writeexcel_workbookbig.inc.php'; - require_once PHP_WRITEEXCEL_PATH.'class.writeexcel_worksheet.inc.php'; - require_once PHP_WRITEEXCEL_PATH.'functions.writeexcel_utility.inc.php'; - $this->workbook = new writeexcel_workbookbig($file); - $this->workbook->set_tempdir($conf->export->dir_temp); // Set temporary directory - $this->workbook->set_sheetname($outputlangs->trans("Sheet")); - $this->worksheet = &$this->workbook->addworksheet(); - } - else - { - //require_once PHPEXCEL_PATH.'PHPExcel.php'; - //require_once PHPEXCEL_PATH.'PHPExcel/Style/Alignment.php'; - require_once DOL_DOCUMENT_ROOT.'/includes/phpoffice/autoloader.php'; - require_once DOL_DOCUMENT_ROOT.'/includes/Psr/autoloader.php'; - require_once PHPEXCELNEW_PATH.'Spreadsheet.php'; - if ($this->id == 'excel2007') - { - if (! class_exists('ZipArchive')) // For Excel2007, PHPExcel need ZipArchive - { - $langs->load("errors"); - $this->error=$langs->trans('ErrorPHPNeedModule','zip'); - return -1; - } - } + //require_once PHPEXCEL_PATH.'PHPExcel.php'; + //require_once PHPEXCEL_PATH.'PHPExcel/Style/Alignment.php'; + require_once DOL_DOCUMENT_ROOT.'/includes/phpoffice/autoloader.php'; + require_once DOL_DOCUMENT_ROOT.'/includes/Psr/autoloader.php'; + require_once PHPEXCELNEW_PATH.'Spreadsheet.php'; - //$this->workbook = new PHPExcel(); - $this->workbook = new Spreadsheet(); - $this->workbook->getProperties()->setCreator($user->getFullName($outputlangs).' - Dolibarr '.DOL_VERSION); - //$this->workbook->getProperties()->setLastModifiedBy('Dolibarr '.DOL_VERSION); - $this->workbook->getProperties()->setTitle($outputlangs->trans("Export").' - '.$file); - $this->workbook->getProperties()->setSubject($outputlangs->trans("Export").' - '.$file); - $this->workbook->getProperties()->setDescription($outputlangs->trans("Export").' - '.$file); + if ($this->id == 'excel2007new') + { + if (! class_exists('ZipArchive')) // For Excel2007, PHPExcel need ZipArchive + { + $langs->load("errors"); + $this->error=$langs->trans('ErrorPHPNeedModule','zip'); + return -1; + } + } + + //$this->workbook = new PHPExcel(); + $this->workbook = new Spreadsheet(); + $this->workbook->getProperties()->setCreator($user->getFullName($outputlangs).' - '.DOL_APPLICATION_TITLE.' '.DOL_VERSION); + //$this->workbook->getProperties()->setLastModifiedBy('Dolibarr '.DOL_VERSION); + $this->workbook->getProperties()->setTitle(basename($file)); + $this->workbook->getProperties()->setSubject(basename($file)); + $this->workbook->getProperties()->setDescription(DOL_APPLICATION_TITLE.' '.DOL_VERSION); + + $this->workbook->setActiveSheetIndex(0); + $this->workbook->getActiveSheet()->setTitle($outputlangs->trans("Sheet")); + $this->workbook->getActiveSheet()->getDefaultRowDimension()->setRowHeight(16); - $this->workbook->setActiveSheetIndex(0); - $this->workbook->getActiveSheet()->setTitle($outputlangs->trans("Sheet")); - $this->workbook->getActiveSheet()->getDefaultRowDimension()->setRowHeight(16); - } return $ret; } @@ -270,22 +259,8 @@ class ExportExcelnew extends ModeleExports global $conf; // Create a format for the column headings - if (! empty($conf->global->MAIN_USE_PHP_WRITEEXCEL)) - { - $outputlangs->charset_output='ISO-8859-1'; // Because Excel 5 format is ISO - - $formatheader =$this->workbook->addformat(); - $formatheader->set_bold(); - $formatheader->set_color('blue'); - //$formatheader->set_size(12); - //$formatheader->set_font("Courier New"); - //$formatheader->set_align('center'); - } - else - { - $this->workbook->getActiveSheet()->getStyle('1')->getFont()->setBold(true); - $this->workbook->getActiveSheet()->getStyle('1')->getAlignment()->setHorizontal(PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT); - } + $this->workbook->getActiveSheet()->getStyle('1')->getFont()->setBold(true); + $this->workbook->getActiveSheet()->getStyle('1')->getAlignment()->setHorizontal(PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT); $this->col=0; foreach($array_selected_sorted as $code => $value) @@ -326,12 +301,6 @@ class ExportExcelnew extends ModeleExports // phpcs:enable global $conf; - // Create a format for the column headings - if (! empty($conf->global->MAIN_USE_PHP_WRITEEXCEL)) - { - $outputlangs->charset_output='ISO-8859-1'; // Because Excel 5 format is ISO - } - // Define first row $this->col=0; @@ -364,52 +333,20 @@ class ExportExcelnew extends ModeleExports if (preg_match('/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/i',$newvalue)) { - if (! empty($conf->global->MAIN_USE_PHP_WRITEEXCEL)) - { - $formatdate=$this->workbook->addformat(); - $formatdate->set_num_format('yyyy-mm-dd'); - //$formatdate->set_num_format(0x0f); - $arrayvalue=preg_split('/[.,]/',xl_parse_date($newvalue)); - //print "x".$arrayvalue[0].'.'.strval($arrayvalue[1]).'
'; - $newvalue=strval($arrayvalue[0]).'.'.strval($arrayvalue[1]); // $newvalue=strval(36892.521); directly does not work because . will be convert into , later - $this->worksheet->write($this->row, $this->col, $newvalue, \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($formatdate)); - } - else - { $newvalue=dol_stringtotime($newvalue); $this->workbook->getActiveSheet()->SetCellValueByColumnAndRow($this->col, $this->row+1, \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($newvalue)); $coord=$this->workbook->getActiveSheet()->getCellByColumnAndRow($this->col, $this->row+1)->getCoordinate(); $this->workbook->getActiveSheet()->getStyle($coord)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); - } } elseif (preg_match('/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]$/i',$newvalue)) { - if (! empty($conf->global->MAIN_USE_PHP_WRITEEXCEL)) - { - $formatdatehour=$this->workbook->addformat(); - $formatdatehour->set_num_format('yyyy-mm-dd hh:mm:ss'); - //$formatdatehour->set_num_format(0x0f); - $arrayvalue=preg_split('/[.,]/',xl_parse_date($newvalue)); - //print "x".$arrayvalue[0].'.'.strval($arrayvalue[1]).'
'; - $newvalue=strval($arrayvalue[0]).'.'.strval($arrayvalue[1]); // $newvalue=strval(36892.521); directly does not work because . will be convert into , later - $this->worksheet->write($this->row, $this->col, $newvalue, $formatdatehour); - } - else - { $newvalue=dol_stringtotime($newvalue); $this->workbook->getActiveSheet()->SetCellValueByColumnAndRow($this->col, $this->row+1, \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($newvalue)); $coord=$this->workbook->getActiveSheet()->getCellByColumnAndRow($this->col, $this->row+1)->getCoordinate(); $this->workbook->getActiveSheet()->getStyle($coord)->getNumberFormat()->setFormatCode('yyyy-mm-dd h:mm:ss'); - } } else { - if (! empty($conf->global->MAIN_USE_PHP_WRITEEXCEL)) - { - $this->worksheet->write($this->row, $this->col, $newvalue); - } - else - { if ($typefield == 'Text' || $typefield == 'TextAuto') { //$this->workbook->getActiveSheet()->getCellByColumnAndRow($this->col, $this->row+1)->setValueExplicit($newvalue, PHPExcel_Cell_DataType::TYPE_STRING); @@ -422,7 +359,6 @@ class ExportExcelnew extends ModeleExports { $this->workbook->getActiveSheet()->SetCellValueByColumnAndRow($this->col, $this->row+1, $newvalue); } - } } $this->col++; } From 0cba7e1613c003cf31db00d32d7f14e79de0950d Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Mon, 21 Jan 2019 14:30:50 +0100 Subject: [PATCH 62/63] FIX Hide path into exported filename --- htdocs/core/modules/export/export_excel.modules.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/htdocs/core/modules/export/export_excel.modules.php b/htdocs/core/modules/export/export_excel.modules.php index 1982fffb07e..5dc574a150f 100644 --- a/htdocs/core/modules/export/export_excel.modules.php +++ b/htdocs/core/modules/export/export_excel.modules.php @@ -236,11 +236,11 @@ class ExportExcel extends ModeleExports } $this->workbook = new PHPExcel(); - $this->workbook->getProperties()->setCreator($user->getFullName($outputlangs).' - Dolibarr '.DOL_VERSION); + $this->workbook->getProperties()->setCreator($user->getFullName($outputlangs).' - '.DOL_APPLICATION_TITLE.' '.DOL_VERSION); //$this->workbook->getProperties()->setLastModifiedBy('Dolibarr '.DOL_VERSION); - $this->workbook->getProperties()->setTitle($outputlangs->trans("Export").' - '.$file); - $this->workbook->getProperties()->setSubject($outputlangs->trans("Export").' - '.$file); - $this->workbook->getProperties()->setDescription($outputlangs->trans("Export").' - '.$file); + $this->workbook->getProperties()->setTitle(basename($file)); + $this->workbook->getProperties()->setSubject(basename($file)); + $this->workbook->getProperties()->setDescription(DOL_APPLICATION_TITLE.' '.DOL_VERSION); $this->workbook->setActiveSheetIndex(0); $this->workbook->getActiveSheet()->setTitle($outputlangs->trans("Sheet")); From 35c08e48426bd98ef42d4bbebf3971fa8fda7983 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Mon, 21 Jan 2019 14:45:33 +0100 Subject: [PATCH 63/63] Exclude lib for php 5.6 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5e4207c92b8..685c0e4b5f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -298,7 +298,7 @@ script: # Ensure we catch errors set -e #parallel-lint --exclude htdocs/includes --blame . - parallel-lint --exclude htdocs/includes/sabre --exclude htdocs/includes/sebastian --exclude htdocs/includes/squizlabs/php_codesniffer/tests --exclude htdocs/includes/jakub-onderka/php-parallel-lint/tests --exclude htdocs/includes/mike42/escpos-php/example --exclude htdocs/includes/phpunit/php-token-stream/tests --exclude htdocs/includes/composer/autoload_static.php --blame . + parallel-lint --exclude htdocs/includes/sabre --exclude htdocs/includes/phpoffice/PhpSpreadsheet --exclude htdocs/includes/sebastian --exclude htdocs/includes/squizlabs/php_codesniffer/tests --exclude htdocs/includes/jakub-onderka/php-parallel-lint/tests --exclude htdocs/includes/mike42/escpos-php/example --exclude htdocs/includes/phpunit/php-token-stream/tests --exclude htdocs/includes/composer/autoload_static.php --blame . set +e echo
element because TCPDF + // does not recognize e.g.
element because TCPDF + // does not recognize e.g.