Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b450a345f6 | ||
|
|
1997624065 | ||
|
|
71f20da637 | ||
|
|
7aeb0ed016 | ||
|
|
7c25513336 | ||
|
|
165a0b371c | ||
|
|
815266383b | ||
|
|
c68e5f8599 | ||
|
|
bd9ab7304e | ||
|
|
4e156a09f7 | ||
|
|
bfa70402a9 | ||
|
|
3a64934eb5 | ||
|
|
4201adb571 | ||
|
|
a7a8244a33 | ||
|
|
693b277002 | ||
|
|
a69a8950b3 | ||
|
|
63e0cf0ec2 |
No files matched your search
+22
-3
@@ -16,8 +16,23 @@ $hasBudgetItems = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_items")->fet
|
|||||||
// Le minimum vital pour que ça tourne
|
// Le minimum vital pour que ça tourne
|
||||||
$isBudgetSetupOk = ($hasCategories && $hasSalaries && $hasAccounts);
|
$isBudgetSetupOk = ($hasCategories && $hasSalaries && $hasAccounts);
|
||||||
|
|
||||||
// Gestion de l'onglet actif (par défaut 'suivi')
|
// ============================================================================
|
||||||
|
// GESTION DES VUES (MANDATORY VS OPTIONAL)
|
||||||
|
// ============================================================================
|
||||||
|
$enabledViews = $_SESSION['enabled_views'] ?? ['budget_prev', 'budget_epargne', 'budget_provisions'];
|
||||||
|
|
||||||
|
// Vues obligatoires pour le cœur du budget
|
||||||
|
$allowedTabs = ['suivi', 'recap'];
|
||||||
|
|
||||||
|
if (in_array('budget_prev', $enabledViews)) $allowedTabs[] = 'budget_prev';
|
||||||
|
if (in_array('budget_epargne', $enabledViews)) $allowedTabs[] = 'epargne';
|
||||||
|
if (in_array('budget_provisions', $enabledViews)) $allowedTabs[] = 'provisions';
|
||||||
|
|
||||||
|
// Gestion de l'onglet actif avec fallback de sécurité
|
||||||
$tab = $_GET['tab'] ?? 'suivi';
|
$tab = $_GET['tab'] ?? 'suivi';
|
||||||
|
if (!in_array($tab, $allowedTabs)) {
|
||||||
|
$tab = 'suivi';
|
||||||
|
}
|
||||||
|
|
||||||
$pageTitle = tr('budget_page_title');
|
$pageTitle = tr('budget_page_title');
|
||||||
$activePage = "budget";
|
$activePage = "budget";
|
||||||
@@ -30,7 +45,6 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-hero budget-hero">
|
<div class="pf-hero budget-hero">
|
||||||
<div class="budget-title-group">
|
<div class="budget-title-group">
|
||||||
<h1><?= tr('budget_main_header') ?></h1>
|
<h1><?= tr('budget_main_header') ?></h1>
|
||||||
<!-- Bouton avec séparation claire entre l'engrenage animable et l'alerte fixe -->
|
|
||||||
<button class="btn-header-settings" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
|
<button class="btn-header-settings" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
|
||||||
<span class="gear-icon-anim">⚙️</span>
|
<span class="gear-icon-anim">⚙️</span>
|
||||||
<?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?>
|
<?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?>
|
||||||
@@ -45,26 +59,31 @@ require __DIR__ . '/header.php';
|
|||||||
<span><?= tr('budget_tab_tracking') ?></span>
|
<span><?= tr('budget_tab_tracking') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<?php if (in_array('budget_prev', $allowedTabs)): ?>
|
||||||
<a href="?tab=budget_prev" class="tab-item <?= $tab == 'budget_prev' ? 'active' : '' ?>">
|
<a href="?tab=budget_prev" class="tab-item <?= $tab == 'budget_prev' ? 'active' : '' ?>">
|
||||||
<span class="tab-icon">🎯</span>
|
<span class="tab-icon">🎯</span>
|
||||||
<span><?= tr('budget_tab_prev') ?></span>
|
<span><?= tr('budget_tab_prev') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (in_array('epargne', $allowedTabs)): ?>
|
||||||
<a href="?tab=epargne" class="tab-item <?= $tab == 'epargne' ? 'active' : '' ?>">
|
<a href="?tab=epargne" class="tab-item <?= $tab == 'epargne' ? 'active' : '' ?>">
|
||||||
<span class="tab-icon">🐷</span>
|
<span class="tab-icon">🐷</span>
|
||||||
<span><?= tr('budget_tab_savings') ?></span>
|
<span><?= tr('budget_tab_savings') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<a href="?tab=recap" class="tab-item <?= $tab == 'recap' ? 'active' : '' ?>">
|
<a href="?tab=recap" class="tab-item <?= $tab == 'recap' ? 'active' : '' ?>">
|
||||||
<span class="tab-icon">📊</span>
|
<span class="tab-icon">📊</span>
|
||||||
<span><?= tr('budget_tab_recap') ?></span>
|
<span><?= tr('budget_tab_recap') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<?php if (in_array('provisions', $allowedTabs)): ?>
|
||||||
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
|
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
|
||||||
<span class="tab-icon">🧮</span>
|
<span class="tab-icon">🧮</span>
|
||||||
<span><?= tr('budget_tab_provisions') ?></span>
|
<span><?= tr('budget_tab_provisions') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"smalot/pdfparser": "^2.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+155
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "f7c071b38f69a0b82ff94c1527c44426",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "smalot/pdfparser",
|
||||||
|
"version": "v2.12.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/smalot/pdfparser.git",
|
||||||
|
"reference": "2cfa0d92bd557875c9f52a75fde0e8392302a354"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/smalot/pdfparser/zipball/2cfa0d92bd557875c9f52a75fde0e8392302a354",
|
||||||
|
"reference": "2cfa0d92bd557875c9f52a75fde0e8392302a354",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php": ">=7.1",
|
||||||
|
"symfony/polyfill-mbstring": "^1.18"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Smalot\\PdfParser\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-3.0"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sebastien MALOT",
|
||||||
|
"email": "sebastien@malot.fr"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Pdf parser library. Can read and extract information from pdf file.",
|
||||||
|
"homepage": "https://www.pdfparser.org",
|
||||||
|
"keywords": [
|
||||||
|
"extract",
|
||||||
|
"parse",
|
||||||
|
"parser",
|
||||||
|
"pdf",
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/smalot/pdfparser/issues",
|
||||||
|
"source": "https://github.com/smalot/pdfparser/tree/v2.12.5"
|
||||||
|
},
|
||||||
|
"time": "2026-04-17T11:37:58+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-mbstring",
|
||||||
|
"version": "v1.38.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
|
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||||
|
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"php": ">=7.2"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"ext-mbstring": "*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-mbstring": "For best performance"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/polyfill",
|
||||||
|
"name": "symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony polyfill for the Mbstring extension",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"compatibility",
|
||||||
|
"mbstring",
|
||||||
|
"polyfill",
|
||||||
|
"portable",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-27T06:59:30+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {},
|
||||||
|
"platform-dev": {},
|
||||||
|
"plugin-api-version": "2.9.0"
|
||||||
|
}
|
||||||
@@ -208,3 +208,15 @@
|
|||||||
[data-theme="dark"] .month-actions .btn-icon-small:hover {
|
[data-theme="dark"] .month-actions .btn-icon-small:hover {
|
||||||
background: rgba(255, 255, 255, 0.06) !important;
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-module-card-wrapper:has(input.js-mod-checkbox:checked) {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .pf-sub-module-pill {
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .pf-sub-module-pill:has(input:checked) {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS families (
|
|||||||
invite_code VARCHAR(32) NOT NULL UNIQUE,
|
invite_code VARCHAR(32) NOT NULL UNIQUE,
|
||||||
is_active TINYINT(1) DEFAULT 1,
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
enabled_modules JSON DEFAULT NULL,
|
enabled_modules JSON DEFAULT NULL,
|
||||||
|
enabled_views JSON DEFAULT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,26 @@ require __DIR__ . '/includes/db.php';
|
|||||||
require_once __DIR__ . '/includes/i18n.php';
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
require_login('/login.php');
|
require_login('/login.php');
|
||||||
|
|
||||||
// Gestion de l'onglet actif
|
// 1. Détection des vues actives
|
||||||
$tab = $_GET['tab'] ?? 'meals';
|
$enabledViews = $_SESSION['enabled_views'] ?? ['food_meals', 'food_liste'];
|
||||||
|
$allowedTabs = [];
|
||||||
|
|
||||||
|
if (in_array('food_meals', $enabledViews)) $allowedTabs[] = 'meals';
|
||||||
|
if (in_array('food_liste', $enabledViews)) $allowedTabs[] = 'liste';
|
||||||
|
|
||||||
|
// 2. Fallback intelligent de l'onglet actif
|
||||||
|
if (empty($allowedTabs)) {
|
||||||
|
$tab = 'empty'; // L'utilisateur a désactivé les deux vues
|
||||||
|
} else {
|
||||||
|
// Par défaut, on prend le premier onglet disponible (Meals, ou Liste si Meals est désactivé)
|
||||||
|
$defaultTab = $allowedTabs[0];
|
||||||
|
$tab = $_GET['tab'] ?? $defaultTab;
|
||||||
|
|
||||||
|
// Sécurité URL : Si on force un onglet non autorisé
|
||||||
|
if (!in_array($tab, $allowedTabs)) {
|
||||||
|
$tab = $defaultTab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$pageTitle = tr('mod_food_name');
|
$pageTitle = tr('mod_food_name');
|
||||||
$activePage = "food";
|
$activePage = "food";
|
||||||
@@ -21,30 +39,44 @@ require __DIR__ . '/header.php';
|
|||||||
<h1>🍳 <?= tr('mod_food_name') ?></h1>
|
<h1>🍳 <?= tr('mod_food_name') ?></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($tab !== 'empty'): ?>
|
||||||
<nav class="budget-tabs-container">
|
<nav class="budget-tabs-container">
|
||||||
|
<?php if (in_array('meals', $allowedTabs)): ?>
|
||||||
<a href="?tab=meals" class="tab-item <?= $tab == 'meals' ? 'active' : '' ?>">
|
<a href="?tab=meals" class="tab-item <?= $tab == 'meals' ? 'active' : '' ?>">
|
||||||
<span class="tab-icon">🍽️</span>
|
<span class="tab-icon">🍽️</span>
|
||||||
<span><?= tr('food_tab_meals') ?></span>
|
<span><?= tr('food_tab_meals') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (in_array('liste', $allowedTabs)): ?>
|
||||||
<a href="?tab=liste" class="tab-item <?= $tab == 'liste' ? 'active' : '' ?>">
|
<a href="?tab=liste" class="tab-item <?= $tab == 'liste' ? 'active' : '' ?>">
|
||||||
<span class="tab-icon">🛒</span>
|
<span class="tab-icon">🛒</span>
|
||||||
<span><?= tr('food_tab_list') ?></span>
|
<span><?= tr('food_tab_list') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<?php
|
<?php
|
||||||
$allowedTabs = ['meals', 'liste'];
|
if ($tab === 'empty') {
|
||||||
if (in_array($tab, $allowedTabs)) {
|
// État Vide Intégré (Zéro vue activée)
|
||||||
|
echo "
|
||||||
|
<div class='pf-empty-state' style='text-align:center; padding: 60px 20px; max-width: 600px; margin: 0 auto; background: var(--bg-panel); border-radius: 16px; border: 1px dashed var(--border-strong); box-shadow: var(--shadow-sm);'>
|
||||||
|
<div style='font-size: 4rem; margin-bottom: 20px; opacity: 0.5;'>👀</div>
|
||||||
|
<h3 style='color: var(--text-main); margin-bottom: 10px; font-size: 1.3rem;'>" . tr('food_err_no_views') . "</h3>
|
||||||
|
<p style='color: var(--text-muted); margin-bottom: 30px; font-size: 0.95rem; line-height: 1.5;'>" . tr('food_err_no_views_desc') . "</p>
|
||||||
|
<a href='/settings.php' class='pf-btn'>⚙️ " . tr('settings') . "</a>
|
||||||
|
</div>";
|
||||||
|
} else {
|
||||||
|
// Routage classique
|
||||||
$viewPath = __DIR__ . "/modules/food/views/{$tab}.php";
|
$viewPath = __DIR__ . "/modules/food/views/{$tab}.php";
|
||||||
if (file_exists($viewPath)) {
|
if (file_exists($viewPath)) {
|
||||||
require $viewPath;
|
require $viewPath;
|
||||||
} else {
|
} else {
|
||||||
echo "<p>Vue introuvable : " . htmlspecialchars($tab) . "</p>";
|
echo "<p>Vue introuvable : " . htmlspecialchars($tab) . "</p>";
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
echo "<p>" . tr('error_occured') . "</p>";
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+112
-13
@@ -410,25 +410,124 @@ body.pf-home .pf-mobile-close-btn {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tuile module (checkbox) — état coché via :has(), compatible thème */
|
/* --- MODULES & SOUS-VUES UNIFIÉS (Settings) --- */
|
||||||
.pf-module-tile {
|
.pf-module-card-wrapper {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 8px;
|
||||||
padding: 14px 16px;
|
transition: all 0.2s ease;
|
||||||
border: 2px solid var(--border-light);
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--bg-page);
|
|
||||||
transition:
|
|
||||||
border-color 0.15s ease,
|
|
||||||
background 0.15s ease;
|
|
||||||
}
|
}
|
||||||
.pf-module-tile:has(input:checked) {
|
|
||||||
|
/* Le fond bleu léger s'applique à TOUTE la carte si le module est actif */
|
||||||
|
.pf-module-card-wrapper:has(input.js-mod-checkbox:checked) {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
background: var(--bg-soft);
|
background: var(--bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ligne supérieure : Titre + Pills (Alignement horizontal + Wrap auto) */
|
||||||
|
.pf-module-top-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-module-tile-compact {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-tile-icon {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-tile-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Sous-Vues (Pills) --- */
|
||||||
|
.pf-module-inline-pills {
|
||||||
|
display: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-module-card-wrapper:has(input.js-mod-checkbox:checked)
|
||||||
|
.pf-module-inline-pills {
|
||||||
|
display: flex;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-sub-module-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-sub-module-pill input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-sub-module-pill .pf-pill-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-sub-module-pill:hover {
|
||||||
|
border-color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* État actif de la Pill */
|
||||||
|
.pf-sub-module-pill:has(input:checked) {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 1px 2px rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-sub-module-pill:has(input:checked) .pf-pill-text {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Note informative en bas de carte */
|
||||||
|
.pf-module-info-footer {
|
||||||
|
display: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px dashed var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-module-card-wrapper:has(input.js-mod-checkbox:checked)
|
||||||
|
.pf-module-info-footer {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
.pf-checkbox-lg {
|
.pf-checkbox-lg {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|||||||
+97
-17
@@ -23,6 +23,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<link rel="apple-touch-icon" href="/favicon.png">
|
<link rel="apple-touch-icon" href="/favicon.png">
|
||||||
<link rel="stylesheet" href="/global.css">
|
<link rel="stylesheet" href="/global.css">
|
||||||
@@ -122,19 +123,13 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
<main class="pf-main<?= ($mainClass ?? '') ? ' '.htmlspecialchars($mainClass) : '' ?>">
|
<main class="pf-main<?= ($mainClass ?? '') ? ' '.htmlspecialchars($mainClass) : '' ?>">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// CSRF avant I18N : le parseur HTML coupe le script dès la séquence de fin de balise « script » (même dans un commentaire JS).
|
// CSRF avant I18N
|
||||||
window.CSRF_TOKEN = "<?= htmlspecialchars(function_exists('csrf_token') ? csrf_token() : '', ENT_QUOTES, 'UTF-8') ?>";
|
window.CSRF_TOKEN = "<?= htmlspecialchars(function_exists('csrf_token') ? csrf_token() : '', ENT_QUOTES, 'UTF-8') ?>";
|
||||||
|
|
||||||
/**
|
|
||||||
* Pont d'Internationalisation et Configuration
|
|
||||||
*/
|
|
||||||
window.I18N = <?php echo json_encode($current_translations_array ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>;
|
window.I18N = <?php echo json_encode($current_translations_array ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>;
|
||||||
|
|
||||||
// ✅ Injection sécurisée
|
|
||||||
window.CONFIG = {
|
window.CONFIG = {
|
||||||
// Liste complète des profils de la famille (Membres, Nounou, Enfants)
|
|
||||||
PEOPLE: <?= isset($familyPeople) ? json_encode($familyPeople) : '[]' ?>,
|
PEOPLE: <?= isset($familyPeople) ? json_encode($familyPeople) : '[]' ?>,
|
||||||
// ID du user global connecté (pour savoir "qui" clique)
|
|
||||||
CURRENT_USER_ID: <?= isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 'null' ?>,
|
CURRENT_USER_ID: <?= isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 'null' ?>,
|
||||||
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
|
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
|
||||||
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
|
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
|
||||||
@@ -144,14 +139,20 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
return window.I18N[key] || key;
|
return window.I18N[key] || key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thème clair/sombre — état visuel géré entièrement par CSS via [data-theme="dark"]
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js').catch(err => {
|
||||||
|
console.error('Service Worker registration failed:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toggleTheme(){
|
function toggleTheme(){
|
||||||
const dark=document.documentElement.getAttribute('data-theme')==='dark';
|
const dark=document.documentElement.getAttribute('data-theme')==='dark';
|
||||||
document.documentElement.setAttribute('data-theme',dark?'light':'dark');
|
document.documentElement.setAttribute('data-theme',dark?'light':'dark');
|
||||||
localStorage.setItem('hh-theme',dark?'light':'dark');
|
localStorage.setItem('hh-theme',dark?'light':'dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion du menu mobile (Off-Canvas Sidebar)
|
|
||||||
<?php if (isset($_SESSION['user'])): ?>
|
<?php if (isset($_SESSION['user'])): ?>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const burgerBtn = document.querySelector('.pf-burger-btn');
|
const burgerBtn = document.querySelector('.pf-burger-btn');
|
||||||
@@ -174,14 +175,13 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
if(burgerBtn && mobileMenu && overlay && closeBtn) {
|
if(burgerBtn && mobileMenu && overlay && closeBtn) {
|
||||||
burgerBtn.addEventListener('click', openMenu);
|
burgerBtn.addEventListener('click', openMenu);
|
||||||
closeBtn.addEventListener('click', closeMenu);
|
closeBtn.addEventListener('click', closeMenu);
|
||||||
// Fermer le menu si on clique en dehors (sur l'overlay sombre)
|
|
||||||
overlay.addEventListener('click', closeMenu);
|
overlay.addEventListener('click', closeMenu);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pachaFetch : Utilitaire de requête robuste
|
* pachaFetch : Utilitaire de requête robuste avec gestion Offline Cross-Browser
|
||||||
*/
|
*/
|
||||||
async function pachaFetch(url, options = {}) {
|
async function pachaFetch(url, options = {}) {
|
||||||
const finalUrl = url.startsWith('/') ? url.substring(1) : url;
|
const finalUrl = url.startsWith('/') ? url.substring(1) : url;
|
||||||
@@ -200,25 +200,108 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(rawText);
|
return JSON.parse(rawText);
|
||||||
} catch (jsonErr) {
|
} catch (jsonErr) {
|
||||||
console.error("Réponse corrompue (HTML reçu au lieu de JSON) :", rawText);
|
console.error("Réponse corrompue (HTML) :", rawText);
|
||||||
throw new Error("Erreur serveur : format JSON invalide.");
|
throw new Error("Erreur serveur : format JSON invalide.");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Si hors ligne ET modification (POST/PUT/DELETE)
|
||||||
|
if (!navigator.onLine && options.method && options.method.toUpperCase() !== 'GET') {
|
||||||
|
await enqueueOfflineAction(finalUrl, options);
|
||||||
|
|
||||||
|
// Chrome/Android : Déclenche le Background Sync
|
||||||
|
if ('serviceWorker' in navigator && 'SyncManager' in window) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.sync.register('househub-sync').catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Mémorisé hors ligne 📡", "success");
|
||||||
|
return { success: true, offline: true };
|
||||||
|
}
|
||||||
|
|
||||||
console.error(`Erreur pachaFetch [${finalUrl}] :`, err);
|
console.error(`Erreur pachaFetch [${finalUrl}] :`, err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI Utility : Toasts (Notifications)
|
* Utilitaire PWA : Ajout à la file d'attente (IndexedDB)
|
||||||
*/
|
*/
|
||||||
|
function enqueueOfflineAction(url, options) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open('househub-offline-sync', 1);
|
||||||
|
request.onupgradeneeded = event => {
|
||||||
|
event.target.result.createObjectStore('sync-queue', { autoIncrement: true });
|
||||||
|
};
|
||||||
|
request.onsuccess = event => {
|
||||||
|
const db = event.target.result;
|
||||||
|
const tx = db.transaction('sync-queue', 'readwrite');
|
||||||
|
const store = tx.objectStore('sync-queue');
|
||||||
|
|
||||||
|
let bodyData = options.body;
|
||||||
|
if (bodyData instanceof FormData) {
|
||||||
|
bodyData = new URLSearchParams(bodyData).toString();
|
||||||
|
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
}
|
||||||
|
|
||||||
|
store.add({
|
||||||
|
url: url,
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers,
|
||||||
|
body: bodyData,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
request.onerror = event => reject(event.target.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback pour Firefox et iOS Safari (Dépilement manuel au retour réseau)
|
||||||
|
*/
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
const request = indexedDB.open('househub-offline-sync', 1);
|
||||||
|
request.onsuccess = event => {
|
||||||
|
const db = event.target.result;
|
||||||
|
if (!db.objectStoreNames.contains('sync-queue')) return;
|
||||||
|
|
||||||
|
const tx = db.transaction('sync-queue', 'readonly');
|
||||||
|
const store = tx.objectStore('sync-queue');
|
||||||
|
const getAll = store.getAll();
|
||||||
|
|
||||||
|
getAll.onsuccess = async () => {
|
||||||
|
const actions = getAll.result;
|
||||||
|
if (actions && actions.length > 0) {
|
||||||
|
showToast("Synchronisation des données... 🔄", "success");
|
||||||
|
for (const action of actions) {
|
||||||
|
try {
|
||||||
|
await fetch(action.url, {
|
||||||
|
method: action.method,
|
||||||
|
headers: action.headers,
|
||||||
|
body: action.body
|
||||||
|
});
|
||||||
|
const delTx = db.transaction('sync-queue', 'readwrite');
|
||||||
|
delTx.objectStore('sync-queue').delete(action.id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Échec de la synchro", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast("Synchronisation terminée ! ✅", "success");
|
||||||
|
// Rafraîchissement léger recommandé après synchro de masse
|
||||||
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function showToast(message, type = 'success') {
|
function showToast(message, type = 'success') {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `pf-toast pf-toast--${type}`;
|
toast.className = `pf-toast pf-toast--${type}`;
|
||||||
toast.innerHTML = message;
|
toast.innerHTML = message;
|
||||||
document.body.appendChild(toast);
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
// Animation et suppression
|
|
||||||
setTimeout(() => toast.classList.add('is-visible'), 100);
|
setTimeout(() => toast.classList.add('is-visible'), 100);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.classList.remove('is-visible');
|
toast.classList.remove('is-visible');
|
||||||
@@ -226,9 +309,6 @@ function showToast(message, type = 'success') {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* UI Utility : Confirmation stylisée (Remplace confirm())
|
|
||||||
*/
|
|
||||||
async function pachaConfirm(title, message) {
|
async function pachaConfirm(title, message) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
|
|||||||
+2
-1
@@ -35,7 +35,7 @@ function require_login(?string $loginPage = '/login.php'): void
|
|||||||
|
|
||||||
$stmt = $meta_pdo->prepare("
|
$stmt = $meta_pdo->prepare("
|
||||||
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang, u.remember_token,
|
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang, u.remember_token,
|
||||||
f.db_name, f.is_active as family_active, f.enabled_modules
|
f.db_name, f.is_active as family_active, f.enabled_modules, f.enabled_views
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN families f ON f.id = u.family_id
|
LEFT JOIN families f ON f.id = u.family_id
|
||||||
WHERE u.id = ?
|
WHERE u.id = ?
|
||||||
@@ -57,6 +57,7 @@ function require_login(?string $loginPage = '/login.php'): void
|
|||||||
$_SESSION['family_db'] = $user['db_name'];
|
$_SESSION['family_db'] = $user['db_name'];
|
||||||
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
||||||
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
||||||
|
$_SESSION['enabled_views'] = json_decode($user['enabled_views'] ?? '["food_meals","food_liste"]', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-5
@@ -105,6 +105,7 @@ return [
|
|||||||
'mod_printvault_desc'=> 'Gestioneu els vostres fitxers d\'impressió 3D — STL, 3MF, GCode.',
|
'mod_printvault_desc'=> 'Gestioneu els vostres fitxers d\'impressió 3D — STL, 3MF, GCode.',
|
||||||
'mod_planka_name' => 'Planka',
|
'mod_planka_name' => 'Planka',
|
||||||
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
|
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
|
||||||
|
'budget_mandatory_note' => 'Les pestanyes de Seguiment i Resum són obligatòries i sempre actives.',
|
||||||
|
|
||||||
'cta_open' => 'Obrir',
|
'cta_open' => 'Obrir',
|
||||||
'cta_explore' => 'Explorar',
|
'cta_explore' => 'Explorar',
|
||||||
@@ -586,7 +587,6 @@ return [
|
|||||||
'bs_currency_label' => 'Divisa de la llar',
|
'bs_currency_label' => 'Divisa de la llar',
|
||||||
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
|
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
|
||||||
'bs_currency_updated' => 'Divisa actualitzada correctament!',
|
'bs_currency_updated' => 'Divisa actualitzada correctament!',
|
||||||
'bud_settings_dyn_title' => 'Estimacions Dinàmiques (Nens)',
|
|
||||||
'bud_dyn_nanny_fixed' => 'Sou fix Cangur (€)',
|
'bud_dyn_nanny_fixed' => 'Sou fix Cangur (€)',
|
||||||
'bud_dyn_nanny_daily' => 'Despeses diàries de manteniment (€)',
|
'bud_dyn_nanny_daily' => 'Despeses diàries de manteniment (€)',
|
||||||
'bud_dyn_nanny_aid' => 'Ajuda del govern (€)',
|
'bud_dyn_nanny_aid' => 'Ajuda del govern (€)',
|
||||||
@@ -600,7 +600,16 @@ return [
|
|||||||
'bud_audit_child' => '👦 %s :',
|
'bud_audit_child' => '👦 %s :',
|
||||||
'bud_audit_school_fee' => 'Menjador (%dd) + Acollida (%dd) + Casal (%dd) = %.2f€',
|
'bud_audit_school_fee' => 'Menjador (%dd) + Acollida (%dd) + Casal (%dd) = %.2f€',
|
||||||
'bud_audit_sick' => 'Dies de malaltia deduïts : %dd',
|
'bud_audit_sick' => 'Dies de malaltia deduïts : %dd',
|
||||||
'bud_audit_nanny_full' => '💼 %s : Fix (%.2f€) + Despeses (%dd x %.2f€ = %.2f€) - Ajut (%.2f€) - CESU (%.2f€) = %.2f€',
|
'bud_audit_nanny_full' => '💼 %s : Fix (%.2f€) + Despeses (%dd x %.2f€ = %.2f€) - CMG (%.2f€) - %s (%.2f€) = %.2f€',
|
||||||
|
'bud_audit_cesu_avg' => 'CESU (Mitj.)',
|
||||||
|
'bud_audit_cesu_adj' => 'CESU (Ajustat)',
|
||||||
|
'bud_dyn_cesu_overrides' => 'Ajustaments puntuals del CESU',
|
||||||
|
'bud_dyn_cesu_add' => 'Afegir un mes',
|
||||||
|
'bud_dyn_cesu_empty' => 'Cap ajustament registrat.',
|
||||||
|
'bud_settings_dyn_title' => 'Cura i Escola',
|
||||||
|
'bud_settings_dyn_desc' => 'Tarifes per al càlcul automatitzat de les despeses.',
|
||||||
|
'bud_dyn_sub_nanny' => 'Cangur',
|
||||||
|
'bud_dyn_sub_school' => 'Extraescolars i Casal',
|
||||||
|
|
||||||
// CSV
|
// CSV
|
||||||
'bs_csv_title' => 'Format de la importació CSV',
|
'bs_csv_title' => 'Format de la importació CSV',
|
||||||
@@ -1012,9 +1021,9 @@ return [
|
|||||||
'meal_saved' => 'Menjar guardat!',
|
'meal_saved' => 'Menjar guardat!',
|
||||||
'meal_btn_import_pdf' => 'Import Menu PDF',
|
'meal_btn_import_pdf' => 'Import Menu PDF',
|
||||||
'meal_import_title' => 'Import canteen menu',
|
'meal_import_title' => 'Import canteen menu',
|
||||||
'meal_import_child' => 'For which child?',
|
'meal_import_child' => 'Per quina persona?',
|
||||||
'meal_select_child' => 'Select child',
|
'meal_select_child' => 'Sélectionner un membre :',
|
||||||
'meal_pdf_file' => 'PDF File (Convivio)',
|
'meal_pdf_file' => 'PDF File',
|
||||||
'meal_drop_pdf' => 'Click to add PDF',
|
'meal_drop_pdf' => 'Click to add PDF',
|
||||||
'meal_import_success' => 'Menus imported successfully!',
|
'meal_import_success' => 'Menus imported successfully!',
|
||||||
'mod_food_name' => 'Cuina i Compres',
|
'mod_food_name' => 'Cuina i Compres',
|
||||||
@@ -1023,4 +1032,13 @@ return [
|
|||||||
'menu_food' => 'Menjar',
|
'menu_food' => 'Menjar',
|
||||||
'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.',
|
'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.',
|
||||||
'cta_food' => 'Cuinar',
|
'cta_food' => 'Cuinar',
|
||||||
|
'food_err_no_views' => "Cap vista activada per a aquest mòdul. Modifiqueu la vostra configuració d'espai.",
|
||||||
|
'food_err_no_views_desc' => 'Heu desactivat totes les funcions d\'aquest mòdul. Aneu a la configuració per reactivar-les.',
|
||||||
|
'meal_paste_success' => 'Graella enganxada amb èxit!',
|
||||||
|
'meal_err_filename' => 'Format del nom del fitxer no reconegut. Exemple esperat: "Menus-du-28-septembre-au-4-octobre-2026.pdf".',
|
||||||
|
|
||||||
|
'offline_title' => 'Mode fora de línia',
|
||||||
|
'offline_heading' => 'Estàs fora de línia',
|
||||||
|
'offline_message' => 'L\'aplicació HouseHub no està disponible sense connexió a internet ara mateix.',
|
||||||
|
'offline_retry_btn' => 'Tornar-ho a intentar',
|
||||||
];
|
];
|
||||||
+24
-5
@@ -542,6 +542,7 @@ return [
|
|||||||
'bud_err_tech' => 'A technical error occurred.',
|
'bud_err_tech' => 'A technical error occurred.',
|
||||||
'bud_err_server' => 'Server error: ',
|
'bud_err_server' => 'Server error: ',
|
||||||
'bud_err_delete' => 'Error while deleting.',
|
'bud_err_delete' => 'Error while deleting.',
|
||||||
|
'budget_mandatory_note' => 'Tracking and Summary tabs are mandatory and always active.',
|
||||||
|
|
||||||
// Categories (Budget)
|
// Categories (Budget)
|
||||||
'cat_income' => 'Income',
|
'cat_income' => 'Income',
|
||||||
@@ -588,7 +589,6 @@ return [
|
|||||||
'bs_currency_label' => 'Household Currency',
|
'bs_currency_label' => 'Household Currency',
|
||||||
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
|
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
|
||||||
'bs_currency_updated' => 'Currency updated successfully!',
|
'bs_currency_updated' => 'Currency updated successfully!',
|
||||||
'bud_settings_dyn_title' => 'Dynamic Estimates (Children)',
|
|
||||||
'bud_dyn_nanny_fixed' => 'Nanny fixed salary (€)',
|
'bud_dyn_nanny_fixed' => 'Nanny fixed salary (€)',
|
||||||
'bud_dyn_nanny_daily' => 'Daily maintenance fee (€)',
|
'bud_dyn_nanny_daily' => 'Daily maintenance fee (€)',
|
||||||
'bud_dyn_nanny_aid' => 'Government aid (€)',
|
'bud_dyn_nanny_aid' => 'Government aid (€)',
|
||||||
@@ -602,7 +602,16 @@ return [
|
|||||||
'bud_audit_child' => '👦 %s :',
|
'bud_audit_child' => '👦 %s :',
|
||||||
'bud_audit_school_fee' => 'Canteen (%dd) + Aftercare (%dd) + Center (%dd) = %.2f€',
|
'bud_audit_school_fee' => 'Canteen (%dd) + Aftercare (%dd) + Center (%dd) = %.2f€',
|
||||||
'bud_audit_sick' => 'Sick days deducted : %dd',
|
'bud_audit_sick' => 'Sick days deducted : %dd',
|
||||||
'bud_audit_nanny_full' => '💼 %s : Fixed (%.2f€) + Fees (%dd x %.2f€ = %.2f€) - Aid (%.2f€) - CESU (%.2f€) = %.2f€',
|
'bud_audit_nanny_full' => '💼 %s : Fixed (%.2f€) + Fees (%dd x %.2f€ = %.2f€) - Aid (%.2f€) - %s (%.2f€) = %.2f€',
|
||||||
|
'bud_audit_cesu_avg' => 'CESU (Avg.)',
|
||||||
|
'bud_audit_cesu_adj' => 'CESU (Adjusted)',
|
||||||
|
'bud_dyn_cesu_overrides' => 'Specific CESU adjustments',
|
||||||
|
'bud_dyn_cesu_add' => 'Add a month',
|
||||||
|
'bud_dyn_cesu_empty' => 'No adjustments recorded.',
|
||||||
|
'bud_settings_dyn_title' => 'Care & School',
|
||||||
|
'bud_settings_dyn_desc' => 'Rates for automated childcare fee calculations.',
|
||||||
|
'bud_dyn_sub_nanny' => 'Nanny',
|
||||||
|
'bud_dyn_sub_school' => 'Afterschool & Center',
|
||||||
|
|
||||||
// CSV Mapping (Budget)
|
// CSV Mapping (Budget)
|
||||||
'bs_csv_title' => 'CSV Import Format',
|
'bs_csv_title' => 'CSV Import Format',
|
||||||
@@ -1015,9 +1024,9 @@ return [
|
|||||||
'meal_saved' => 'Meal saved!',
|
'meal_saved' => 'Meal saved!',
|
||||||
'meal_btn_import_pdf' => 'Import Menu PDF',
|
'meal_btn_import_pdf' => 'Import Menu PDF',
|
||||||
'meal_import_title' => 'Import canteen menu',
|
'meal_import_title' => 'Import canteen menu',
|
||||||
'meal_import_child' => 'For which child?',
|
'meal_import_child' => 'For which person?',
|
||||||
'meal_select_child' => 'Select child',
|
'meal_select_child' => 'Sélectionner un membre :',
|
||||||
'meal_pdf_file' => 'PDF File (Convivio)',
|
'meal_pdf_file' => 'PDF File',
|
||||||
'meal_drop_pdf' => 'Click to add PDF',
|
'meal_drop_pdf' => 'Click to add PDF',
|
||||||
'meal_import_success' => 'Menus imported successfully!',
|
'meal_import_success' => 'Menus imported successfully!',
|
||||||
'mod_food_name' => 'Kitchen & Groceries',
|
'mod_food_name' => 'Kitchen & Groceries',
|
||||||
@@ -1026,4 +1035,14 @@ return [
|
|||||||
'menu_food' => 'Food',
|
'menu_food' => 'Food',
|
||||||
'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.',
|
'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.',
|
||||||
'cta_food' => 'Cook up',
|
'cta_food' => 'Cook up',
|
||||||
|
'food_err_no_views' => "No view is enabled for this module. Please check your space settings.",
|
||||||
|
'food_err_no_views_desc' => 'You have disabled all features for this module. Please visit your settings to re-enable them.',
|
||||||
|
'meal_paste_success' => 'Grid pasted successfully!',
|
||||||
|
'meal_err_filename' => 'Unrecognized file name format. Expected example: "Menus-du-28-septembre-au-4-octobre-2026.pdf".',
|
||||||
|
|
||||||
|
'offline_title' => 'Offline Mode',
|
||||||
|
'offline_heading' => 'You are offline',
|
||||||
|
'offline_message' => 'The HouseHub application cannot be reached without an internet connection at the moment.',
|
||||||
|
'offline_retry_btn' => 'Retry connection',
|
||||||
|
|
||||||
];
|
];
|
||||||
+24
-5
@@ -105,6 +105,7 @@ return [
|
|||||||
'mod_printvault_desc'=> 'Gérez vos fichiers d\'impression 3D — STL, 3MF, GCode avec viewer interactif.',
|
'mod_printvault_desc'=> 'Gérez vos fichiers d\'impression 3D — STL, 3MF, GCode avec viewer interactif.',
|
||||||
'mod_planka_name' => 'Planka',
|
'mod_planka_name' => 'Planka',
|
||||||
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
|
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
|
||||||
|
'budget_mandatory_note' => 'Les onglets Suivi et Récapitulatif sont indispensables et toujours actifs.',
|
||||||
|
|
||||||
'cta_open' => 'Ouvrir',
|
'cta_open' => 'Ouvrir',
|
||||||
'cta_explore' => 'Explorer',
|
'cta_explore' => 'Explorer',
|
||||||
@@ -588,7 +589,6 @@ return [
|
|||||||
'bs_currency_label' => 'Devise du Foyer',
|
'bs_currency_label' => 'Devise du Foyer',
|
||||||
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
|
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
|
||||||
'bs_currency_updated' => 'Devise mise à jour avec succès !',
|
'bs_currency_updated' => 'Devise mise à jour avec succès !',
|
||||||
'bud_settings_dyn_title' => 'Estimations Dynamiques (Enfants)',
|
|
||||||
'bud_dyn_nanny_fixed' => 'Salaire fixe Assistante Maternelle (€)',
|
'bud_dyn_nanny_fixed' => 'Salaire fixe Assistante Maternelle (€)',
|
||||||
'bud_dyn_nanny_daily' => 'Frais d\'entretien journaliers (€)',
|
'bud_dyn_nanny_daily' => 'Frais d\'entretien journaliers (€)',
|
||||||
'bud_dyn_nanny_aid' => 'Aide de l\'État (CMG) (€)',
|
'bud_dyn_nanny_aid' => 'Aide de l\'État (CMG) (€)',
|
||||||
@@ -602,7 +602,16 @@ return [
|
|||||||
'bud_audit_child' => '👦 %s :',
|
'bud_audit_child' => '👦 %s :',
|
||||||
'bud_audit_school_fee' => 'Cantine (%dj) + Garderie (%dj) + Centre (%dj) = %.2f€',
|
'bud_audit_school_fee' => 'Cantine (%dj) + Garderie (%dj) + Centre (%dj) = %.2f€',
|
||||||
'bud_audit_sick' => 'Jours maladie déduits : %dj',
|
'bud_audit_sick' => 'Jours maladie déduits : %dj',
|
||||||
'bud_audit_nanny_full' => '💼 %s : Fixe (%.2f€) + Frais (%dj x %.2f€ = %.2f€) - Aide (%.2f€) - CESU (%.2f€) = %.2f€',
|
'bud_audit_nanny_full' => '💼 %s : Fixe (%.2f€) + Frais (%dj x %.2f€ = %.2f€) - CMG (%.2f€) - %s (%.2f€) = %.2f€',
|
||||||
|
'bud_audit_cesu_avg' => 'CESU (Moy.)',
|
||||||
|
'bud_audit_cesu_adj' => 'CESU (Ajusté)',
|
||||||
|
'bud_dyn_cesu_overrides' => 'Ajustements ponctuels du CESU',
|
||||||
|
'bud_dyn_cesu_add' => 'Ajouter un mois',
|
||||||
|
'bud_dyn_cesu_empty' => 'Aucun ajustement enregistré.',
|
||||||
|
'bud_settings_dyn_title' => 'Garde & École',
|
||||||
|
'bud_settings_dyn_desc' => 'Tarifs pour le calcul automatisé des frais de garde.',
|
||||||
|
'bud_dyn_sub_nanny' => 'Nounou',
|
||||||
|
'bud_dyn_sub_school' => 'Périscolaire',
|
||||||
|
|
||||||
// CSV Mapping (Budget)
|
// CSV Mapping (Budget)
|
||||||
'bs_csv_title' => 'Format de l\'import CSV',
|
'bs_csv_title' => 'Format de l\'import CSV',
|
||||||
@@ -1011,9 +1020,9 @@ return [
|
|||||||
'meal_saved' => 'Repas enregistré !',
|
'meal_saved' => 'Repas enregistré !',
|
||||||
'meal_btn_import_pdf' => 'Importer Menu PDF',
|
'meal_btn_import_pdf' => 'Importer Menu PDF',
|
||||||
'meal_import_title' => 'Importer un menu de cantine',
|
'meal_import_title' => 'Importer un menu de cantine',
|
||||||
'meal_import_child' => 'Pour quel enfant ?',
|
'meal_import_child' => 'Pour quel membre ?',
|
||||||
'meal_select_child' => 'Sélectionner l\'enfant',
|
'meal_select_child' => 'Sélectionner un membre :',
|
||||||
'meal_pdf_file' => 'Fichier PDF (Convivio)',
|
'meal_pdf_file' => 'Fichier PDF',
|
||||||
'meal_drop_pdf' => 'Cliquez pour ajouter le PDF',
|
'meal_drop_pdf' => 'Cliquez pour ajouter le PDF',
|
||||||
'meal_import_success' => 'Menus importés avec succès !',
|
'meal_import_success' => 'Menus importés avec succès !',
|
||||||
'mod_food_name' => 'Cuisine & Courses',
|
'mod_food_name' => 'Cuisine & Courses',
|
||||||
@@ -1022,4 +1031,14 @@ return [
|
|||||||
'menu_food' => 'Food',
|
'menu_food' => 'Food',
|
||||||
'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.',
|
'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.',
|
||||||
'cta_food' => 'Mijoter',
|
'cta_food' => 'Mijoter',
|
||||||
|
'food_err_no_views' => "Aucune vue n'est activée pour ce module. Modifiez vos paramètres d'espace.",
|
||||||
|
'food_err_no_views_desc' => 'Vous avez désactivé toutes les fonctionnalités de ce module. Rendez-vous dans vos paramètres pour les réactiver.',
|
||||||
|
'meal_err_filename' => 'Format de nom de fichier non reconnu. Exemple attendu : "Menus-du-28-septembre-au-4-octobre-2026.pdf".',
|
||||||
|
'meal_paste_success' => 'Grille collée avec succès !',
|
||||||
|
|
||||||
|
'offline_title' => 'Mode hors ligne',
|
||||||
|
'offline_heading' => 'Vous êtes hors ligne',
|
||||||
|
'offline_message' => 'L\'application HouseHub n\'est pas joignable sans connexion internet pour le moment.',
|
||||||
|
'offline_retry_btn' => 'Réessayer de se connecter',
|
||||||
|
|
||||||
];
|
];
|
||||||
@@ -25,8 +25,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$error = tr('error_missing_fields');
|
$error = tr('error_missing_fields');
|
||||||
} else {
|
} else {
|
||||||
$stmt = $meta_pdo->prepare("
|
$stmt = $meta_pdo->prepare("
|
||||||
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang,
|
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang, u.remember_token,
|
||||||
f.db_name, f.is_active as family_active, f.enabled_modules
|
f.db_name, f.is_active as family_active, f.enabled_modules, f.enabled_views
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN families f ON f.id = u.family_id
|
LEFT JOIN families f ON f.id = u.family_id
|
||||||
WHERE u.username = ?
|
WHERE u.username = ?
|
||||||
@@ -51,6 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$_SESSION['family_db'] = $user['db_name'];
|
$_SESSION['family_db'] = $user['db_name'];
|
||||||
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
||||||
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
||||||
|
$_SESSION['enabled_views'] = json_decode($user['enabled_views'] ?? '["food_meals","food_liste"]', true);
|
||||||
|
|
||||||
// --- NOUVEAU : Logique "Se souvenir de moi" ---
|
// --- NOUVEAU : Logique "Se souvenir de moi" ---
|
||||||
if (isset($_POST['remember_me']) && $_POST['remember_me'] === '1') {
|
if (isset($_POST['remember_me']) && $_POST['remember_me'] === '1') {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "HouseHub OS",
|
||||||
|
"short_name": "HouseHub",
|
||||||
|
"description": "Système de gestion familiale",
|
||||||
|
"start_url": "/index.php",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#f8fafc",
|
||||||
|
"theme_color": "#3b82f6",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/favicon.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+19
-72
@@ -1,81 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
// migrate.php
|
// migrate.php
|
||||||
require __DIR__ . '/includes/auth.php';
|
|
||||||
|
// 1. Initialisation autonome de la connexion Meta
|
||||||
require __DIR__ . '/includes/meta_db.php';
|
require __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
|
// 2. FIX CHICKEN & EGG 🛑 : Création de la colonne AVANT de charger l'authentification
|
||||||
|
try {
|
||||||
|
$checkMetaCol = $meta_pdo->query("SHOW COLUMNS FROM families LIKE 'enabled_views'");
|
||||||
|
if ($checkMetaCol->rowCount() == 0) {
|
||||||
|
$meta_pdo->exec("ALTER TABLE families ADD COLUMN enabled_views JSON DEFAULT NULL AFTER enabled_modules");
|
||||||
|
echo "✅ Colonne 'enabled_views' ajoutée à la table families (Bypass Auth).<br><br>";
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("❌ Erreur critique lors de la mise à jour structurelle : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Chargement sécurisé de l'authentification (qui fonctionne maintenant)
|
||||||
|
require __DIR__ . '/includes/auth.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
|
// 4. Sécurisation de la page
|
||||||
if (empty($_SESSION['user']['is_admin'])) {
|
if (empty($_SESSION['user']['is_admin'])) {
|
||||||
die("Accès refusé. Droits administrateur requis.");
|
die("Accès refusé. Droits administrateur requis.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
echo "<h1>Migration Meta</h1>";
|
||||||
$stmt = $meta_pdo->query("SELECT db_name FROM families");
|
echo "🎉 L'infrastructure est à jour et la session a pu démarrer correctement.";
|
||||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
foreach ($families as $fam) {
|
|
||||||
$dbName = $fam['db_name'];
|
|
||||||
echo "Migration de la base : $dbName ... <br>";
|
|
||||||
|
|
||||||
// --- DÉBUT DU BLOC MODIFIÉ ---
|
|
||||||
$envHost = getenv('DB_HOST');
|
|
||||||
if ($envHost === 'househub-db') {
|
|
||||||
$host = 'househub-db';
|
|
||||||
$user = getenv('DB_USER') ?: 'househub';
|
|
||||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
|
||||||
} else {
|
|
||||||
$host = '127.0.0.1';
|
|
||||||
$user = 'root';
|
|
||||||
$pass = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$familyPdo = new PDO(
|
|
||||||
"mysql:host=$host;dbname=$dbName;charset=utf8mb4",
|
|
||||||
$user,
|
|
||||||
$pass,
|
|
||||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
|
||||||
);
|
|
||||||
// --- FIN DU BLOC MODIFIÉ ---
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_meals_plan (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
plan_date DATE NOT NULL,
|
|
||||||
service ENUM('lunch', 'dinner') NOT NULL,
|
|
||||||
person_id INT NOT NULL,
|
|
||||||
meal_name VARCHAR(500) DEFAULT NULL,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uq_meal_plan (plan_date, service, person_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
";
|
|
||||||
|
|
||||||
$familyPdo->exec($sql);
|
|
||||||
echo "✅ Table pf_meals_plan créée pour $dbName.<br>";
|
|
||||||
}
|
|
||||||
echo "<br>🎉 Migration terminée avec succès.";
|
|
||||||
|
|
||||||
// À ajouter dans migrate.php
|
|
||||||
echo "Migration : Ajout des colonnes dynamiques au Budget...\n";
|
|
||||||
$stmtFam = $meta_pdo->query("SELECT id, db_name FROM families");
|
|
||||||
$families = $stmtFam->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
foreach ($families as $fam) {
|
|
||||||
$dbName = $fam['db_name'];
|
|
||||||
try {
|
|
||||||
$famPdo = new PDO("mysql:host=$meta_host;dbname=$dbName;charset=utf8mb4", $meta_user, $meta_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
||||||
|
|
||||||
// Vérification si la colonne n'existe pas déjà
|
|
||||||
$checkCol = $famPdo->query("SHOW COLUMNS FROM pf_budget_items LIKE 'is_dynamic'");
|
|
||||||
if ($checkCol->rowCount() == 0) {
|
|
||||||
$famPdo->exec("ALTER TABLE pf_budget_items
|
|
||||||
ADD COLUMN is_dynamic TINYINT(1) DEFAULT 0 AFTER is_estimate,
|
|
||||||
ADD COLUMN dynamic_code VARCHAR(50) DEFAULT NULL AFTER is_dynamic");
|
|
||||||
echo "Famille {$fam['id']} ($dbName) : Colonnes is_dynamic et dynamic_code ajoutées.\n";
|
|
||||||
}
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
echo "Erreur sur la famille {$fam['id']} ($dbName) : " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die("❌ Erreur lors de la migration : " . $e->getMessage());
|
|
||||||
}
|
|
||||||
-626
@@ -1,626 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
||||||
/>
|
|
||||||
<title>HouseHub - Planning Repas</title>
|
|
||||||
<style>
|
|
||||||
/* --- VARIABLES --- */
|
|
||||||
:root {
|
|
||||||
--primary: #3b82f6;
|
|
||||||
--bg-page: #f8fafc;
|
|
||||||
--bg-panel: #ffffff;
|
|
||||||
--bg-subtle: #f1f5f9;
|
|
||||||
--text-main: #0f172a;
|
|
||||||
--text-muted: #64748b;
|
|
||||||
--border-light: #e2e8f0;
|
|
||||||
--border-strong: #cbd5e1;
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
||||||
--shadow-lg: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
--alert-success-bg: #dcfce7;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family:
|
|
||||||
"Inter",
|
|
||||||
-apple-system,
|
|
||||||
sans-serif;
|
|
||||||
background: var(--bg-page);
|
|
||||||
color: var(--text-main);
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px;
|
|
||||||
overscroll-behavior-y: none;
|
|
||||||
padding-bottom: 70px; /* Espace pour la toolbar fixe */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- EN-TÊTE --- */
|
|
||||||
.header-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.header-bar h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: var(--text-main);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.pf-btn {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
color: var(--text-main);
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
.pf-btn.primary {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- MATRICE ULTRA-COMPACTE --- */
|
|
||||||
.meals-wrapper {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.meals-table {
|
|
||||||
width: 100%;
|
|
||||||
table-layout: fixed;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
.meals-table th,
|
|
||||||
.meals-table td {
|
|
||||||
border: 1px solid var(--border-light);
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.meals-table th {
|
|
||||||
background: var(--bg-subtle);
|
|
||||||
font-size: 0.6rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-muted);
|
|
||||||
padding: 6px 2px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-sticky {
|
|
||||||
width: 40px;
|
|
||||||
background: var(--bg-subtle);
|
|
||||||
border-right: 2px solid var(--border-strong) !important;
|
|
||||||
padding: 4px 2px !important;
|
|
||||||
}
|
|
||||||
.day-label {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-main);
|
|
||||||
text-transform: uppercase;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
.day-label.invisible {
|
|
||||||
visibility: hidden;
|
|
||||||
height: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.service-icon {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 1rem;
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
transition: transform 0.1s;
|
|
||||||
}
|
|
||||||
.service-icon:active {
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-lunch td {
|
|
||||||
border-top: 2px solid var(--border-strong) !important;
|
|
||||||
}
|
|
||||||
.row-dinner td {
|
|
||||||
border-top: 1px dashed var(--border-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meal-input-cell {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--bg-panel);
|
|
||||||
}
|
|
||||||
.meal-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 44px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 4px;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-family: inherit;
|
|
||||||
resize: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.meal-input:focus {
|
|
||||||
background: #eff6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- BARRE CONTEXTUELLE (FIXE EN BAS) --- */
|
|
||||||
.context-toolbar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: -80px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 60px;
|
|
||||||
background: var(--bg-panel);
|
|
||||||
border-top: 1px solid var(--border-strong);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
transition: bottom 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
z-index: 1000;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
.context-toolbar.active {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
.ctx-btn {
|
|
||||||
background: var(--bg-subtle);
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-main);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.ctx-btn.primary {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- MODALE : COPIER VERS (DESIGN GRILLE) --- */
|
|
||||||
.pf-modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(15, 23, 42, 0.6);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
.pf-modal.open {
|
|
||||||
display: flex;
|
|
||||||
animation: fadeInBg 0.2s;
|
|
||||||
}
|
|
||||||
.pf-modal-content {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
width: 95%;
|
|
||||||
max-width: 600px;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.modal-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-weight: 800;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.modal-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-muted);
|
|
||||||
line-height: 1;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
.modal-source {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grille des Jours */
|
|
||||||
.week-section-title {
|
|
||||||
background: var(--bg-subtle);
|
|
||||||
text-align: center;
|
|
||||||
padding: 6px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 15px 0 10px 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
border: 1px solid var(--border-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.days-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-card {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
border: 1px solid var(--border-light);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
.day-card-title {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
.day-card-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.day-action-btn {
|
|
||||||
background: var(--bg-page);
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 6px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.1s;
|
|
||||||
}
|
|
||||||
.day-action-btn:active {
|
|
||||||
transform: scale(0.9);
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 380px) {
|
|
||||||
.days-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes fadeInBg {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header-bar">
|
|
||||||
<h1>
|
|
||||||
<span style="opacity: 0.5">🍽️</span>
|
|
||||||
<span data-i18n="meals_board_title">Planning Repas</span>
|
|
||||||
</h1>
|
|
||||||
<button class="pf-btn" title="Générer les courses">
|
|
||||||
🛒 <span data-i18n="btn_groceries">Courses</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- BARRE CONTEXTUELLE FIXE (Évite le conflit avec le clavier mobile) -->
|
|
||||||
<div class="context-toolbar" id="contextToolbar">
|
|
||||||
<button
|
|
||||||
class="ctx-btn"
|
|
||||||
onmousedown="
|
|
||||||
event.preventDefault();
|
|
||||||
applyToAllInRow();
|
|
||||||
"
|
|
||||||
>
|
|
||||||
➡️ <span data-i18n="btn_apply_all">Tous</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="ctx-btn primary"
|
|
||||||
onmousedown="
|
|
||||||
event.preventDefault();
|
|
||||||
openCopySheetFromCell();
|
|
||||||
"
|
|
||||||
>
|
|
||||||
🔄 <span data-i18n="btn_copy_to">Copier vers...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="meals-wrapper">
|
|
||||||
<table class="meals-table" id="mealsTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="col-sticky"></th>
|
|
||||||
<th>PAPA</th>
|
|
||||||
<th>MAMAN</th>
|
|
||||||
<th>E1</th>
|
|
||||||
<th>E2</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<!-- Injecté via JS -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MODALE DE COPIE (GRILLE LUNDI-DIMANCHE) -->
|
|
||||||
<div id="copyModal" class="pf-modal">
|
|
||||||
<div class="pf-modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 class="modal-title">
|
|
||||||
🔄 <span data-i18n="modal_copy_title">Copier vers...</span>
|
|
||||||
</h3>
|
|
||||||
<button class="modal-close" onclick="closeCopyModal()">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="copySourceLabel" class="modal-source">
|
|
||||||
Source : Plat individuel (Lundi Midi)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="week-section-title" data-i18n="week_current">
|
|
||||||
Semaine actuelle
|
|
||||||
</div>
|
|
||||||
<div class="days-grid" id="currentWeekGrid"></div>
|
|
||||||
|
|
||||||
<div class="week-section-title" data-i18n="week_next">
|
|
||||||
Semaine prochaine
|
|
||||||
</div>
|
|
||||||
<div class="days-grid" id="nextWeekGrid"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// --- I18N MOCKUP ---
|
|
||||||
window.I18N = {
|
|
||||||
meals_board_title: "Planning Repas",
|
|
||||||
btn_groceries: "Courses",
|
|
||||||
btn_apply_all: "Tous",
|
|
||||||
btn_copy_to: "Copier vers...",
|
|
||||||
modal_copy_title: "Copier vers...",
|
|
||||||
source_individual: "Plat individuel",
|
|
||||||
source_row: "Repas complet",
|
|
||||||
week_current: "Semaine actuelle",
|
|
||||||
week_next: "Semaine prochaine",
|
|
||||||
};
|
|
||||||
document
|
|
||||||
.querySelectorAll("[data-i18n]")
|
|
||||||
.forEach(
|
|
||||||
(el) => (el.innerHTML = window.I18N[el.getAttribute("data-i18n")]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const days = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
|
|
||||||
const fullDays = [
|
|
||||||
"Lundi",
|
|
||||||
"Mardi",
|
|
||||||
"Mercredi",
|
|
||||||
"Jeudi",
|
|
||||||
"Vendredi",
|
|
||||||
"Samedi",
|
|
||||||
"Dimanche",
|
|
||||||
];
|
|
||||||
const membersCount = 4;
|
|
||||||
const tbody = document.querySelector("#mealsTable tbody");
|
|
||||||
|
|
||||||
let copyState = {
|
|
||||||
type: null,
|
|
||||||
dayIdx: null,
|
|
||||||
service: null,
|
|
||||||
personIdx: null,
|
|
||||||
sourceElements: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- 1. GÉNÉRATION DE LA GRILLE PRINCIPALE ---
|
|
||||||
days.forEach((day, index) => {
|
|
||||||
let trLunch = document.createElement("tr");
|
|
||||||
trLunch.className = "row-lunch";
|
|
||||||
trLunch.innerHTML = `
|
|
||||||
<td class="col-sticky">
|
|
||||||
<span class="day-label">${day}</span>
|
|
||||||
<div class="service-icon" onclick="openCopySheetFromRow(${index}, 'lunch')" title="Copier la ligne">☀️</div>
|
|
||||||
</td>`;
|
|
||||||
for (let p = 0; p < membersCount; p++) {
|
|
||||||
trLunch.innerHTML += `<td class="meal-input-cell"><textarea class="meal-input js-meal-input" data-d="${index}" data-s="lunch" data-p="${p}" placeholder="-"></textarea></td>`;
|
|
||||||
}
|
|
||||||
tbody.appendChild(trLunch);
|
|
||||||
|
|
||||||
let trDinner = document.createElement("tr");
|
|
||||||
trDinner.className = "row-dinner";
|
|
||||||
trDinner.innerHTML = `
|
|
||||||
<td class="col-sticky">
|
|
||||||
<span class="day-label invisible">${day}</span>
|
|
||||||
<div class="service-icon" onclick="openCopySheetFromRow(${index}, 'dinner')" title="Copier la ligne">🌙</div>
|
|
||||||
</td>`;
|
|
||||||
for (let p = 0; p < membersCount; p++) {
|
|
||||||
trDinner.innerHTML += `<td class="meal-input-cell"><textarea class="meal-input js-meal-input" data-d="${index}" data-s="dinner" data-p="${p}" placeholder="-"></textarea></td>`;
|
|
||||||
}
|
|
||||||
tbody.appendChild(trDinner);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 2. GÉNÉRATION DES GRILLES DE LA MODALE COPIE ---
|
|
||||||
function buildModalGrid(containerId, weekTarget) {
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
let html = "";
|
|
||||||
fullDays.forEach((dayName, idx) => {
|
|
||||||
html += `
|
|
||||||
<div class="day-card">
|
|
||||||
<div class="day-card-title">${dayName}</div>
|
|
||||||
<div class="day-card-actions">
|
|
||||||
<button type="button" class="day-action-btn" onclick="executeCopy(${idx}, 'lunch', '${weekTarget}')">☀️</button>
|
|
||||||
<button type="button" class="day-action-btn" onclick="executeCopy(${idx}, 'dinner', '${weekTarget}')">🌙</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
buildModalGrid("currentWeekGrid", "current");
|
|
||||||
buildModalGrid("nextWeekGrid", "next");
|
|
||||||
|
|
||||||
// --- 3. GESTION DU FOCUS & DE LA TOOLBAR ---
|
|
||||||
document.addEventListener("focusin", (e) => {
|
|
||||||
if (e.target.classList.contains("js-meal-input")) {
|
|
||||||
copyState = {
|
|
||||||
type: "cell",
|
|
||||||
dayIdx: e.target.getAttribute("data-d"),
|
|
||||||
service: e.target.getAttribute("data-s"),
|
|
||||||
personIdx: e.target.getAttribute("data-p"),
|
|
||||||
sourceElements: [e.target],
|
|
||||||
};
|
|
||||||
document.getElementById("contextToolbar").classList.add("active");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("focusout", (e) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!document.activeElement.classList.contains("js-meal-input")) {
|
|
||||||
document
|
|
||||||
.getElementById("contextToolbar")
|
|
||||||
.classList.remove("active");
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
|
|
||||||
function flashSuccess(input) {
|
|
||||||
input.style.transition = "background 0.3s";
|
|
||||||
input.style.background = "var(--alert-success-bg)";
|
|
||||||
setTimeout(() => (input.style.background = "transparent"), 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 4. ACTIONS DE COPIE ---
|
|
||||||
function applyToAllInRow() {
|
|
||||||
if (
|
|
||||||
copyState.type !== "cell" ||
|
|
||||||
!copyState.sourceElements[0].value.trim()
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const val = copyState.sourceElements[0].value;
|
|
||||||
const inputs = document.querySelectorAll(
|
|
||||||
`.js-meal-input[data-d="${copyState.dayIdx}"][data-s="${copyState.service}"]`,
|
|
||||||
);
|
|
||||||
inputs.forEach((inp) => {
|
|
||||||
if (inp !== copyState.sourceElements[0]) {
|
|
||||||
inp.value = val;
|
|
||||||
flashSuccess(inp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openCopySheetFromCell() {
|
|
||||||
if (
|
|
||||||
!copyState.sourceElements[0] ||
|
|
||||||
!copyState.sourceElements[0].value.trim()
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
document.getElementById("contextToolbar").classList.remove("active");
|
|
||||||
document.activeElement.blur(); // Masque le clavier mobile
|
|
||||||
|
|
||||||
const svcLabel = copyState.service === "lunch" ? "Midi" : "Soir";
|
|
||||||
document.getElementById("copySourceLabel").innerText =
|
|
||||||
`Source : ${window.I18N.source_individual} (${fullDays[copyState.dayIdx]} ${svcLabel})`;
|
|
||||||
document.getElementById("copyModal").classList.add("open");
|
|
||||||
document.body.classList.add("no-scroll");
|
|
||||||
}
|
|
||||||
|
|
||||||
function openCopySheetFromRow(dIdx, service) {
|
|
||||||
const inputs = document.querySelectorAll(
|
|
||||||
`.js-meal-input[data-d="${dIdx}"][data-s="${service}"]`,
|
|
||||||
);
|
|
||||||
const hasContent = Array.from(inputs).some(
|
|
||||||
(inp) => inp.value.trim() !== "",
|
|
||||||
);
|
|
||||||
if (!hasContent) return;
|
|
||||||
|
|
||||||
copyState = {
|
|
||||||
type: "row",
|
|
||||||
dayIdx: dIdx,
|
|
||||||
service: service,
|
|
||||||
sourceElements: Array.from(inputs),
|
|
||||||
};
|
|
||||||
const svcLabel = service === "lunch" ? "Midi" : "Soir";
|
|
||||||
document.getElementById("copySourceLabel").innerText =
|
|
||||||
`Source : ${window.I18N.source_row} (${fullDays[dIdx]} ${svcLabel})`;
|
|
||||||
document.getElementById("copyModal").classList.add("open");
|
|
||||||
document.body.classList.add("no-scroll");
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeCopyModal() {
|
|
||||||
document.getElementById("copyModal").classList.remove("open");
|
|
||||||
document.body.classList.remove("no-scroll");
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeCopy(targetDayIdx, targetService, targetWeek) {
|
|
||||||
if (targetWeek === "next") {
|
|
||||||
alert("Simulation : Copié avec succès vers la SEMAINE PROCHAINE !");
|
|
||||||
closeCopyModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (copyState.type === "cell") {
|
|
||||||
const val = copyState.sourceElements[0].value;
|
|
||||||
const targetInput = document.querySelector(
|
|
||||||
`.js-meal-input[data-d="${targetDayIdx}"][data-s="${targetService}"][data-p="${copyState.personIdx}"]`,
|
|
||||||
);
|
|
||||||
if (targetInput) {
|
|
||||||
targetInput.value = val;
|
|
||||||
flashSuccess(targetInput);
|
|
||||||
}
|
|
||||||
} else if (copyState.type === "row") {
|
|
||||||
const targetInputs = document.querySelectorAll(
|
|
||||||
`.js-meal-input[data-d="${targetDayIdx}"][data-s="${targetService}"]`,
|
|
||||||
);
|
|
||||||
copyState.sourceElements.forEach((inp, idx) => {
|
|
||||||
if (inp.value.trim() !== "") {
|
|
||||||
targetInputs[idx].value = inp.value;
|
|
||||||
flashSuccess(targetInputs[idx]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
closeCopyModal();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -2701,3 +2701,36 @@ body.sum-mode-active .sum-target {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- 16. AJUSTEMENTS CESU (JSON OVERRIDES) --- */
|
||||||
|
.bs-cesu-wrapper {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.bs-cesu-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.bs-cesu-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.bs-cesu-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.bs-cesu-row input[type="month"] {
|
||||||
|
width: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.bs-cesu-row input[type="number"] {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
@@ -32,6 +32,10 @@ class DynamicBudgetCalculator {
|
|||||||
return isset($this->settings[$key]) ? (float)$this->settings[$key] : (float)$default;
|
return isset($this->settings[$key]) ? (float)$this->settings[$key] : (float)$default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getSettingString(string $key, string $default = ''): string {
|
||||||
|
return isset($this->settings[$key]) ? (string)$this->settings[$key] : $default;
|
||||||
|
}
|
||||||
|
|
||||||
private function loadPeople(): void {
|
private function loadPeople(): void {
|
||||||
// 1. Chargement des enfants sans doublons
|
// 1. Chargement des enfants sans doublons
|
||||||
$stmt = $this->pdo->query("SELECT id, name, care_modes FROM pf_people WHERE role IN ('enfant', 'child') AND is_active = 1");
|
$stmt = $this->pdo->query("SELECT id, name, care_modes FROM pf_people WHERE role IN ('enfant', 'child') AND is_active = 1");
|
||||||
@@ -85,7 +89,13 @@ class DynamicBudgetCalculator {
|
|||||||
|
|
||||||
$zone = $this->foyerSettings['zone_scolaire'] ?? 'C';
|
$zone = $this->foyerSettings['zone_scolaire'] ?? 'C';
|
||||||
try {
|
try {
|
||||||
$url = "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='{$this->year}-" . ($this->year + 1) . "' AND zones LIKE '%Zone {$zone}%'&limit=100";
|
// 🔥 FIX STATE OF THE ART : On cible uniquement la population "Élèves"
|
||||||
|
$prevYear = $this->year - 1;
|
||||||
|
$nextYear = $this->year + 1;
|
||||||
|
$where = "(annee_scolaire='{$prevYear}-{$this->year}' OR annee_scolaire='{$this->year}-{$nextYear}') AND zones LIKE '%Zone {$zone}%' AND population = 'Élèves'";
|
||||||
|
|
||||||
|
$url = "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=" . urlencode($where) . "&limit=100";
|
||||||
|
|
||||||
$jsonVacances = @file_get_contents($url);
|
$jsonVacances = @file_get_contents($url);
|
||||||
if ($jsonVacances) {
|
if ($jsonVacances) {
|
||||||
$data = json_decode($jsonVacances, true);
|
$data = json_decode($jsonVacances, true);
|
||||||
@@ -93,7 +103,9 @@ class DynamicBudgetCalculator {
|
|||||||
foreach ($data['results'] as $r) {
|
foreach ($data['results'] as $r) {
|
||||||
$start = new DateTime(substr($r['start_date'], 0, 10));
|
$start = new DateTime(substr($r['start_date'], 0, 10));
|
||||||
$end = new DateTime(substr($r['end_date'], 0, 10));
|
$end = new DateTime(substr($r['end_date'], 0, 10));
|
||||||
|
// Sécurité : si la vacance commence le vendredi soir, on la compte à partir du samedi
|
||||||
if ($start->format('N') == 5) $start->modify('+1 day');
|
if ($start->format('N') == 5) $start->modify('+1 day');
|
||||||
|
|
||||||
while ($start < $end) {
|
while ($start < $end) {
|
||||||
$offDays['vacances'][] = $start->format('Y-m-d');
|
$offDays['vacances'][] = $start->format('Y-m-d');
|
||||||
$start->modify('+1 day');
|
$start->modify('+1 day');
|
||||||
@@ -120,7 +132,16 @@ class DynamicBudgetCalculator {
|
|||||||
$nannyFixed = $this->getSetting('budget_nanny_fixed', 700);
|
$nannyFixed = $this->getSetting('budget_nanny_fixed', 700);
|
||||||
$nannyDaily = $this->getSetting('budget_nanny_daily', 4);
|
$nannyDaily = $this->getSetting('budget_nanny_daily', 4);
|
||||||
$nannyAid = $this->getSetting('budget_nanny_aid', 120);
|
$nannyAid = $this->getSetting('budget_nanny_aid', 120);
|
||||||
|
|
||||||
$cesuAvg = $this->getSetting('budget_nanny_cesu_avg', 170);
|
$cesuAvg = $this->getSetting('budget_nanny_cesu_avg', 170);
|
||||||
|
$overridesJson = $this->getSettingString('budget_cesu_overrides', '{}');
|
||||||
|
$cesuOverrides = json_decode($overridesJson, true) ?: [];
|
||||||
|
|
||||||
|
$monthKey = sprintf("%04d-%02d", $this->year, $this->month);
|
||||||
|
$isCesuOverride = isset($cesuOverrides[$monthKey]);
|
||||||
|
$cesuApplied = $isCesuOverride ? (float)$cesuOverrides[$monthKey] : $cesuAvg;
|
||||||
|
$cesuLabelText = $isCesuOverride ? tr('bud_audit_cesu_adj') : tr('bud_audit_cesu_avg');
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
$schoolMeal = $this->getSetting('budget_school_meal', 5.62);
|
$schoolMeal = $this->getSetting('budget_school_meal', 5.62);
|
||||||
$schoolAftercare = $this->getSetting('budget_school_aftercare', 1.97);
|
$schoolAftercare = $this->getSetting('budget_school_aftercare', 1.97);
|
||||||
@@ -150,7 +171,7 @@ class DynamicBudgetCalculator {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, (int)$this->month, $this->year);
|
$daysInMonth = (int)date('t', strtotime(sprintf("%04d-%02d-01", $this->year, $this->month)));
|
||||||
|
|
||||||
// 2. Itération SANS pointeurs de référence
|
// 2. Itération SANS pointeurs de référence
|
||||||
for ($d = 1; $d <= $daysInMonth; $d++) {
|
for ($d = 1; $d <= $daysInMonth; $d++) {
|
||||||
@@ -161,6 +182,8 @@ class DynamicBudgetCalculator {
|
|||||||
if (in_array($dateStr, $offDays['feries'])) continue;
|
if (in_array($dateStr, $offDays['feries'])) continue;
|
||||||
|
|
||||||
$isVacances = in_array($dateStr, $offDays['vacances']);
|
$isVacances = in_array($dateStr, $offDays['vacances']);
|
||||||
|
|
||||||
|
|
||||||
$events = $this->calendarEvents[$dateStr] ?? [];
|
$events = $this->calendarEvents[$dateStr] ?? [];
|
||||||
|
|
||||||
$nannyOffToday = false;
|
$nannyOffToday = false;
|
||||||
@@ -209,13 +232,15 @@ class DynamicBudgetCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$totalNannyFees = $totalNannyDays * $nannyDaily;
|
$totalNannyFees = $totalNannyDays * $nannyDaily;
|
||||||
$nannyGlobalCost = $nannyFixed + $totalNannyFees - $nannyAid - $cesuAvg;
|
// On soustrait $cesuApplied au lieu de $cesuAvg
|
||||||
|
$nannyGlobalCost = $nannyFixed + $totalNannyFees - $nannyAid - $cesuApplied;
|
||||||
|
|
||||||
// 3. Rédaction du Bilan
|
// 3. Rédaction du Bilan
|
||||||
if ($hasNanny) {
|
if ($hasNanny) {
|
||||||
$totalCost += max(0, $nannyGlobalCost);
|
$totalCost += max(0, $nannyGlobalCost);
|
||||||
|
// On passe $cesuLabelText au sprintf
|
||||||
$details .= sprintf(tr('bud_audit_nanny_full') . "\n",
|
$details .= sprintf(tr('bud_audit_nanny_full') . "\n",
|
||||||
$this->helperName, $nannyFixed, $totalNannyDays, $nannyDaily, $totalNannyFees, $nannyAid, $cesuAvg, max(0, $nannyGlobalCost));
|
$this->helperName, $nannyFixed, $totalNannyDays, $nannyDaily, $totalNannyFees, $nannyAid, $cesuLabelText, $cesuApplied, max(0, $nannyGlobalCost));
|
||||||
|
|
||||||
if ($nannyOffDays > 0) {
|
if ($nannyOffDays > 0) {
|
||||||
$details .= sprintf(tr('bud_audit_nanny_abs') . "\n", $this->helperName, $nannyOffDays);
|
$details .= sprintf(tr('bud_audit_nanny_abs') . "\n", $this->helperName, $nannyOffDays);
|
||||||
|
|||||||
@@ -205,7 +205,8 @@ try {
|
|||||||
if ($action === 'save_dynamic_estimates') {
|
if ($action === 'save_dynamic_estimates') {
|
||||||
$keys = [
|
$keys = [
|
||||||
'budget_nanny_fixed', 'budget_nanny_daily', 'budget_nanny_aid', 'budget_nanny_cesu_avg',
|
'budget_nanny_fixed', 'budget_nanny_daily', 'budget_nanny_aid', 'budget_nanny_cesu_avg',
|
||||||
'budget_school_meal', 'budget_school_aftercare', 'budget_school_fullday'
|
'budget_school_meal', 'budget_school_aftercare', 'budget_school_fullday',
|
||||||
|
'budget_cesu_overrides'
|
||||||
];
|
];
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|||||||
@@ -224,10 +224,11 @@ if (!defined('CURRENCY')) {
|
|||||||
<!-- NOUVEAU PANE ENFANTS -->
|
<!-- NOUVEAU PANE ENFANTS -->
|
||||||
<div id="pane-children" class="bs-pane">
|
<div id="pane-children" class="bs-pane">
|
||||||
<h4 class="bs-section-title">👶 <?= tr('bud_settings_dyn_title') ?></h4>
|
<h4 class="bs-section-title">👶 <?= tr('bud_settings_dyn_title') ?></h4>
|
||||||
<p class="pf-muted-tiny bs-desc">Configurez ici les tarifs pour le calcul dynamique des frais de garde et de cantine.</p>
|
<p class="pf-muted-tiny bs-desc"><?= tr('bud_settings_dyn_desc') ?></p>
|
||||||
|
|
||||||
<form id="form-dynamic-estimates" onsubmit="saveDynamicEstimates(event)">
|
<form id="form-dynamic-estimates" onsubmit="saveDynamicEstimates(event)">
|
||||||
<h5 class="bs-section-subtitle bordered">Assistante Maternelle</h5>
|
|
||||||
|
<h5 class="bs-section-subtitle bordered"><?= tr('bud_dyn_sub_nanny') ?></h5>
|
||||||
<div class="bs-grid-2">
|
<div class="bs-grid-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="pf-label"><?= tr('bud_dyn_nanny_fixed') ?></label>
|
<label class="pf-label"><?= tr('bud_dyn_nanny_fixed') ?></label>
|
||||||
@@ -247,7 +248,17 @@ if (!defined('CURRENCY')) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5 class="bs-section-subtitle bordered" style="margin-top: 20px;">École & Centre de loisirs</h5>
|
<!-- Ajustements ponctuels du CESU -->
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<div class="bs-cesu-header" style="display:flex; justify-content:space-between; margin-bottom:12px;">
|
||||||
|
<label class="pf-label pf-m-0"><?= tr('bud_dyn_cesu_overrides') ?></label>
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-sm" onclick="addCesuOverride()">+ <?= tr('bud_dyn_cesu_add') ?></button>
|
||||||
|
</div>
|
||||||
|
<div id="cesu-overrides-list" class="bs-cesu-list"></div>
|
||||||
|
<input type="hidden" name="budget_cesu_overrides" id="budget_cesu_overrides_input" value="{}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="bs-section-subtitle bordered pf-mt-md"><?= tr('bud_dyn_sub_school') ?></h5>
|
||||||
<div class="bs-grid-2">
|
<div class="bs-grid-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="pf-label"><?= tr('bud_dyn_school_meal') ?></label>
|
<label class="pf-label"><?= tr('bud_dyn_school_meal') ?></label>
|
||||||
@@ -263,8 +274,8 @@ if (!defined('CURRENCY')) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bs-text-right" style="margin-top: 20px;">
|
<div class="bs-text-right pf-mt-md">
|
||||||
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save') ?></button>
|
<button type="submit" class="pf-btn btn-secondary">💾 <?= tr('btn_save') ?></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -341,6 +352,10 @@ async function loadBudgetSettingsData() {
|
|||||||
if (document.getElementById('budget_school_aftercare')) document.getElementById('budget_school_aftercare').value = bs.budget_school_aftercare || 1.97;
|
if (document.getElementById('budget_school_aftercare')) document.getElementById('budget_school_aftercare').value = bs.budget_school_aftercare || 1.97;
|
||||||
if (document.getElementById('budget_school_fullday')) document.getElementById('budget_school_fullday').value = bs.budget_school_fullday || 21.89;
|
if (document.getElementById('budget_school_fullday')) document.getElementById('budget_school_fullday').value = bs.budget_school_fullday || 21.89;
|
||||||
|
|
||||||
|
try { cesuOverrides = JSON.parse(bs.budget_cesu_overrides || '{}'); }
|
||||||
|
catch (e) { cesuOverrides = {}; }
|
||||||
|
renderCesuOverrides();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur chargement paramètres :", err);
|
console.error("Erreur chargement paramètres :", err);
|
||||||
const errorMsg = `<div class="pf-alert pf-alert--error bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
|
const errorMsg = `<div class="pf-alert pf-alert--error bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
|
||||||
@@ -657,4 +672,79 @@ async function saveDynamicEstimates(e) {
|
|||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- LOGIQUE DES AJUSTEMENTS CESU ---
|
||||||
|
let cesuOverrides = {};
|
||||||
|
|
||||||
|
function renderCesuOverrides() {
|
||||||
|
const list = document.getElementById('cesu-overrides-list');
|
||||||
|
const input = document.getElementById('budget_cesu_overrides_input');
|
||||||
|
if (!list || !input) return;
|
||||||
|
|
||||||
|
list.innerHTML = '';
|
||||||
|
// Tri décroissant pour afficher le mois le plus lointain en haut
|
||||||
|
const keys = Object.keys(cesuOverrides).sort().reverse();
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
list.innerHTML = `<div class="pf-empty-dashed" style="margin:0; padding:12px;"><em class="pf-muted-tiny">${window.I18N['bud_dyn_cesu_empty'] || 'Aucun ajustement enregistré.'}</em></div>`;
|
||||||
|
} else {
|
||||||
|
keys.forEach(k => {
|
||||||
|
const btnDelText = window.I18N['btn_delete'] || 'Supprimer';
|
||||||
|
// UI : Design aéré type "Carte HouseHub" avec des inputs sans bordures agressives
|
||||||
|
list.innerHTML += `
|
||||||
|
<div style="display: flex; gap: 10px; align-items: center; background: var(--bg-panel); padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border-light); box-shadow: var(--shadow-sm);">
|
||||||
|
<input type="month" class="pf-input" style="width: 150px; margin: 0; padding: 6px 10px;" value="${k}" onchange="updateCesuOverrideKey('${k}', this.value)">
|
||||||
|
<input type="number" step="0.01" class="pf-input" style="flex: 1; margin: 0; padding: 6px 10px; text-align: right;" value="${cesuOverrides[k]}" onchange="updateCesuOverrideValue('${k}', this.value)">
|
||||||
|
<span style="color: var(--text-muted); font-weight: 600; margin-right: 5px;">€</span>
|
||||||
|
<button type="button" class="btn-icon-action delete" style="margin: 0; background: var(--bg-danger-soft); width: 32px; height: 32px;" onclick="removeCesuOverride('${k}')" title="${btnDelText}">🗑️</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
input.value = JSON.stringify(cesuOverrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCesuOverride() {
|
||||||
|
const keys = Object.keys(cesuOverrides).sort();
|
||||||
|
let nextDate = new Date();
|
||||||
|
nextDate.setMonth(nextDate.getMonth() + 1); // Base : le mois prochain
|
||||||
|
|
||||||
|
// S'il y a déjà des mois dans la liste, on prend le plus lointain et on ajoute 1 mois
|
||||||
|
if (keys.length > 0) {
|
||||||
|
const lastKey = keys[keys.length - 1]; // ex: "2026-10"
|
||||||
|
const parts = lastKey.split('-');
|
||||||
|
// L'astuce : Le mois en JS commence à 0. Donc en lui passant "10", on pointe déjà sur le mois d'après (Novembre) !
|
||||||
|
nextDate = new Date(parts[0], parseInt(parts[1], 10), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sécurité anti-boucle infinie
|
||||||
|
let m = nextDate.getFullYear() + '-' + String(nextDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
let safety = 0;
|
||||||
|
while (cesuOverrides[m] !== undefined && safety < 24) {
|
||||||
|
nextDate.setMonth(nextDate.getMonth() + 1);
|
||||||
|
m = nextDate.getFullYear() + '-' + String(nextDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
safety++;
|
||||||
|
}
|
||||||
|
|
||||||
|
cesuOverrides[m] = 0;
|
||||||
|
renderCesuOverrides();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCesuOverrideKey(oldK, newK) {
|
||||||
|
if (oldK !== newK && newK) {
|
||||||
|
cesuOverrides[newK] = cesuOverrides[oldK];
|
||||||
|
delete cesuOverrides[oldK];
|
||||||
|
renderCesuOverrides();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCesuOverrideValue(k, val) {
|
||||||
|
cesuOverrides[k] = parseFloat(val) || 0;
|
||||||
|
renderCesuOverrides();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCesuOverride(k) {
|
||||||
|
delete cesuOverrides[k];
|
||||||
|
renderCesuOverrides();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
+117
-43
@@ -116,11 +116,35 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
$csvData = [];
|
$csvData = [];
|
||||||
$showPreview = false;
|
$showPreview = false;
|
||||||
|
$importError = null;
|
||||||
|
|
||||||
|
if (isset($_FILES['csv_file'])) {
|
||||||
|
if ($_FILES['csv_file']['error'] == 0) {
|
||||||
|
|
||||||
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
|
||||||
$file = $_FILES['csv_file']['tmp_name'];
|
$file = $_FILES['csv_file']['tmp_name'];
|
||||||
|
|
||||||
|
// 🔥 FIX INDESTRUCTIBLE PHP 8.3+ POUR LES FINS DE LIGNE iPHONE / MAC
|
||||||
|
// On normalise brutalement tous les types de sauts de ligne en \n
|
||||||
|
$fileContent = file_get_contents($file);
|
||||||
|
$fileContent = preg_replace("/\r\n|\r|\n/", "\n", $fileContent);
|
||||||
|
file_put_contents($file, $fileContent);
|
||||||
|
|
||||||
$handle = fopen($file, "r");
|
$handle = fopen($file, "r");
|
||||||
|
|
||||||
|
// 1. Récupération dynamique du mapping configuré dans les paramètres
|
||||||
|
$stmtMap = $pdo->query("SELECT csv_mapping FROM pf_foyer_settings LIMIT 1");
|
||||||
|
$csvMapRaw = $stmtMap->fetchColumn();
|
||||||
|
$mapping = $csvMapRaw ? json_decode($csvMapRaw, true) : null;
|
||||||
|
|
||||||
|
$delimiter = $mapping['delimiter'] ?? ';';
|
||||||
|
$colDate = isset($mapping['col_date']) ? (int)$mapping['col_date'] : 0;
|
||||||
|
$colLabel = isset($mapping['col_label']) ? (int)$mapping['col_label'] : 1;
|
||||||
|
$amountType = $mapping['amount_type'] ?? 'split';
|
||||||
|
$colDebit = isset($mapping['col_debit']) ? (int)$mapping['col_debit'] : 8;
|
||||||
|
$colCredit = isset($mapping['col_credit']) ? (int)$mapping['col_credit'] : 9;
|
||||||
|
$colRef = isset($mapping['col_ref']) ? (int)$mapping['col_ref'] : 3;
|
||||||
|
$dateFormat = $mapping['date_format'] ?? 'd/m/Y';
|
||||||
|
|
||||||
$rules = [];
|
$rules = [];
|
||||||
try {
|
try {
|
||||||
$stmtRules = $pdo->query("SELECT keyword, category, budget_item_id FROM pf_import_rules");
|
$stmtRules = $pdo->query("SELECT keyword, category, budget_item_id FROM pf_import_rules");
|
||||||
@@ -129,20 +153,59 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
|||||||
}
|
}
|
||||||
} catch(Exception $e){}
|
} catch(Exception $e){}
|
||||||
|
|
||||||
$existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){}
|
$existingRefs = [];
|
||||||
fgetcsv($handle, 1000, ";", "\"", "\\");
|
try {
|
||||||
|
$existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
} catch(Exception $e){}
|
||||||
|
|
||||||
while (($data = fgetcsv($handle, 1000, ";", "\"", "\\")) !== FALSE) {
|
fgetcsv($handle, 1000, $delimiter, "\"", "\\"); // Skip header
|
||||||
$rawDebit = $data[8] ?? ''; $rawCredit = $data[9] ?? '';
|
|
||||||
$amount = 0; $isCredit = 0;
|
|
||||||
if (!empty(trim($rawCredit))) { $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawCredit))); $isCredit = 1; }
|
|
||||||
elseif (!empty(trim($rawDebit))) { $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawDebit))); }
|
|
||||||
else continue;
|
|
||||||
|
|
||||||
$dateParts = explode('/', $data[0]);
|
while (($data = fgetcsv($handle, 1000, $delimiter, "\"", "\\")) !== FALSE) {
|
||||||
$dateSql = (count($dateParts) == 3) ? $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0] : date('Y-m-d');
|
// Si la ligne est vide on passe
|
||||||
$label = trim($data[1]) ?: trim($data[2]);
|
if (empty(array_filter($data))) continue;
|
||||||
$refCSV = trim($data[3]);
|
|
||||||
|
$amount = 0;
|
||||||
|
$isCredit = 0;
|
||||||
|
|
||||||
|
// 2. Traitement dynamique du montant
|
||||||
|
if ($amountType === 'single') {
|
||||||
|
$val = $data[$colDebit] ?? '';
|
||||||
|
if (!empty(trim($val))) {
|
||||||
|
$floatVal = (float)str_replace([' ', ','], ['', '.'], $val);
|
||||||
|
$amount = abs($floatVal);
|
||||||
|
$isCredit = $floatVal > 0 ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rawDebit = $data[$colDebit] ?? '';
|
||||||
|
$rawCredit = $data[$colCredit] ?? '';
|
||||||
|
if (!empty(trim($rawCredit))) {
|
||||||
|
$amount = abs((float)str_replace([' ', ','], ['', '.'], $rawCredit));
|
||||||
|
$isCredit = 1;
|
||||||
|
} elseif (!empty(trim($rawDebit))) {
|
||||||
|
$amount = abs((float)str_replace([' ', ','], ['', '.'], $rawDebit));
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Traitement dynamique de la date
|
||||||
|
$rawDate = $data[$colDate] ?? '';
|
||||||
|
$dateSql = date('Y-m-d');
|
||||||
|
if ($dateFormat === 'd/m/Y') {
|
||||||
|
$dateParts = explode('/', $rawDate);
|
||||||
|
if (count($dateParts) >= 3) {
|
||||||
|
$dateSql = $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$ts = strtotime($rawDate);
|
||||||
|
if ($ts) $dateSql = date('Y-m-d', $ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On prend la colonne Label paramétrée, ou la 2ème en secours
|
||||||
|
$label = trim($data[$colLabel] ?? '') ?: trim($data[2] ?? '');
|
||||||
|
$refCSV = trim($data[$colRef] ?? '');
|
||||||
$uniqueKey = !empty($refCSV) ? "REF_".$refCSV : "HASH_".md5($dateSql.$label.number_format($amount, 2).$isCredit);
|
$uniqueKey = !empty($refCSV) ? "REF_".$refCSV : "HASH_".md5($dateSql.$label.number_format($amount, 2).$isCredit);
|
||||||
$isDuplicate = in_array($uniqueKey, $existingRefs);
|
$isDuplicate = in_array($uniqueKey, $existingRefs);
|
||||||
|
|
||||||
@@ -160,6 +223,10 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
|||||||
}
|
}
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
$showPreview = true;
|
$showPreview = true;
|
||||||
|
} else {
|
||||||
|
$importError = "Échec d'upload iOS/Serveur (Code : " . $_FILES['csv_file']['error'] . "). Si le fichier vient d'iCloud, vérifiez qu'il est bien téléchargé localement sur le téléphone.";
|
||||||
|
$showPreview = true; //
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -238,11 +305,28 @@ $fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
|||||||
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// On exclut "SAVINGS" pour s'aligner à 100% avec le recap.php
|
// On exclut "SAVINGS" pour s'aligner à 100% avec le recap.php
|
||||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
|
$stmt = $pdo->query("SELECT * FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
|
||||||
$estimatesList = []; // On prépare la liste pour retenir nos estimations multi-catégories
|
$estimatesList = []; // On prépare la liste pour retenir nos estimations multi-catégories
|
||||||
|
|
||||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$absAmount = abs((float)$item['amount']);
|
$absAmount = abs((float)$item['amount']);
|
||||||
|
|
||||||
|
// --- 🚀 INTERCEPTION DYNAMIQUE ---
|
||||||
|
$calcDetails = "";
|
||||||
|
if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) {
|
||||||
|
require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php';
|
||||||
|
// Facturation à terme échu : on recule d'un mois par rapport au mois actif du suivi
|
||||||
|
$prevDate = date('Y-m-d', strtotime('-1 month', strtotime($viewMonthDate)));
|
||||||
|
$calcYear = date('Y', strtotime($prevDate));
|
||||||
|
$calcMonth = date('m', strtotime($prevDate));
|
||||||
|
|
||||||
|
$calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth);
|
||||||
|
$estimateData = $calc->getEstimate($item['dynamic_code']);
|
||||||
|
$absAmount = $estimateData['amount'];
|
||||||
|
$calcDetails = $estimateData['details'];
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||||
$name = trim($item['name']);
|
$name = trim($item['name']);
|
||||||
$catCode = $item['category'];
|
$catCode = $item['category'];
|
||||||
@@ -280,39 +364,23 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
}
|
}
|
||||||
if (!$isPaid) {
|
if (!$isPaid) {
|
||||||
$reste_a_venir_calc += $absAmount;
|
$reste_a_venir_calc += $absAmount;
|
||||||
// Ajouté dans le tableau avec un tooltip vide
|
// On inclut les détails de l'interception dans le tooltip natif du suivi
|
||||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => 'Charge fixe en attente'];
|
$tooltip = !empty($calcDetails) ? $calcDetails : 'Charge fixe en attente';
|
||||||
|
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => $tooltip];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. C'est une estimation (Variable)
|
// 2. C'est une estimation (Variable)
|
||||||
if ((int)$item['is_estimate'] === 1) {
|
if ((int)$item['is_estimate'] === 1) {
|
||||||
$calcDetails = "";
|
|
||||||
|
|
||||||
// --- 🚀 INTERCEPTION DYNAMIQUE ---
|
|
||||||
if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) {
|
|
||||||
require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php';
|
|
||||||
// Facturation à terme échu : on recule d'un mois
|
|
||||||
$prevDate = date('Y-m-d', strtotime('-1 month', strtotime("$viewY-$viewM-01")));
|
|
||||||
$calcYear = date('Y', strtotime($prevDate));
|
|
||||||
$calcMonth = date('m', strtotime($prevDate));
|
|
||||||
|
|
||||||
$calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth);
|
|
||||||
$estimateData = $calc->getEstimate($item['dynamic_code']);
|
|
||||||
$amt = $estimateData['amount'];
|
|
||||||
$calcDetails = $estimateData['details'];
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
$estimatesList[] = [
|
$estimatesList[] = [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'amount' => $amt,
|
'amount' => $amt,
|
||||||
'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : [],
|
'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : [],
|
||||||
'details' => $calcDetails
|
'calc_details' => $calcDetails // Transmission pour le second bloc
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Attribution du budget aux jauges visuelles de la page
|
// 3. Attribution du budget aux jauges visuelles de la page
|
||||||
// Pour ne pas tout casser, on donne tout le plafond visuel à la 1ère catégorie de la liste
|
|
||||||
if (!empty($catCode)) {
|
if (!empty($catCode)) {
|
||||||
$catCodesArray = array_map('trim', explode(',', $catCode));
|
$catCodesArray = array_map('trim', explode(',', $catCode));
|
||||||
$firstCat = $catCodesArray[0];
|
$firstCat = $catCodesArray[0];
|
||||||
@@ -369,9 +437,11 @@ foreach ($estimatesList as $est) {
|
|||||||
$spentForEstimate = 0;
|
$spentForEstimate = 0;
|
||||||
$detailsHover = [];
|
$detailsHover = [];
|
||||||
|
|
||||||
|
// On additionne les dépenses de toutes les catégories liées à cette estimation
|
||||||
foreach ($est['categories'] as $cCode) {
|
foreach ($est['categories'] as $cCode) {
|
||||||
if (!empty($cCode) && isset($totals[$cCode])) {
|
if (!empty($cCode) && isset($totals[$cCode])) {
|
||||||
$spentForEstimate += $totals[$cCode];
|
$spentForEstimate += $totals[$cCode];
|
||||||
|
// On prépare le détail pour la petite bulle d'info (hover)
|
||||||
if ($totals[$cCode] > 0 && isset($categoriesConfig[$cCode])) {
|
if ($totals[$cCode] > 0 && isset($categoriesConfig[$cCode])) {
|
||||||
$detailsHover[] = strip_tags($categoriesConfig[$cCode]['label']) . " : " . number_format($totals[$cCode], 0) . "€";
|
$detailsHover[] = strip_tags($categoriesConfig[$cCode]['label']) . " : " . number_format($totals[$cCode], 0) . "€";
|
||||||
}
|
}
|
||||||
@@ -381,12 +451,9 @@ foreach ($estimatesList as $est) {
|
|||||||
$rem = max(0, $est['amount'] - $spentForEstimate);
|
$rem = max(0, $est['amount'] - $spentForEstimate);
|
||||||
if ($rem > 0) {
|
if ($rem > 0) {
|
||||||
$reste_a_venir_calc += $rem;
|
$reste_a_venir_calc += $rem;
|
||||||
$tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
// On fusionne les détails du calcul dynamique (s'il y en a) avec les détails des dépenses réelles
|
||||||
|
$baseTooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
||||||
// --- INJECTION DU DÉTAIL DYNAMIQUE ---
|
$tooltip = !empty($est['calc_details']) ? $est['calc_details'] . " | " . $baseTooltip : $baseTooltip;
|
||||||
if (!empty($est['details'])) {
|
|
||||||
$tooltip .= "\n\n📊 Détail du calcul :\n" . $est['details'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$pending_charges[] = [
|
$pending_charges[] = [
|
||||||
'name' => 'Reste ' . $est['name'],
|
'name' => 'Reste ' . $est['name'],
|
||||||
@@ -750,8 +817,15 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<button type="button" onclick="closeSuiviModal('importCsvModal')" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer;">×</button>
|
<button type="button" onclick="closeSuiviModal('importCsvModal')" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer;">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($importError)): ?>
|
||||||
|
<div class="pf-alert pf-alert--error" style="margin-bottom: 15px;">
|
||||||
|
<?= htmlspecialchars($importError) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!$showPreview): ?>
|
<?php if (!$showPreview): ?>
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<!-- L'action sécurise le fait qu'on poste bien sur le mois et la vue actuels même sur mobile -->
|
||||||
|
<form method="POST" enctype="multipart/form-data" action="?tab=suivi&m=<?= htmlspecialchars($viewM) ?>&y=<?= htmlspecialchars($viewY) ?>">
|
||||||
<div class="import-dropzone">
|
<div class="import-dropzone">
|
||||||
<div style="font-size: 2.5rem; margin-bottom: 10px;">📄</div>
|
<div style="font-size: 2.5rem; margin-bottom: 10px;">📄</div>
|
||||||
<label class="pf-label"><?= tr('bud_csv_file') ?></label>
|
<label class="pf-label"><?= tr('bud_csv_file') ?></label>
|
||||||
@@ -763,7 +837,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<form method="POST" id="formMapping">
|
<form method="POST" id="formMapping" action="?tab=suivi&m=<?= htmlspecialchars($viewM) ?>&y=<?= htmlspecialchars($viewY) ?>">
|
||||||
<input type="hidden" name="action" value="save_import">
|
<input type="hidden" name="action" value="save_import">
|
||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; flex-wrap:wrap; gap:10px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; flex-wrap:wrap; gap:10px;">
|
||||||
|
|||||||
@@ -1078,7 +1078,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
),
|
),
|
||||||
this.fetchApi("/modules/family-calendar/includes/api/get-events.php"),
|
this.fetchApi("/modules/family-calendar/includes/api/get-events.php"),
|
||||||
this.fetchPublicHolidays(),
|
this.fetchPublicHolidays(),
|
||||||
this.fetchSchoolHolidays(zone), // 🏖️ NOUVEAU : On appelle l'API
|
this.fetchSchoolHolidays(zone),
|
||||||
this.fetchApi("/modules/family-calendar/includes/api/get-leaves.php"),
|
this.fetchApi("/modules/family-calendar/includes/api/get-leaves.php"),
|
||||||
this.fetchApi(
|
this.fetchApi(
|
||||||
"/modules/family-calendar/includes/api/get-leave-snapshots.php",
|
"/modules/family-calendar/includes/api/get-leave-snapshots.php",
|
||||||
@@ -1132,8 +1132,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (!zone || zone === "Autre") return [];
|
if (!zone || zone === "Autre") return [];
|
||||||
try {
|
try {
|
||||||
const yearStr = `${this.currentSchoolYearStart}-${this.currentSchoolYearStart + 1}`;
|
const yearStr = `${this.currentSchoolYearStart}-${this.currentSchoolYearStart + 1}`;
|
||||||
// Ton URL qui utilise le LIKE, beaucoup plus robuste !
|
|
||||||
const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100`;
|
const whereClause = `annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%' AND population='Élèves'`;
|
||||||
|
const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=${encodeURIComponent(whereClause)}&limit=100`;
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -1196,7 +1197,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
try {
|
try {
|
||||||
const year = this.modalSelectedYear || this.currentSchoolYearStart;
|
const year = this.modalSelectedYear || this.currentSchoolYearStart;
|
||||||
const yearStr = `${year}-${year + 1}`;
|
const yearStr = `${year}-${year + 1}`;
|
||||||
const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100&order_by=start_date`;
|
|
||||||
|
const whereClause = `annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%' AND population='Élèves'`;
|
||||||
|
const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=${encodeURIComponent(whereClause)}&limit=100&order_by=start_date`;
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@@ -1127,3 +1127,16 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meal-dropzone.drag-over {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-dropzone.has-file {
|
||||||
|
border-color: var(--success);
|
||||||
|
background: var(--alert-success-bg);
|
||||||
|
color: var(--alert-success-text);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// modules/meals/includes/api/import-pdf.php
|
// modules/meals/includes/api/import-pdf.php
|
||||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require dirname(__DIR__, 4) . '/vendor/autoload.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
@@ -18,55 +19,29 @@ if ($personId <= 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// PARSEUR PDF SPATIAL NATIF (100% PHP SANS LIBRAIRIE)
|
// PARSEUR PDF SPATIAL (via smalot/pdfparser pour un décodage
|
||||||
|
// fiable des polices Type0/CID + tables ToUnicode)
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
class NativePdfParser {
|
class NativePdfParser {
|
||||||
|
// strtoupper() seul ne gère pas les accents (ex: "Déclinaison" -> "DCLINAISON"
|
||||||
|
// au lieu de "DECLINAISON"), ce qui empêchait ce label d'être reconnu.
|
||||||
|
private static function cleanLabel($text) {
|
||||||
|
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);
|
||||||
|
return strtoupper(preg_replace('/[^A-Z]/i', '', $ascii ?: $text));
|
||||||
|
}
|
||||||
|
|
||||||
public static function getMealData($filename) {
|
public static function getMealData($filename) {
|
||||||
$content = file_get_contents($filename);
|
|
||||||
$texts = [];
|
$texts = [];
|
||||||
|
|
||||||
// 1. Décompression et Tokenization Mathématique
|
$parser = new \Smalot\PdfParser\Parser();
|
||||||
if (preg_match_all('/stream(.*?)endstream/is', $content, $matches)) {
|
$pdf = $parser->parseFile($filename);
|
||||||
foreach ($matches[1] as $stream) {
|
|
||||||
$stream = ltrim($stream, "\r\n");
|
|
||||||
$decoded = @gzuncompress($stream);
|
|
||||||
if (!$decoded) $decoded = $stream;
|
|
||||||
|
|
||||||
// On éclate le flux par opérateurs PDF clés (Position et Texte)
|
foreach ($pdf->getPages() as $page) {
|
||||||
$parts = preg_split('/(Tm|Td|Tj|TJ)/', $decoded, -1, PREG_SPLIT_DELIM_CAPTURE);
|
foreach ($page->getDataTm() as $entry) {
|
||||||
$x = 0; $y = 0;
|
[$tm, $rawText] = $entry;
|
||||||
|
$textVal = trim((string) $rawText);
|
||||||
for ($i = 0; $i < count($parts); $i++) {
|
if ($textVal === '') continue;
|
||||||
$token = $parts[$i];
|
self::addText($texts, $textVal, (float) $tm[4], (float) $tm[5]);
|
||||||
if ($token === 'Tm' || $token === 'Td') {
|
|
||||||
$prev = $parts[$i-1];
|
|
||||||
preg_match_all('/[0-9.-]+/', $prev, $nums);
|
|
||||||
if (count($nums[0]) >= 2) {
|
|
||||||
$yRaw = array_pop($nums[0]);
|
|
||||||
$xRaw = array_pop($nums[0]);
|
|
||||||
if ($token === 'Tm') {
|
|
||||||
$x = (float)$xRaw; $y = (float)$yRaw;
|
|
||||||
} else {
|
|
||||||
$x += (float)$xRaw; $y += (float)$yRaw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($token === 'Tj') {
|
|
||||||
$prev = $parts[$i-1];
|
|
||||||
if (preg_match('/\((.*?)\)$/', trim($prev), $m)) {
|
|
||||||
self::addText($texts, $m[1], $x, $y);
|
|
||||||
}
|
|
||||||
} elseif ($token === 'TJ') {
|
|
||||||
$prev = $parts[$i-1];
|
|
||||||
if (preg_match('/\[(.*?)\]$/', trim($prev), $m)) {
|
|
||||||
preg_match_all('/\((.*?)\)/', $m[1], $tjs);
|
|
||||||
$tjX = $x;
|
|
||||||
foreach ($tjs[1] as $tj) {
|
|
||||||
self::addText($texts, $tj, $tjX, $y);
|
|
||||||
$tjX += 10; // Décalage estimé pour les mots suivants du tableau
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +51,7 @@ class NativePdfParser {
|
|||||||
$dayLabels = ['LUNDI', 'MARDI', 'MERCREDI', 'JEUDI', 'VENDREDI'];
|
$dayLabels = ['LUNDI', 'MARDI', 'MERCREDI', 'JEUDI', 'VENDREDI'];
|
||||||
$colXs = [];
|
$colXs = [];
|
||||||
foreach ($texts as $t) {
|
foreach ($texts as $t) {
|
||||||
$clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
|
$clean = self::cleanLabel($t['text']);
|
||||||
if (in_array($clean, $dayLabels)) {
|
if (in_array($clean, $dayLabels)) {
|
||||||
$colXs[$clean] = $t['x'];
|
$colXs[$clean] = $t['x'];
|
||||||
}
|
}
|
||||||
@@ -103,7 +78,7 @@ class NativePdfParser {
|
|||||||
// 3. Définition des Tranches Horizontales (Y) via les Titres de Rangée (Zone Extrême Gauche)
|
// 3. Définition des Tranches Horizontales (Y) via les Titres de Rangée (Zone Extrême Gauche)
|
||||||
$rowHeaders = [];
|
$rowHeaders = [];
|
||||||
foreach ($texts as $t) {
|
foreach ($texts as $t) {
|
||||||
$clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
|
$clean = self::cleanLabel($t['text']);
|
||||||
if (in_array($clean, ['HORSDEUVRE', 'DECLINAISON', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'DESSERT', 'LESGOUTERS'])) {
|
if (in_array($clean, ['HORSDEUVRE', 'DECLINAISON', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'DESSERT', 'LESGOUTERS'])) {
|
||||||
// On s'assure de ne capter que la colonne tout à gauche
|
// On s'assure de ne capter que la colonne tout à gauche
|
||||||
if ($t['x'] < ($sortedXs[0] - 20)) {
|
if ($t['x'] < ($sortedXs[0] - 20)) {
|
||||||
@@ -112,32 +87,14 @@ class NativePdfParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tri de haut en bas (Y du PDF est décroissant du haut vers le bas)
|
if (!in_array('PLATCHAUD', array_column($rowHeaders, 'label')) || !in_array('GARNITURE', array_column($rowHeaders, 'label'))) {
|
||||||
usort($rowHeaders, fn($a, $b) => $b['y'] <=> $a['y']);
|
|
||||||
|
|
||||||
$yPlatTop = null; $yPlatBot = null;
|
|
||||||
$yGarTop = null; $yGarBot = null;
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($rowHeaders); $i++) {
|
|
||||||
if ($rowHeaders[$i]['label'] === 'PLATCHAUD') {
|
|
||||||
$yPlatTop = $rowHeaders[$i]['y'];
|
|
||||||
// La limite basse est le label suivant, ou -50 unités
|
|
||||||
$yPlatBot = $rowHeaders[$i+1]['y'] ?? ($yPlatTop - 50);
|
|
||||||
}
|
|
||||||
if ($rowHeaders[$i]['label'] === 'GARNITURE') {
|
|
||||||
$yGarTop = $rowHeaders[$i]['y'];
|
|
||||||
$yGarBot = $rowHeaders[$i+1]['y'] ?? ($yGarTop - 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$yPlatTop || !$yGarTop) {
|
|
||||||
throw new Exception("Structure du menu non reconnue (Mots-clés 'PLAT CHAUD' et 'GARNITURE' introuvables).");
|
throw new Exception("Structure du menu non reconnue (Mots-clés 'PLAT CHAUD' et 'GARNITURE' introuvables).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Affectation Chirurgicale par Intersection Grille (X, Y)
|
// 4. Affectation Chirurgicale par Intersection Grille (X, Y)
|
||||||
$days = [[], [], [], [], []];
|
$days = [[], [], [], [], []];
|
||||||
foreach ($texts as $t) {
|
foreach ($texts as $t) {
|
||||||
$cleanAlpha = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
|
$cleanAlpha = self::cleanLabel($t['text']);
|
||||||
|
|
||||||
// Rejet des labels génériques et de la fameuse ligne "Déclinaison"
|
// Rejet des labels génériques et de la fameuse ligne "Déclinaison"
|
||||||
if (in_array($cleanAlpha, ['DECLINAISON', 'MENUVEGETARIEN', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'HORSDEUVRE', 'LESGOUTERS', 'DESSERT'])) continue;
|
if (in_array($cleanAlpha, ['DECLINAISON', 'MENUVEGETARIEN', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'HORSDEUVRE', 'LESGOUTERS', 'DESSERT'])) continue;
|
||||||
@@ -145,15 +102,26 @@ class NativePdfParser {
|
|||||||
// Retrait des labels BIO/HVE et mentions superflues
|
// Retrait des labels BIO/HVE et mentions superflues
|
||||||
$textVal = trim(preg_replace('/\b(AB|HVE|MSC|Label Rouge)\b/i', '', $t['text']));
|
$textVal = trim(preg_replace('/\b(AB|HVE|MSC|Label Rouge)\b/i', '', $t['text']));
|
||||||
$textVal = str_replace(['(plat', 'complet)'], '', $textVal);
|
$textVal = str_replace(['(plat', 'complet)'], '', $textVal);
|
||||||
$textVal = trim(preg_replace('/[^a-zA-ZÀ-ÿ0-9\s&\-]/u', '', $textVal));
|
$textVal = trim(preg_replace('/[^a-zA-ZÀ-ÿŒœÆæ0-9\s&\-]/u', '', $textVal));
|
||||||
|
|
||||||
if (strlen($textVal) < 3) continue;
|
if (strlen($textVal) < 3) continue;
|
||||||
|
|
||||||
// Filtre Vertical : Est-on dans la tranche Plat Chaud ou Garniture ? (Tolérance +/- 5 unités)
|
// Filtre Vertical : de quelle rangée (en-tête) ce texte est-il le plus proche ?
|
||||||
$inPlat = ($t['y'] <= $yPlatTop + 5 && $t['y'] >= $yPlatBot + 5);
|
// Le texte d'une rangée déborde souvent au-dessus de son propre label (lignes
|
||||||
$inGar = ($t['y'] <= $yGarTop + 5 && $t['y'] >= $yGarBot + 5);
|
// empilées vers le haut) sur une hauteur variable d'un menu à l'autre : une
|
||||||
|
// fenêtre à tolérance fixe est donc peu fiable. Le plus proche voisin (même
|
||||||
|
// principe que pour les colonnes de jour ci-dessous) s'adapte automatiquement.
|
||||||
|
$bestRow = null;
|
||||||
|
$minRowDiff = PHP_FLOAT_MAX;
|
||||||
|
foreach ($rowHeaders as $rh) {
|
||||||
|
$diff = abs($t['y'] - $rh['y']);
|
||||||
|
if ($diff < $minRowDiff) {
|
||||||
|
$minRowDiff = $diff;
|
||||||
|
$bestRow = $rh['label'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($inPlat || $inGar) {
|
if ($bestRow === 'PLATCHAUD' || $bestRow === 'GARNITURE') {
|
||||||
// Filtre Horizontal : Quel est le jour le plus proche ?
|
// Filtre Horizontal : Quel est le jour le plus proche ?
|
||||||
$bestCol = 0;
|
$bestCol = 0;
|
||||||
$minDiff = PHP_FLOAT_MAX;
|
$minDiff = PHP_FLOAT_MAX;
|
||||||
@@ -164,7 +132,7 @@ class NativePdfParser {
|
|||||||
$bestCol = $idx;
|
$bestCol = $idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$days[$bestCol][] = ['text' => $textVal, 'y' => $t['y'], 'type' => $inPlat ? 'plat' : 'gar'];
|
$days[$bestCol][] = ['text' => $textVal, 'y' => $t['y'], 'type' => $bestRow === 'PLATCHAUD' ? 'plat' : 'gar'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,19 +174,9 @@ class NativePdfParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static function addText(&$texts, $rawText, $x, $y) {
|
private static function addText(&$texts, $rawText, $x, $y) {
|
||||||
// Décodage de l'octal \123
|
// smalot/pdfparser décode déjà correctement les polices Type0/CID + ToUnicode :
|
||||||
$m = preg_replace_callback('/\\\\([0-7]{1,3})/', function($cb) { return chr(octdec($cb[1])); }, $rawText);
|
// il ne reste plus qu'à nettoyer les caractères parasites résiduels.
|
||||||
|
$m = str_replace("\0", "", $rawText);
|
||||||
// Détection de l'UTF-16BE (Le BOM \xFE\xFF indique que le texte est double encodé)
|
|
||||||
if (str_starts_with($m, "\xFE\xFF")) {
|
|
||||||
$m = mb_convert_encoding(substr($m, 2), 'UTF-8', 'UTF-16BE');
|
|
||||||
} else {
|
|
||||||
// Fallback Latin-1 si pas de BOM
|
|
||||||
$m = mb_convert_encoding($m, 'UTF-8', 'ISO-8859-1');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppression radicale des octets nuls responsables de l'espacement forcé (E s c a l o p e)
|
|
||||||
$m = str_replace("\0", "", $m);
|
|
||||||
$m = trim(preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\+&\'’\-]/u', '', $m));
|
$m = trim(preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\+&\'’\-]/u', '', $m));
|
||||||
|
|
||||||
if ($m !== '') {
|
if ($m !== '') {
|
||||||
@@ -232,14 +190,26 @@ try {
|
|||||||
|
|
||||||
// 1. Extraction robuste de la date depuis le nom de fichier (Menus-du-14-au-20-septembre-2026.pdf)
|
// 1. Extraction robuste de la date depuis le nom de fichier (Menus-du-14-au-20-septembre-2026.pdf)
|
||||||
$mondayDate = null;
|
$mondayDate = null;
|
||||||
if (preg_match('/(\d{1,2})[\s-]+au[\s-]+\d{1,2}[\s-]+([a-zèéû]+)[\s-]+(\d{4})/i', $filename, $m)) {
|
if (preg_match('/(?:du[\s-]+)?(\d{1,2})[\s-]+(?:([a-zèéû]+)[\s-]+)?au[\s-]+\d{1,2}[\s-]+([a-zèéû]+)[\s-]+(\d{4})/i', $filename, $m)) {
|
||||||
$monthsMap = ['janvier'=>'01','fevrier'=>'02','février'=>'02','mars'=>'03','avril'=>'04','mai'=>'05','juin'=>'06','juillet'=>'07','aout'=>'08','août'=>'08','septembre'=>'09','octobre'=>'10','novembre'=>'11','decembre'=>'12','décembre'=>'12'];
|
$monthsMap = ['janvier'=>'01','fevrier'=>'02','février'=>'02','mars'=>'03','avril'=>'04','mai'=>'05','juin'=>'06','juillet'=>'07','aout'=>'08','août'=>'08','septembre'=>'09','octobre'=>'10','novembre'=>'11','decembre'=>'12','décembre'=>'12'];
|
||||||
$month = $monthsMap[strtolower($m[2])] ?? '09';
|
|
||||||
$mondayDate = $m[3] . '-' . $month . '-' . str_pad($m[1], 2, '0', STR_PAD_LEFT);
|
$startMonthRaw = !empty($m[2]) ? strtolower($m[2]) : strtolower($m[3]);
|
||||||
|
$endMonthRaw = strtolower($m[3]);
|
||||||
|
|
||||||
|
$startMonthNum = $monthsMap[$startMonthRaw] ?? '09';
|
||||||
|
$endMonthNum = $monthsMap[$endMonthRaw] ?? '09';
|
||||||
|
|
||||||
|
$startYear = (int)$m[4];
|
||||||
|
// Règle métier : Bascule d'année (ex: 28 décembre au 3 janvier 2026 -> 2025-12-28)
|
||||||
|
if ($startMonthNum === '12' && $endMonthNum === '01') {
|
||||||
|
$startYear--;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mondayDate = $startYear . '-' . $startMonthNum . '-' . str_pad($m[1], 2, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$mondayDate) {
|
if (!$mondayDate) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Veuillez nommer le fichier "Menus-du-14-au-20-septembre-2026.pdf".']);
|
echo json_encode(['success' => false, 'error' => tr('meal_err_filename')]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,10 +167,10 @@ while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
|
|
||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label"><?= tr('meal_pdf_file') ?></label>
|
<label class="pf-label"><?= tr('meal_pdf_file') ?></label>
|
||||||
<div class="meal-dropzone" onclick="document.getElementById('canteenPdf').click()">
|
<div class="meal-dropzone" id="mealDropzone" onclick="document.getElementById('canteenPdf').click()">
|
||||||
<span class="meal-dropzone-icon">📥</span>
|
<span class="meal-dropzone-icon" id="mealDropzoneIcon">📥</span>
|
||||||
<span id="pdfFileName"><?= tr('meal_drop_pdf') ?></span>
|
<span id="pdfFileName"><?= tr('meal_drop_pdf') ?></span>
|
||||||
<input type="file" id="canteenPdf" name="pdf_file" accept=".pdf" style="display: none;" required onchange="document.getElementById('pdfFileName').innerText = this.files[0] ? this.files[0].name : '<?= addslashes(tr('meal_drop_pdf')) ?>';">
|
<input type="file" id="canteenPdf" name="pdf_file" accept=".pdf" style="display: none;" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Sauvegarde automatique AJAX à chaque modification
|
// 1. Sauvegarde automatique AJAX à chaque modification
|
||||||
document.querySelectorAll('.js-meal-input').forEach(input => {
|
document.querySelectorAll('.js-meal-input').forEach(input => {
|
||||||
input.addEventListener('change', async function() {
|
input.addEventListener('change', async function() {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
@@ -202,11 +202,86 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2. NOUVEAU : Initialisation du Drag & Drop pour l'import PDF
|
||||||
|
const dropzone = document.getElementById('mealDropzone');
|
||||||
|
const fileInput = document.getElementById('canteenPdf');
|
||||||
|
|
||||||
|
if (dropzone && fileInput) {
|
||||||
|
// Empêcher l'ouverture automatique du fichier par le navigateur
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
|
dropzone.addEventListener(eventName, e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus Context Bar
|
// Feedback visuel au survol
|
||||||
|
['dragenter', 'dragover'].forEach(eventName => {
|
||||||
|
dropzone.addEventListener(eventName, () => dropzone.classList.add('drag-over'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragleave', 'drop'].forEach(eventName => {
|
||||||
|
dropzone.addEventListener(eventName, () => dropzone.classList.remove('drag-over'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupération du fichier au lâcher (Drop)
|
||||||
|
dropzone.addEventListener('drop', e => {
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
|
fileInput.files = e.dataTransfer.files;
|
||||||
|
updateDropzoneState(fileInput.files[0]);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Récupération au clic classique via l'input
|
||||||
|
fileInput.addEventListener('change', function() {
|
||||||
|
updateDropzoneState(this.files[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOUVEAU : Fonction utilitaire pour gérer l'état visuel de la Dropzone
|
||||||
|
window.updateDropzoneState = function(file) {
|
||||||
|
const dropzone = document.getElementById('mealDropzone');
|
||||||
|
const fileNameDisplay = document.getElementById('pdfFileName');
|
||||||
|
const dropzoneIcon = document.getElementById('mealDropzoneIcon');
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
fileNameDisplay.innerText = file.name;
|
||||||
|
if (dropzone) dropzone.classList.add('has-file');
|
||||||
|
if (dropzoneIcon) dropzoneIcon.innerText = '✅';
|
||||||
|
} else {
|
||||||
|
fileNameDisplay.innerText = window.I18N['meal_drop_pdf'] || 'Cliquer pour ajouter le PDF';
|
||||||
|
if (dropzone) dropzone.classList.remove('has-file');
|
||||||
|
if (dropzoneIcon) dropzoneIcon.innerText = '📥';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Focus Context Bar & Gestion dynamique du clavier (Mobile)
|
||||||
|
// ============================================================================
|
||||||
let copyState = { type: null, sourceDate: null, service: null, personIdx: null, sourceElements: [], fullDayName: '' };
|
let copyState = { type: null, sourceDate: null, service: null, personIdx: null, sourceElements: [], fullDayName: '' };
|
||||||
|
|
||||||
|
// Fonction de calcul pour maintenir la barre au-dessus du clavier virtuel
|
||||||
|
const updateToolbarPosition = () => {
|
||||||
|
const toolbar = document.getElementById('contextToolbar');
|
||||||
|
if (toolbar && toolbar.classList.contains('active') && window.visualViewport) {
|
||||||
|
const layoutHeight = document.documentElement.clientHeight;
|
||||||
|
const visualHeight = window.visualViewport.height;
|
||||||
|
const offsetTop = window.visualViewport.offsetTop;
|
||||||
|
|
||||||
|
// Calcul exact de l'espace masqué par le clavier
|
||||||
|
const bottomOffset = layoutHeight - (visualHeight + offsetTop);
|
||||||
|
toolbar.style.bottom = Math.max(0, bottomOffset) + 'px';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Écouteurs pour s'adapter au redimensionnement (ouverture clavier) et au scroll
|
||||||
|
if (window.visualViewport) {
|
||||||
|
window.visualViewport.addEventListener('resize', updateToolbarPosition);
|
||||||
|
window.visualViewport.addEventListener('scroll', updateToolbarPosition);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('focusin', (e) => {
|
document.addEventListener('focusin', (e) => {
|
||||||
if(e.target.classList.contains('js-meal-input')) {
|
if(e.target.classList.contains('js-meal-input')) {
|
||||||
copyState = {
|
copyState = {
|
||||||
@@ -218,13 +293,20 @@ document.addEventListener('focusin', (e) => {
|
|||||||
fullDayName: ''
|
fullDayName: ''
|
||||||
};
|
};
|
||||||
document.getElementById('contextToolbar').classList.add('active');
|
document.getElementById('contextToolbar').classList.add('active');
|
||||||
|
|
||||||
|
// On déclenche le repositionnement avec un léger délai pour laisser le clavier monter
|
||||||
|
setTimeout(updateToolbarPosition, 150);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('focusout', (e) => {
|
document.addEventListener('focusout', (e) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if(document.activeElement && !document.activeElement.classList.contains('js-meal-input')) {
|
if(document.activeElement && !document.activeElement.classList.contains('js-meal-input')) {
|
||||||
document.getElementById('contextToolbar').classList.remove('active');
|
const toolbar = document.getElementById('contextToolbar');
|
||||||
|
if (toolbar) {
|
||||||
|
toolbar.classList.remove('active');
|
||||||
|
toolbar.style.bottom = ''; // On retire le style JS pour redonner le contrôle au CSS (-80px)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
@@ -312,7 +394,7 @@ window.executeCopy = async function(targetDate, targetService) {
|
|||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('action', 'save_bulk');
|
fd.append('action', 'save_bulk');
|
||||||
fd.append('updates', JSON.stringify(updates));
|
fd.append('updates', JSON.stringify(updates));
|
||||||
await pachaFetch('/modules/meals/includes/api/save-meals.php', { method: 'POST', body: fd });
|
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -322,7 +404,7 @@ function flashSuccess(input) {
|
|||||||
setTimeout(() => input.style.background = 'transparent', 400);
|
setTimeout(() => input.style.background = 'transparent', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOUVEAU : Fonction Javascript d'Import PDF
|
// Fonction Javascript d'Import PDF mise à jour
|
||||||
window.importPdfMenu = async function(e) {
|
window.importPdfMenu = async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = e.target;
|
const form = e.target;
|
||||||
@@ -351,7 +433,80 @@ window.importPdfMenu = async function(e) {
|
|||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerText = oldText;
|
btn.innerText = oldText;
|
||||||
form.reset();
|
form.reset();
|
||||||
document.getElementById('pdfFileName').innerText = window.I18N['meal_drop_pdf'] || 'Cliquer pour ajouter le PDF';
|
updateDropzoneState(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOUVEAU : Interception du collage depuis Excel / Google Sheets
|
||||||
|
document.addEventListener('paste', async function(e) {
|
||||||
|
// On ne s'active que si on colle dans un textarea de repas
|
||||||
|
if (!e.target.classList.contains('js-meal-input')) return;
|
||||||
|
|
||||||
|
const clipboardData = e.clipboardData || window.clipboardData;
|
||||||
|
const pastedText = clipboardData.getData('text/plain');
|
||||||
|
|
||||||
|
// Si le texte ne contient ni tabulation ni saut de ligne, c'est un collage simple.
|
||||||
|
// On laisse le navigateur faire son comportement par défaut.
|
||||||
|
if (!pastedText.includes('\t') && !pastedText.includes('\n')) return;
|
||||||
|
|
||||||
|
// C'est une grille ! On bloque le comportement par défaut
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const rows = pastedText.split(/\r?\n/);
|
||||||
|
const startTd = e.target.closest('td');
|
||||||
|
const startTr = e.target.closest('tr');
|
||||||
|
const startColIndex = Array.from(startTr.children).indexOf(startTd);
|
||||||
|
|
||||||
|
let currentTr = startTr;
|
||||||
|
let updates = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (!currentTr) break; // Fin du tableau visuel atteinte
|
||||||
|
|
||||||
|
// Ignorer la dernière ligne vide souvent générée par les tableurs lors d'une copie
|
||||||
|
if (rows[i].trim() === '' && i === rows.length - 1) continue;
|
||||||
|
|
||||||
|
const cells = rows[i].split('\t');
|
||||||
|
const trChildren = Array.from(currentTr.children);
|
||||||
|
|
||||||
|
for (let j = 0; j < cells.length; j++) {
|
||||||
|
const targetTd = trChildren[startColIndex + j];
|
||||||
|
if (!targetTd) continue; // Fin de la colonne visuelle atteinte
|
||||||
|
|
||||||
|
const targetTextarea = targetTd.querySelector('.js-meal-input');
|
||||||
|
if (targetTextarea) {
|
||||||
|
const val = cells[j].trim();
|
||||||
|
targetTextarea.value = val;
|
||||||
|
|
||||||
|
// Animation de succès visuel
|
||||||
|
if (typeof flashSuccess === 'function') flashSuccess(targetTextarea);
|
||||||
|
|
||||||
|
// Préparation pour le Bulk Save
|
||||||
|
updates.push({
|
||||||
|
plan_date: targetTextarea.dataset.d,
|
||||||
|
service: targetTextarea.dataset.s,
|
||||||
|
person_id: targetTextarea.dataset.p,
|
||||||
|
meal_name: val
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Descendre d'une ligne dans le tableau (ex: passer de Midi à Soir, ou de Soir au Midi du lendemain)
|
||||||
|
currentTr = currentTr.nextElementSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sauvegarde groupée optimisée
|
||||||
|
if (updates.length > 0) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_bulk');
|
||||||
|
fd.append('updates', JSON.stringify(updates));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
|
if (window.showToast) showToast(window.I18N['meal_paste_success'] || 'Grille collée avec succès !', 'success');
|
||||||
|
} catch(err) {
|
||||||
|
console.error("Erreur de sauvegarde lors du collage", err);
|
||||||
|
if (window.showToast) showToast(window.I18N['error_occured'], 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
// offline.php
|
||||||
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
|
||||||
|
$pageTitle = tr('offline_title');
|
||||||
|
$activePage = "offline";
|
||||||
|
require __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container" style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; text-align: center; padding: 20px;">
|
||||||
|
<div style="font-size: 5rem; margin-bottom: 20px; filter: grayscale(1); opacity: 0.7;">📡</div>
|
||||||
|
<h1 style="color: var(--text-main); margin-bottom: 10px;"><?= tr('offline_heading') ?></h1>
|
||||||
|
<p style="color: var(--text-muted); font-size: 1.1rem; max-width: 400px; margin-bottom: 30px;">
|
||||||
|
<?= tr('offline_message') ?>
|
||||||
|
</p>
|
||||||
|
<button onclick="window.location.reload();" class="pf-btn">
|
||||||
|
🔄 <?= tr('offline_retry_btn') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
+767
@@ -0,0 +1,767 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<title>Preview Mobile - Import CSV</title>
|
||||||
|
<style>
|
||||||
|
/* === Variables HouseHub (Simulées) === */
|
||||||
|
:root {
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--primary-dark: #1d4ed8;
|
||||||
|
--success: #10b981;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--bg-page: #f8fafc;
|
||||||
|
--bg-panel: #ffffff;
|
||||||
|
--bg-subtle: #f1f5f9;
|
||||||
|
--text-main: #0f172a;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--border-light: #e2e8f0;
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
--radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conteneur simulant un écran mobile */
|
||||||
|
.mobile-simulator {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 414px; /* Taille iPhone Max */
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 896px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* En-tête de la modale */
|
||||||
|
.preview-header {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.preview-title {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.preview-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Système d'onglets pour la preview */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--primary);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conteneur principal scrollable */
|
||||||
|
.preview-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: 80px; /* Espace pour le bouton valider */
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-section {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
.view-section.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Composants de formulaire communs */
|
||||||
|
.pf-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.pf-select,
|
||||||
|
.pf-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--text-main);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.pf-select:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-badge {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.amount-expense {
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.amount-income {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
PROPOSITION 1 : CARTES VERTICALES
|
||||||
|
========================================= */
|
||||||
|
.prop1-card {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.prop1-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.prop1-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.prop1-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.prop1-ignore-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.prop1-ignore-row input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
PROPOSITION 2 : ACCORDÉON (Liste compacte)
|
||||||
|
========================================= */
|
||||||
|
.prop2-container {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.prop2-item {
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.prop2-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.prop2-summary {
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.prop2-summary-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
.prop2-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.prop2-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.prop2-status {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--danger);
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.prop2-status.ok {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop2-details {
|
||||||
|
display: none;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border-top: 1px dashed var(--border-light);
|
||||||
|
}
|
||||||
|
.prop2-item.open .prop2-details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.prop2-item.open .prop2-summary {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
PROPOSITION 3 : BOTTOM SHEET (Native feel)
|
||||||
|
========================================= */
|
||||||
|
.prop3-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.prop3-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.prop3-item:active {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
}
|
||||||
|
.prop3-item.categorized {
|
||||||
|
border-left: 4px solid var(--success);
|
||||||
|
}
|
||||||
|
.prop3-item.pending {
|
||||||
|
border-left: 4px solid var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 100;
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.bottom-sheet {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
padding: 24px;
|
||||||
|
z-index: 101;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
box-shadow: 0 -10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.bottom-sheet.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.bottom-sheet-overlay.open {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.bs-handle {
|
||||||
|
width: 40px;
|
||||||
|
height: 5px;
|
||||||
|
background: var(--border-strong);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: -10px auto 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer fixe */
|
||||||
|
.preview-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
.pf-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="mobile-simulator">
|
||||||
|
<div class="preview-header">
|
||||||
|
<h2 class="preview-title">
|
||||||
|
<span>📥 Importation CSV</span>
|
||||||
|
<button class="preview-close">×</button>
|
||||||
|
</h2>
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchView('prop1')">
|
||||||
|
1. Cartes
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" onclick="switchView('prop2')">
|
||||||
|
2. Accordéon
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" onclick="switchView('prop3')">
|
||||||
|
3. Bottom Sheet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-body">
|
||||||
|
<!-- ======================================================= -->
|
||||||
|
<!-- PROPOSITION 1 : CARTES VERTICALES -->
|
||||||
|
<!-- ======================================================= -->
|
||||||
|
<div id="prop1" class="view-section active">
|
||||||
|
<p
|
||||||
|
style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0"
|
||||||
|
>
|
||||||
|
Vision aérée. Parfait s'il y a peu de lignes à importer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Ligne Dépense -->
|
||||||
|
<div class="prop1-card">
|
||||||
|
<div class="prop1-header">
|
||||||
|
<div>
|
||||||
|
<div class="prop1-date">12/10/2026</div>
|
||||||
|
<h4 class="prop1-label">AUCHAN SUPERMARCHE</h4>
|
||||||
|
</div>
|
||||||
|
<div class="amount-badge amount-expense">- 45,20 €</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="pf-label">Catégorie</label>
|
||||||
|
<select
|
||||||
|
class="pf-select"
|
||||||
|
onchange="toggleLinkedItem(this, 'prop1-linked-1')"
|
||||||
|
>
|
||||||
|
<option value="">-- À définir --</option>
|
||||||
|
<option value="FMCG">🛒 Courses (FMCG)</option>
|
||||||
|
<option value="FIXED">🏢 Charges Fixes</option>
|
||||||
|
<option value="INCOME">💵 Revenus</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="prop1-linked-1" style="display: none">
|
||||||
|
<label class="pf-label">Lier à une charge fixe</label>
|
||||||
|
<select class="pf-select">
|
||||||
|
<option value="">-- Sélectionner --</option>
|
||||||
|
<option value="1">Assurance Auto</option>
|
||||||
|
<option value="2">Loyer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="prop1-ignore-row pf-label"
|
||||||
|
style="margin: 0; font-weight: 500; cursor: pointer"
|
||||||
|
>
|
||||||
|
<input type="checkbox" /> Ignorer cette ligne
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ligne Abonnement -->
|
||||||
|
<div class="prop1-card">
|
||||||
|
<div class="prop1-header">
|
||||||
|
<div>
|
||||||
|
<div class="prop1-date">14/10/2026</div>
|
||||||
|
<h4 class="prop1-label">NETFLIX.COM</h4>
|
||||||
|
</div>
|
||||||
|
<div class="amount-badge amount-expense">- 13,49 €</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="pf-label">Catégorie</label>
|
||||||
|
<select
|
||||||
|
class="pf-select"
|
||||||
|
onchange="toggleLinkedItem(this, 'prop1-linked-2')"
|
||||||
|
>
|
||||||
|
<option value="FIXED" selected>🏢 Charges Fixes</option>
|
||||||
|
<option value="FMCG">🛒 Courses (FMCG)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="prop1-linked-2" style="display: block">
|
||||||
|
<label class="pf-label">Lier à une charge fixe</label>
|
||||||
|
<select class="pf-select">
|
||||||
|
<option value="3" selected>Abonnement Netflix</option>
|
||||||
|
<option value="1">Assurance Auto</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ligne Revenu -->
|
||||||
|
<div class="prop1-card">
|
||||||
|
<div class="prop1-header">
|
||||||
|
<div>
|
||||||
|
<div class="prop1-date">15/10/2026</div>
|
||||||
|
<h4 class="prop1-label">VIR SEPA M. PERCO</h4>
|
||||||
|
</div>
|
||||||
|
<div class="amount-badge amount-income">+ 1 500,00 €</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="pf-label">Catégorie</label>
|
||||||
|
<select class="pf-select">
|
||||||
|
<option value="INCOME" selected>💵 Revenus</option>
|
||||||
|
<option value="FMCG">🛒 Courses (FMCG)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================================================= -->
|
||||||
|
<!-- PROPOSITION 2 : ACCORDÉON -->
|
||||||
|
<!-- ======================================================= -->
|
||||||
|
<div id="prop2" class="view-section">
|
||||||
|
<p
|
||||||
|
style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0"
|
||||||
|
>
|
||||||
|
Vision compacte. Idéal pour un gros volume d'imports.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="prop2-container">
|
||||||
|
<!-- Item 1 -->
|
||||||
|
<div class="prop2-item" onclick="toggleAccordion(this)">
|
||||||
|
<div class="prop2-summary">
|
||||||
|
<div class="prop2-summary-left">
|
||||||
|
<div class="prop2-date">12/10/2026</div>
|
||||||
|
<div class="prop2-label">AUCHAN SUPERMARCHE</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-badge amount-expense">- 45,20 €</div>
|
||||||
|
<div class="prop2-status" title="À définir"></div>
|
||||||
|
</div>
|
||||||
|
<div class="prop2-details" onclick="event.stopPropagation()">
|
||||||
|
<label class="pf-label">Catégorie</label>
|
||||||
|
<select
|
||||||
|
class="pf-select"
|
||||||
|
onchange="
|
||||||
|
toggleLinkedItem(this, 'prop2-linked-1');
|
||||||
|
updateAccordionStatus(this);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<option value="">-- À définir --</option>
|
||||||
|
<option value="FMCG">🛒 Courses (FMCG)</option>
|
||||||
|
<option value="FIXED">🏢 Charges Fixes</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="prop2-linked-1" style="display: none">
|
||||||
|
<label class="pf-label">Lier à une charge fixe</label>
|
||||||
|
<select class="pf-select">
|
||||||
|
<option value="">-- Sélectionner --</option>
|
||||||
|
<option value="1">Assurance Auto</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
class="prop1-ignore-row pf-label"
|
||||||
|
style="margin: 0; font-weight: 500"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
onchange="updateAccordionStatus(this, true)"
|
||||||
|
/>
|
||||||
|
Ignorer
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item 2 -->
|
||||||
|
<div class="prop2-item">
|
||||||
|
<div
|
||||||
|
class="prop2-summary"
|
||||||
|
onclick="toggleAccordion(this.parentElement)"
|
||||||
|
>
|
||||||
|
<div class="prop2-summary-left">
|
||||||
|
<div class="prop2-date">14/10/2026</div>
|
||||||
|
<div class="prop2-label">NETFLIX.COM</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-badge amount-expense">- 13,49 €</div>
|
||||||
|
<div class="prop2-status ok" title="Validé"></div>
|
||||||
|
</div>
|
||||||
|
<div class="prop2-details" onclick="event.stopPropagation()">
|
||||||
|
<label class="pf-label">Catégorie</label>
|
||||||
|
<select class="pf-select">
|
||||||
|
<option value="FIXED" selected>🏢 Charges Fixes</option>
|
||||||
|
</select>
|
||||||
|
<label class="pf-label">Lien</label>
|
||||||
|
<select class="pf-select">
|
||||||
|
<option value="3" selected>Abonnement Netflix</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ======================================================= -->
|
||||||
|
<!-- PROPOSITION 3 : LISTE + BOTTOM SHEET -->
|
||||||
|
<!-- ======================================================= -->
|
||||||
|
<div id="prop3" class="view-section">
|
||||||
|
<p
|
||||||
|
style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0"
|
||||||
|
>
|
||||||
|
Expérience 100% Mobile Native. Garde la liste toujours visible.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="prop3-list">
|
||||||
|
<div
|
||||||
|
class="prop3-item pending"
|
||||||
|
onclick="
|
||||||
|
openBottomSheet('AUCHAN SUPERMARCHE', '-45.20', 'expense', '')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="prop1-date">12/10/2026</div>
|
||||||
|
<div class="prop1-label" style="font-size: 0.85rem">
|
||||||
|
AUCHAN SUPERMARCHE
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--danger);
|
||||||
|
margin-top: 2px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
À définir
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-badge amount-expense">- 45,20 €</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="prop3-item categorized"
|
||||||
|
onclick="
|
||||||
|
openBottomSheet('NETFLIX.COM', '-13.49', 'expense', 'FIXED')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="prop1-date">14/10/2026</div>
|
||||||
|
<div class="prop1-label" style="font-size: 0.85rem">
|
||||||
|
NETFLIX.COM
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--success);
|
||||||
|
margin-top: 2px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Charges Fixes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-badge amount-expense">- 13,49 €</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Fixe -->
|
||||||
|
<div class="preview-footer">
|
||||||
|
<button class="pf-btn">Valider l'importation</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modale Bottom Sheet (Pour Prop 3) -->
|
||||||
|
<div
|
||||||
|
class="bottom-sheet-overlay"
|
||||||
|
id="bs-overlay"
|
||||||
|
onclick="closeBottomSheet()"
|
||||||
|
></div>
|
||||||
|
<div class="bottom-sheet" id="bs-modal">
|
||||||
|
<div class="bs-handle"></div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3
|
||||||
|
id="bs-title"
|
||||||
|
style="margin: 0; font-size: 1.1rem; color: var(--text-main)"
|
||||||
|
>
|
||||||
|
Titre
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
id="bs-date"
|
||||||
|
style="font-size: 0.8rem; color: var(--text-muted)"
|
||||||
|
>
|
||||||
|
12/10/2026
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="bs-amount" class="amount-badge">0,00 €</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="pf-label">Catégorie</label>
|
||||||
|
<select
|
||||||
|
class="pf-select"
|
||||||
|
id="bs-select-cat"
|
||||||
|
onchange="toggleLinkedItem(this, 'bs-linked-item')"
|
||||||
|
>
|
||||||
|
<option value="">-- À définir --</option>
|
||||||
|
<option value="FMCG">🛒 Courses (FMCG)</option>
|
||||||
|
<option value="FIXED">🏢 Charges Fixes</option>
|
||||||
|
<option value="INCOME">💵 Revenus</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="bs-linked-item" style="display: none">
|
||||||
|
<label class="pf-label">Lier à une charge / revenu</label>
|
||||||
|
<select class="pf-select">
|
||||||
|
<option value="">-- Sélectionner --</option>
|
||||||
|
<option value="1">Loyer</option>
|
||||||
|
<option value="2">Assurance</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; margin-top: 20px">
|
||||||
|
<button
|
||||||
|
class="pf-btn"
|
||||||
|
style="background: var(--bg-subtle); color: var(--danger); flex: 1"
|
||||||
|
onclick="closeBottomSheet()"
|
||||||
|
>
|
||||||
|
Ignorer
|
||||||
|
</button>
|
||||||
|
<button class="pf-btn" style="flex: 2" onclick="closeBottomSheet()">
|
||||||
|
Appliquer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Navigation entre les propositions
|
||||||
|
function switchView(id) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".view-section")
|
||||||
|
.forEach((el) => el.classList.remove("active"));
|
||||||
|
document
|
||||||
|
.querySelectorAll(".tab-btn")
|
||||||
|
.forEach((el) => el.classList.remove("active"));
|
||||||
|
|
||||||
|
document.getElementById(id).classList.add("active");
|
||||||
|
event.target.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affichage conditionnel des selects liés (Charges fixes / Revenus)
|
||||||
|
function toggleLinkedItem(selectEl, targetId) {
|
||||||
|
const target = document.getElementById(targetId);
|
||||||
|
if (selectEl.value === "FIXED" || selectEl.value === "INCOME") {
|
||||||
|
target.style.display = "block";
|
||||||
|
} else {
|
||||||
|
target.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logique Accordéon (Prop 2)
|
||||||
|
function toggleAccordion(itemEl) {
|
||||||
|
const isOpen = itemEl.classList.contains("open");
|
||||||
|
document
|
||||||
|
.querySelectorAll(".prop2-item")
|
||||||
|
.forEach((el) => el.classList.remove("open"));
|
||||||
|
if (!isOpen) {
|
||||||
|
itemEl.classList.add("open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAccordionStatus(element, isIgnore = false) {
|
||||||
|
const itemEl = element.closest(".prop2-item");
|
||||||
|
const statusDot = itemEl.querySelector(".prop2-status");
|
||||||
|
|
||||||
|
if (isIgnore && element.checked) {
|
||||||
|
statusDot.className = "prop2-status ok";
|
||||||
|
statusDot.style.background = "var(--text-muted)";
|
||||||
|
} else if (itemEl.querySelector("select").value !== "") {
|
||||||
|
statusDot.className = "prop2-status ok";
|
||||||
|
statusDot.style.background = "var(--success)";
|
||||||
|
} else {
|
||||||
|
statusDot.className = "prop2-status";
|
||||||
|
statusDot.style.background = "var(--danger)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logique Bottom Sheet (Prop 3)
|
||||||
|
function openBottomSheet(title, amount, type, currentCat) {
|
||||||
|
document.getElementById("bs-title").innerText = title;
|
||||||
|
document.getElementById("bs-amount").innerText = amount + " €";
|
||||||
|
document.getElementById("bs-amount").className =
|
||||||
|
"amount-badge " +
|
||||||
|
(type === "expense" ? "amount-expense" : "amount-income");
|
||||||
|
|
||||||
|
const select = document.getElementById("bs-select-cat");
|
||||||
|
select.value = currentCat;
|
||||||
|
toggleLinkedItem(select, "bs-linked-item");
|
||||||
|
|
||||||
|
document.getElementById("bs-overlay").classList.add("open");
|
||||||
|
document.getElementById("bs-modal").classList.add("open");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBottomSheet() {
|
||||||
|
document.getElementById("bs-overlay").classList.remove("open");
|
||||||
|
document.getElementById("bs-modal").classList.remove("open");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+54
-27
@@ -41,14 +41,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
if ($action === 'set_modules' && $family_id) {
|
if ($action === 'set_modules' && $family_id) {
|
||||||
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'food', 'calendar_ios', 'printvault', 'planka'];
|
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka', 'food'];
|
||||||
|
$allViews = ['food_meals', 'food_liste', 'budget_prev', 'budget_epargne', 'budget_provisions'];
|
||||||
|
|
||||||
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
||||||
|
$enabledViews = array_values(array_filter($allViews, fn($v) => isset($_POST['view_' . $v])));
|
||||||
|
|
||||||
if (empty($enabled)) {
|
if (empty($enabled)) {
|
||||||
$error = tr('set_err_min_module');
|
$error = tr('set_err_min_module');
|
||||||
} else {
|
} else {
|
||||||
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
|
$meta_pdo->prepare("UPDATE families SET enabled_modules = ?, enabled_views = ? WHERE id = ?")
|
||||||
->execute([json_encode($enabled), $family_id]);
|
->execute([json_encode($enabled), json_encode($enabledViews), $family_id]);
|
||||||
$_SESSION['enabled_modules'] = $enabled;
|
$_SESSION['enabled_modules'] = $enabled;
|
||||||
|
$_SESSION['enabled_views'] = $enabledViews;
|
||||||
$success = tr('set_success_modules');
|
$success = tr('set_success_modules');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,15 +401,34 @@ require __DIR__ . '/header.php';
|
|||||||
<p class="pf-muted-note"><?= tr('set_desc_modules') ?></p>
|
<p class="pf-muted-note"><?= tr('set_desc_modules') ?></p>
|
||||||
<?php
|
<?php
|
||||||
$enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios'];
|
$enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios'];
|
||||||
|
$enabledViews = $_SESSION['enabled_views'] ?? ['food_meals', 'food_liste', 'budget_prev', 'budget_epargne', 'budget_provisions'];
|
||||||
|
|
||||||
$allModules = [
|
$allModules = [
|
||||||
'calendar' => ['icon' => '📅', 'label' => tr('menu_calendar')],
|
'calendar' => ['icon' => '📅', 'label' => tr('menu_calendar')],
|
||||||
'budget' => ['icon' => '💰', 'label' => tr('menu_budget')],
|
'budget' => [
|
||||||
|
'icon' => '💰',
|
||||||
|
'label' => tr('menu_budget'),
|
||||||
|
'mandatory_note' => tr('budget_mandatory_note'),
|
||||||
|
'views' => [
|
||||||
|
'budget_prev' => tr('budget_tab_prev'),
|
||||||
|
'budget_epargne' => tr('budget_tab_savings'),
|
||||||
|
'budget_provisions' => tr('budget_tab_provisions')
|
||||||
|
]
|
||||||
|
],
|
||||||
'holidays' => ['icon' => '🏖️', 'label' => tr('menu_holidays')],
|
'holidays' => ['icon' => '🏖️', 'label' => tr('menu_holidays')],
|
||||||
'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')],
|
'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')],
|
||||||
'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')],
|
'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')],
|
||||||
'memo' => ['icon' => '📝', 'label' => tr('menu_memo')],
|
'memo' => ['icon' => '📝', 'label' => tr('menu_memo')],
|
||||||
'todo' => ['icon' => '✅', 'label' => tr('menu_todo')],
|
'todo' => ['icon' => '✅', 'label' => tr('menu_todo')],
|
||||||
'food' => ['icon' => '🍳', 'label' => tr('menu_food')],
|
'liste' => ['icon' => '🛒', 'label' => tr('menu_liste')],
|
||||||
|
'food' => [
|
||||||
|
'icon' => '🍳',
|
||||||
|
'label' => tr('mod_food_name'),
|
||||||
|
'views' => [
|
||||||
|
'food_meals' => tr('food_tab_meals'),
|
||||||
|
'food_liste' => tr('food_tab_list')
|
||||||
|
]
|
||||||
|
],
|
||||||
'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')],
|
'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')],
|
||||||
'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')],
|
'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')],
|
||||||
'planka' => ['icon' => '📋', 'label' => tr('menu_planka')],
|
'planka' => ['icon' => '📋', 'label' => tr('menu_planka')],
|
||||||
@@ -415,11 +439,34 @@ require __DIR__ . '/header.php';
|
|||||||
<input type="hidden" name="action" value="set_modules">
|
<input type="hidden" name="action" value="set_modules">
|
||||||
<div class="pf-stack-md">
|
<div class="pf-stack-md">
|
||||||
<?php foreach ($allModules as $key => $mod): $active = in_array($key, $enabledMods); ?>
|
<?php foreach ($allModules as $key => $mod): $active = in_array($key, $enabledMods); ?>
|
||||||
<label class="pf-module-tile">
|
<div class="pf-module-card-wrapper">
|
||||||
<input type="checkbox" name="mod_<?= $key ?>" <?= $active ? 'checked' : '' ?> class="pf-checkbox-lg">
|
|
||||||
|
<div class="pf-module-top-row">
|
||||||
|
<label class="pf-module-tile-compact">
|
||||||
|
<input type="checkbox" name="mod_<?= $key ?>" <?= $active ? 'checked' : '' ?> class="pf-checkbox-lg js-mod-checkbox">
|
||||||
<span class="pf-tile-icon"><?= $mod['icon'] ?></span>
|
<span class="pf-tile-icon"><?= $mod['icon'] ?></span>
|
||||||
<span class="pf-tile-label"><?= $mod['label'] ?></span>
|
<span class="pf-tile-label"><?= $mod['label'] ?></span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<?php if (!empty($mod['views'])): ?>
|
||||||
|
<div class="pf-module-inline-pills">
|
||||||
|
<?php foreach ($mod['views'] as $vKey => $vLabel): ?>
|
||||||
|
<label class="pf-sub-module-pill">
|
||||||
|
<input type="checkbox" name="view_<?= $vKey ?>" <?= in_array($vKey, $enabledViews) ? 'checked' : '' ?>>
|
||||||
|
<span class="pf-pill-text"><?= htmlspecialchars($vLabel) ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($mod['mandatory_note'])): ?>
|
||||||
|
<div class="pf-module-info-footer">
|
||||||
|
ℹ️ <?= $mod['mandatory_note'] ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
@@ -427,26 +474,6 @@ require __DIR__ . '/header.php';
|
|||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
|
||||||
$enabledModsForFood = $_SESSION['enabled_modules'] ?? [];
|
|
||||||
if ($family_id && in_array('food', $enabledModsForFood, true)):
|
|
||||||
?>
|
|
||||||
<section class="pf-panel-card">
|
|
||||||
<h2 class="pf-card-h2 pf-card-h2--tight">📝 <?= htmlspecialchars(tr('liste_settings_title')) ?></h2>
|
|
||||||
<p class="pf-muted-note"><?= htmlspecialchars(tr('liste_settings_intro')) ?></p>
|
|
||||||
<form method="post" class="pf-stack-md pf-mt-md">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
|
||||||
<input type="hidden" name="action" value="grocery_history_max">
|
|
||||||
<div class="pf-form-group">
|
|
||||||
<label class="pf-label" for="history_max"><?= htmlspecialchars(tr('liste_settings_history_max')) ?></label>
|
|
||||||
<input type="number" name="history_max" id="history_max" class="pf-input pf-input-sm" min="1" max="50" step="1" value="<?= (int) $groceryHistoryMaxSetting ?>" required>
|
|
||||||
<p class="pf-muted-note pf-mt-xs"><?= htmlspecialchars(tr('liste_settings_history_hint')) ?></p>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="pf-btn"><?= htmlspecialchars(tr('btn_save')) ?></button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section class="pf-panel-card">
|
<section class="pf-panel-card">
|
||||||
<h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_ios') ?></h2>
|
<h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_ios') ?></h2>
|
||||||
<p class="pf-muted-note"><?= tr('set_desc_ios') ?></p>
|
<p class="pf-muted-note"><?= tr('set_desc_ios') ?></p>
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
const CACHE_NAME = "househub-static-v4";
|
||||||
|
const API_CACHE_NAME = "househub-api-v1";
|
||||||
|
const ASSETS_TO_CACHE = [
|
||||||
|
"/offline.php",
|
||||||
|
"/global.css",
|
||||||
|
"/dark-mode.css",
|
||||||
|
"/favicon.png",
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Gestion de la file d'attente (IndexedDB) pour les actions hors ligne ---
|
||||||
|
function openDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open("househub-offline-sync", 1);
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
if (!db.objectStoreNames.contains("sync-queue")) {
|
||||||
|
db.createObjectStore("sync-queue", { autoIncrement: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.onsuccess = (event) => resolve(event.target.result);
|
||||||
|
request.onerror = (event) => reject(event.target.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dequeueSyncActions() {
|
||||||
|
try {
|
||||||
|
const db = await openDB();
|
||||||
|
const transaction = db.transaction("sync-queue", "readwrite");
|
||||||
|
const store = transaction.objectStore("sync-queue");
|
||||||
|
const request = store.getAll();
|
||||||
|
|
||||||
|
request.onsuccess = async () => {
|
||||||
|
const actions = request.result;
|
||||||
|
for (const action of actions) {
|
||||||
|
try {
|
||||||
|
await fetch(action.url, {
|
||||||
|
method: action.method,
|
||||||
|
headers: action.headers,
|
||||||
|
body: action.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Suppression si succès
|
||||||
|
const delTx = db.transaction("sync-queue", "readwrite");
|
||||||
|
delTx.objectStore("sync-queue").delete(action.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur de synchronisation en arrière-plan :", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Erreur ouverture DB dans le Service Worker", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background Sync exclusif à Chromium
|
||||||
|
self.addEventListener("sync", (event) => {
|
||||||
|
if (event.tag === "househub-sync") {
|
||||||
|
event.waitUntil(dequeueSyncActions());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
// Utilise catch pour éviter qu'un fichier manquant (ex: favicon) fasse crasher l'installation
|
||||||
|
return cache
|
||||||
|
.addAll(ASSETS_TO_CACHE)
|
||||||
|
.catch((err) => console.error("Erreur de cache initial", err));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
if (cacheName !== CACHE_NAME && cacheName !== API_CACHE_NAME) {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
// Les requêtes non-GET (formulaires, uploads de fichiers type import CSV) ne doivent
|
||||||
|
// jamais passer par le service worker : sur iOS (WebKit / Firefox iOS), refaire un fetch()
|
||||||
|
// d'une navigation POST multipart perd le fichier, et la page revient sans rien.
|
||||||
|
if (event.request.method !== "GET") return;
|
||||||
|
|
||||||
|
const requestUrl = new URL(event.request.url);
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestUrl.pathname.includes("/api/") ||
|
||||||
|
requestUrl.pathname.includes("api.php")
|
||||||
|
) {
|
||||||
|
if (event.request.method === "GET") {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
const responseClone = response.clone();
|
||||||
|
caches
|
||||||
|
.open(API_CACHE_NAME)
|
||||||
|
.then((cache) => cache.put(event.request, responseClone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match(event.request)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.request.mode === "navigate") {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
const responseClone = response.clone();
|
||||||
|
caches
|
||||||
|
.open(CACHE_NAME)
|
||||||
|
.then((cache) => cache.put(event.request, responseClone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return caches.match(event.request).then((cachedResponse) => {
|
||||||
|
return cachedResponse || caches.match("/offline.php");
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((response) => {
|
||||||
|
if (response) return response;
|
||||||
|
return fetch(event.request)
|
||||||
|
.then((networkResponse) => {
|
||||||
|
if (
|
||||||
|
networkResponse &&
|
||||||
|
networkResponse.status === 200 &&
|
||||||
|
(networkResponse.type === "basic" ||
|
||||||
|
networkResponse.type === "cors")
|
||||||
|
) {
|
||||||
|
const responseToCache = networkResponse.clone();
|
||||||
|
caches
|
||||||
|
.open(CACHE_NAME)
|
||||||
|
.then((cache) => cache.put(event.request, responseToCache));
|
||||||
|
}
|
||||||
|
return networkResponse;
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload.php @generated by Composer
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 50600) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, $err);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RuntimeException($err);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/composer/autoload_real.php';
|
||||||
|
|
||||||
|
return ComposerAutoloaderInit3ffbe28fb745b145fa2fcd19500ebf5e::getLoader();
|
||||||
Vendored
+579
@@ -0,0 +1,579 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||||
|
*
|
||||||
|
* $loader = new \Composer\Autoload\ClassLoader();
|
||||||
|
*
|
||||||
|
* // register classes with namespaces
|
||||||
|
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||||
|
* $loader->add('Symfony', __DIR__.'/framework');
|
||||||
|
*
|
||||||
|
* // activate the autoloader
|
||||||
|
* $loader->register();
|
||||||
|
*
|
||||||
|
* // to enable searching the include path (eg. for PEAR packages)
|
||||||
|
* $loader->setUseIncludePath(true);
|
||||||
|
*
|
||||||
|
* In this example, if you try to use a class in the Symfony\Component
|
||||||
|
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||||
|
* the autoloader will first look for the class under the component/
|
||||||
|
* directory, and it will then fallback to the framework/ directory if not
|
||||||
|
* found before giving up.
|
||||||
|
*
|
||||||
|
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
* @see https://www.php-fig.org/psr/psr-0/
|
||||||
|
* @see https://www.php-fig.org/psr/psr-4/
|
||||||
|
*/
|
||||||
|
class ClassLoader
|
||||||
|
{
|
||||||
|
/** @var \Closure(string):void */
|
||||||
|
private static $includeFile;
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $vendorDir;
|
||||||
|
|
||||||
|
// PSR-4
|
||||||
|
/**
|
||||||
|
* @var array<string, array<string, int>>
|
||||||
|
*/
|
||||||
|
private $prefixLengthsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var array<string, list<string>>
|
||||||
|
*/
|
||||||
|
private $prefixDirsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr4 = array();
|
||||||
|
|
||||||
|
// PSR-0
|
||||||
|
/**
|
||||||
|
* List of PSR-0 prefixes
|
||||||
|
*
|
||||||
|
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||||
|
*
|
||||||
|
* @var array<string, array<string, list<string>>>
|
||||||
|
*/
|
||||||
|
private $prefixesPsr0 = array();
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr0 = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $useIncludePath = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private $classMap = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $classMapAuthoritative = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, bool>
|
||||||
|
*/
|
||||||
|
private $missingClasses = array();
|
||||||
|
|
||||||
|
/** @var string|null */
|
||||||
|
private $apcuPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, self>
|
||||||
|
*/
|
||||||
|
private static $registeredLoaders = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $vendorDir
|
||||||
|
*/
|
||||||
|
public function __construct($vendorDir = null)
|
||||||
|
{
|
||||||
|
$this->vendorDir = $vendorDir;
|
||||||
|
self::initializeIncludeClosure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixes()
|
||||||
|
{
|
||||||
|
if (!empty($this->prefixesPsr0)) {
|
||||||
|
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixesPsr4()
|
||||||
|
{
|
||||||
|
return $this->prefixDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirs()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirsPsr4()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> Array of classname => path
|
||||||
|
*/
|
||||||
|
public function getClassMap()
|
||||||
|
{
|
||||||
|
return $this->classMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $classMap Class to filename map
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addClassMap(array $classMap)
|
||||||
|
{
|
||||||
|
if ($this->classMap) {
|
||||||
|
$this->classMap = array_merge($this->classMap, $classMap);
|
||||||
|
} else {
|
||||||
|
$this->classMap = $classMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix, either
|
||||||
|
* appending or prepending to the ones previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param list<string>|string $paths The PSR-0 root directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
$paths = (array) $paths;
|
||||||
|
if (!$prefix) {
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$this->fallbackDirsPsr0,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = $prefix[0];
|
||||||
|
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($prepend) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixesPsr0[$first][$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$this->prefixesPsr0[$first][$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace, either
|
||||||
|
* appending or prepending to the ones previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param list<string>|string $paths The PSR-4 base directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addPsr4($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
$paths = (array) $paths;
|
||||||
|
if (!$prefix) {
|
||||||
|
// Register directories for the root namespace.
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->fallbackDirsPsr4
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$this->fallbackDirsPsr4,
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||||
|
// Register directories for a new namespace.
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||||
|
} elseif ($prepend) {
|
||||||
|
// Prepend directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$paths,
|
||||||
|
$this->prefixDirsPsr4[$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Append directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$this->prefixDirsPsr4[$prefix],
|
||||||
|
$paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix,
|
||||||
|
* replacing any others previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param list<string>|string $paths The PSR-0 base directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr0 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace,
|
||||||
|
* replacing any others previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param list<string>|string $paths The PSR-4 base directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setPsr4($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr4 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns on searching the include path for class files.
|
||||||
|
*
|
||||||
|
* @param bool $useIncludePath
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setUseIncludePath($useIncludePath)
|
||||||
|
{
|
||||||
|
$this->useIncludePath = $useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used to check if the autoloader uses the include path to check
|
||||||
|
* for classes.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function getUseIncludePath()
|
||||||
|
{
|
||||||
|
return $this->useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off searching the prefix and fallback directories for classes
|
||||||
|
* that have not been registered with the class map.
|
||||||
|
*
|
||||||
|
* @param bool $classMapAuthoritative
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||||
|
{
|
||||||
|
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should class lookup fail if not found in the current class map?
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isClassMapAuthoritative()
|
||||||
|
{
|
||||||
|
return $this->classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||||
|
*
|
||||||
|
* @param string|null $apcuPrefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setApcuPrefix($apcuPrefix)
|
||||||
|
{
|
||||||
|
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getApcuPrefix()
|
||||||
|
{
|
||||||
|
return $this->apcuPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @param bool $prepend Whether to prepend the autoloader or not
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register($prepend = false)
|
||||||
|
{
|
||||||
|
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||||
|
|
||||||
|
if (null === $this->vendorDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prepend) {
|
||||||
|
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||||
|
} else {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function unregister()
|
||||||
|
{
|
||||||
|
spl_autoload_unregister(array($this, 'loadClass'));
|
||||||
|
|
||||||
|
if (null !== $this->vendorDir) {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given class or interface.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
* @return true|null True if loaded, null otherwise
|
||||||
|
*/
|
||||||
|
public function loadClass($class)
|
||||||
|
{
|
||||||
|
if ($file = $this->findFile($class)) {
|
||||||
|
$includeFile = self::$includeFile;
|
||||||
|
$includeFile($file);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the path to the file where the class is defined.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
*
|
||||||
|
* @return string|false The path if found, false otherwise
|
||||||
|
*/
|
||||||
|
public function findFile($class)
|
||||||
|
{
|
||||||
|
// class map lookup
|
||||||
|
if (isset($this->classMap[$class])) {
|
||||||
|
return $this->classMap[$class];
|
||||||
|
}
|
||||||
|
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||||
|
if ($hit) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->findFileWithExtension($class, '.php');
|
||||||
|
|
||||||
|
// Search for Hack files if we are running on HHVM
|
||||||
|
if (false === $file && defined('HHVM_VERSION')) {
|
||||||
|
$file = $this->findFileWithExtension($class, '.hh');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
apcu_add($this->apcuPrefix.$class, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $file) {
|
||||||
|
// Remember that this class does not exist.
|
||||||
|
$this->missingClasses[$class] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||||
|
*
|
||||||
|
* @return array<string, self>
|
||||||
|
*/
|
||||||
|
public static function getRegisteredLoaders()
|
||||||
|
{
|
||||||
|
return self::$registeredLoaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @param string $ext
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
private function findFileWithExtension($class, $ext)
|
||||||
|
{
|
||||||
|
// PSR-4 lookup
|
||||||
|
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
|
||||||
|
$first = $class[0];
|
||||||
|
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||||
|
$subPath = $class;
|
||||||
|
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||||
|
$subPath = substr($subPath, 0, $lastPos);
|
||||||
|
$search = $subPath . '\\';
|
||||||
|
if (isset($this->prefixDirsPsr4[$search])) {
|
||||||
|
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||||
|
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||||
|
if (file_exists($file = $dir . $pathEnd)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-4 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 lookup
|
||||||
|
if (false !== $pos = strrpos($class, '\\')) {
|
||||||
|
// namespaced class name
|
||||||
|
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||||
|
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||||
|
} else {
|
||||||
|
// PEAR-like class name
|
||||||
|
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->prefixesPsr0[$first])) {
|
||||||
|
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||||
|
if (0 === strpos($class, $prefix)) {
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 include paths.
|
||||||
|
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function initializeIncludeClosure()
|
||||||
|
{
|
||||||
|
if (self::$includeFile !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope isolated include.
|
||||||
|
*
|
||||||
|
* Prevents access to $this/self from included files.
|
||||||
|
*
|
||||||
|
* @param string $file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
self::$includeFile = \Closure::bind(static function($file) {
|
||||||
|
include $file;
|
||||||
|
}, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
+396
@@ -0,0 +1,396 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
use Composer\Autoload\ClassLoader;
|
||||||
|
use Composer\Semver\VersionParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is copied in every Composer installed project and available to all
|
||||||
|
*
|
||||||
|
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||||
|
*
|
||||||
|
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class InstalledVersions
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private static $selfDir = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var mixed[]|null
|
||||||
|
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||||
|
*/
|
||||||
|
private static $installed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static $installedIsLocalDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
private static $canGetVendors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static $installedByVendor = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackages()
|
||||||
|
{
|
||||||
|
$packages = array();
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
$packages[] = array_keys($installed['versions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === \count($packages)) {
|
||||||
|
return $packages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names with a specific type e.g. 'library'
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackagesByType($type)
|
||||||
|
{
|
||||||
|
$packagesByType = array();
|
||||||
|
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
foreach ($installed['versions'] as $name => $package) {
|
||||||
|
if (isset($package['type']) && $package['type'] === $type) {
|
||||||
|
$packagesByType[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packagesByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package is installed
|
||||||
|
*
|
||||||
|
* This also returns true if the package name is provided or replaced by another package
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @param bool $includeDevRequirements
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (isset($installed['versions'][$packageName])) {
|
||||||
|
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package satisfies a version constraint
|
||||||
|
*
|
||||||
|
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||||
|
*
|
||||||
|
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||||
|
*
|
||||||
|
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||||
|
* @param string $packageName
|
||||||
|
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||||
|
{
|
||||||
|
$constraint = $parser->parseConstraints((string) $constraint);
|
||||||
|
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||||
|
|
||||||
|
return $provided->matches($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||||
|
*
|
||||||
|
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||||
|
* whether a given version of a package is installed, and not just whether it exists
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string Version constraint usable with composer/semver
|
||||||
|
*/
|
||||||
|
public static function getVersionRanges($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranges = array();
|
||||||
|
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' || ', $ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getPrettyVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||||
|
*/
|
||||||
|
public static function getReference($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['reference'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||||
|
*/
|
||||||
|
public static function getInstallPath($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||||
|
*/
|
||||||
|
public static function getRootPackage()
|
||||||
|
{
|
||||||
|
$installed = self::getInstalled();
|
||||||
|
|
||||||
|
return $installed[0]['root'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw installed.php data for custom implementations
|
||||||
|
*
|
||||||
|
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||||
|
*/
|
||||||
|
public static function getRawData()
|
||||||
|
{
|
||||||
|
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||||
|
self::$installed = include __DIR__ . '/installed.php';
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||||
|
*
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
public static function getAllRawData()
|
||||||
|
{
|
||||||
|
return self::getInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets you reload the static array from another file
|
||||||
|
*
|
||||||
|
* This is only useful for complex integrations in which a project needs to use
|
||||||
|
* this class but then also needs to execute another project's autoloader in process,
|
||||||
|
* and wants to ensure both projects have access to their version of installed.php.
|
||||||
|
*
|
||||||
|
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||||
|
* the data it needs from this class, then call reload() with
|
||||||
|
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||||
|
* the project in which it runs can then also use this class safely, without
|
||||||
|
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||||
|
*
|
||||||
|
* @param array[] $data A vendor/composer/installed.php data set
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||||
|
*/
|
||||||
|
public static function reload($data)
|
||||||
|
{
|
||||||
|
self::$installed = $data;
|
||||||
|
self::$installedByVendor = array();
|
||||||
|
|
||||||
|
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||||
|
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||||
|
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||||
|
// all installed packages for example
|
||||||
|
self::$installedIsLocalDir = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function getSelfDir()
|
||||||
|
{
|
||||||
|
if (self::$selfDir === null) {
|
||||||
|
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$selfDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static function getInstalled()
|
||||||
|
{
|
||||||
|
if (null === self::$canGetVendors) {
|
||||||
|
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed = array();
|
||||||
|
$copiedLocalDir = false;
|
||||||
|
|
||||||
|
if (self::$canGetVendors) {
|
||||||
|
$selfDir = self::getSelfDir();
|
||||||
|
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||||
|
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||||
|
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||||
|
$installed[] = self::$installedByVendor[$vendorDir];
|
||||||
|
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require $vendorDir.'/composer/installed.php';
|
||||||
|
self::$installedByVendor[$vendorDir] = $required;
|
||||||
|
$installed[] = $required;
|
||||||
|
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||||
|
self::$installed = $required;
|
||||||
|
self::$installedIsLocalDir = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||||
|
$copiedLocalDir = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require __DIR__ . '/installed.php';
|
||||||
|
self::$installed = $required;
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||||
|
$installed[] = self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_classmap.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||||
|
);
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_files.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||||
|
);
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_namespaces.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Smalot\\PdfParser\\' => array($vendorDir . '/smalot/pdfparser/src'),
|
||||||
|
);
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_psr4.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||||
|
);
|
||||||
Vendored
+50
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_real.php @generated by Composer
|
||||||
|
|
||||||
|
class ComposerAutoloaderInit3ffbe28fb745b145fa2fcd19500ebf5e
|
||||||
|
{
|
||||||
|
private static $loader;
|
||||||
|
|
||||||
|
public static function loadClassLoader($class)
|
||||||
|
{
|
||||||
|
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||||
|
require __DIR__ . '/ClassLoader.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Composer\Autoload\ClassLoader
|
||||||
|
*/
|
||||||
|
public static function getLoader()
|
||||||
|
{
|
||||||
|
if (null !== self::$loader) {
|
||||||
|
return self::$loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
require __DIR__ . '/platform_check.php';
|
||||||
|
|
||||||
|
spl_autoload_register(array('ComposerAutoloaderInit3ffbe28fb745b145fa2fcd19500ebf5e', 'loadClassLoader'), true, true);
|
||||||
|
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||||
|
spl_autoload_unregister(array('ComposerAutoloaderInit3ffbe28fb745b145fa2fcd19500ebf5e', 'loadClassLoader'));
|
||||||
|
|
||||||
|
require __DIR__ . '/autoload_static.php';
|
||||||
|
call_user_func(\Composer\Autoload\ComposerStaticInit3ffbe28fb745b145fa2fcd19500ebf5e::getInitializer($loader));
|
||||||
|
|
||||||
|
$loader->register(true);
|
||||||
|
|
||||||
|
$filesToLoad = \Composer\Autoload\ComposerStaticInit3ffbe28fb745b145fa2fcd19500ebf5e::$files;
|
||||||
|
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
||||||
|
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||||
|
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||||
|
|
||||||
|
require $file;
|
||||||
|
}
|
||||||
|
}, null, null);
|
||||||
|
foreach ($filesToLoad as $fileIdentifier => $file) {
|
||||||
|
$requireFile($fileIdentifier, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $loader;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+51
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_static.php @generated by Composer
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
class ComposerStaticInit3ffbe28fb745b145fa2fcd19500ebf5e
|
||||||
|
{
|
||||||
|
public static $files = array (
|
||||||
|
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $prefixLengthsPsr4 = array (
|
||||||
|
'S' =>
|
||||||
|
array (
|
||||||
|
'Symfony\\Polyfill\\Mbstring\\' => 26,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $prefixDirsPsr4 = array (
|
||||||
|
'Symfony\\Polyfill\\Mbstring\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $prefixesPsr0 = array (
|
||||||
|
'S' =>
|
||||||
|
array (
|
||||||
|
'Smalot\\PdfParser\\' =>
|
||||||
|
array (
|
||||||
|
0 => __DIR__ . '/..' . '/smalot/pdfparser/src',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public static $classMap = array (
|
||||||
|
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
public static function getInitializer(ClassLoader $loader)
|
||||||
|
{
|
||||||
|
return \Closure::bind(function () use ($loader) {
|
||||||
|
$loader->prefixLengthsPsr4 = ComposerStaticInit3ffbe28fb745b145fa2fcd19500ebf5e::$prefixLengthsPsr4;
|
||||||
|
$loader->prefixDirsPsr4 = ComposerStaticInit3ffbe28fb745b145fa2fcd19500ebf5e::$prefixDirsPsr4;
|
||||||
|
$loader->prefixesPsr0 = ComposerStaticInit3ffbe28fb745b145fa2fcd19500ebf5e::$prefixesPsr0;
|
||||||
|
$loader->classMap = ComposerStaticInit3ffbe28fb745b145fa2fcd19500ebf5e::$classMap;
|
||||||
|
|
||||||
|
}, null, ClassLoader::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+148
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "smalot/pdfparser",
|
||||||
|
"version": "v2.12.5",
|
||||||
|
"version_normalized": "2.12.5.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/smalot/pdfparser.git",
|
||||||
|
"reference": "2cfa0d92bd557875c9f52a75fde0e8392302a354"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/smalot/pdfparser/zipball/2cfa0d92bd557875c9f52a75fde0e8392302a354",
|
||||||
|
"reference": "2cfa0d92bd557875c9f52a75fde0e8392302a354",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php": ">=7.1",
|
||||||
|
"symfony/polyfill-mbstring": "^1.18"
|
||||||
|
},
|
||||||
|
"time": "2026-04-17T11:37:58+00:00",
|
||||||
|
"type": "library",
|
||||||
|
"installation-source": "dist",
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Smalot\\PdfParser\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-3.0"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sebastien MALOT",
|
||||||
|
"email": "sebastien@malot.fr"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Pdf parser library. Can read and extract information from pdf file.",
|
||||||
|
"homepage": "https://www.pdfparser.org",
|
||||||
|
"keywords": [
|
||||||
|
"extract",
|
||||||
|
"parse",
|
||||||
|
"parser",
|
||||||
|
"pdf",
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/smalot/pdfparser/issues",
|
||||||
|
"source": "https://github.com/smalot/pdfparser/tree/v2.12.5"
|
||||||
|
},
|
||||||
|
"install-path": "../smalot/pdfparser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-mbstring",
|
||||||
|
"version": "v1.38.2",
|
||||||
|
"version_normalized": "1.38.2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
|
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||||
|
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"php": ">=7.2"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"ext-mbstring": "*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-mbstring": "For best performance"
|
||||||
|
},
|
||||||
|
"time": "2026-05-27T06:59:30+00:00",
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/polyfill",
|
||||||
|
"name": "symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"installation-source": "dist",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony polyfill for the Mbstring extension",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"compatibility",
|
||||||
|
"mbstring",
|
||||||
|
"polyfill",
|
||||||
|
"portable",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"install-path": "../symfony/polyfill-mbstring"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"dev-package-names": []
|
||||||
|
}
|
||||||
Vendored
+41
@@ -0,0 +1,41 @@
|
|||||||
|
<?php return array(
|
||||||
|
'root' => array(
|
||||||
|
'name' => '__root__',
|
||||||
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
|
'version' => '1.0.0.0',
|
||||||
|
'reference' => null,
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../../',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev' => true,
|
||||||
|
),
|
||||||
|
'versions' => array(
|
||||||
|
'__root__' => array(
|
||||||
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
|
'version' => '1.0.0.0',
|
||||||
|
'reference' => null,
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../../',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'smalot/pdfparser' => array(
|
||||||
|
'pretty_version' => 'v2.12.5',
|
||||||
|
'version' => '2.12.5.0',
|
||||||
|
'reference' => '2cfa0d92bd557875c9f52a75fde0e8392302a354',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../smalot/pdfparser',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
'symfony/polyfill-mbstring' => array(
|
||||||
|
'pretty_version' => 'v1.38.2',
|
||||||
|
'version' => '1.38.2.0',
|
||||||
|
'reference' => 'd3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// platform_check.php @generated by Composer
|
||||||
|
|
||||||
|
$issues = array();
|
||||||
|
|
||||||
|
if (!(PHP_VERSION_ID >= 70200)) {
|
||||||
|
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($issues) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+165
@@ -0,0 +1,165 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
|
License, supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
|
other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
|
ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from
|
||||||
|
a header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
|
material is not limited to numerical parameters, data structure
|
||||||
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the object code that the
|
||||||
|
Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that,
|
||||||
|
taken together, effectively do not restrict modification of the
|
||||||
|
portions of the Library contained in the Combined Work and reverse
|
||||||
|
engineering for debugging such modifications, if you also do each of
|
||||||
|
the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
d) Do one of the following:
|
||||||
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
|
License, and the Corresponding Application Code in a form
|
||||||
|
suitable for, and under terms that permit, the user to
|
||||||
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.
|
||||||
|
|
||||||
|
1) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (a) uses at run time
|
||||||
|
a copy of the Library already present on the user's computer
|
||||||
|
system, and (b) will operate properly with a modified version
|
||||||
|
of the Library that is interface-compatible with the Linked
|
||||||
|
Version.
|
||||||
|
|
||||||
|
e) Provide Installation Information, but only if you would otherwise
|
||||||
|
be required to provide such information under section 6 of the
|
||||||
|
GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
|
you use option 4d0, the Installation Information must accompany
|
||||||
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
|
Code. If you use option 4d1, you must provide the Installation
|
||||||
|
Information in the manner specified by section 6 of the GNU GPL
|
||||||
|
for conveying Corresponding Source.)
|
||||||
|
|
||||||
|
5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the
|
||||||
|
Library side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work based
|
||||||
|
on the Library, uncombined with any other library facilities,
|
||||||
|
conveyed under the terms of this License.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Library as you received it specifies that a certain numbered version
|
||||||
|
of the GNU Lesser General Public License "or any later version"
|
||||||
|
applies to it, you have the option of following the terms and
|
||||||
|
conditions either of that published version or of any later version
|
||||||
|
published by the Free Software Foundation. If the Library as you
|
||||||
|
received it does not specify a version number of the GNU Lesser
|
||||||
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
|
General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
||||||
Vendored
+59
@@ -0,0 +1,59 @@
|
|||||||
|
# PDF parser
|
||||||
|
|
||||||
|
[](//packagist.org/packages/smalot/pdfparser)
|
||||||
|

|
||||||
|

|
||||||
|
[](https://scrutinizer-ci.com/g/smalot/pdfparser/?branch=master)
|
||||||
|
[](//packagist.org/packages/smalot/pdfparser)
|
||||||
|
|
||||||
|
The `smalot/pdfparser` is a standalone PHP package that provides various tools to extract data from PDF files.
|
||||||
|
|
||||||
|
This library is under **active maintenance**.
|
||||||
|
There is no active development by the author of this library (at the moment), but we welcome any pull request adding/extending functionality!
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for further information about how to contribute.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Load/parse objects and headers
|
||||||
|
- Extract metadata (author, description, ...)
|
||||||
|
- Extract text from ordered pages
|
||||||
|
- Support of compressed PDFs
|
||||||
|
- Support of MAC OS Roman charset encoding
|
||||||
|
- Handling of hexa and octal encoding in text sections
|
||||||
|
- Create custom configurations (see [CustomConfig.md](/doc/CustomConfig.md)).
|
||||||
|
|
||||||
|
Currently, secured documents and extracting form data are not supported.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This library is under the [LGPLv3 license](https://github.com/smalot/pdfparser/blob/master/LICENSE.txt).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
This library requires PHP 7.1+ since [v1](https://github.com/smalot/pdfparser/releases/tag/v1.0.0).
|
||||||
|
You can install it via [Composer](https://getcomposer.org/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require smalot/pdfparser
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you can't use Composer, you can include `alt_autoload.php-dist`. It will include all required files automatically.
|
||||||
|
|
||||||
|
## Quick example
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Parse PDF file and build necessary objects.
|
||||||
|
$parser = new \Smalot\PdfParser\Parser();
|
||||||
|
$pdf = $parser->parseFile('/path/to/document.pdf');
|
||||||
|
|
||||||
|
$text = $pdf->getText();
|
||||||
|
echo $text;
|
||||||
|
```
|
||||||
|
|
||||||
|
Further usage information can be found [here](/doc/Usage.md).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Documentation can be found in the [doc](/doc) folder.
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "smalot/pdfparser",
|
||||||
|
"description": "Pdf parser library. Can read and extract information from pdf file.",
|
||||||
|
"keywords": ["PDF", "text", "parser", "parse", "extract"],
|
||||||
|
"type": "library",
|
||||||
|
"license": "LGPL-3.0",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sebastien MALOT",
|
||||||
|
"email": "sebastien@malot.fr"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/smalot/pdfparser/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://www.pdfparser.org",
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1",
|
||||||
|
"symfony/polyfill-mbstring": "^1.18",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"ext-iconv": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Smalot\\PdfParser\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PerformanceTests\\": "tests/Performance/",
|
||||||
|
"PHPUnitTests\\": "tests/PHPUnit/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"process-timeout": 1200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Konrad Abicht <hi@inspirito.de>
|
||||||
|
*
|
||||||
|
* @date 2020-11-22
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains configurations used in various classes. You can override them
|
||||||
|
* manually, in case default values aren't working.
|
||||||
|
*
|
||||||
|
* @see https://github.com/smalot/pdfparser/issues/305
|
||||||
|
*/
|
||||||
|
class Config
|
||||||
|
{
|
||||||
|
private $fontSpaceLimit = -50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $horizontalOffset = ' ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents: (NUL, HT, LF, FF, CR, SP)
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $pdfWhitespaces = "\0\t\n\f\r ";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents: (NUL, HT, LF, FF, CR, SP)
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $pdfWhitespacesRegex = '[\0\t\n\f\r ]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to retain raw image data as content or discard it to save memory
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $retainImageContent = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory limit to use when de-compressing files, in bytes.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $decodeMemoryLimit = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to include font id and size in dataTm array
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $dataTmFontInfoHasToBeIncluded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to attempt to read PDFs even if they are marked as encrypted.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $ignoreEncryption = false;
|
||||||
|
|
||||||
|
public function getFontSpaceLimit()
|
||||||
|
{
|
||||||
|
return $this->fontSpaceLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFontSpaceLimit($value)
|
||||||
|
{
|
||||||
|
$this->fontSpaceLimit = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHorizontalOffset(): string
|
||||||
|
{
|
||||||
|
return $this->horizontalOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHorizontalOffset($value): void
|
||||||
|
{
|
||||||
|
$this->horizontalOffset = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPdfWhitespaces(): string
|
||||||
|
{
|
||||||
|
return $this->pdfWhitespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPdfWhitespaces(string $pdfWhitespaces): void
|
||||||
|
{
|
||||||
|
$this->pdfWhitespaces = $pdfWhitespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPdfWhitespacesRegex(): string
|
||||||
|
{
|
||||||
|
return $this->pdfWhitespacesRegex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPdfWhitespacesRegex(string $pdfWhitespacesRegex): void
|
||||||
|
{
|
||||||
|
$this->pdfWhitespacesRegex = $pdfWhitespacesRegex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRetainImageContent(): bool
|
||||||
|
{
|
||||||
|
return $this->retainImageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRetainImageContent(bool $retainImageContent): void
|
||||||
|
{
|
||||||
|
$this->retainImageContent = $retainImageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDecodeMemoryLimit(): int
|
||||||
|
{
|
||||||
|
return $this->decodeMemoryLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDecodeMemoryLimit(int $decodeMemoryLimit): void
|
||||||
|
{
|
||||||
|
$this->decodeMemoryLimit = $decodeMemoryLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDataTmFontInfoHasToBeIncluded(): bool
|
||||||
|
{
|
||||||
|
return $this->dataTmFontInfoHasToBeIncluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDataTmFontInfoHasToBeIncluded(bool $dataTmFontInfoHasToBeIncluded): void
|
||||||
|
{
|
||||||
|
$this->dataTmFontInfoHasToBeIncluded = $dataTmFontInfoHasToBeIncluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIgnoreEncryption(): bool
|
||||||
|
{
|
||||||
|
return $this->ignoreEncryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated this is a temporary workaround, don't rely on it
|
||||||
|
* @see https://github.com/smalot/pdfparser/pull/653
|
||||||
|
*/
|
||||||
|
public function setIgnoreEncryption(bool $ignoreEncryption): void
|
||||||
|
{
|
||||||
|
$this->ignoreEncryption = $ignoreEncryption;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Encoding\PDFDocEncoding;
|
||||||
|
use Smalot\PdfParser\Exception\MissingCatalogException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Technical references :
|
||||||
|
* - http://www.mactech.com/articles/mactech/Vol.15/15.09/PDFIntro/index.html
|
||||||
|
* - http://framework.zend.com/issues/secure/attachment/12512/Pdf.php
|
||||||
|
* - http://www.php.net/manual/en/ref.pdf.php#74211
|
||||||
|
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin1Encoding.pm
|
||||||
|
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin9Encoding.pm
|
||||||
|
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/StandardEncoding.pm
|
||||||
|
* - http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/WinAnsiEncoding.pm
|
||||||
|
*
|
||||||
|
* Class Document
|
||||||
|
*/
|
||||||
|
class Document
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PDFObject[]
|
||||||
|
*/
|
||||||
|
protected $objects = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dictionary = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Header
|
||||||
|
*/
|
||||||
|
protected $trailer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<mixed>
|
||||||
|
*/
|
||||||
|
protected $metadata = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $details;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->trailer = new Header([], $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
$this->buildDictionary();
|
||||||
|
|
||||||
|
$this->buildDetails();
|
||||||
|
|
||||||
|
// Propagate init to objects.
|
||||||
|
foreach ($this->objects as $object) {
|
||||||
|
$object->getHeader()->init();
|
||||||
|
$object->init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build dictionary based on type header field.
|
||||||
|
*/
|
||||||
|
protected function buildDictionary()
|
||||||
|
{
|
||||||
|
// Build dictionary.
|
||||||
|
$this->dictionary = [];
|
||||||
|
|
||||||
|
foreach ($this->objects as $id => $object) {
|
||||||
|
// Cache objects by type and subtype
|
||||||
|
$type = $object->getHeader()->get('Type')->getContent();
|
||||||
|
|
||||||
|
if (null != $type) {
|
||||||
|
if (!isset($this->dictionary[$type])) {
|
||||||
|
$this->dictionary[$type] = [
|
||||||
|
'all' => [],
|
||||||
|
'subtype' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dictionary[$type]['all'][$id] = $object;
|
||||||
|
|
||||||
|
$subtype = $object->getHeader()->get('Subtype')->getContent();
|
||||||
|
if (null != $subtype) {
|
||||||
|
if (!isset($this->dictionary[$type]['subtype'][$subtype])) {
|
||||||
|
$this->dictionary[$type]['subtype'][$subtype] = [];
|
||||||
|
}
|
||||||
|
$this->dictionary[$type]['subtype'][$subtype][$id] = $object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build details array.
|
||||||
|
*/
|
||||||
|
protected function buildDetails()
|
||||||
|
{
|
||||||
|
// Build details array.
|
||||||
|
$details = [];
|
||||||
|
|
||||||
|
// Extract document info
|
||||||
|
if ($this->trailer->has('Info')) {
|
||||||
|
/** @var PDFObject $info */
|
||||||
|
$info = $this->trailer->get('Info');
|
||||||
|
// This could be an ElementMissing object, so we need to check for
|
||||||
|
// the getHeader method first.
|
||||||
|
if (null !== $info && method_exists($info, 'getHeader')) {
|
||||||
|
$details = $info->getHeader()->getDetails();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the page count
|
||||||
|
try {
|
||||||
|
$pages = $this->getPages();
|
||||||
|
$details['Pages'] = \count($pages);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$details['Pages'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and repair encoded document properties
|
||||||
|
foreach ($details as $key => $value) {
|
||||||
|
if (\is_string($value)) {
|
||||||
|
// If the string is already UTF-8 encoded, that means we only
|
||||||
|
// need to repair Adobe's ham-fisted insertion of line-feeds
|
||||||
|
// every ~127 characters, which doesn't seem to be multi-byte
|
||||||
|
// safe
|
||||||
|
if (mb_check_encoding($value, 'UTF-8')) {
|
||||||
|
// Remove literal backslash + line-feed "\\r"
|
||||||
|
$value = str_replace("\x5c\x0d", '', $value);
|
||||||
|
|
||||||
|
// Remove backslash plus bytes written into high part of
|
||||||
|
// multibyte unicode character
|
||||||
|
while (preg_match("/\x5c\x5c\xe0([\xb4-\xb8])(.)/", $value, $match)) {
|
||||||
|
$diff = (\ord($match[1]) - 182) * 64;
|
||||||
|
$newbyte = PDFDocEncoding::convertPDFDoc2UTF8(\chr(\ord($match[2]) + $diff));
|
||||||
|
$value = preg_replace("/\x5c\x5c\xe0".$match[1].$match[2].'/', $newbyte, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove bytes written into low part of multibyte unicode
|
||||||
|
// character
|
||||||
|
while (preg_match("/(.)\x9c\xe0([\xb3-\xb7])/", $value, $match)) {
|
||||||
|
$diff = \ord($match[2]) - 181;
|
||||||
|
$newbyte = \chr(\ord($match[1]) + $diff);
|
||||||
|
$value = preg_replace('/'.$match[1]."\x9c\xe0".$match[2].'/', $newbyte, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove this byte string that Adobe occasionally adds
|
||||||
|
// between two single byte characters in a unicode string
|
||||||
|
$value = str_replace("\xe5\xb0\x8d", '', $value);
|
||||||
|
|
||||||
|
$details[$key] = $value;
|
||||||
|
} else {
|
||||||
|
// If the string is just PDFDocEncoding, remove any line-feeds
|
||||||
|
// and decode the whole thing.
|
||||||
|
$value = str_replace("\\\r", '', $value);
|
||||||
|
$details[$key] = PDFDocEncoding::convertPDFDoc2UTF8($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$details = array_merge($details, $this->metadata);
|
||||||
|
|
||||||
|
$this->details = $details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract XMP Metadata
|
||||||
|
*/
|
||||||
|
public function extractXMPMetadata(string $content): void
|
||||||
|
{
|
||||||
|
$xml = xml_parser_create();
|
||||||
|
xml_parser_set_option($xml, \XML_OPTION_SKIP_WHITE, 1);
|
||||||
|
|
||||||
|
if (1 === xml_parse_into_struct($xml, $content, $values, $index)) {
|
||||||
|
/*
|
||||||
|
* short overview about the following code parts:
|
||||||
|
*
|
||||||
|
* The output of xml_parse_into_struct is a single dimensional array (= $values), and the $stack is a last-on,
|
||||||
|
* first-off array of pointers to positions in $metadata, while iterating through it, that potentially turn the
|
||||||
|
* results into a more intuitive multi-dimensional array. When an "open" XML tag is encountered,
|
||||||
|
* we save the current $metadata context in the $stack, then create a child array of $metadata and
|
||||||
|
* make that the current $metadata context. When a "close" XML tag is encountered, the operations are
|
||||||
|
* reversed: the most recently added $metadata context from $stack (IOW, the parent of the current
|
||||||
|
* element) is set as the current $metadata context.
|
||||||
|
*/
|
||||||
|
$metadata = [];
|
||||||
|
$stack = [];
|
||||||
|
foreach ($values as $val) {
|
||||||
|
// Standardize to lowercase
|
||||||
|
$val['tag'] = strtolower($val['tag']);
|
||||||
|
|
||||||
|
// Ignore structural x: and rdf: XML elements
|
||||||
|
if (0 === strpos($val['tag'], 'x:')) {
|
||||||
|
continue;
|
||||||
|
} elseif (0 === strpos($val['tag'], 'rdf:') && 'rdf:li' != $val['tag']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($val['type']) {
|
||||||
|
case 'open':
|
||||||
|
// Create an array of list items
|
||||||
|
if ('rdf:li' == $val['tag']) {
|
||||||
|
$metadata[] = [];
|
||||||
|
|
||||||
|
// Move up one level in the stack
|
||||||
|
$stack[\count($stack)] = &$metadata;
|
||||||
|
$metadata = &$metadata[\count($metadata) - 1];
|
||||||
|
} else {
|
||||||
|
// Else create an array of named values
|
||||||
|
$metadata[$val['tag']] = [];
|
||||||
|
|
||||||
|
// Move up one level in the stack
|
||||||
|
$stack[\count($stack)] = &$metadata;
|
||||||
|
$metadata = &$metadata[$val['tag']];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'complete':
|
||||||
|
if (isset($val['value'])) {
|
||||||
|
// Assign a value to this list item
|
||||||
|
if ('rdf:li' == $val['tag']) {
|
||||||
|
$metadata[] = $val['value'];
|
||||||
|
|
||||||
|
// Else assign a value to this property
|
||||||
|
} else {
|
||||||
|
$metadata[$val['tag']] = $val['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'close':
|
||||||
|
// If the value of this property is an array
|
||||||
|
if (\is_array($metadata)) {
|
||||||
|
// If the value is a single element array
|
||||||
|
// where the element is of type string, use
|
||||||
|
// the value of the first list item as the
|
||||||
|
// value for this property
|
||||||
|
if (1 == \count($metadata) && isset($metadata[0]) && \is_string($metadata[0])) {
|
||||||
|
$metadata = $metadata[0];
|
||||||
|
} elseif (0 == \count($metadata)) {
|
||||||
|
// if the value is an empty array, set
|
||||||
|
// the value of this property to the empty
|
||||||
|
// string
|
||||||
|
$metadata = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move down one level in the stack
|
||||||
|
$metadata = &$stack[\count($stack) - 1];
|
||||||
|
unset($stack[\count($stack) - 1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only use this metadata if it's referring to a PDF
|
||||||
|
if (!isset($metadata['dc:format']) || 'application/pdf' == $metadata['dc:format']) {
|
||||||
|
// According to the XMP specifications: 'Conflict resolution
|
||||||
|
// for separate packets that describe the same resource is
|
||||||
|
// beyond the scope of this document.' - Section 6.1
|
||||||
|
// Source: https://www.adobe.com/devnet/xmp.html
|
||||||
|
// Source: https://github.com/adobe/XMP-Toolkit-SDK/blob/main/docs/XMPSpecificationPart1.pdf
|
||||||
|
// So if there are multiple XMP blocks, just merge the values
|
||||||
|
// of each found block over top of the existing values
|
||||||
|
$this->metadata = array_merge($this->metadata, $metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove this if-clause and its content when dropping PHP 7 support
|
||||||
|
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
|
||||||
|
// ref: https://www.php.net/manual/en/function.xml-parser-free.php
|
||||||
|
xml_parser_free($xml);
|
||||||
|
|
||||||
|
// to avoid memory leaks; documentation said:
|
||||||
|
// > it was necessary to also explicitly unset the reference to parser to avoid memory leaks
|
||||||
|
unset($xml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDictionary(): array
|
||||||
|
{
|
||||||
|
return $this->dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param PDFObject[] $objects
|
||||||
|
*/
|
||||||
|
public function setObjects($objects = [])
|
||||||
|
{
|
||||||
|
$this->objects = (array) $objects;
|
||||||
|
|
||||||
|
$this->init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PDFObject[]
|
||||||
|
*/
|
||||||
|
public function getObjects()
|
||||||
|
{
|
||||||
|
return $this->objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PDFObject|Font|Page|Element|null
|
||||||
|
*/
|
||||||
|
public function getObjectById(string $id)
|
||||||
|
{
|
||||||
|
if (isset($this->objects[$id])) {
|
||||||
|
return $this->objects[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasObjectsByType(string $type, ?string $subtype = null): bool
|
||||||
|
{
|
||||||
|
return 0 < \count($this->getObjectsByType($type, $subtype));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getObjectsByType(string $type, ?string $subtype = null): array
|
||||||
|
{
|
||||||
|
if (!isset($this->dictionary[$type])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null != $subtype) {
|
||||||
|
if (!isset($this->dictionary[$type]['subtype'][$subtype])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dictionary[$type]['subtype'][$subtype];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dictionary[$type]['all'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Font[]
|
||||||
|
*/
|
||||||
|
public function getFonts()
|
||||||
|
{
|
||||||
|
return $this->getObjectsByType('Font');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFirstFont(): ?Font
|
||||||
|
{
|
||||||
|
$fonts = $this->getFonts();
|
||||||
|
if ([] === $fonts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reset($fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Page[]
|
||||||
|
*
|
||||||
|
* @throws MissingCatalogException
|
||||||
|
*/
|
||||||
|
public function getPages()
|
||||||
|
{
|
||||||
|
if ($this->hasObjectsByType('Catalog')) {
|
||||||
|
// Search for catalog to list pages.
|
||||||
|
$catalogues = $this->getObjectsByType('Catalog');
|
||||||
|
$catalogue = reset($catalogues);
|
||||||
|
|
||||||
|
/** @var Pages $object */
|
||||||
|
$object = $catalogue->get('Pages');
|
||||||
|
if (method_exists($object, 'getPages')) {
|
||||||
|
return $object->getPages(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasObjectsByType('Pages')) {
|
||||||
|
// Search for pages to list kids.
|
||||||
|
$pages = [];
|
||||||
|
|
||||||
|
/** @var Pages[] $objects */
|
||||||
|
$objects = $this->getObjectsByType('Pages');
|
||||||
|
foreach ($objects as $object) {
|
||||||
|
$pages = array_merge($pages, $object->getPages(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasObjectsByType('Page')) {
|
||||||
|
// Search for 'page' (unordered pages).
|
||||||
|
$pages = $this->getObjectsByType('Page');
|
||||||
|
|
||||||
|
return array_values($pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingCatalogException('Missing catalog.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getText(?int $pageLimit = null): string
|
||||||
|
{
|
||||||
|
$texts = [];
|
||||||
|
$pages = $this->getPages();
|
||||||
|
|
||||||
|
// Only use the first X number of pages if $pageLimit is set and numeric.
|
||||||
|
if (\is_int($pageLimit) && 0 < $pageLimit) {
|
||||||
|
$pages = \array_slice($pages, 0, $pageLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pages as $index => $page) {
|
||||||
|
/**
|
||||||
|
* In some cases, the $page variable may be null.
|
||||||
|
*/
|
||||||
|
if (null === $page) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($text = trim($page->getText())) {
|
||||||
|
$texts[] = $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n\n", $texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTrailer(): Header
|
||||||
|
{
|
||||||
|
return $this->trailer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTrailer(Header $trailer)
|
||||||
|
{
|
||||||
|
$this->trailer = $trailer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetails(): array
|
||||||
|
{
|
||||||
|
return $this->details;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Element\ElementArray;
|
||||||
|
use Smalot\PdfParser\Element\ElementBoolean;
|
||||||
|
use Smalot\PdfParser\Element\ElementDate;
|
||||||
|
use Smalot\PdfParser\Element\ElementHexa;
|
||||||
|
use Smalot\PdfParser\Element\ElementName;
|
||||||
|
use Smalot\PdfParser\Element\ElementNull;
|
||||||
|
use Smalot\PdfParser\Element\ElementNumeric;
|
||||||
|
use Smalot\PdfParser\Element\ElementString;
|
||||||
|
use Smalot\PdfParser\Element\ElementStruct;
|
||||||
|
use Smalot\PdfParser\Element\ElementXRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Element
|
||||||
|
*/
|
||||||
|
class Element
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Document|null
|
||||||
|
*/
|
||||||
|
protected $document;
|
||||||
|
|
||||||
|
protected $value;
|
||||||
|
|
||||||
|
public function __construct($value, ?Document $document = null)
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
$this->document = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
return $value == $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contains($value): bool
|
||||||
|
{
|
||||||
|
if (\is_array($this->value)) {
|
||||||
|
/** @var Element $val */
|
||||||
|
foreach ($this->value as $val) {
|
||||||
|
if ($val->equals($value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->equals($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent()
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return (string) $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$position = 0)
|
||||||
|
{
|
||||||
|
$args = \func_get_args();
|
||||||
|
$only_values = isset($args[3]) ? $args[3] : false;
|
||||||
|
$content = trim($content);
|
||||||
|
$values = [];
|
||||||
|
|
||||||
|
do {
|
||||||
|
$old_position = $position;
|
||||||
|
|
||||||
|
if (!$only_values) {
|
||||||
|
if (!preg_match('/\G\s*(?P<name>\/[A-Z#0-9\._]+)(?P<value>.*)/si', $content, $match, 0, $position)) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
$name = preg_replace_callback(
|
||||||
|
'/#([0-9a-f]{2})/i',
|
||||||
|
function ($m) {
|
||||||
|
return \chr(base_convert($m[1], 16, 10));
|
||||||
|
},
|
||||||
|
ltrim($match['name'], '/')
|
||||||
|
);
|
||||||
|
$value = $match['value'];
|
||||||
|
$position = strpos($content, $value, $position + \strlen($match['name']));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$name = \count($values);
|
||||||
|
$value = substr($content, $position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($element = ElementName::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementXRef::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementNumeric::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementStruct::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementBoolean::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementNull::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementDate::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementString::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementHexa::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} elseif ($element = ElementArray::parse($value, $document, $position)) {
|
||||||
|
$values[$name] = $element;
|
||||||
|
} else {
|
||||||
|
$position = $old_position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while ($position < \strlen($content));
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
use Smalot\PdfParser\Header;
|
||||||
|
use Smalot\PdfParser\PDFObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementArray
|
||||||
|
*/
|
||||||
|
class ElementArray extends Element
|
||||||
|
{
|
||||||
|
public function __construct($value, ?Document $document = null)
|
||||||
|
{
|
||||||
|
parent::__construct($value, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent()
|
||||||
|
{
|
||||||
|
foreach ($this->value as $name => $element) {
|
||||||
|
$this->resolveXRef($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRawContent(): array
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetails(bool $deep = true): array
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
$elements = $this->getContent();
|
||||||
|
|
||||||
|
foreach ($elements as $key => $element) {
|
||||||
|
if ($element instanceof Header && $deep) {
|
||||||
|
$values[$key] = $element->getDetails($deep);
|
||||||
|
} elseif ($element instanceof PDFObject && $deep) {
|
||||||
|
$values[$key] = $element->getDetails(false);
|
||||||
|
} elseif ($element instanceof self) {
|
||||||
|
if ($deep) {
|
||||||
|
$values[$key] = $element->getDetails();
|
||||||
|
}
|
||||||
|
} elseif ($element instanceof Element && !($element instanceof self)) {
|
||||||
|
$values[$key] = $element->getContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return implode(',', $this->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Element|PDFObject
|
||||||
|
*/
|
||||||
|
protected function resolveXRef(string $name)
|
||||||
|
{
|
||||||
|
if (($obj = $this->value[$name]) instanceof ElementXRef) {
|
||||||
|
/** @var ElementXRef $obj */
|
||||||
|
$obj = $this->document->getObjectById($obj->getId());
|
||||||
|
$this->value[$name] = $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->value[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo: These methods return mixed and mismatched types throughout the hierarchy
|
||||||
|
*
|
||||||
|
* @return bool|ElementArray
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*\[(?P<array>.*)/is', $content, $match)) {
|
||||||
|
preg_match_all('/(.*?)(\[|\])/s', trim($content), $matches);
|
||||||
|
|
||||||
|
$level = 0;
|
||||||
|
$sub = '';
|
||||||
|
foreach ($matches[0] as $part) {
|
||||||
|
$sub .= $part;
|
||||||
|
$level += (false !== strpos($part, '[') ? 1 : -1);
|
||||||
|
if ($level <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes 1 level [ and ].
|
||||||
|
$sub = substr(trim($sub), 1, -1);
|
||||||
|
$sub_offset = 0;
|
||||||
|
$values = Element::parse($sub, $document, $sub_offset, true);
|
||||||
|
|
||||||
|
$offset += strpos($content, '[') + 1;
|
||||||
|
// Find next ']' position
|
||||||
|
$offset += \strlen($sub) + 1;
|
||||||
|
|
||||||
|
return new self($values, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementBoolean
|
||||||
|
*/
|
||||||
|
class ElementBoolean extends Element
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string|bool $value
|
||||||
|
*/
|
||||||
|
public function __construct($value)
|
||||||
|
{
|
||||||
|
parent::__construct('true' == strtolower($value) || true === $value, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->value ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
return $this->getContent() === $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|ElementBoolean
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*(?P<value>true|false)/is', $content, $match)) {
|
||||||
|
$value = $match['value'];
|
||||||
|
$offset += strpos($content, $value) + \strlen($value);
|
||||||
|
|
||||||
|
return new self($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHPi, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementDate
|
||||||
|
*/
|
||||||
|
class ElementDate extends ElementString
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int,string>
|
||||||
|
*/
|
||||||
|
protected static $formats = [
|
||||||
|
4 => 'Y',
|
||||||
|
6 => 'Ym',
|
||||||
|
8 => 'Ymd',
|
||||||
|
10 => 'YmdH',
|
||||||
|
12 => 'YmdHi',
|
||||||
|
14 => 'YmdHis',
|
||||||
|
15 => 'YmdHise',
|
||||||
|
17 => 'YmdHisO',
|
||||||
|
18 => 'YmdHisO',
|
||||||
|
19 => 'YmdHisO',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $format = 'c';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \DateTime
|
||||||
|
*/
|
||||||
|
protected $value;
|
||||||
|
|
||||||
|
public function __construct($value)
|
||||||
|
{
|
||||||
|
if (!($value instanceof \DateTime)) {
|
||||||
|
throw new \Exception('DateTime required.'); // FIXME: Sometimes strings are passed to this function
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFormat(string $format)
|
||||||
|
{
|
||||||
|
$this->format = $format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
if ($value instanceof \DateTime) {
|
||||||
|
$timestamp = $value->getTimeStamp();
|
||||||
|
} else {
|
||||||
|
$timestamp = strtotime($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $timestamp == $this->value->getTimeStamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return (string) $this->value->format($this->format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|ElementDate
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*\(D\:(?P<name>.*?)\)/s', $content, $match)) {
|
||||||
|
$name = $match['name'];
|
||||||
|
$name = str_replace("'", '', $name);
|
||||||
|
$date = false;
|
||||||
|
|
||||||
|
// Smallest format : Y
|
||||||
|
// Full format : YmdHisP
|
||||||
|
if (preg_match('/^\d{4}(\d{2}(\d{2}(\d{2}(\d{2}(\d{2}(Z(\d{2,4})?|[\+-]?\d{2}(\d{2})?)?)?)?)?)?)?$/', $name)) {
|
||||||
|
if ($pos = strpos($name, 'Z')) {
|
||||||
|
$name = substr($name, 0, $pos + 1);
|
||||||
|
} elseif (18 == \strlen($name) && preg_match('/[^\+-]0000$/', $name)) {
|
||||||
|
$name = substr($name, 0, -4).'+0000';
|
||||||
|
}
|
||||||
|
|
||||||
|
$format = self::$formats[\strlen($name)];
|
||||||
|
$date = \DateTime::createFromFormat($format, $name, new \DateTimeZone('UTC'));
|
||||||
|
} else {
|
||||||
|
// special cases
|
||||||
|
if (preg_match('/^\d{1,2}-\d{1,2}-\d{4},?\s+\d{2}:\d{2}:\d{2}[\+-]\d{4}$/', $name)) {
|
||||||
|
$name = str_replace(',', '', $name);
|
||||||
|
$format = 'n-j-Y H:i:sO';
|
||||||
|
$date = \DateTime::createFromFormat($format, $name, new \DateTimeZone('UTC'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset += strpos($content, '(D:') + \strlen($match['name']) + 4; // 1 for '(D:' and ')'
|
||||||
|
|
||||||
|
return new self($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementHexa
|
||||||
|
*/
|
||||||
|
class ElementHexa extends ElementString
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return bool|ElementHexa|ElementDate
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*\<(?P<name>[A-F0-9]+)\>/is', $content, $match)) {
|
||||||
|
$name = $match['name'];
|
||||||
|
$offset += strpos($content, '<'.$name) + \strlen($name) + 2; // 1 for '>'
|
||||||
|
// repackage string as standard
|
||||||
|
$name = '('.self::decode($name).')';
|
||||||
|
$element = ElementDate::parse($name, $document);
|
||||||
|
|
||||||
|
if (!$element) {
|
||||||
|
$element = ElementString::parse($name, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decode(string $value): string
|
||||||
|
{
|
||||||
|
$text = '';
|
||||||
|
|
||||||
|
// Filter $value of non-hexadecimal characters
|
||||||
|
$value = (string) preg_replace('/[^0-9a-f]/i', '', $value);
|
||||||
|
|
||||||
|
// Check for leading zeros (4-byte hexadecimal indicator), or
|
||||||
|
// the BE BOM
|
||||||
|
if ('00' === substr($value, 0, 2) || 'feff' === strtolower(substr($value, 0, 4))) {
|
||||||
|
$value = (string) preg_replace('/^feff/i', '', $value);
|
||||||
|
for ($i = 0, $length = \strlen($value); $i < $length; $i += 4) {
|
||||||
|
$hex = substr($value, $i, 4);
|
||||||
|
$text .= '&#'.str_pad(hexdec($hex), 4, '0', \STR_PAD_LEFT).';';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise decode this as 2-byte hexadecimal
|
||||||
|
for ($i = 0, $length = \strlen($value); $i < $length; $i += 2) {
|
||||||
|
$hex = substr($value, $i, 2);
|
||||||
|
$text .= \chr(hexdec($hex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = html_entity_decode($text, \ENT_NOQUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementMissing
|
||||||
|
*/
|
||||||
|
class ElementMissing extends Element
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contains($value): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementName
|
||||||
|
*/
|
||||||
|
class ElementName extends Element
|
||||||
|
{
|
||||||
|
public function __construct(string $value)
|
||||||
|
{
|
||||||
|
parent::__construct($value, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
return $value == $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|ElementName
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*\/([A-Z0-9\-\+,#\.]+)/is', $content, $match)) {
|
||||||
|
$name = $match[1];
|
||||||
|
$offset += strpos($content, $name) + \strlen($name);
|
||||||
|
$name = Font::decodeEntities($name);
|
||||||
|
|
||||||
|
return new self($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementNull
|
||||||
|
*/
|
||||||
|
class ElementNull extends Element
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
return $this->getContent() === $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|ElementNull
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*(null)/s', $content, $match)) {
|
||||||
|
$offset += strpos($content, 'null') + \strlen('null');
|
||||||
|
|
||||||
|
return new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementNumeric
|
||||||
|
*/
|
||||||
|
class ElementNumeric extends Element
|
||||||
|
{
|
||||||
|
public function __construct(string $value)
|
||||||
|
{
|
||||||
|
parent::__construct((float) $value, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|ElementNumeric
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*(?P<value>\-?[0-9\.]+)/s', $content, $match)) {
|
||||||
|
$value = $match['value'];
|
||||||
|
$offset += strpos($content, $value) + \strlen($value);
|
||||||
|
|
||||||
|
return new self($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementString
|
||||||
|
*/
|
||||||
|
class ElementString extends Element
|
||||||
|
{
|
||||||
|
public function __construct($value)
|
||||||
|
{
|
||||||
|
parent::__construct($value, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
return $value == $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|ElementString
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*\((?P<name>.*)/s', $content, $match)) {
|
||||||
|
$name = $match['name'];
|
||||||
|
|
||||||
|
// Find next ')' not escaped.
|
||||||
|
$cur_start_text = $start_search_end = 0;
|
||||||
|
while (false !== ($cur_start_pos = strpos($name, ')', $start_search_end))) {
|
||||||
|
$cur_extract = substr($name, $cur_start_text, $cur_start_pos - $cur_start_text);
|
||||||
|
preg_match('/(?P<escape>[\\\]*)$/s', $cur_extract, $match);
|
||||||
|
if (!(\strlen($match['escape']) % 2)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$start_search_end = $cur_start_pos + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract string.
|
||||||
|
$name = substr($name, 0, (int) $cur_start_pos);
|
||||||
|
$offset += strpos($content, '(') + $cur_start_pos + 2; // 2 for '(' and ')'
|
||||||
|
$name = str_replace(
|
||||||
|
['\\\\', '\\ ', '\\/', '\(', '\)', '\n', '\r', '\t'],
|
||||||
|
['\\', ' ', '/', '(', ')', "\n", "\r", "\t"],
|
||||||
|
$name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decode string.
|
||||||
|
$name = Font::decodeOctal($name);
|
||||||
|
$name = Font::decodeEntities($name);
|
||||||
|
$name = Font::decodeHexadecimal($name, false);
|
||||||
|
$name = Font::decodeUnicode($name);
|
||||||
|
|
||||||
|
return new self($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
use Smalot\PdfParser\Header;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementStruct
|
||||||
|
*/
|
||||||
|
class ElementStruct extends Element
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return false|Header
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*<<(?P<struct>.*)/is', $content)) {
|
||||||
|
preg_match_all('/(.*?)(<<|>>)/s', trim($content), $matches);
|
||||||
|
|
||||||
|
$level = 0;
|
||||||
|
$sub = '';
|
||||||
|
foreach ($matches[0] as $part) {
|
||||||
|
$sub .= $part;
|
||||||
|
$level += (false !== strpos($part, '<<') ? 1 : -1);
|
||||||
|
if ($level <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset += strpos($content, '<<') + \strlen(rtrim($sub));
|
||||||
|
|
||||||
|
// Removes '<<' and '>>'.
|
||||||
|
$sub = trim((string) preg_replace('/^\s*<<(.*)>>\s*$/s', '\\1', $sub));
|
||||||
|
|
||||||
|
$position = 0;
|
||||||
|
$elements = Element::parse($sub, $document, $position);
|
||||||
|
|
||||||
|
return new Header($elements, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Document;
|
||||||
|
use Smalot\PdfParser\Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ElementXRef
|
||||||
|
*/
|
||||||
|
class ElementXRef extends Element
|
||||||
|
{
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return $this->getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getObject()
|
||||||
|
{
|
||||||
|
return $this->document->getObjectById($this->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals($value): bool
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* In case $value is a number and $this->value is a string like 5_0
|
||||||
|
*
|
||||||
|
* Without this if-clause code like:
|
||||||
|
*
|
||||||
|
* $element = new ElementXRef('5_0');
|
||||||
|
* $this->assertTrue($element->equals(5));
|
||||||
|
*
|
||||||
|
* would fail (= 5_0 and 5 are not equal in PHP 8.0+).
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
true === is_numeric($value)
|
||||||
|
&& true === \is_string($this->getContent())
|
||||||
|
&& 1 === preg_match('/[0-9]+\_[0-9]+/', $this->getContent(), $matches)
|
||||||
|
) {
|
||||||
|
return (float) $this->getContent() == $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = ($value instanceof self) ? $value->getId() : $value;
|
||||||
|
|
||||||
|
return $this->getId() == $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return '#Obj#'.$this->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|ElementXRef
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, ?Document $document = null, int &$offset = 0)
|
||||||
|
{
|
||||||
|
if (preg_match('/^\s*(?P<id>[0-9]+\s+[0-9]+\s+R)/s', $content, $match)) {
|
||||||
|
$id = $match['id'];
|
||||||
|
$offset += strpos($content, $id) + \strlen($id);
|
||||||
|
$id = str_replace(' ', '_', rtrim($id, ' R'));
|
||||||
|
|
||||||
|
return new self($id, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Element\ElementNumeric;
|
||||||
|
use Smalot\PdfParser\Encoding\EncodingLocator;
|
||||||
|
use Smalot\PdfParser\Encoding\PostScriptGlyphs;
|
||||||
|
use Smalot\PdfParser\Exception\EncodingNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Encoding
|
||||||
|
*/
|
||||||
|
class Encoding extends PDFObject
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $encoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $differences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $mapping;
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
$this->mapping = [];
|
||||||
|
$this->differences = [];
|
||||||
|
$this->encoding = [];
|
||||||
|
|
||||||
|
if ($this->has('BaseEncoding')) {
|
||||||
|
$this->encoding = EncodingLocator::getEncoding($this->getEncodingClass())->getTranslations();
|
||||||
|
|
||||||
|
// Build table including differences.
|
||||||
|
$differences = $this->get('Differences')->getContent();
|
||||||
|
$code = 0;
|
||||||
|
|
||||||
|
if (!\is_array($differences)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($differences as $difference) {
|
||||||
|
/** @var ElementNumeric $difference */
|
||||||
|
if ($difference instanceof ElementNumeric) {
|
||||||
|
$code = $difference->getContent();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElementName
|
||||||
|
$this->differences[$code] = $difference;
|
||||||
|
if (\is_object($difference)) {
|
||||||
|
$this->differences[$code] = $difference->getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the next char.
|
||||||
|
++$code;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mapping = $this->encoding;
|
||||||
|
foreach ($this->differences as $code => $difference) {
|
||||||
|
/* @var string $difference */
|
||||||
|
$this->mapping[$code] = $difference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetails(bool $deep = true): array
|
||||||
|
{
|
||||||
|
$details = [];
|
||||||
|
|
||||||
|
$details['BaseEncoding'] = ($this->has('BaseEncoding') ? (string) $this->get('BaseEncoding') : 'Ansi');
|
||||||
|
$details['Differences'] = ($this->has('Differences') ? (string) $this->get('Differences') : '');
|
||||||
|
|
||||||
|
$details += parent::getDetails($deep);
|
||||||
|
|
||||||
|
return $details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function translateChar($dec): ?int
|
||||||
|
{
|
||||||
|
if (isset($this->mapping[$dec])) {
|
||||||
|
$dec = $this->mapping[$dec];
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostScriptGlyphs::getCodePoint($dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns encoding class name if available or empty string (only prior PHP 7.4).
|
||||||
|
*
|
||||||
|
* @throws \Exception On PHP 7.4+ an exception is thrown if encoding class doesn't exist.
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->getEncodingClass();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// prior to PHP 7.4 toString has to return an empty string.
|
||||||
|
if (version_compare(\PHP_VERSION, '7.4.0', '<')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws EncodingNotFoundException
|
||||||
|
*/
|
||||||
|
protected function getEncodingClass(): string
|
||||||
|
{
|
||||||
|
// Load reference table charset.
|
||||||
|
$baseEncoding = preg_replace('/[^A-Z0-9]/is', '', $this->get('BaseEncoding')->getContent());
|
||||||
|
|
||||||
|
// Check for empty BaseEncoding field value
|
||||||
|
if (!\is_string($baseEncoding) || 0 == \strlen($baseEncoding)) {
|
||||||
|
$baseEncoding = 'StandardEncoding';
|
||||||
|
}
|
||||||
|
|
||||||
|
$className = '\\Smalot\\PdfParser\\Encoding\\'.$baseEncoding;
|
||||||
|
|
||||||
|
if (!class_exists($className)) {
|
||||||
|
throw new EncodingNotFoundException('Missing encoding data for: "'.$baseEncoding.'".');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $className;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
abstract class AbstractEncoding
|
||||||
|
{
|
||||||
|
abstract public function getTranslations(): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
class EncodingLocator
|
||||||
|
{
|
||||||
|
protected static $encodings;
|
||||||
|
|
||||||
|
public static function getEncoding(string $encodingClassName): AbstractEncoding
|
||||||
|
{
|
||||||
|
if (!isset(self::$encodings[$encodingClassName])) {
|
||||||
|
self::$encodings[$encodingClassName] = new $encodingClassName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$encodings[$encodingClassName];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin1Encoding.pm
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ISOLatin1Encoding
|
||||||
|
*/
|
||||||
|
class ISOLatin1Encoding extends AbstractEncoding
|
||||||
|
{
|
||||||
|
public function getTranslations(): array
|
||||||
|
{
|
||||||
|
$encoding =
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'space exclam quotedbl numbersign dollar percent ampersand quoteright '.
|
||||||
|
'parenleft parenright asterisk plus comma minus period slash zero one '.
|
||||||
|
'two three four five six seven eight nine colon semicolon less equal '.
|
||||||
|
'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '.
|
||||||
|
'Y Z bracketleft backslash bracketright asciicircum underscore '.
|
||||||
|
'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '.
|
||||||
|
'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef dotlessi grave acute '.
|
||||||
|
'circumflex tilde macron breve dotaccent dieresis .notdef ring '.
|
||||||
|
'cedilla .notdef hungarumlaut ogonek caron space exclamdown cent '.
|
||||||
|
'sterling currency yen brokenbar section dieresis copyright '.
|
||||||
|
'ordfeminine guillemotleft logicalnot hyphen registered macron degree '.
|
||||||
|
'plusminus twosuperior threesuperior acute mu paragraph '.
|
||||||
|
'periodcentered cedilla onesuperior ordmasculine guillemotright '.
|
||||||
|
'onequarter onehalf threequarters questiondown Agrave Aacute '.
|
||||||
|
'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '.
|
||||||
|
'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '.
|
||||||
|
'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '.
|
||||||
|
'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '.
|
||||||
|
'acircumflex atilde adieresis aring ae ccedilla egrave eacute '.
|
||||||
|
'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '.
|
||||||
|
'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '.
|
||||||
|
'uacute ucircumflex udieresis yacute thorn ydieresis';
|
||||||
|
|
||||||
|
return explode(' ', $encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/ISOLatin9Encoding.pm
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ISOLatin9Encoding
|
||||||
|
*/
|
||||||
|
class ISOLatin9Encoding extends AbstractEncoding
|
||||||
|
{
|
||||||
|
public function getTranslations(): array
|
||||||
|
{
|
||||||
|
$encoding =
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'space exclam quotedbl numbersign dollar percent ampersand quoteright '.
|
||||||
|
'parenleft parenright asterisk plus comma minus period slash zero one '.
|
||||||
|
'two three four five six seven eight nine colon semicolon less equal '.
|
||||||
|
'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '.
|
||||||
|
'Y Z bracketleft backslash bracketright asciicircum underscore '.
|
||||||
|
'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '.
|
||||||
|
'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef dotlessi grave acute '.
|
||||||
|
'circumflex tilde macron breve dotaccent dieresis .notdef ring '.
|
||||||
|
'cedilla .notdef hungarumlaut ogonek caron space exclamdown cent '.
|
||||||
|
'sterling Euro yen Scaron section scaron copyright '.
|
||||||
|
'ordfeminine guillemotleft logicalnot hyphen registered macron degree '.
|
||||||
|
'plusminus twosuperior threesuperior Zcaron mu paragraph '.
|
||||||
|
'periodcentered zcaron onesuperior ordmasculine guillemotright '.
|
||||||
|
'OE oe Ydieresis questiondown Agrave Aacute '.
|
||||||
|
'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '.
|
||||||
|
'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '.
|
||||||
|
'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '.
|
||||||
|
'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '.
|
||||||
|
'acircumflex atilde adieresis aring ae ccedilla egrave eacute '.
|
||||||
|
'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '.
|
||||||
|
'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '.
|
||||||
|
'uacute ucircumflex udieresis yacute thorn ydieresis';
|
||||||
|
|
||||||
|
return explode(' ', $encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source : http://www.opensource.apple.com/source/vim/vim-34/vim/runtime/print/mac-roman.ps
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MacRomanEncoding
|
||||||
|
*/
|
||||||
|
class MacRomanEncoding extends AbstractEncoding
|
||||||
|
{
|
||||||
|
public function getTranslations(): array
|
||||||
|
{
|
||||||
|
$encoding =
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'space exclam quotedbl numbersign dollar percent ampersand quotesingle '.
|
||||||
|
'parenleft parenright asterisk plus comma minus period slash '.
|
||||||
|
'zero one two three four five six seven '.
|
||||||
|
'eight nine colon semicolon less equal greater question '.
|
||||||
|
'at A B C D E F G '.
|
||||||
|
'H I J K L M N O '.
|
||||||
|
'P Q R S T U V W '.
|
||||||
|
'X Y Z bracketleft backslash bracketright asciicircum underscore '.
|
||||||
|
'grave a b c d e f g '.
|
||||||
|
'h i j k l m n o '.
|
||||||
|
'p q r s t u v w '.
|
||||||
|
'x y z braceleft bar braceright asciitilde .notdef '.
|
||||||
|
'Adieresis Aring Ccedilla Eacute Ntilde Odieresis Udieresis aacute '.
|
||||||
|
'agrave acircumflex adieresis atilde aring ccedilla eacute egrave '.
|
||||||
|
'ecircumflex edieresis iacute igrave icircumflex idieresis ntilde oacute '.
|
||||||
|
'ograve ocircumflex odieresis otilde uacute ugrave ucircumflex udieresis '.
|
||||||
|
'dagger degree cent sterling section bullet paragraph germandbls '.
|
||||||
|
'registered copyright trademark acute dieresis notequal AE Oslash '.
|
||||||
|
'infinity plusminus lessequal greaterequal yen mu partialdiff summation '.
|
||||||
|
'Pi pi integral ordfeminine ordmasculine Omega ae oslash '.
|
||||||
|
'questiondown exclamdown logicalnot radical florin approxequal delta guillemotleft '.
|
||||||
|
'guillemotright ellipsis space Agrave Atilde Otilde OE oe '.
|
||||||
|
'endash emdash quotedblleft quotedblright quoteleft quoteright divide lozenge '.
|
||||||
|
'ydieresis Ydieresis fraction currency guilsinglleft guilsinglright fi fl '.
|
||||||
|
'daggerdbl periodcentered quotesinglbase quotedblbase perthousand Acircumflex Ecircumflex Aacute '.
|
||||||
|
'Edieresis Egrave Iacute Icircumflex Idieresis Igrave Oacute Ocircumflex '.
|
||||||
|
'heart Ograve Uacute Ucircumflex Ugrave dotlessi circumflex tilde '.
|
||||||
|
'macron breve dotaccent ring cedilla hungarumlaut ogonek caron';
|
||||||
|
|
||||||
|
return explode(' ', $encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Brian Huisman <bhuisman@greywyvern.com>
|
||||||
|
*
|
||||||
|
* @date 2023-06-28
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source : https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.2.pdf
|
||||||
|
// Source : https://ia801001.us.archive.org/1/items/pdf1.7/pdf_reference_1-7.pdf
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class PDFDocEncoding
|
||||||
|
*/
|
||||||
|
class PDFDocEncoding
|
||||||
|
{
|
||||||
|
public static function getCodePage(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
"\x18" => "\u{02d8}", // breve
|
||||||
|
"\x19" => "\u{02c7}", // caron
|
||||||
|
"\x1a" => "\u{02c6}", // circumflex
|
||||||
|
"\x1b" => "\u{02d9}", // dotaccent
|
||||||
|
"\x1c" => "\u{02dd}", // hungarumlaut
|
||||||
|
"\x1d" => "\u{02db}", // ogonek
|
||||||
|
"\x1e" => "\u{02de}", // ring
|
||||||
|
"\x1f" => "\u{02dc}", // tilde
|
||||||
|
"\x7f" => '',
|
||||||
|
"\x80" => "\u{2022}", // bullet
|
||||||
|
"\x81" => "\u{2020}", // dagger
|
||||||
|
"\x82" => "\u{2021}", // daggerdbl
|
||||||
|
"\x83" => "\u{2026}", // ellipsis
|
||||||
|
"\x84" => "\u{2014}", // emdash
|
||||||
|
"\x85" => "\u{2013}", // endash
|
||||||
|
"\x86" => "\u{0192}", // florin
|
||||||
|
"\x87" => "\u{2044}", // fraction
|
||||||
|
"\x88" => "\u{2039}", // guilsinglleft
|
||||||
|
"\x89" => "\u{203a}", // guilsinglright
|
||||||
|
"\x8a" => "\u{2212}", // minus
|
||||||
|
"\x8b" => "\u{2030}", // perthousand
|
||||||
|
"\x8c" => "\u{201e}", // quotedblbase
|
||||||
|
"\x8d" => "\u{201c}", // quotedblleft
|
||||||
|
"\x8e" => "\u{201d}", // quotedblright
|
||||||
|
"\x8f" => "\u{2018}", // quoteleft
|
||||||
|
"\x90" => "\u{2019}", // quoteright
|
||||||
|
"\x91" => "\u{201a}", // quotesinglbase
|
||||||
|
"\x92" => "\u{2122}", // trademark
|
||||||
|
"\x93" => "\u{fb01}", // fi
|
||||||
|
"\x94" => "\u{fb02}", // fl
|
||||||
|
"\x95" => "\u{0141}", // Lslash
|
||||||
|
"\x96" => "\u{0152}", // OE
|
||||||
|
"\x97" => "\u{0160}", // Scaron
|
||||||
|
"\x98" => "\u{0178}", // Ydieresis
|
||||||
|
"\x99" => "\u{017d}", // Zcaron
|
||||||
|
"\x9a" => "\u{0131}", // dotlessi
|
||||||
|
"\x9b" => "\u{0142}", // lslash
|
||||||
|
"\x9c" => "\u{0153}", // oe
|
||||||
|
"\x9d" => "\u{0161}", // scaron
|
||||||
|
"\x9e" => "\u{017e}", // zcaron
|
||||||
|
"\x9f" => '',
|
||||||
|
"\xa0" => "\u{20ac}", // Euro
|
||||||
|
"\xa1" => "\u{00a1}", // exclamdown
|
||||||
|
"\xa2" => "\u{00a2}", // cent
|
||||||
|
"\xa3" => "\u{00a3}", // sterling
|
||||||
|
"\xa4" => "\u{00a4}", // currency
|
||||||
|
"\xa5" => "\u{00a5}", // yen
|
||||||
|
"\xa6" => "\u{00a6}", // brokenbar
|
||||||
|
"\xa7" => "\u{00a7}", // section
|
||||||
|
"\xa8" => "\u{00a8}", // dieresis
|
||||||
|
"\xa9" => "\u{00a9}", // copyright
|
||||||
|
"\xaa" => "\u{00aa}", // ordfeminine
|
||||||
|
"\xab" => "\u{00ab}", // guillemotleft
|
||||||
|
"\xac" => "\u{00ac}", // logicalnot
|
||||||
|
"\xad" => '',
|
||||||
|
"\xae" => "\u{00ae}", // registered
|
||||||
|
"\xaf" => "\u{00af}", // macron
|
||||||
|
"\xb0" => "\u{00b0}", // degree
|
||||||
|
"\xb1" => "\u{00b1}", // plusminus
|
||||||
|
"\xb2" => "\u{00b2}", // twosuperior
|
||||||
|
"\xb3" => "\u{00b3}", // threesuperior
|
||||||
|
"\xb4" => "\u{00b4}", // acute
|
||||||
|
"\xb5" => "\u{00b5}", // mu
|
||||||
|
"\xb6" => "\u{00b6}", // paragraph
|
||||||
|
"\xb7" => "\u{00b7}", // periodcentered
|
||||||
|
"\xb8" => "\u{00b8}", // cedilla
|
||||||
|
"\xb9" => "\u{00b9}", // onesuperior
|
||||||
|
"\xba" => "\u{00ba}", // ordmasculine
|
||||||
|
"\xbb" => "\u{00bb}", // guillemotright
|
||||||
|
"\xbc" => "\u{00bc}", // onequarter
|
||||||
|
"\xbd" => "\u{00bd}", // onehalf
|
||||||
|
"\xbe" => "\u{00be}", // threequarters
|
||||||
|
"\xbf" => "\u{00bf}", // questiondown
|
||||||
|
"\xc0" => "\u{00c0}", // Agrave
|
||||||
|
"\xc1" => "\u{00c1}", // Aacute
|
||||||
|
"\xc2" => "\u{00c2}", // Acircumflex
|
||||||
|
"\xc3" => "\u{00c3}", // Atilde
|
||||||
|
"\xc4" => "\u{00c4}", // Adieresis
|
||||||
|
"\xc5" => "\u{00c5}", // Aring
|
||||||
|
"\xc6" => "\u{00c6}", // AE
|
||||||
|
"\xc7" => "\u{00c7}", // Ccedill
|
||||||
|
"\xc8" => "\u{00c8}", // Egrave
|
||||||
|
"\xc9" => "\u{00c9}", // Eacute
|
||||||
|
"\xca" => "\u{00ca}", // Ecircumflex
|
||||||
|
"\xcb" => "\u{00cb}", // Edieresis
|
||||||
|
"\xcc" => "\u{00cc}", // Igrave
|
||||||
|
"\xcd" => "\u{00cd}", // Iacute
|
||||||
|
"\xce" => "\u{00ce}", // Icircumflex
|
||||||
|
"\xcf" => "\u{00cf}", // Idieresis
|
||||||
|
"\xd0" => "\u{00d0}", // Eth
|
||||||
|
"\xd1" => "\u{00d1}", // Ntilde
|
||||||
|
"\xd2" => "\u{00d2}", // Ograve
|
||||||
|
"\xd3" => "\u{00d3}", // Oacute
|
||||||
|
"\xd4" => "\u{00d4}", // Ocircumflex
|
||||||
|
"\xd5" => "\u{00d5}", // Otilde
|
||||||
|
"\xd6" => "\u{00d6}", // Odieresis
|
||||||
|
"\xd7" => "\u{00d7}", // multiply
|
||||||
|
"\xd8" => "\u{00d8}", // Oslash
|
||||||
|
"\xd9" => "\u{00d9}", // Ugrave
|
||||||
|
"\xda" => "\u{00da}", // Uacute
|
||||||
|
"\xdb" => "\u{00db}", // Ucircumflex
|
||||||
|
"\xdc" => "\u{00dc}", // Udieresis
|
||||||
|
"\xdd" => "\u{00dd}", // Yacute
|
||||||
|
"\xde" => "\u{00de}", // Thorn
|
||||||
|
"\xdf" => "\u{00df}", // germandbls
|
||||||
|
"\xe0" => "\u{00e0}", // agrave
|
||||||
|
"\xe1" => "\u{00e1}", // aacute
|
||||||
|
"\xe2" => "\u{00e2}", // acircumflex
|
||||||
|
"\xe3" => "\u{00e3}", // atilde
|
||||||
|
"\xe4" => "\u{00e4}", // adieresis
|
||||||
|
"\xe5" => "\u{00e5}", // aring
|
||||||
|
"\xe6" => "\u{00e6}", // ae
|
||||||
|
"\xe7" => "\u{00e7}", // ccedilla
|
||||||
|
"\xe8" => "\u{00e8}", // egrave
|
||||||
|
"\xe9" => "\u{00e9}", // eacute
|
||||||
|
"\xea" => "\u{00ea}", // ecircumflex
|
||||||
|
"\xeb" => "\u{00eb}", // edieresis
|
||||||
|
"\xec" => "\u{00ec}", // igrave
|
||||||
|
"\xed" => "\u{00ed}", // iacute
|
||||||
|
"\xee" => "\u{00ee}", // icircumflex
|
||||||
|
"\xef" => "\u{00ef}", // idieresis
|
||||||
|
"\xf0" => "\u{00f0}", // eth
|
||||||
|
"\xf1" => "\u{00f1}", // ntilde
|
||||||
|
"\xf2" => "\u{00f2}", // ograve
|
||||||
|
"\xf3" => "\u{00f3}", // oacute
|
||||||
|
"\xf4" => "\u{00f4}", // ocircumflex
|
||||||
|
"\xf5" => "\u{00f5}", // otilde
|
||||||
|
"\xf6" => "\u{00f6}", // odieresis
|
||||||
|
"\xf7" => "\u{00f7}", // divide
|
||||||
|
"\xf8" => "\u{00f8}", // oslash
|
||||||
|
"\xf9" => "\u{00f9}", // ugrave
|
||||||
|
"\xfa" => "\u{00fa}", // uacute
|
||||||
|
"\xfb" => "\u{00fb}", // ucircumflex
|
||||||
|
"\xfc" => "\u{00fc}", // udieresis
|
||||||
|
"\xfd" => "\u{00fd}", // yacute
|
||||||
|
"\xfe" => "\u{00fe}", // thorn
|
||||||
|
"\xff" => "\u{00ff}", // ydieresis
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function convertPDFDoc2UTF8(string $content): string
|
||||||
|
{
|
||||||
|
return strtr($content, static::getCodePage());
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/StandardEncoding.pm
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class StandardEncoding
|
||||||
|
*/
|
||||||
|
class StandardEncoding extends AbstractEncoding
|
||||||
|
{
|
||||||
|
public function getTranslations(): array
|
||||||
|
{
|
||||||
|
$encoding =
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'space exclam quotedbl numbersign dollar percent ampersand quoteright '.
|
||||||
|
'parenleft parenright asterisk plus comma hyphen period slash zero '.
|
||||||
|
'one two three four five six seven eight nine colon semicolon less '.
|
||||||
|
'equal greater question at A B C D E F G H I J K L M N O P Q R S T U '.
|
||||||
|
'V W X Y Z bracketleft backslash bracketright asciicircum underscore '.
|
||||||
|
'quoteleft a b c d e f g h i j k l m n o p q r s t u v w x y z '.
|
||||||
|
'braceleft bar braceright asciitilde .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef exclamdown cent '.
|
||||||
|
'sterling fraction yen florin section currency quotesingle '.
|
||||||
|
'quotedblleft guillemotleft guilsinglleft guilsinglright fi fl '.
|
||||||
|
'.notdef endash dagger daggerdbl periodcentered .notdef paragraph '.
|
||||||
|
'bullet quotesinglbase quotedblbase quotedblright guillemotright '.
|
||||||
|
'ellipsis perthousand .notdef questiondown .notdef grave acute '.
|
||||||
|
'circumflex tilde macron breve dotaccent dieresis .notdef ring '.
|
||||||
|
'cedilla .notdef hungarumlaut ogonek caron emdash .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef AE .notdef '.
|
||||||
|
'ordfeminine .notdef .notdef .notdef .notdef Lslash Oslash OE '.
|
||||||
|
'ordmasculine .notdef .notdef .notdef .notdef .notdef ae .notdef '.
|
||||||
|
'.notdef .notdef dotlessi .notdef .notdef lslash oslash oe germandbls '.
|
||||||
|
'.notdef .notdef .notdef .notdef';
|
||||||
|
|
||||||
|
return explode(' ', $encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source : http://cpansearch.perl.org/src/JV/PostScript-Font-1.10.02/lib/PostScript/WinANSIEncoding.pm
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Encoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class WinAnsiEncoding
|
||||||
|
*/
|
||||||
|
class WinAnsiEncoding extends AbstractEncoding
|
||||||
|
{
|
||||||
|
public function getTranslations(): array
|
||||||
|
{
|
||||||
|
$encoding =
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'.notdef .notdef .notdef .notdef .notdef .notdef .notdef .notdef '.
|
||||||
|
'space exclam quotedbl numbersign dollar percent ampersand quotesingle '.
|
||||||
|
'parenleft parenright asterisk plus comma hyphen period slash zero one '.
|
||||||
|
'two three four five six seven eight nine colon semicolon less equal '.
|
||||||
|
'greater question at A B C D E F G H I J K L M N O P Q R S T U V W X '.
|
||||||
|
'Y Z bracketleft backslash bracketright asciicircum underscore '.
|
||||||
|
'grave a b c d e f g h i j k l m n o p q r s t u v w x y z '.
|
||||||
|
'braceleft bar braceright asciitilde bullet Euro bullet quotesinglbase '.
|
||||||
|
'florin quotedblbase ellipsis dagger daggerdbl circumflex perthousand '.
|
||||||
|
'Scaron guilsinglleft OE bullet Zcaron bullet bullet quoteleft quoteright '.
|
||||||
|
'quotedblleft quotedblright bullet endash emdash tilde trademark scaron '.
|
||||||
|
'guilsinglright oe bullet zcaron Ydieresis space exclamdown cent '.
|
||||||
|
'sterling currency yen brokenbar section dieresis copyright '.
|
||||||
|
'ordfeminine guillemotleft logicalnot hyphen registered macron degree '.
|
||||||
|
'plusminus twosuperior threesuperior acute mu paragraph '.
|
||||||
|
'periodcentered cedilla onesuperior ordmasculine guillemotright '.
|
||||||
|
'onequarter onehalf threequarters questiondown Agrave Aacute '.
|
||||||
|
'Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute '.
|
||||||
|
'Ecircumflex Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde '.
|
||||||
|
'Ograve Oacute Ocircumflex Otilde Odieresis multiply Oslash Ugrave '.
|
||||||
|
'Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute '.
|
||||||
|
'acircumflex atilde adieresis aring ae ccedilla egrave eacute '.
|
||||||
|
'ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde '.
|
||||||
|
'ograve oacute ocircumflex otilde odieresis divide oslash ugrave '.
|
||||||
|
'uacute ucircumflex udieresis yacute thorn ydieresis';
|
||||||
|
|
||||||
|
return explode(' ', $encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception is thrown when no PDF data was given.
|
||||||
|
*/
|
||||||
|
class EmptyPdfException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Exception;
|
||||||
|
|
||||||
|
class EncodingNotFoundException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is thrown when an invalid dictionary object is encountered.
|
||||||
|
*/
|
||||||
|
class InvalidDictionaryObjectException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is thrown when the catalog is missing.
|
||||||
|
*/
|
||||||
|
class MissingCatalogException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception is thrown when the %PDF- header is missing.
|
||||||
|
*/
|
||||||
|
class MissingPdfHeaderException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Exception is thrown when a functionality has not yet been implemented.
|
||||||
|
*/
|
||||||
|
class NotImplementedException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,709 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Encoding\WinAnsiEncoding;
|
||||||
|
use Smalot\PdfParser\Exception\EncodingNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Font
|
||||||
|
*/
|
||||||
|
class Font extends PDFObject
|
||||||
|
{
|
||||||
|
public const MISSING = '?';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $tableSizes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches results from uchr.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private static $uchrCache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In some PDF-files encoding could be referenced by object id but object itself does not contain
|
||||||
|
* `/Type /Encoding` in its dictionary. These objects wouldn't be initialized as Encoding in
|
||||||
|
* \Smalot\PdfParser\PDFObject::factory() during file parsing (they would be just PDFObject).
|
||||||
|
*
|
||||||
|
* Therefore, we create an instance of Encoding from them during decoding and cache this value in this property.
|
||||||
|
*
|
||||||
|
* @var Encoding
|
||||||
|
*
|
||||||
|
* @see https://github.com/smalot/pdfparser/pull/500
|
||||||
|
*/
|
||||||
|
private $initializedEncodingByPdfObject;
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
// Load translate table.
|
||||||
|
$this->loadTranslateTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->has('BaseFont') ? (string) $this->get('BaseFont') : '[Unknown]';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return (string) $this->header->get('Subtype');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetails(bool $deep = true): array
|
||||||
|
{
|
||||||
|
$details = [];
|
||||||
|
|
||||||
|
$details['Name'] = $this->getName();
|
||||||
|
$details['Type'] = $this->getType();
|
||||||
|
$details['Encoding'] = ($this->has('Encoding') ? (string) $this->get('Encoding') : 'Ansi');
|
||||||
|
|
||||||
|
$details += parent::getDetails($deep);
|
||||||
|
|
||||||
|
return $details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
public function translateChar(string $char, bool $use_default = true)
|
||||||
|
{
|
||||||
|
$dec = hexdec(bin2hex($char));
|
||||||
|
|
||||||
|
if (\array_key_exists($dec, $this->table)) {
|
||||||
|
return $this->table[$dec];
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback for decoding single-byte ANSI characters that are not in the lookup table
|
||||||
|
$fallbackDecoded = $char;
|
||||||
|
if (
|
||||||
|
\strlen($char) < 2
|
||||||
|
&& $this->has('Encoding')
|
||||||
|
&& $this->get('Encoding') instanceof Encoding
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (WinAnsiEncoding::class === $this->get('Encoding')->__toString()) {
|
||||||
|
$fallbackDecoded = self::uchr($dec);
|
||||||
|
}
|
||||||
|
} catch (EncodingNotFoundException $e) {
|
||||||
|
// Encoding->getEncodingClass() throws EncodingNotFoundException when BaseEncoding doesn't exists
|
||||||
|
// See table 5.11 on PDF 1.5 specs for more info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $use_default ? self::MISSING : $fallbackDecoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert unicode character code to "utf-8" encoded string.
|
||||||
|
*
|
||||||
|
* @param int|float $code Unicode character code. Will be casted to int internally!
|
||||||
|
*/
|
||||||
|
public static function uchr($code): string
|
||||||
|
{
|
||||||
|
// note:
|
||||||
|
// $code was typed as int before, but changed in https://github.com/smalot/pdfparser/pull/623
|
||||||
|
// because in some cases uchr was called with a float instead of an integer.
|
||||||
|
$code = (int) $code;
|
||||||
|
|
||||||
|
if (!isset(self::$uchrCache[$code])) {
|
||||||
|
// html_entity_decode() will not work with UTF-16 or UTF-32 char entities,
|
||||||
|
// therefore, we use mb_convert_encoding() instead
|
||||||
|
self::$uchrCache[$code] = mb_convert_encoding("&#{$code};", 'UTF-8', 'HTML-ENTITIES');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$uchrCache[$code];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init internal chars translation table by ToUnicode CMap.
|
||||||
|
*/
|
||||||
|
public function loadTranslateTable(): array
|
||||||
|
{
|
||||||
|
if (null !== $this->table) {
|
||||||
|
return $this->table;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->table = [];
|
||||||
|
$this->tableSizes = [
|
||||||
|
'from' => 1,
|
||||||
|
'to' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->has('ToUnicode')) {
|
||||||
|
$content = $this->get('ToUnicode')->getContent();
|
||||||
|
$matches = [];
|
||||||
|
|
||||||
|
// Support for multiple spacerange sections
|
||||||
|
if (preg_match_all('/begincodespacerange(?P<sections>.*?)endcodespacerange/s', $content, $matches)) {
|
||||||
|
foreach ($matches['sections'] as $section) {
|
||||||
|
$regexp = '/<(?P<from>[0-9A-F]+)> *<(?P<to>[0-9A-F]+)>[ \r\n]+/is';
|
||||||
|
|
||||||
|
preg_match_all($regexp, $section, $matches);
|
||||||
|
|
||||||
|
$this->tableSizes = [
|
||||||
|
'from' => max(1, \strlen(current($matches['from'])) / 2),
|
||||||
|
'to' => max(1, \strlen(current($matches['to'])) / 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support for multiple bfchar sections
|
||||||
|
if (preg_match_all('/beginbfchar(?P<sections>.*?)endbfchar/s', $content, $matches)) {
|
||||||
|
foreach ($matches['sections'] as $section) {
|
||||||
|
$regexp = '/<(?P<from>[0-9A-F]+)> *<(?P<to>[0-9A-F]+)>[ \r\n]+/is';
|
||||||
|
|
||||||
|
preg_match_all($regexp, $section, $matches);
|
||||||
|
|
||||||
|
$this->tableSizes['from'] = max(1, \strlen(current($matches['from'])) / 2);
|
||||||
|
|
||||||
|
foreach ($matches['from'] as $key => $from) {
|
||||||
|
$parts = preg_split(
|
||||||
|
'/([0-9A-F]{4})/i',
|
||||||
|
$matches['to'][$key],
|
||||||
|
0,
|
||||||
|
\PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE
|
||||||
|
);
|
||||||
|
$text = '';
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$text .= self::uchr(hexdec($part));
|
||||||
|
}
|
||||||
|
$this->table[hexdec($from)] = $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support for multiple bfrange sections
|
||||||
|
if (preg_match_all('/beginbfrange(?P<sections>.*?)endbfrange/s', $content, $matches)) {
|
||||||
|
foreach ($matches['sections'] as $section) {
|
||||||
|
/**
|
||||||
|
* Regexp to capture <from>, <to>, and either <offset> or [...] items.
|
||||||
|
* - (?P<from>...) Source range's start
|
||||||
|
* - (?P<to>...) Source range's end
|
||||||
|
* - (?P<dest>...) Destination range's offset or each char code
|
||||||
|
* Some PDF file has 2-byte Unicode values on new lines > added \r\n
|
||||||
|
*/
|
||||||
|
$regexp = '/<(?P<from>[0-9A-F]+)> *<(?P<to>[0-9A-F]+)> *(?P<dest><[0-9A-F]+>|\[[\r\n<>0-9A-F ]+\])[ \r\n]+/is';
|
||||||
|
|
||||||
|
preg_match_all($regexp, $section, $matches);
|
||||||
|
|
||||||
|
foreach ($matches['from'] as $key => $from) {
|
||||||
|
$char_from = hexdec($from);
|
||||||
|
$char_to = hexdec($matches['to'][$key]);
|
||||||
|
$dest = $matches['dest'][$key];
|
||||||
|
|
||||||
|
if (1 === preg_match('/^<(?P<offset>[0-9A-F]+)>$/i', $dest, $offset_matches)) {
|
||||||
|
// Support for : <srcCode1> <srcCode2> <dstString>
|
||||||
|
$offset = hexdec($offset_matches['offset']);
|
||||||
|
|
||||||
|
for ($char = $char_from; $char <= $char_to; ++$char) {
|
||||||
|
$this->table[$char] = self::uchr($char - $char_from + $offset);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Support for : <srcCode1> <srcCodeN> [<dstString1> <dstString2> ... <dstStringN>]
|
||||||
|
$strings = [];
|
||||||
|
$matched = preg_match_all('/<(?P<string>[0-9A-F]+)> */is', $dest, $strings);
|
||||||
|
if (false === $matched || 0 === $matched) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($strings['string'] as $position => $string) {
|
||||||
|
$parts = preg_split(
|
||||||
|
'/([0-9A-F]{4})/i',
|
||||||
|
$string,
|
||||||
|
0,
|
||||||
|
\PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE
|
||||||
|
);
|
||||||
|
if (false === $parts) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$text = '';
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$text .= self::uchr(hexdec($part));
|
||||||
|
}
|
||||||
|
$this->table[$char_from + $position] = $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom char translation table where:
|
||||||
|
* - key - integer character code;
|
||||||
|
* - value - "utf-8" encoded value;
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setTable(array $table)
|
||||||
|
{
|
||||||
|
$this->table = $table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate text width with data from header 'Widths'. If width of character is not found then character is added to missing array.
|
||||||
|
*/
|
||||||
|
public function calculateTextWidth(string $text, ?array &$missing = null): ?float
|
||||||
|
{
|
||||||
|
$index_map = array_flip($this->table);
|
||||||
|
$details = $this->getDetails();
|
||||||
|
|
||||||
|
// Usually, Widths key is set in $details array, but if it isn't use an empty array instead.
|
||||||
|
$widths = $details['Widths'] ?? [];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Widths array is zero indexed but table is not. We must map them based on FirstChar and LastChar
|
||||||
|
*
|
||||||
|
* Note: Without the change you would see warnings in PHP 8.4 because the values of FirstChar or LastChar
|
||||||
|
* can be null sometimes.
|
||||||
|
*/
|
||||||
|
$width_map = array_flip(range((int) $details['FirstChar'], (int) $details['LastChar']));
|
||||||
|
|
||||||
|
$width = null;
|
||||||
|
$missing = [];
|
||||||
|
$textLength = mb_strlen($text);
|
||||||
|
for ($i = 0; $i < $textLength; ++$i) {
|
||||||
|
$char = mb_substr($text, $i, 1);
|
||||||
|
if (
|
||||||
|
!\array_key_exists($char, $index_map)
|
||||||
|
|| !\array_key_exists($index_map[$char], $width_map)
|
||||||
|
|| !\array_key_exists($width_map[$index_map[$char]], $widths)
|
||||||
|
) {
|
||||||
|
$missing[] = $char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$width_index = $width_map[$index_map[$char]];
|
||||||
|
$width += $widths[$width_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode hexadecimal encoded string. If $add_braces is true result value would be wrapped by parentheses.
|
||||||
|
*/
|
||||||
|
public static function decodeHexadecimal(string $hexa, bool $add_braces = false): string
|
||||||
|
{
|
||||||
|
// Special shortcut for XML content.
|
||||||
|
if (false !== stripos($hexa, '<?xml')) {
|
||||||
|
return $hexa;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = '';
|
||||||
|
$parts = preg_split('/(<[a-f0-9\s]+>)/si', $hexa, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (preg_match('/^<[a-f0-9\s]+>$/si', $part)) {
|
||||||
|
// strip whitespace
|
||||||
|
$part = preg_replace("/\s/", '', $part);
|
||||||
|
$part = trim($part, '<>');
|
||||||
|
if ($add_braces) {
|
||||||
|
$text .= '(';
|
||||||
|
}
|
||||||
|
|
||||||
|
$part = pack('H*', $part);
|
||||||
|
$text .= ($add_braces ? preg_replace('/\\\/s', '\\\\\\', $part) : $part);
|
||||||
|
|
||||||
|
if ($add_braces) {
|
||||||
|
$text .= ')';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$text .= $part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode string with octal-decoded chunks.
|
||||||
|
*/
|
||||||
|
public static function decodeOctal(string $text): string
|
||||||
|
{
|
||||||
|
// Replace all double backslashes \\ with a special string
|
||||||
|
$text = strtr($text, ['\\\\' => '[**pdfparserdblslsh**]']);
|
||||||
|
|
||||||
|
// Now we can replace all octal codes without worrying about
|
||||||
|
// escaped backslashes
|
||||||
|
$text = preg_replace_callback('/\\\\([0-7]{1,3})/', function ($m) {
|
||||||
|
return \chr(octdec($m[1]));
|
||||||
|
}, $text);
|
||||||
|
|
||||||
|
// Unescape any parentheses
|
||||||
|
$text = str_replace(['\\(', '\\)'], ['(', ')'], $text);
|
||||||
|
|
||||||
|
// Replace instances of the special string with a single backslash
|
||||||
|
return str_replace('[**pdfparserdblslsh**]', '\\', $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode string with html entity encoded chars.
|
||||||
|
*/
|
||||||
|
public static function decodeEntities(string $text): string
|
||||||
|
{
|
||||||
|
return preg_replace_callback('/#([0-9a-f]{2})/i', function ($m) {
|
||||||
|
return \chr(hexdec($m[1]));
|
||||||
|
}, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if given string is Unicode text (by BOM);
|
||||||
|
* If true - decode to "utf-8" encoded string.
|
||||||
|
* Otherwise - return text as is.
|
||||||
|
*
|
||||||
|
* @todo Rename in next major release to make the name correspond to reality (for ex. decodeIfUnicode())
|
||||||
|
*/
|
||||||
|
public static function decodeUnicode(string $text): string
|
||||||
|
{
|
||||||
|
if ("\xFE\xFF" === substr($text, 0, 2)) {
|
||||||
|
// Strip U+FEFF byte order marker.
|
||||||
|
$decode = substr($text, 2);
|
||||||
|
$text = '';
|
||||||
|
$length = \strlen($decode);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; $i += 2) {
|
||||||
|
$text .= self::uchr(hexdec(bin2hex(substr($decode, $i, 2))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Deprecated, use $this->config->getFontSpaceLimit() instead.
|
||||||
|
*/
|
||||||
|
protected function getFontSpaceLimit(): int
|
||||||
|
{
|
||||||
|
return $this->config->getFontSpaceLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode text by commands array.
|
||||||
|
*/
|
||||||
|
public function decodeText(array $commands, float $fontFactor = 4): string
|
||||||
|
{
|
||||||
|
$word_position = 0;
|
||||||
|
$words = [];
|
||||||
|
$font_space = $this->getFontSpaceLimit() * abs($fontFactor) / 4;
|
||||||
|
|
||||||
|
foreach ($commands as $command) {
|
||||||
|
switch ($command[PDFObject::TYPE]) {
|
||||||
|
case 'n':
|
||||||
|
$offset = (float) trim($command[PDFObject::COMMAND]);
|
||||||
|
if ($offset - (float) $font_space < 0) {
|
||||||
|
$word_position = \count($words);
|
||||||
|
}
|
||||||
|
continue 2;
|
||||||
|
case '<':
|
||||||
|
// Decode hexadecimal.
|
||||||
|
$text = self::decodeHexadecimal('<'.$command[PDFObject::COMMAND].'>');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Decode octal (if necessary).
|
||||||
|
$text = self::decodeOctal($command[PDFObject::COMMAND]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace escaped chars
|
||||||
|
$text = str_replace(
|
||||||
|
['\\\\', '\(', '\)', '\n', '\r', '\t', '\f', '\ ', '\b'],
|
||||||
|
[\chr(92), \chr(40), \chr(41), \chr(10), \chr(13), \chr(9), \chr(12), \chr(32), \chr(8)],
|
||||||
|
$text
|
||||||
|
);
|
||||||
|
|
||||||
|
// add content to result string
|
||||||
|
if (isset($words[$word_position])) {
|
||||||
|
$words[$word_position] .= $text;
|
||||||
|
} else {
|
||||||
|
$words[$word_position] = $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($words as &$word) {
|
||||||
|
$word = $this->decodeContent($word);
|
||||||
|
$word = str_replace("\t", ' ', $word);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove internal "words" that are just spaces, but leave them
|
||||||
|
// if they are at either end of the array of words. This fixes,
|
||||||
|
// for example, lines that are justified to fill
|
||||||
|
// a whole row.
|
||||||
|
for ($x = \count($words) - 2; $x >= 1; --$x) {
|
||||||
|
if ('' === trim($words[$x], ' ')) {
|
||||||
|
unset($words[$x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$words = array_values($words);
|
||||||
|
|
||||||
|
// Cut down on the number of unnecessary internal spaces by
|
||||||
|
// imploding the string on the null byte, and checking if the
|
||||||
|
// text includes extra spaces on either side. If so, merge
|
||||||
|
// where appropriate.
|
||||||
|
$words = implode("\x00\x00", $words);
|
||||||
|
$words = str_replace(
|
||||||
|
[" \x00\x00 ", "\x00\x00 ", " \x00\x00", "\x00\x00"],
|
||||||
|
[' ', ' ', ' ', ' '],
|
||||||
|
$words
|
||||||
|
);
|
||||||
|
|
||||||
|
return $words;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode given $text to "utf-8" encoded string.
|
||||||
|
*
|
||||||
|
* @param bool $unicode This parameter is deprecated and might be removed in a future release
|
||||||
|
*/
|
||||||
|
public function decodeContent(string $text, ?bool &$unicode = null): string
|
||||||
|
{
|
||||||
|
// If this string begins with a UTF-16BE BOM, then decode it
|
||||||
|
// directly as Unicode
|
||||||
|
if ("\xFE\xFF" === substr($text, 0, 2)) {
|
||||||
|
return $this->decodeUnicode($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->has('ToUnicode')) {
|
||||||
|
return $this->decodeContentByToUnicodeCMapOrDescendantFonts($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->has('Encoding')) {
|
||||||
|
$result = $this->decodeContentByEncoding($text);
|
||||||
|
|
||||||
|
if (null !== $result) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->decodeContentByAutodetectIfNecessary($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First try to decode $text by ToUnicode CMap.
|
||||||
|
* If char translation not found in ToUnicode CMap tries:
|
||||||
|
* - If DescendantFonts exists tries to decode char by one of that fonts.
|
||||||
|
* - If have no success to decode by DescendantFonts interpret $text as a string with "Windows-1252" encoding.
|
||||||
|
* - If DescendantFonts does not exist just return "?" as decoded char.
|
||||||
|
*
|
||||||
|
* @todo Seems this is invalid algorithm that do not follow pdf-format specification. Must be rewritten.
|
||||||
|
*/
|
||||||
|
private function decodeContentByToUnicodeCMapOrDescendantFonts(string $text): string
|
||||||
|
{
|
||||||
|
$bytes = $this->tableSizes['from'];
|
||||||
|
|
||||||
|
if ($bytes) {
|
||||||
|
$result = '';
|
||||||
|
$length = \strlen($text);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; $i += $bytes) {
|
||||||
|
$char = substr($text, $i, $bytes);
|
||||||
|
|
||||||
|
if (false !== ($decoded = $this->translateChar($char, false))) {
|
||||||
|
$char = $decoded;
|
||||||
|
} elseif ($this->has('DescendantFonts')) {
|
||||||
|
if ($this->get('DescendantFonts') instanceof PDFObject) {
|
||||||
|
$fonts = $this->get('DescendantFonts')->getHeader()->getElements();
|
||||||
|
} else {
|
||||||
|
$fonts = $this->get('DescendantFonts')->getContent();
|
||||||
|
}
|
||||||
|
$decoded = false;
|
||||||
|
|
||||||
|
foreach ($fonts as $font) {
|
||||||
|
if ($font instanceof self) {
|
||||||
|
if (false !== ($decoded = $font->translateChar($char, false))) {
|
||||||
|
$decoded = mb_convert_encoding($decoded, 'UTF-8', 'Windows-1252');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false !== $decoded) {
|
||||||
|
$char = $decoded;
|
||||||
|
} else {
|
||||||
|
$char = mb_convert_encoding($char, 'UTF-8', 'Windows-1252');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$char = self::MISSING;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result .= $char;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode content by any type of Encoding (dictionary's item) instance.
|
||||||
|
*/
|
||||||
|
private function decodeContentByEncoding(string $text): ?string
|
||||||
|
{
|
||||||
|
$encoding = $this->get('Encoding');
|
||||||
|
|
||||||
|
// When Encoding referenced by object id (/Encoding 520 0 R) but object itself does not contain `/Type /Encoding` in it's dictionary.
|
||||||
|
if ($encoding instanceof PDFObject) {
|
||||||
|
$encoding = $this->getInitializedEncodingByPdfObject($encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When Encoding referenced by object id (/Encoding 520 0 R) but object itself contains `/Type /Encoding` in it's dictionary.
|
||||||
|
if ($encoding instanceof Encoding) {
|
||||||
|
return $this->decodeContentByEncodingEncoding($text, $encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When Encoding is just string (/Encoding /WinAnsiEncoding)
|
||||||
|
if ($encoding instanceof Element) { // todo: ElementString class must by used?
|
||||||
|
return $this->decodeContentByEncodingElement($text, $encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't double-encode strings already in UTF-8
|
||||||
|
if (!mb_check_encoding($text, 'UTF-8')) {
|
||||||
|
return mb_convert_encoding($text, 'UTF-8', 'Windows-1252');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns already created or create a new one if not created before Encoding instance by PDFObject instance.
|
||||||
|
*/
|
||||||
|
private function getInitializedEncodingByPdfObject(PDFObject $PDFObject): Encoding
|
||||||
|
{
|
||||||
|
if (!$this->initializedEncodingByPdfObject) {
|
||||||
|
$this->initializedEncodingByPdfObject = $this->createInitializedEncodingByPdfObject($PDFObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->initializedEncodingByPdfObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode content when $encoding (given by $this->get('Encoding')) is instance of Encoding.
|
||||||
|
*/
|
||||||
|
private function decodeContentByEncodingEncoding(string $text, Encoding $encoding): string
|
||||||
|
{
|
||||||
|
$result = '';
|
||||||
|
$length = \strlen($text);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; ++$i) {
|
||||||
|
$dec_av = hexdec(bin2hex($text[$i]));
|
||||||
|
$dec_ap = $encoding->translateChar($dec_av);
|
||||||
|
$result .= self::uchr($dec_ap ?? $dec_av);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode content when $encoding (given by $this->get('Encoding')) is instance of Element.
|
||||||
|
*/
|
||||||
|
private function decodeContentByEncodingElement(string $text, Element $encoding): ?string
|
||||||
|
{
|
||||||
|
$pdfEncodingName = $encoding->getContent();
|
||||||
|
|
||||||
|
// mb_convert_encoding does not support MacRoman/macintosh,
|
||||||
|
// so we use iconv() here
|
||||||
|
$iconvEncodingName = $this->getIconvEncodingNameOrNullByPdfEncodingName($pdfEncodingName);
|
||||||
|
|
||||||
|
return $iconvEncodingName ? iconv($iconvEncodingName, 'UTF-8//TRANSLIT//IGNORE', $text) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert PDF encoding name to iconv-known encoding name.
|
||||||
|
*/
|
||||||
|
private function getIconvEncodingNameOrNullByPdfEncodingName(string $pdfEncodingName): ?string
|
||||||
|
{
|
||||||
|
$pdfToIconvEncodingNameMap = [
|
||||||
|
'StandardEncoding' => 'ISO-8859-1',
|
||||||
|
'MacRomanEncoding' => 'MACINTOSH',
|
||||||
|
'WinAnsiEncoding' => 'CP1252',
|
||||||
|
];
|
||||||
|
|
||||||
|
return \array_key_exists($pdfEncodingName, $pdfToIconvEncodingNameMap)
|
||||||
|
? $pdfToIconvEncodingNameMap[$pdfEncodingName]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If string seems like "utf-8" encoded string do nothing and just return given string as is.
|
||||||
|
* Otherwise, interpret string as "Window-1252" encoded string.
|
||||||
|
*
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
private function decodeContentByAutodetectIfNecessary(string $text)
|
||||||
|
{
|
||||||
|
if (mb_check_encoding($text, 'UTF-8')) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mb_convert_encoding($text, 'UTF-8', 'Windows-1252');
|
||||||
|
// todo: Why exactly `Windows-1252` used?
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Encoding instance by PDFObject instance and init it.
|
||||||
|
*/
|
||||||
|
private function createInitializedEncodingByPdfObject(PDFObject $PDFObject): Encoding
|
||||||
|
{
|
||||||
|
$encoding = $this->createEncodingByPdfObject($PDFObject);
|
||||||
|
$encoding->init();
|
||||||
|
|
||||||
|
return $encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Encoding instance by PDFObject instance (without init).
|
||||||
|
*/
|
||||||
|
private function createEncodingByPdfObject(PDFObject $PDFObject): Encoding
|
||||||
|
{
|
||||||
|
$document = $PDFObject->getDocument();
|
||||||
|
$header = $PDFObject->getHeader();
|
||||||
|
$content = $PDFObject->getContent();
|
||||||
|
$config = $PDFObject->getConfig();
|
||||||
|
|
||||||
|
return new Encoding($document, $header, $content, $config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FontCIDFontType0
|
||||||
|
*/
|
||||||
|
class FontCIDFontType0 extends Font
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FontCIDFontType2
|
||||||
|
*/
|
||||||
|
class FontCIDFontType2 extends Font
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FontTrueType
|
||||||
|
*/
|
||||||
|
class FontTrueType extends Font
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FontType0
|
||||||
|
*/
|
||||||
|
class FontType0 extends Font
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FontType1
|
||||||
|
*/
|
||||||
|
class FontType1 extends Font
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FontType3
|
||||||
|
*/
|
||||||
|
class FontType3 extends Font
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Element\ElementArray;
|
||||||
|
use Smalot\PdfParser\Element\ElementMissing;
|
||||||
|
use Smalot\PdfParser\Element\ElementStruct;
|
||||||
|
use Smalot\PdfParser\Element\ElementXRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Header
|
||||||
|
*/
|
||||||
|
class Header
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Document|null
|
||||||
|
*/
|
||||||
|
protected $document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Element[]
|
||||||
|
*/
|
||||||
|
protected $elements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Element[] $elements list of elements
|
||||||
|
* @param Document $document document
|
||||||
|
*/
|
||||||
|
public function __construct(array $elements = [], ?Document $document = null)
|
||||||
|
{
|
||||||
|
$this->elements = $elements;
|
||||||
|
$this->document = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
foreach ($this->elements as $element) {
|
||||||
|
if ($element instanceof Element) {
|
||||||
|
$element->init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all elements.
|
||||||
|
*/
|
||||||
|
public function getElements()
|
||||||
|
{
|
||||||
|
foreach ($this->elements as $name => $element) {
|
||||||
|
$this->resolveXRef($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used only for debug.
|
||||||
|
*/
|
||||||
|
public function getElementTypes(): array
|
||||||
|
{
|
||||||
|
$types = [];
|
||||||
|
|
||||||
|
foreach ($this->elements as $key => $element) {
|
||||||
|
$types[$key] = \get_class($element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetails(bool $deep = true): array
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
$elements = $this->getElements();
|
||||||
|
|
||||||
|
foreach ($elements as $key => $element) {
|
||||||
|
if ($element instanceof self && $deep) {
|
||||||
|
$values[$key] = $element->getDetails($deep);
|
||||||
|
} elseif ($element instanceof PDFObject && $deep) {
|
||||||
|
$values[$key] = $element->getDetails(false);
|
||||||
|
} elseif ($element instanceof ElementArray) {
|
||||||
|
if ($deep) {
|
||||||
|
$values[$key] = $element->getDetails();
|
||||||
|
}
|
||||||
|
} elseif ($element instanceof Element) {
|
||||||
|
$values[$key] = (string) $element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate if an element name is available in header.
|
||||||
|
*
|
||||||
|
* @param string $name the name of the element
|
||||||
|
*/
|
||||||
|
public function has(string $name): bool
|
||||||
|
{
|
||||||
|
return \array_key_exists($name, $this->elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Element|PDFObject
|
||||||
|
*/
|
||||||
|
public function get(string $name)
|
||||||
|
{
|
||||||
|
if (\array_key_exists($name, $this->elements) && $element = $this->resolveXRef($name)) {
|
||||||
|
return $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ElementMissing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve XRef to object.
|
||||||
|
*
|
||||||
|
* @return Element|PDFObject
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function resolveXRef(string $name)
|
||||||
|
{
|
||||||
|
if (($obj = $this->elements[$name]) instanceof ElementXRef && null !== $this->document) {
|
||||||
|
/** @var ElementXRef $obj */
|
||||||
|
$object = $this->document->getObjectById($obj->getId());
|
||||||
|
|
||||||
|
if (null === $object) {
|
||||||
|
return new ElementMissing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update elements list for future calls.
|
||||||
|
$this->elements[$name] = $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->elements[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $content The content to parse
|
||||||
|
* @param Document $document The document
|
||||||
|
* @param int $position The new position of the cursor after parsing
|
||||||
|
*/
|
||||||
|
public static function parse(string $content, Document $document, int &$position = 0): self
|
||||||
|
{
|
||||||
|
/* @var Header $header */
|
||||||
|
if ('<<' == substr(trim($content), 0, 2)) {
|
||||||
|
$header = ElementStruct::parse($content, $document, $position);
|
||||||
|
} else {
|
||||||
|
$elements = ElementArray::parse($content, $document, $position);
|
||||||
|
$header = new self([], $document);
|
||||||
|
|
||||||
|
if ($elements) {
|
||||||
|
$header = new self($elements->getRawContent(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($header) {
|
||||||
|
return $header;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build an empty header.
|
||||||
|
return new self([], $document);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+1014
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Element\ElementArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Pages
|
||||||
|
*/
|
||||||
|
class Pages extends PDFObject
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<\Smalot\PdfParser\Font>|null
|
||||||
|
*/
|
||||||
|
protected $fonts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Objects other than Pages or Page might need to be treated specifically
|
||||||
|
* in order to get Page objects out of them.
|
||||||
|
*
|
||||||
|
* @see https://github.com/smalot/pdfparser/issues/331
|
||||||
|
*/
|
||||||
|
public function getPages(bool $deep = false): array
|
||||||
|
{
|
||||||
|
if (!$this->has('Kids')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ElementArray $kidsElement */
|
||||||
|
$kidsElement = $this->get('Kids');
|
||||||
|
|
||||||
|
if (!$deep) {
|
||||||
|
return $kidsElement->getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to apply the Pages' object's fonts to each page
|
||||||
|
if (false === \is_array($this->fonts)) {
|
||||||
|
$this->setupFonts();
|
||||||
|
}
|
||||||
|
$fontsAvailable = 0 < \count($this->fonts);
|
||||||
|
|
||||||
|
$kids = $kidsElement->getContent();
|
||||||
|
$pages = [];
|
||||||
|
|
||||||
|
foreach ($kids as $kid) {
|
||||||
|
if ($kid instanceof self) {
|
||||||
|
$pages = array_merge($pages, $kid->getPages(true));
|
||||||
|
} elseif ($kid instanceof Page) {
|
||||||
|
if ($fontsAvailable) {
|
||||||
|
$kid->setFonts($this->fonts);
|
||||||
|
}
|
||||||
|
$pages[] = $kid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers information about fonts and collects them in a list.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected function setupFonts()
|
||||||
|
{
|
||||||
|
$resources = $this->get('Resources');
|
||||||
|
|
||||||
|
if (method_exists($resources, 'has') && $resources->has('Font')) {
|
||||||
|
// no fonts available, therefore stop here
|
||||||
|
if ($resources->get('Font') instanceof Element\ElementMissing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resources->get('Font') instanceof Header) {
|
||||||
|
$fonts = $resources->get('Font')->getElements();
|
||||||
|
} else {
|
||||||
|
$fonts = $resources->get('Font')->getHeader()->getElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = [];
|
||||||
|
|
||||||
|
foreach ($fonts as $id => $font) {
|
||||||
|
if ($font instanceof Font) {
|
||||||
|
$table[$id] = $font;
|
||||||
|
|
||||||
|
// Store too on cleaned id value (only numeric)
|
||||||
|
$id = preg_replace('/[^0-9\.\-_]/', '', $id);
|
||||||
|
if ('' != $id) {
|
||||||
|
$table[$id] = $font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fonts = $table;
|
||||||
|
} else {
|
||||||
|
$this->fonts = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Element\ElementArray;
|
||||||
|
use Smalot\PdfParser\Element\ElementBoolean;
|
||||||
|
use Smalot\PdfParser\Element\ElementDate;
|
||||||
|
use Smalot\PdfParser\Element\ElementHexa;
|
||||||
|
use Smalot\PdfParser\Element\ElementName;
|
||||||
|
use Smalot\PdfParser\Element\ElementNull;
|
||||||
|
use Smalot\PdfParser\Element\ElementNumeric;
|
||||||
|
use Smalot\PdfParser\Element\ElementString;
|
||||||
|
use Smalot\PdfParser\Element\ElementXRef;
|
||||||
|
use Smalot\PdfParser\RawData\RawDataParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Parser
|
||||||
|
*/
|
||||||
|
class Parser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Config
|
||||||
|
*/
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PDFObject[]
|
||||||
|
*/
|
||||||
|
protected $objects = [];
|
||||||
|
|
||||||
|
protected $rawDataParser;
|
||||||
|
|
||||||
|
public function __construct($cfg = [], ?Config $config = null)
|
||||||
|
{
|
||||||
|
$this->config = $config ?: new Config();
|
||||||
|
$this->rawDataParser = new RawDataParser($cfg, $this->config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfig(): Config
|
||||||
|
{
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function parseFile(string $filename): Document
|
||||||
|
{
|
||||||
|
$content = file_get_contents($filename);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 2018/06/20 @doganoo as multiple times a
|
||||||
|
* users have complained that the parseFile()
|
||||||
|
* method dies silently, it is an better option
|
||||||
|
* to remove the error control operator (@) and
|
||||||
|
* let the users know that the method throws an exception
|
||||||
|
* by adding @throws tag to PHPDoc.
|
||||||
|
*
|
||||||
|
* See here for an example: https://github.com/smalot/pdfparser/issues/204
|
||||||
|
*/
|
||||||
|
return $this->parseContent($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $content PDF content to parse
|
||||||
|
*
|
||||||
|
* @throws \Exception if secured PDF file was detected
|
||||||
|
* @throws \Exception if no object list was found
|
||||||
|
*/
|
||||||
|
public function parseContent(string $content): Document
|
||||||
|
{
|
||||||
|
// Create structure from raw data.
|
||||||
|
list($xref, $data) = $this->rawDataParser->parseData($content);
|
||||||
|
|
||||||
|
if (isset($xref['trailer']['encrypt']) && false === $this->config->getIgnoreEncryption()) {
|
||||||
|
throw new \Exception('Secured pdf file are currently not supported.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
throw new \Exception('Object list not found. Possible secured file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create destination object.
|
||||||
|
$document = new Document();
|
||||||
|
$this->objects = [];
|
||||||
|
|
||||||
|
foreach ($data as $id => $structure) {
|
||||||
|
$this->parseObject($id, $structure, $document);
|
||||||
|
unset($data[$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->setTrailer($this->parseTrailer($xref['trailer'], $document));
|
||||||
|
$document->setObjects($this->objects);
|
||||||
|
|
||||||
|
return $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseTrailer(array $structure, ?Document $document)
|
||||||
|
{
|
||||||
|
$trailer = [];
|
||||||
|
|
||||||
|
foreach ($structure as $name => $values) {
|
||||||
|
$name = ucfirst($name);
|
||||||
|
|
||||||
|
if (is_numeric($values)) {
|
||||||
|
$trailer[$name] = new ElementNumeric($values);
|
||||||
|
} elseif (\is_array($values)) {
|
||||||
|
$value = $this->parseTrailer($values, null);
|
||||||
|
$trailer[$name] = new ElementArray($value, null);
|
||||||
|
} elseif (false !== strpos($values, '_')) {
|
||||||
|
$trailer[$name] = new ElementXRef($values, $document);
|
||||||
|
} else {
|
||||||
|
$trailer[$name] = $this->parseHeaderElement('(', $values, $document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Header($trailer, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseObject(string $id, array $structure, ?Document $document)
|
||||||
|
{
|
||||||
|
$header = new Header([], $document);
|
||||||
|
$content = '';
|
||||||
|
|
||||||
|
foreach ($structure as $position => $part) {
|
||||||
|
if (\is_int($part)) {
|
||||||
|
$part = [null, null];
|
||||||
|
}
|
||||||
|
switch ($part[0]) {
|
||||||
|
case '[':
|
||||||
|
$elements = [];
|
||||||
|
|
||||||
|
foreach ($part[1] as $sub_element) {
|
||||||
|
$sub_type = $sub_element[0];
|
||||||
|
$sub_value = $sub_element[1];
|
||||||
|
$elements[] = $this->parseHeaderElement($sub_type, $sub_value, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = new Header($elements, $document);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '<<':
|
||||||
|
$header = $this->parseHeader($part[1], $document);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stream':
|
||||||
|
$content = isset($part[3][0]) ? $part[3][0] : $part[1];
|
||||||
|
|
||||||
|
if ($header->get('Type')->equals('ObjStm')) {
|
||||||
|
$match = [];
|
||||||
|
|
||||||
|
// Split xrefs and contents.
|
||||||
|
preg_match('/^((\d+\s+\d+\s*)*)(.*)$/s', $content, $match);
|
||||||
|
$content = $match[3];
|
||||||
|
|
||||||
|
// Extract xrefs.
|
||||||
|
$xrefs = preg_split(
|
||||||
|
'/(\d+\s+\d+\s*)/s',
|
||||||
|
$match[1],
|
||||||
|
-1,
|
||||||
|
\PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE
|
||||||
|
);
|
||||||
|
$table = [];
|
||||||
|
|
||||||
|
foreach ($xrefs as $xref) {
|
||||||
|
list($id, $position) = preg_split("/\s+/", trim($xref));
|
||||||
|
$table[$position] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($table);
|
||||||
|
|
||||||
|
$ids = array_values($table);
|
||||||
|
$positions = array_keys($table);
|
||||||
|
|
||||||
|
foreach ($positions as $index => $position) {
|
||||||
|
$id = $ids[$index].'_0';
|
||||||
|
$next_position = isset($positions[$index + 1]) ? $positions[$index + 1] : \strlen($content);
|
||||||
|
$sub_content = substr($content, $position, (int) $next_position - (int) $position);
|
||||||
|
|
||||||
|
$sub_header = Header::parse($sub_content, $document);
|
||||||
|
$object = PDFObject::factory($document, $sub_header, '', $this->config);
|
||||||
|
$this->objects[$id] = $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is not necessary to store this content.
|
||||||
|
|
||||||
|
return;
|
||||||
|
} elseif ($header->get('Type')->equals('Metadata')) {
|
||||||
|
// Attempt to parse XMP XML Metadata
|
||||||
|
$document->extractXMPMetadata($content);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if ('null' != $part) {
|
||||||
|
$element = $this->parseHeaderElement($part[0], $part[1], $document);
|
||||||
|
|
||||||
|
if ($element) {
|
||||||
|
$header = new Header([$element], $document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->objects[$id])) {
|
||||||
|
$this->objects[$id] = PDFObject::factory($document, $header, $content, $this->config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function parseHeader(array $structure, ?Document $document): Header
|
||||||
|
{
|
||||||
|
$elements = [];
|
||||||
|
$count = \count($structure);
|
||||||
|
|
||||||
|
for ($position = 0; $position < $count; $position += 2) {
|
||||||
|
$name = $structure[$position][1];
|
||||||
|
$type = $structure[$position + 1][0];
|
||||||
|
$value = $structure[$position + 1][1];
|
||||||
|
|
||||||
|
$elements[$name] = $this->parseHeaderElement($type, $value, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Header($elements, $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|array $value
|
||||||
|
*
|
||||||
|
* @return Element|Header|null
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function parseHeaderElement(?string $type, $value, ?Document $document)
|
||||||
|
{
|
||||||
|
$valueIsEmpty = null == $value || '' == $value || false == $value;
|
||||||
|
if (('<<' === $type || '>>' === $type) && $valueIsEmpty) {
|
||||||
|
$value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case '<<':
|
||||||
|
case '>>':
|
||||||
|
$header = $this->parseHeader($value, $document);
|
||||||
|
PDFObject::factory($document, $header, null, $this->config);
|
||||||
|
|
||||||
|
return $header;
|
||||||
|
|
||||||
|
case 'numeric':
|
||||||
|
return new ElementNumeric($value);
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
return new ElementBoolean($value);
|
||||||
|
|
||||||
|
case 'null':
|
||||||
|
return new ElementNull();
|
||||||
|
|
||||||
|
case '(':
|
||||||
|
if ($date = ElementDate::parse('('.$value.')', $document)) {
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ElementString::parse('('.$value.')', $document);
|
||||||
|
|
||||||
|
case '<':
|
||||||
|
return $this->parseHeaderElement('(', ElementHexa::decode($value), $document);
|
||||||
|
|
||||||
|
case '/':
|
||||||
|
return ElementName::parse('/'.$value, $document);
|
||||||
|
|
||||||
|
case 'ojbref': // old mistake in tcpdf parser
|
||||||
|
case 'objref':
|
||||||
|
return new ElementXRef($value, $document);
|
||||||
|
|
||||||
|
case '[':
|
||||||
|
$values = [];
|
||||||
|
|
||||||
|
if (\is_array($value)) {
|
||||||
|
foreach ($value as $sub_element) {
|
||||||
|
$sub_type = $sub_element[0];
|
||||||
|
$sub_value = $sub_element[1];
|
||||||
|
$values[] = $this->parseHeaderElement($sub_type, $sub_value, $document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ElementArray($values, $document);
|
||||||
|
|
||||||
|
case 'endstream':
|
||||||
|
case 'obj': // I don't know what it means but got my project fixed.
|
||||||
|
case '':
|
||||||
|
// Nothing to do with.
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new \Exception('Invalid type: "'.$type.'".');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is based on code of tecnickcom/TCPDF PDF library.
|
||||||
|
*
|
||||||
|
* Original author Nicola Asuni (info@tecnick.com) and
|
||||||
|
* contributors (https://github.com/tecnickcom/TCPDF/graphs/contributors).
|
||||||
|
*
|
||||||
|
* @see https://github.com/tecnickcom/TCPDF
|
||||||
|
*
|
||||||
|
* Original code was licensed on the terms of the LGPL v3.
|
||||||
|
*
|
||||||
|
* ------------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* @file This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Konrad Abicht <k.abicht@gmail.com>
|
||||||
|
*
|
||||||
|
* @date 2020-01-06
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\RawData;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Exception\NotImplementedException;
|
||||||
|
|
||||||
|
class FilterHelper
|
||||||
|
{
|
||||||
|
protected $availableFilters = ['ASCIIHexDecode', 'ASCII85Decode', 'LZWDecode', 'FlateDecode', 'RunLengthDecode'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode data using the specified filter type.
|
||||||
|
*
|
||||||
|
* @param string $filter Filter name
|
||||||
|
* @param string $data Data to decode
|
||||||
|
*
|
||||||
|
* @return string Decoded data string
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
* @throws \Smalot\PdfParser\Exception\NotImplementedException if a certain decode function is not implemented yet
|
||||||
|
*/
|
||||||
|
public function decodeFilter(string $filter, string $data, int $decodeMemoryLimit = 0): string
|
||||||
|
{
|
||||||
|
switch ($filter) {
|
||||||
|
case 'ASCIIHexDecode':
|
||||||
|
return $this->decodeFilterASCIIHexDecode($data);
|
||||||
|
|
||||||
|
case 'ASCII85Decode':
|
||||||
|
return $this->decodeFilterASCII85Decode($data);
|
||||||
|
|
||||||
|
case 'LZWDecode':
|
||||||
|
return $this->decodeFilterLZWDecode($data);
|
||||||
|
|
||||||
|
case 'FlateDecode':
|
||||||
|
return $this->decodeFilterFlateDecode($data, $decodeMemoryLimit);
|
||||||
|
|
||||||
|
case 'RunLengthDecode':
|
||||||
|
return $this->decodeFilterRunLengthDecode($data);
|
||||||
|
|
||||||
|
case 'CCITTFaxDecode':
|
||||||
|
throw new NotImplementedException('Decode CCITTFaxDecode not implemented yet.');
|
||||||
|
case 'JBIG2Decode':
|
||||||
|
throw new NotImplementedException('Decode JBIG2Decode not implemented yet.');
|
||||||
|
case 'DCTDecode':
|
||||||
|
throw new NotImplementedException('Decode DCTDecode not implemented yet.');
|
||||||
|
case 'JPXDecode':
|
||||||
|
throw new NotImplementedException('Decode JPXDecode not implemented yet.');
|
||||||
|
case 'Crypt':
|
||||||
|
throw new NotImplementedException('Decode Crypt not implemented yet.');
|
||||||
|
default:
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASCIIHexDecode
|
||||||
|
*
|
||||||
|
* Decodes data encoded in an ASCII hexadecimal representation, reproducing the original binary data.
|
||||||
|
*
|
||||||
|
* @param string $data Data to decode
|
||||||
|
*
|
||||||
|
* @return string data string
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function decodeFilterASCIIHexDecode(string $data): string
|
||||||
|
{
|
||||||
|
// all white-space characters shall be ignored
|
||||||
|
$data = preg_replace('/[\s]/', '', $data);
|
||||||
|
// check for EOD character: GREATER-THAN SIGN (3Eh)
|
||||||
|
$eod = strpos($data, '>');
|
||||||
|
if (false !== $eod) {
|
||||||
|
// remove EOD and extra data (if any)
|
||||||
|
$data = substr($data, 0, $eod);
|
||||||
|
$eod = true;
|
||||||
|
}
|
||||||
|
// get data length
|
||||||
|
$data_length = \strlen($data);
|
||||||
|
if (0 != ($data_length % 2)) {
|
||||||
|
// odd number of hexadecimal digits
|
||||||
|
if ($eod) {
|
||||||
|
// EOD shall behave as if a 0 (zero) followed the last digit
|
||||||
|
$data = substr($data, 0, -1).'0'.substr($data, -1);
|
||||||
|
} else {
|
||||||
|
throw new \Exception('decodeFilterASCIIHexDecode: invalid code');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check for invalid characters
|
||||||
|
if (preg_match('/[^a-fA-F\d]/', $data) > 0) {
|
||||||
|
throw new \Exception('decodeFilterASCIIHexDecode: invalid code');
|
||||||
|
}
|
||||||
|
// get one byte of binary data for each pair of ASCII hexadecimal digits
|
||||||
|
$decoded = pack('H*', $data);
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASCII85Decode
|
||||||
|
*
|
||||||
|
* Decodes data encoded in an ASCII base-85 representation, reproducing the original binary data.
|
||||||
|
*
|
||||||
|
* @param string $data Data to decode
|
||||||
|
*
|
||||||
|
* @return string data string
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function decodeFilterASCII85Decode(string $data): string
|
||||||
|
{
|
||||||
|
// initialize string to return
|
||||||
|
$decoded = '';
|
||||||
|
// all white-space characters shall be ignored
|
||||||
|
$data = preg_replace('/[\s]/', '', $data);
|
||||||
|
// remove start sequence 2-character sequence <~ (3Ch)(7Eh)
|
||||||
|
if (0 === strpos($data, '<~')) {
|
||||||
|
// remove EOD and extra data (if any)
|
||||||
|
$data = substr($data, 2);
|
||||||
|
}
|
||||||
|
// check for EOD: 2-character sequence ~> (7Eh)(3Eh)
|
||||||
|
$eod = strpos($data, '~>');
|
||||||
|
if (\strlen($data) - 2 === $eod) {
|
||||||
|
// remove EOD and extra data (if any)
|
||||||
|
$data = substr($data, 0, $eod);
|
||||||
|
}
|
||||||
|
// data length
|
||||||
|
$data_length = \strlen($data);
|
||||||
|
// check for invalid characters
|
||||||
|
if (preg_match('/[^\x21-\x75,\x74]/', $data) > 0) {
|
||||||
|
throw new \Exception('decodeFilterASCII85Decode: invalid code');
|
||||||
|
}
|
||||||
|
// z sequence
|
||||||
|
$zseq = \chr(0).\chr(0).\chr(0).\chr(0);
|
||||||
|
// position inside a group of 4 bytes (0-3)
|
||||||
|
$group_pos = 0;
|
||||||
|
$tuple = 0;
|
||||||
|
$pow85 = [85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1];
|
||||||
|
|
||||||
|
// for each byte
|
||||||
|
for ($i = 0; $i < $data_length; ++$i) {
|
||||||
|
// get char value
|
||||||
|
$char = \ord($data[$i]);
|
||||||
|
if (122 == $char) { // 'z'
|
||||||
|
if (0 == $group_pos) {
|
||||||
|
$decoded .= $zseq;
|
||||||
|
} else {
|
||||||
|
throw new \Exception('decodeFilterASCII85Decode: invalid code');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the value represented by a group of 5 characters should never be greater than 2^32 - 1
|
||||||
|
$tuple += (($char - 33) * $pow85[$group_pos]);
|
||||||
|
if (4 == $group_pos) {
|
||||||
|
// The following if-clauses are an attempt to fix/suppress the following deprecation warning:
|
||||||
|
// chr(): Providing a value not in-between 0 and 255 is deprecated, this is because a byte value
|
||||||
|
// must be in the [0, 255] interval. The value used will be constrained using % 256
|
||||||
|
// I know this is ugly and there might be more fancier ways. If you know one, feel free to provide a pull request.
|
||||||
|
if (255 < $tuple >> 8) {
|
||||||
|
$chr8Part = \chr(($tuple >> 8) % 256);
|
||||||
|
} else {
|
||||||
|
$chr8Part = \chr($tuple >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (255 < $tuple >> 16) {
|
||||||
|
$chr16Part = \chr(($tuple >> 16) % 256);
|
||||||
|
} else {
|
||||||
|
$chr16Part = \chr($tuple >> 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (255 < $tuple >> 24) {
|
||||||
|
$chr24Part = \chr(($tuple >> 24) % 256);
|
||||||
|
} else {
|
||||||
|
$chr24Part = \chr(($tuple >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (255 < $tuple) {
|
||||||
|
$chrTuple = \chr($tuple % 256);
|
||||||
|
} else {
|
||||||
|
$chrTuple = \chr($tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded .= $chr24Part . $chr16Part . $chr8Part . $chrTuple;
|
||||||
|
$tuple = 0;
|
||||||
|
$group_pos = 0;
|
||||||
|
} else {
|
||||||
|
++$group_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($group_pos > 1) {
|
||||||
|
$tuple += $pow85[$group_pos - 1];
|
||||||
|
}
|
||||||
|
// last tuple (if any)
|
||||||
|
switch ($group_pos) {
|
||||||
|
case 4:
|
||||||
|
$decoded .= \chr(($tuple >> 24) & 0xFF).\chr(($tuple >> 16) & 0xFF).\chr(($tuple >> 8) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
$decoded .= \chr(($tuple >> 24) & 0xFF).\chr(($tuple >> 16) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
$decoded .= \chr(($tuple >> 24) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
throw new \Exception('decodeFilterASCII85Decode: invalid code');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlateDecode
|
||||||
|
*
|
||||||
|
* Decompresses data encoded using the zlib/deflate compression method, reproducing the original text or binary data.
|
||||||
|
*
|
||||||
|
* @param string $data Data to decode
|
||||||
|
* @param int $decodeMemoryLimit Memory limit on deflation
|
||||||
|
*
|
||||||
|
* @return string data string
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function decodeFilterFlateDecode(string $data, int $decodeMemoryLimit): ?string
|
||||||
|
{
|
||||||
|
// Uncatchable E_WARNING for "data error" is @ suppressed
|
||||||
|
// so execution may proceed with an alternate decompression
|
||||||
|
// method.
|
||||||
|
$decoded = @gzuncompress($data, $decodeMemoryLimit);
|
||||||
|
|
||||||
|
if (false === $decoded) {
|
||||||
|
// If gzuncompress() failed, try again using the compress.zlib://
|
||||||
|
// wrapper to decode it in a file-based context.
|
||||||
|
// See: https://www.php.net/manual/en/function.gzuncompress.php#79042
|
||||||
|
// Issue: https://github.com/smalot/pdfparser/issues/592
|
||||||
|
$ztmp = tmpfile();
|
||||||
|
if (false != $ztmp) {
|
||||||
|
fwrite($ztmp, "\x1f\x8b\x08\x00\x00\x00\x00\x00".$data);
|
||||||
|
$file = stream_get_meta_data($ztmp)['uri'];
|
||||||
|
if (0 === $decodeMemoryLimit) {
|
||||||
|
$decoded = file_get_contents('compress.zlib://'.$file);
|
||||||
|
} else {
|
||||||
|
$decoded = file_get_contents('compress.zlib://'.$file, false, null, 0, $decodeMemoryLimit);
|
||||||
|
}
|
||||||
|
fclose($ztmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === \is_string($decoded) || '' === $decoded) {
|
||||||
|
// If the decoded string is empty, that means decoding failed.
|
||||||
|
throw new \Exception('decodeFilterFlateDecode: invalid data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LZWDecode
|
||||||
|
*
|
||||||
|
* Decompresses data encoded using the LZW (Lempel-Ziv-Welch) adaptive compression method, reproducing the original text or binary data.
|
||||||
|
*
|
||||||
|
* @param string $data Data to decode
|
||||||
|
*
|
||||||
|
* @return string Data string
|
||||||
|
*/
|
||||||
|
protected function decodeFilterLZWDecode(string $data): string
|
||||||
|
{
|
||||||
|
// initialize string to return
|
||||||
|
$decoded = '';
|
||||||
|
// data length
|
||||||
|
$data_length = \strlen($data);
|
||||||
|
// convert string to binary string
|
||||||
|
$bitstring = '';
|
||||||
|
for ($i = 0; $i < $data_length; ++$i) {
|
||||||
|
$bitstring .= \sprintf('%08b', \ord($data[$i]));
|
||||||
|
}
|
||||||
|
// get the number of bits
|
||||||
|
$data_length = \strlen($bitstring);
|
||||||
|
// initialize code length in bits
|
||||||
|
$bitlen = 9;
|
||||||
|
// initialize dictionary index
|
||||||
|
$dix = 258;
|
||||||
|
// initialize the dictionary (with the first 256 entries).
|
||||||
|
$dictionary = [];
|
||||||
|
for ($i = 0; $i < 256; ++$i) {
|
||||||
|
$dictionary[$i] = \chr($i);
|
||||||
|
}
|
||||||
|
// previous val
|
||||||
|
$prev_index = 0;
|
||||||
|
// while we encounter EOD marker (257), read code_length bits
|
||||||
|
while (($data_length > 0) && (257 != ($index = bindec(substr($bitstring, 0, $bitlen))))) {
|
||||||
|
// remove read bits from string
|
||||||
|
$bitstring = substr($bitstring, $bitlen);
|
||||||
|
// update number of bits
|
||||||
|
$data_length -= $bitlen;
|
||||||
|
if (256 == $index) { // clear-table marker
|
||||||
|
// reset code length in bits
|
||||||
|
$bitlen = 9;
|
||||||
|
// reset dictionary index
|
||||||
|
$dix = 258;
|
||||||
|
$prev_index = 256;
|
||||||
|
// reset the dictionary (with the first 256 entries).
|
||||||
|
$dictionary = [];
|
||||||
|
for ($i = 0; $i < 256; ++$i) {
|
||||||
|
$dictionary[$i] = \chr($i);
|
||||||
|
}
|
||||||
|
} elseif (256 == $prev_index) {
|
||||||
|
// first entry
|
||||||
|
$decoded .= $dictionary[$index];
|
||||||
|
$prev_index = $index;
|
||||||
|
} else {
|
||||||
|
// check if index exist in the dictionary
|
||||||
|
if ($index < $dix) {
|
||||||
|
// index exist on dictionary
|
||||||
|
$decoded .= $dictionary[$index];
|
||||||
|
$dic_val = $dictionary[$prev_index].$dictionary[$index][0];
|
||||||
|
// store current index
|
||||||
|
$prev_index = $index;
|
||||||
|
} else {
|
||||||
|
// index do not exist on dictionary
|
||||||
|
$dic_val = $dictionary[$prev_index].$dictionary[$prev_index][0];
|
||||||
|
$decoded .= $dic_val;
|
||||||
|
}
|
||||||
|
// update dictionary
|
||||||
|
$dictionary[$dix] = $dic_val;
|
||||||
|
++$dix;
|
||||||
|
// change bit length by case
|
||||||
|
if (2047 == $dix) {
|
||||||
|
$bitlen = 12;
|
||||||
|
} elseif (1023 == $dix) {
|
||||||
|
$bitlen = 11;
|
||||||
|
} elseif (511 == $dix) {
|
||||||
|
$bitlen = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RunLengthDecode
|
||||||
|
*
|
||||||
|
* Decompresses data encoded using a byte-oriented run-length encoding algorithm.
|
||||||
|
*
|
||||||
|
* @param string $data Data to decode
|
||||||
|
*/
|
||||||
|
protected function decodeFilterRunLengthDecode(string $data): string
|
||||||
|
{
|
||||||
|
// initialize string to return
|
||||||
|
$decoded = '';
|
||||||
|
// data length
|
||||||
|
$data_length = \strlen($data);
|
||||||
|
$i = 0;
|
||||||
|
while ($i < $data_length) {
|
||||||
|
// get current byte value
|
||||||
|
$byte = \ord($data[$i]);
|
||||||
|
if (128 == $byte) {
|
||||||
|
// a length value of 128 denote EOD
|
||||||
|
break;
|
||||||
|
} elseif ($byte < 128) {
|
||||||
|
// if the length byte is in the range 0 to 127
|
||||||
|
// the following length + 1 (1 to 128) bytes shall be copied literally during decompression
|
||||||
|
$decoded .= substr($data, $i + 1, $byte + 1);
|
||||||
|
// move to next block
|
||||||
|
$i += ($byte + 2);
|
||||||
|
} else {
|
||||||
|
// if length is in the range 129 to 255,
|
||||||
|
// the following single byte shall be copied 257 - length (2 to 128) times during decompression
|
||||||
|
$decoded .= str_repeat($data[$i + 1], 257 - $byte);
|
||||||
|
// move to next block
|
||||||
|
$i += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array list of available filters
|
||||||
|
*/
|
||||||
|
public function getAvailableFilters(): array
|
||||||
|
{
|
||||||
|
return $this->availableFilters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,990 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is based on code of tecnickcom/TCPDF PDF library.
|
||||||
|
*
|
||||||
|
* Original author Nicola Asuni (info@tecnick.com) and
|
||||||
|
* contributors (https://github.com/tecnickcom/TCPDF/graphs/contributors).
|
||||||
|
*
|
||||||
|
* @see https://github.com/tecnickcom/TCPDF
|
||||||
|
*
|
||||||
|
* Original code was licensed on the terms of the LGPL v3.
|
||||||
|
*
|
||||||
|
* ------------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* @file This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Konrad Abicht <k.abicht@gmail.com>
|
||||||
|
*
|
||||||
|
* @date 2020-01-06
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\RawData;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Config;
|
||||||
|
use Smalot\PdfParser\Exception\EmptyPdfException;
|
||||||
|
use Smalot\PdfParser\Exception\MissingPdfHeaderException;
|
||||||
|
|
||||||
|
class RawDataParser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Config
|
||||||
|
*/
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration array.
|
||||||
|
*
|
||||||
|
* @var array<string,bool>
|
||||||
|
*/
|
||||||
|
protected $cfg = [
|
||||||
|
// if `true` ignore filter decoding errors
|
||||||
|
'ignore_filter_decoding_errors' => true,
|
||||||
|
// if `true` ignore missing filter decoding errors
|
||||||
|
'ignore_missing_filter_decoders' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $filterHelper;
|
||||||
|
protected $objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $cfg Configuration array, default is []
|
||||||
|
*/
|
||||||
|
public function __construct($cfg = [], ?Config $config = null)
|
||||||
|
{
|
||||||
|
// merge given array with default values
|
||||||
|
$this->cfg = array_merge($this->cfg, $cfg);
|
||||||
|
|
||||||
|
$this->filterHelper = new FilterHelper();
|
||||||
|
$this->config = $config ?: new Config();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the specified stream.
|
||||||
|
*
|
||||||
|
* @param string $pdfData PDF data
|
||||||
|
* @param array $sdic Stream's dictionary array
|
||||||
|
* @param string $stream Stream to decode
|
||||||
|
*
|
||||||
|
* @return array containing decoded stream data and remaining filters
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function decodeStream(string $pdfData, array $xref, array $sdic, string $stream): array
|
||||||
|
{
|
||||||
|
// get stream length and filters
|
||||||
|
$slength = \strlen($stream);
|
||||||
|
if ($slength <= 0) {
|
||||||
|
return ['', []];
|
||||||
|
}
|
||||||
|
$filters = [];
|
||||||
|
foreach ($sdic as $k => $v) {
|
||||||
|
if ('/' == $v[0]) {
|
||||||
|
if (('Length' == $v[1]) && (isset($sdic[$k + 1])) && ('numeric' == $sdic[$k + 1][0])) {
|
||||||
|
// get declared stream length
|
||||||
|
$declength = (int) $sdic[$k + 1][1];
|
||||||
|
if ($declength < $slength) {
|
||||||
|
$stream = substr($stream, 0, $declength);
|
||||||
|
$slength = $declength;
|
||||||
|
}
|
||||||
|
} elseif (('Filter' == $v[1]) && (isset($sdic[$k + 1]))) {
|
||||||
|
// resolve indirect object
|
||||||
|
$objval = $this->getObjectVal($pdfData, $xref, $sdic[$k + 1]);
|
||||||
|
if ('/' == $objval[0]) {
|
||||||
|
// single filter
|
||||||
|
$filters[] = $objval[1];
|
||||||
|
} elseif ('[' == $objval[0]) {
|
||||||
|
// array of filters
|
||||||
|
foreach ($objval[1] as $flt) {
|
||||||
|
if ('/' == $flt[0]) {
|
||||||
|
$filters[] = $flt[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode the stream
|
||||||
|
$remaining_filters = [];
|
||||||
|
foreach ($filters as $filter) {
|
||||||
|
if (\in_array($filter, $this->filterHelper->getAvailableFilters(), true)) {
|
||||||
|
try {
|
||||||
|
$stream = $this->filterHelper->decodeFilter($filter, $stream, $this->config->getDecodeMemoryLimit());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$emsg = $e->getMessage();
|
||||||
|
if ((('~' == $emsg[0]) && !$this->cfg['ignore_missing_filter_decoders'])
|
||||||
|
|| (('~' != $emsg[0]) && !$this->cfg['ignore_filter_decoding_errors'])
|
||||||
|
) {
|
||||||
|
throw new \Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// add missing filter to array
|
||||||
|
$remaining_filters[] = $filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$stream, $remaining_filters];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the Cross-Reference section
|
||||||
|
*
|
||||||
|
* @param string $pdfData PDF data
|
||||||
|
* @param int $startxref Offset at which the xref section starts (position of the 'xref' keyword)
|
||||||
|
* @param array $xref Previous xref array (if any)
|
||||||
|
* @param array<int> $visitedOffsets Array of visited offsets to prevent infinite loops
|
||||||
|
*
|
||||||
|
* @return array containing xref and trailer data
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function decodeXref(string $pdfData, int $startxref, array $xref = [], array $visitedOffsets = []): array
|
||||||
|
{
|
||||||
|
$startxref += 4; // 4 is the length of the word 'xref'
|
||||||
|
// skip initial white space chars
|
||||||
|
$offset = $startxref + strspn($pdfData, $this->config->getPdfWhitespaces(), $startxref);
|
||||||
|
// initialize object number
|
||||||
|
$obj_num = 0;
|
||||||
|
// search for cross-reference entries or subsection
|
||||||
|
while (preg_match('/([0-9]+)[\x20]([0-9]+)[\x20]?([nf]?)(\r\n|[\x20]?[\r\n])/', $pdfData, $matches, \PREG_OFFSET_CAPTURE, $offset) > 0) {
|
||||||
|
if ($matches[0][1] != $offset) {
|
||||||
|
// we are on another section
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$offset += \strlen($matches[0][0]);
|
||||||
|
if ('n' == $matches[3][0]) {
|
||||||
|
// create unique object index: [object number]_[generation number]
|
||||||
|
$index = $obj_num.'_'.(int) $matches[2][0];
|
||||||
|
// check if object already exist
|
||||||
|
if (!isset($xref['xref'][$index])) {
|
||||||
|
// store object offset position
|
||||||
|
$xref['xref'][$index] = (int) $matches[1][0];
|
||||||
|
}
|
||||||
|
++$obj_num;
|
||||||
|
} elseif ('f' == $matches[3][0]) {
|
||||||
|
++$obj_num;
|
||||||
|
} else {
|
||||||
|
// object number (index)
|
||||||
|
$obj_num = (int) $matches[1][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get trailer data
|
||||||
|
if (preg_match('/trailer[\s]*<<(.*)>>/isU', $pdfData, $matches, \PREG_OFFSET_CAPTURE, $offset) > 0) {
|
||||||
|
$trailer_data = $matches[1][0];
|
||||||
|
if (!isset($xref['trailer']) || empty($xref['trailer'])) {
|
||||||
|
// get only the last updated version
|
||||||
|
$xref['trailer'] = [];
|
||||||
|
// parse trailer_data
|
||||||
|
if (preg_match('/Size[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) {
|
||||||
|
$xref['trailer']['size'] = (int) $matches[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/Root[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
|
||||||
|
$xref['trailer']['root'] = (int) $matches[1].'_'.(int) $matches[2];
|
||||||
|
}
|
||||||
|
if (preg_match('/Encrypt[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
|
||||||
|
$xref['trailer']['encrypt'] = (int) $matches[1].'_'.(int) $matches[2];
|
||||||
|
}
|
||||||
|
if (preg_match('/Info[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
|
||||||
|
$xref['trailer']['info'] = (int) $matches[1].'_'.(int) $matches[2];
|
||||||
|
}
|
||||||
|
if (preg_match('/ID[\s]*[\[][\s]*[<]([^>]*)[>][\s]*[<]([^>]*)[>]/i', $trailer_data, $matches) > 0) {
|
||||||
|
$xref['trailer']['id'] = [];
|
||||||
|
$xref['trailer']['id'][0] = $matches[1];
|
||||||
|
$xref['trailer']['id'][1] = $matches[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preg_match('/Prev[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) {
|
||||||
|
$offset = (int) $matches[1];
|
||||||
|
if (0 != $offset) {
|
||||||
|
// get previous xref
|
||||||
|
$xref = $this->getXrefData($pdfData, $offset, $xref, $visitedOffsets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new \Exception('Unable to find trailer');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $xref;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the Cross-Reference Stream section
|
||||||
|
*
|
||||||
|
* @param string $pdfData PDF data
|
||||||
|
* @param int $startxref Offset at which the xref section starts
|
||||||
|
* @param array $xref Previous xref array (if any)
|
||||||
|
* @param array<int> $visitedOffsets Array of visited offsets to prevent infinite loops
|
||||||
|
*
|
||||||
|
* @return array containing xref and trailer data
|
||||||
|
*
|
||||||
|
* @throws \Exception if unknown PNG predictor detected
|
||||||
|
*/
|
||||||
|
protected function decodeXrefStream(string $pdfData, int $startxref, array $xref = [], array $visitedOffsets = []): array
|
||||||
|
{
|
||||||
|
// try to read Cross-Reference Stream
|
||||||
|
$xrefobj = $this->getRawObject($pdfData, $startxref);
|
||||||
|
$xrefcrs = $this->getIndirectObject($pdfData, $xref, $xrefobj[1], $startxref, true);
|
||||||
|
if (!isset($xref['trailer']) || empty($xref['trailer'])) {
|
||||||
|
// get only the last updated version
|
||||||
|
$xref['trailer'] = [];
|
||||||
|
$filltrailer = true;
|
||||||
|
} else {
|
||||||
|
$filltrailer = false;
|
||||||
|
}
|
||||||
|
if (!isset($xref['xref'])) {
|
||||||
|
$xref['xref'] = [];
|
||||||
|
}
|
||||||
|
$valid_crs = false;
|
||||||
|
$columns = 0;
|
||||||
|
$predictor = null;
|
||||||
|
$sarr = $xrefcrs[0][1];
|
||||||
|
if (!\is_array($sarr)) {
|
||||||
|
$sarr = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$wb = [];
|
||||||
|
|
||||||
|
foreach ($sarr as $k => $v) {
|
||||||
|
if (
|
||||||
|
('/' == $v[0])
|
||||||
|
&& ('Type' == $v[1])
|
||||||
|
&& (
|
||||||
|
isset($sarr[$k + 1])
|
||||||
|
&& '/' == $sarr[$k + 1][0]
|
||||||
|
&& 'XRef' == $sarr[$k + 1][1]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$valid_crs = true;
|
||||||
|
} elseif (('/' == $v[0]) && ('Index' == $v[1]) && (isset($sarr[$k + 1]))) {
|
||||||
|
// initialize list for: first object number in the subsection / number of objects
|
||||||
|
$index_blocks = [];
|
||||||
|
for ($m = 0; $m < \count($sarr[$k + 1][1]); $m += 2) {
|
||||||
|
$index_blocks[] = [$sarr[$k + 1][1][$m][1], $sarr[$k + 1][1][$m + 1][1]];
|
||||||
|
}
|
||||||
|
} elseif (('/' == $v[0]) && ('Prev' == $v[1]) && (isset($sarr[$k + 1]) && ('numeric' == $sarr[$k + 1][0]))) {
|
||||||
|
// get previous xref offset
|
||||||
|
$prevxref = (int) $sarr[$k + 1][1];
|
||||||
|
} elseif (('/' == $v[0]) && ('W' == $v[1]) && (isset($sarr[$k + 1]))) {
|
||||||
|
// number of bytes (in the decoded stream) of the corresponding field
|
||||||
|
$wb[0] = (int) $sarr[$k + 1][1][0][1];
|
||||||
|
$wb[1] = (int) $sarr[$k + 1][1][1][1];
|
||||||
|
$wb[2] = (int) $sarr[$k + 1][1][2][1];
|
||||||
|
} elseif (('/' == $v[0]) && ('DecodeParms' == $v[1]) && (isset($sarr[$k + 1][1]))) {
|
||||||
|
$decpar = $sarr[$k + 1][1];
|
||||||
|
foreach ($decpar as $kdc => $vdc) {
|
||||||
|
if (
|
||||||
|
'/' == $vdc[0]
|
||||||
|
&& 'Columns' == $vdc[1]
|
||||||
|
&& (
|
||||||
|
isset($decpar[$kdc + 1])
|
||||||
|
&& 'numeric' == $decpar[$kdc + 1][0]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$columns = (int) $decpar[$kdc + 1][1];
|
||||||
|
} elseif (
|
||||||
|
'/' == $vdc[0]
|
||||||
|
&& 'Predictor' == $vdc[1]
|
||||||
|
&& (
|
||||||
|
isset($decpar[$kdc + 1])
|
||||||
|
&& 'numeric' == $decpar[$kdc + 1][0]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$predictor = (int) $decpar[$kdc + 1][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($filltrailer) {
|
||||||
|
if (('/' == $v[0]) && ('Size' == $v[1]) && (isset($sarr[$k + 1]) && ('numeric' == $sarr[$k + 1][0]))) {
|
||||||
|
$xref['trailer']['size'] = $sarr[$k + 1][1];
|
||||||
|
} elseif (('/' == $v[0]) && ('Root' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) {
|
||||||
|
$xref['trailer']['root'] = $sarr[$k + 1][1];
|
||||||
|
} elseif (('/' == $v[0]) && ('Info' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) {
|
||||||
|
$xref['trailer']['info'] = $sarr[$k + 1][1];
|
||||||
|
} elseif (('/' == $v[0]) && ('Encrypt' == $v[1]) && (isset($sarr[$k + 1]) && ('objref' == $sarr[$k + 1][0]))) {
|
||||||
|
$xref['trailer']['encrypt'] = $sarr[$k + 1][1];
|
||||||
|
} elseif (('/' == $v[0]) && ('ID' == $v[1]) && (isset($sarr[$k + 1]))) {
|
||||||
|
$xref['trailer']['id'] = [];
|
||||||
|
$xref['trailer']['id'][0] = $sarr[$k + 1][1][0][1];
|
||||||
|
$xref['trailer']['id'][1] = $sarr[$k + 1][1][1][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode data
|
||||||
|
if ($valid_crs && isset($xrefcrs[1][3][0])) {
|
||||||
|
if (null !== $predictor) {
|
||||||
|
// number of bytes in a row
|
||||||
|
$rowlen = ($columns + 1);
|
||||||
|
// convert the stream into an array of integers
|
||||||
|
/** @var array<int> */
|
||||||
|
$sdata = unpack('C*', $xrefcrs[1][3][0]);
|
||||||
|
// TODO: Handle the case when unpack returns false
|
||||||
|
|
||||||
|
// split the rows
|
||||||
|
$sdata = array_chunk($sdata, $rowlen);
|
||||||
|
|
||||||
|
// initialize decoded array
|
||||||
|
$ddata = [];
|
||||||
|
// initialize first row with zeros
|
||||||
|
$prev_row = array_fill(0, $rowlen, 0);
|
||||||
|
// for each row apply PNG unpredictor
|
||||||
|
foreach ($sdata as $k => $row) {
|
||||||
|
// initialize new row
|
||||||
|
$ddata[$k] = [];
|
||||||
|
// get PNG predictor value
|
||||||
|
$predictor = (10 + $row[0]);
|
||||||
|
// for each byte on the row
|
||||||
|
for ($i = 1; $i <= $columns; ++$i) {
|
||||||
|
// new index
|
||||||
|
$j = ($i - 1);
|
||||||
|
$row_up = $prev_row[$j];
|
||||||
|
if (1 == $i) {
|
||||||
|
$row_left = 0;
|
||||||
|
$row_upleft = 0;
|
||||||
|
} else {
|
||||||
|
$row_left = $row[$i - 1];
|
||||||
|
$row_upleft = $prev_row[$j - 1];
|
||||||
|
}
|
||||||
|
switch ($predictor) {
|
||||||
|
case 10: // PNG prediction (on encoding, PNG None on all rows)
|
||||||
|
$ddata[$k][$j] = $row[$i];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 11: // PNG prediction (on encoding, PNG Sub on all rows)
|
||||||
|
$ddata[$k][$j] = (($row[$i] + $row_left) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 12: // PNG prediction (on encoding, PNG Up on all rows)
|
||||||
|
$ddata[$k][$j] = (($row[$i] + $row_up) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 13: // PNG prediction (on encoding, PNG Average on all rows)
|
||||||
|
$ddata[$k][$j] = (($row[$i] + (($row_left + $row_up) / 2)) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 14: // PNG prediction (on encoding, PNG Paeth on all rows)
|
||||||
|
// initial estimate
|
||||||
|
$p = ($row_left + $row_up - $row_upleft);
|
||||||
|
// distances
|
||||||
|
$pa = abs($p - $row_left);
|
||||||
|
$pb = abs($p - $row_up);
|
||||||
|
$pc = abs($p - $row_upleft);
|
||||||
|
$pmin = min($pa, $pb, $pc);
|
||||||
|
// return minimum distance
|
||||||
|
switch ($pmin) {
|
||||||
|
case $pa:
|
||||||
|
$ddata[$k][$j] = (($row[$i] + $row_left) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $pb:
|
||||||
|
$ddata[$k][$j] = (($row[$i] + $row_up) & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $pc:
|
||||||
|
$ddata[$k][$j] = (($row[$i] + $row_upleft) & 0xFF);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // PNG prediction (on encoding, PNG optimum)
|
||||||
|
throw new \Exception('Unknown PNG predictor: '.$predictor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$prev_row = $ddata[$k];
|
||||||
|
} // end for each row
|
||||||
|
// complete decoding
|
||||||
|
} else {
|
||||||
|
// number of bytes in a row
|
||||||
|
$rowlen = array_sum($wb);
|
||||||
|
if (0 < $rowlen) {
|
||||||
|
// convert the stream into an array of integers
|
||||||
|
$sdata = unpack('C*', $xrefcrs[1][3][0]);
|
||||||
|
// split the rows
|
||||||
|
$ddata = array_chunk($sdata, $rowlen);
|
||||||
|
} else {
|
||||||
|
// if the row length is zero, $ddata should be an empty array as well
|
||||||
|
$ddata = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sdata = [];
|
||||||
|
|
||||||
|
// for every row
|
||||||
|
foreach ($ddata as $k => $row) {
|
||||||
|
// initialize new row
|
||||||
|
$sdata[$k] = [0, 0, 0];
|
||||||
|
if (0 == $wb[0]) {
|
||||||
|
// default type field
|
||||||
|
$sdata[$k][0] = 1;
|
||||||
|
}
|
||||||
|
$i = 0; // count bytes in the row
|
||||||
|
// for every column
|
||||||
|
for ($c = 0; $c < 3; ++$c) {
|
||||||
|
// for every byte on the column
|
||||||
|
for ($b = 0; $b < $wb[$c]; ++$b) {
|
||||||
|
if (isset($row[$i])) {
|
||||||
|
$sdata[$k][$c] += ($row[$i] << (($wb[$c] - 1 - $b) * 8));
|
||||||
|
}
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill xref
|
||||||
|
if (isset($index_blocks)) {
|
||||||
|
// load the first object number of the first /Index entry
|
||||||
|
$obj_num = $index_blocks[0][0];
|
||||||
|
} else {
|
||||||
|
$obj_num = 0;
|
||||||
|
}
|
||||||
|
foreach ($sdata as $k => $row) {
|
||||||
|
switch ($row[0]) {
|
||||||
|
case 0: // (f) linked list of free objects
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // (n) objects that are in use but are not compressed
|
||||||
|
// create unique object index: [object number]_[generation number]
|
||||||
|
$index = $obj_num.'_'.$row[2];
|
||||||
|
// check if object already exist
|
||||||
|
if (!isset($xref['xref'][$index])) {
|
||||||
|
// store object offset position
|
||||||
|
$xref['xref'][$index] = $row[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // compressed objects
|
||||||
|
// $row[1] = object number of the object stream in which this object is stored
|
||||||
|
// $row[2] = index of this object within the object stream
|
||||||
|
$index = $row[1].'_0_'.$row[2];
|
||||||
|
$xref['xref'][$index] = -1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // null objects
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++$obj_num;
|
||||||
|
if (isset($index_blocks)) {
|
||||||
|
// reduce the number of remaining objects
|
||||||
|
--$index_blocks[0][1];
|
||||||
|
if (0 == $index_blocks[0][1]) {
|
||||||
|
// remove the actual used /Index entry
|
||||||
|
array_shift($index_blocks);
|
||||||
|
if (0 < \count($index_blocks)) {
|
||||||
|
// load the first object number of the following /Index entry
|
||||||
|
$obj_num = $index_blocks[0][0];
|
||||||
|
} else {
|
||||||
|
// if there are no more entries, remove $index_blocks to avoid actions on an empty array
|
||||||
|
unset($index_blocks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // end decoding data
|
||||||
|
if (isset($prevxref)) {
|
||||||
|
// get previous xref
|
||||||
|
$xref = $this->getXrefData($pdfData, $prevxref, $xref, $visitedOffsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $xref;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectHeaderPattern(array $objRefs): string
|
||||||
|
{
|
||||||
|
// consider all whitespace character (PDF specifications)
|
||||||
|
return '/'.$objRefs[0].$this->config->getPdfWhitespacesRegex().$objRefs[1].$this->config->getPdfWhitespacesRegex().'obj/';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectHeaderLen(array $objRefs): int
|
||||||
|
{
|
||||||
|
// "4 0 obj"
|
||||||
|
// 2 whitespaces + strlen("obj") = 5
|
||||||
|
return 5 + \strlen($objRefs[0]) + \strlen($objRefs[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content of indirect object.
|
||||||
|
*
|
||||||
|
* @param string $pdfData PDF data
|
||||||
|
* @param string $objRef Object number and generation number separated by underscore character
|
||||||
|
* @param int $offset Object offset
|
||||||
|
* @param bool $decoding If true decode streams
|
||||||
|
*
|
||||||
|
* @return array containing object data
|
||||||
|
*
|
||||||
|
* @throws \Exception if invalid object reference found
|
||||||
|
*/
|
||||||
|
protected function getIndirectObject(string $pdfData, array $xref, string $objRef, int $offset = 0, bool $decoding = true): array
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* build indirect object header
|
||||||
|
*/
|
||||||
|
// $objHeader = "[object number] [generation number] obj"
|
||||||
|
$objRefArr = explode('_', $objRef);
|
||||||
|
if (2 !== \count($objRefArr)) {
|
||||||
|
throw new \Exception('Invalid object reference for $obj.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$objHeaderLen = $this->getObjectHeaderLen($objRefArr);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* check if we are in position
|
||||||
|
*/
|
||||||
|
// ignore whitespace characters at offset
|
||||||
|
$offset += strspn($pdfData, $this->config->getPdfWhitespaces(), $offset);
|
||||||
|
// ignore leading zeros for object number
|
||||||
|
$offset += strspn($pdfData, '0', $offset);
|
||||||
|
if (0 == preg_match($this->getObjectHeaderPattern($objRefArr), substr($pdfData, $offset, $objHeaderLen))) {
|
||||||
|
// an indirect reference to an undefined object shall be considered a reference to the null object
|
||||||
|
return ['null', 'null', $offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* get content
|
||||||
|
*/
|
||||||
|
// starting position of object content
|
||||||
|
$offset += $objHeaderLen;
|
||||||
|
$objContentArr = [];
|
||||||
|
$i = 0; // object main index
|
||||||
|
$header = null;
|
||||||
|
do {
|
||||||
|
$oldOffset = $offset;
|
||||||
|
// get element
|
||||||
|
$element = $this->getRawObject($pdfData, $offset, null != $header ? $header[1] : null);
|
||||||
|
$offset = $element[2];
|
||||||
|
// decode stream using stream's dictionary information
|
||||||
|
if ($decoding && ('stream' === $element[0]) && null != $header) {
|
||||||
|
$element[3] = $this->decodeStream($pdfData, $xref, $header[1], $element[1]);
|
||||||
|
}
|
||||||
|
$objContentArr[$i] = $element;
|
||||||
|
$header = isset($element[0]) && '<<' === $element[0] ? $element : null;
|
||||||
|
++$i;
|
||||||
|
} while (('endobj' !== $element[0]) && ($offset !== $oldOffset));
|
||||||
|
// remove closing delimiter
|
||||||
|
array_pop($objContentArr);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* return raw object content
|
||||||
|
*/
|
||||||
|
return $objContentArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content of object, resolving indirect object reference if necessary.
|
||||||
|
*
|
||||||
|
* @param string $pdfData PDF data
|
||||||
|
* @param array $obj Object value
|
||||||
|
*
|
||||||
|
* @return array containing object data
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function getObjectVal(string $pdfData, $xref, array $obj): array
|
||||||
|
{
|
||||||
|
if ('objref' == $obj[0]) {
|
||||||
|
// reference to indirect object
|
||||||
|
if (isset($this->objects[$obj[1]])) {
|
||||||
|
// this object has been already parsed
|
||||||
|
return $this->objects[$obj[1]];
|
||||||
|
} elseif (isset($xref[$obj[1]])) {
|
||||||
|
// parse new object
|
||||||
|
$this->objects[$obj[1]] = $this->getIndirectObject($pdfData, $xref, $obj[1], $xref[$obj[1]], false);
|
||||||
|
|
||||||
|
return $this->objects[$obj[1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get object type, raw value and offset to next object
|
||||||
|
*
|
||||||
|
* @param int $offset Object offset
|
||||||
|
* @param array|null $headerDic obj header's dictionary, parsed by getRawObject. Used for stream parsing optimization
|
||||||
|
*
|
||||||
|
* @return array containing object type, raw value and offset to next object
|
||||||
|
*/
|
||||||
|
protected function getRawObject(string $pdfData, int $offset = 0, ?array $headerDic = null): array
|
||||||
|
{
|
||||||
|
$objtype = ''; // object type to be returned
|
||||||
|
$objval = ''; // object value to be returned
|
||||||
|
|
||||||
|
// skip initial white space chars
|
||||||
|
$offset += strspn($pdfData, $this->config->getPdfWhitespaces(), $offset);
|
||||||
|
|
||||||
|
// get first char
|
||||||
|
$char = $pdfData[$offset];
|
||||||
|
// get object type
|
||||||
|
switch ($char) {
|
||||||
|
case '%': // \x25 PERCENT SIGN
|
||||||
|
// skip comment and search for next token
|
||||||
|
$next = strcspn($pdfData, "\r\n", $offset);
|
||||||
|
if ($next > 0) {
|
||||||
|
$offset += $next;
|
||||||
|
|
||||||
|
return $this->getRawObject($pdfData, $offset);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '/': // \x2F SOLIDUS
|
||||||
|
// name object
|
||||||
|
$objtype = $char;
|
||||||
|
++$offset;
|
||||||
|
$span = strcspn($pdfData, "\x00\x09\x0a\x0c\x0d\x20\n\t\r\v\f\x28\x29\x3c\x3e\x5b\x5d\x7b\x7d\x2f\x25", $offset, 256);
|
||||||
|
if ($span > 0) {
|
||||||
|
$objval = substr($pdfData, $offset, $span); // unescaped value
|
||||||
|
$offset += $span;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '(': // \x28 LEFT PARENTHESIS
|
||||||
|
case ')': // \x29 RIGHT PARENTHESIS
|
||||||
|
// literal string object
|
||||||
|
$objtype = $char;
|
||||||
|
++$offset;
|
||||||
|
$strpos = $offset;
|
||||||
|
if ('(' == $char) {
|
||||||
|
$open_bracket = 1;
|
||||||
|
while ($open_bracket > 0) {
|
||||||
|
if (!isset($pdfData[$strpos])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$ch = $pdfData[$strpos];
|
||||||
|
switch ($ch) {
|
||||||
|
case '\\': // REVERSE SOLIDUS (5Ch) (Backslash)
|
||||||
|
// skip next character
|
||||||
|
++$strpos;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '(': // LEFT PARENHESIS (28h)
|
||||||
|
++$open_bracket;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ')': // RIGHT PARENTHESIS (29h)
|
||||||
|
--$open_bracket;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++$strpos;
|
||||||
|
}
|
||||||
|
$objval = substr($pdfData, $offset, $strpos - $offset - 1);
|
||||||
|
$offset = $strpos;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '[': // \x5B LEFT SQUARE BRACKET
|
||||||
|
case ']': // \x5D RIGHT SQUARE BRACKET
|
||||||
|
// array object
|
||||||
|
$objtype = $char;
|
||||||
|
++$offset;
|
||||||
|
if ('[' == $char) {
|
||||||
|
// get array content
|
||||||
|
$objval = [];
|
||||||
|
do {
|
||||||
|
$oldOffset = $offset;
|
||||||
|
// get element
|
||||||
|
$element = $this->getRawObject($pdfData, $offset);
|
||||||
|
$offset = $element[2];
|
||||||
|
$objval[] = $element;
|
||||||
|
} while ((']' != $element[0]) && ($offset != $oldOffset));
|
||||||
|
// remove closing delimiter
|
||||||
|
array_pop($objval);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '<': // \x3C LESS-THAN SIGN
|
||||||
|
case '>': // \x3E GREATER-THAN SIGN
|
||||||
|
if (isset($pdfData[$offset + 1]) && ($pdfData[$offset + 1] == $char)) {
|
||||||
|
// dictionary object
|
||||||
|
$objtype = $char.$char;
|
||||||
|
$offset += 2;
|
||||||
|
if ('<' == $char) {
|
||||||
|
// get array content
|
||||||
|
$objval = [];
|
||||||
|
do {
|
||||||
|
$oldOffset = $offset;
|
||||||
|
// get element
|
||||||
|
$element = $this->getRawObject($pdfData, $offset);
|
||||||
|
$offset = $element[2];
|
||||||
|
$objval[] = $element;
|
||||||
|
} while (('>>' != $element[0]) && ($offset != $oldOffset));
|
||||||
|
// remove closing delimiter
|
||||||
|
array_pop($objval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// hexadecimal string object
|
||||||
|
$objtype = $char;
|
||||||
|
++$offset;
|
||||||
|
|
||||||
|
$span = strspn($pdfData, "0123456789abcdefABCDEF\x09\x0a\x0c\x0d\x20", $offset);
|
||||||
|
$dataToCheck = $pdfData[$offset + $span] ?? null;
|
||||||
|
if ('<' == $char && $span > 0 && '>' == $dataToCheck) {
|
||||||
|
// remove white space characters
|
||||||
|
$objval = strtr(substr($pdfData, $offset, $span), $this->config->getPdfWhitespaces(), '');
|
||||||
|
$offset += $span + 1;
|
||||||
|
} elseif (false !== ($endpos = strpos($pdfData, '>', $offset))) {
|
||||||
|
$offset = $endpos + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if ('endobj' == substr($pdfData, $offset, 6)) {
|
||||||
|
// indirect object
|
||||||
|
$objtype = 'endobj';
|
||||||
|
$offset += 6;
|
||||||
|
} elseif ('null' == substr($pdfData, $offset, 4)) {
|
||||||
|
// null object
|
||||||
|
$objtype = 'null';
|
||||||
|
$offset += 4;
|
||||||
|
$objval = 'null';
|
||||||
|
} elseif ('true' == substr($pdfData, $offset, 4)) {
|
||||||
|
// boolean true object
|
||||||
|
$objtype = 'boolean';
|
||||||
|
$offset += 4;
|
||||||
|
$objval = 'true';
|
||||||
|
} elseif ('false' == substr($pdfData, $offset, 5)) {
|
||||||
|
// boolean false object
|
||||||
|
$objtype = 'boolean';
|
||||||
|
$offset += 5;
|
||||||
|
$objval = 'false';
|
||||||
|
} elseif ('stream' == substr($pdfData, $offset, 6)) {
|
||||||
|
// start stream object
|
||||||
|
$objtype = 'stream';
|
||||||
|
$offset += 6;
|
||||||
|
if (1 == preg_match('/^( *[\r]?[\n])/isU', substr($pdfData, $offset, 4), $matches)) {
|
||||||
|
$offset += \strlen($matches[0]);
|
||||||
|
|
||||||
|
// we get stream length here to later help preg_match test less data
|
||||||
|
$streamLen = (int) $this->getHeaderValue($headerDic, 'Length', 'numeric', 0);
|
||||||
|
$skip = false === $this->config->getRetainImageContent() && 'XObject' == $this->getHeaderValue($headerDic, 'Type', '/') && 'Image' == $this->getHeaderValue($headerDic, 'Subtype', '/');
|
||||||
|
|
||||||
|
$pregResult = preg_match(
|
||||||
|
'/(endstream)[\x09\x0a\x0c\x0d\x20]/isU',
|
||||||
|
$pdfData,
|
||||||
|
$matches,
|
||||||
|
\PREG_OFFSET_CAPTURE,
|
||||||
|
$offset + $streamLen
|
||||||
|
);
|
||||||
|
|
||||||
|
if (1 == $pregResult) {
|
||||||
|
$objval = $skip ? '' : substr($pdfData, $offset, $matches[0][1] - $offset);
|
||||||
|
$offset = $matches[1][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ('endstream' == substr($pdfData, $offset, 9)) {
|
||||||
|
// end stream object
|
||||||
|
$objtype = 'endstream';
|
||||||
|
$offset += 9;
|
||||||
|
} elseif (1 == preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+R/iU', substr($pdfData, $offset, 33), $matches)) {
|
||||||
|
// indirect object reference
|
||||||
|
$objtype = 'objref';
|
||||||
|
$offset += \strlen($matches[0]);
|
||||||
|
$objval = (int) $matches[1].'_'.(int) $matches[2];
|
||||||
|
} elseif (1 == preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+obj/iU', substr($pdfData, $offset, 33), $matches)) {
|
||||||
|
// object start
|
||||||
|
$objtype = 'obj';
|
||||||
|
$objval = (int) $matches[1].'_'.(int) $matches[2];
|
||||||
|
$offset += \strlen($matches[0]);
|
||||||
|
} elseif (($numlen = strspn($pdfData, '+-.0123456789', $offset)) > 0) {
|
||||||
|
// numeric object
|
||||||
|
$objtype = 'numeric';
|
||||||
|
$objval = substr($pdfData, $offset, $numlen);
|
||||||
|
$offset += $numlen;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$objtype, $objval, $offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value of an object header's section (obj << YYY >> part ).
|
||||||
|
*
|
||||||
|
* It is similar to Header::get('...')->getContent(), the only difference is it can be used during the parsing process,
|
||||||
|
* when no Smalot\PdfParser\Header objects are created yet.
|
||||||
|
*
|
||||||
|
* @param string $key header's section name
|
||||||
|
* @param string $type type of the section (i.e. 'numeric', '/', '<<', etc.)
|
||||||
|
* @param string|array|null $default default value for header's section
|
||||||
|
*
|
||||||
|
* @return string|array|null value of obj header's section, or default value if none found, or its type doesn't match $type param
|
||||||
|
*/
|
||||||
|
private function getHeaderValue(?array $headerDic, string $key, string $type, $default = '')
|
||||||
|
{
|
||||||
|
if (false === \is_array($headerDic)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* It recieves dictionary of header fields, as it is returned by RawDataParser::getRawObject,
|
||||||
|
* iterates over it, searching for section of type '/' whith requested key.
|
||||||
|
* If such a section is found, it tries to receive it's value (next object in dictionary),
|
||||||
|
* returning it, if it matches requested type, or default value otherwise.
|
||||||
|
*/
|
||||||
|
foreach ($headerDic as $i => $val) {
|
||||||
|
$isSectionName = \is_array($val) && 3 == \count($val) && '/' == $val[0];
|
||||||
|
if (
|
||||||
|
$isSectionName
|
||||||
|
&& $val[1] == $key
|
||||||
|
&& isset($headerDic[$i + 1])
|
||||||
|
) {
|
||||||
|
$isSectionValue = \is_array($headerDic[$i + 1]) && 1 < \count($headerDic[$i + 1]);
|
||||||
|
|
||||||
|
return $isSectionValue && $type == $headerDic[$i + 1][0]
|
||||||
|
? $headerDic[$i + 1][1]
|
||||||
|
: $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Cross-Reference (xref) table and trailer data from PDF document data.
|
||||||
|
*
|
||||||
|
* @param int $offset xref offset (if known)
|
||||||
|
* @param array $xref previous xref array (if any)
|
||||||
|
* @param array<int> $visitedOffsets array of visited offsets to prevent infinite loops
|
||||||
|
*
|
||||||
|
* @return array containing xref and trailer data
|
||||||
|
*
|
||||||
|
* @throws \Exception if it was unable to find startxref
|
||||||
|
* @throws \Exception if it was unable to find xref
|
||||||
|
*/
|
||||||
|
protected function getXrefData(string $pdfData, int $offset = 0, array $xref = [], array $visitedOffsets = []): array
|
||||||
|
{
|
||||||
|
// Check for circular references to prevent infinite loops
|
||||||
|
if (\in_array($offset, $visitedOffsets, true)) {
|
||||||
|
// We've already processed this offset, skip to avoid infinite loop
|
||||||
|
return $xref;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track this offset as visited
|
||||||
|
$visitedOffsets[] = $offset;
|
||||||
|
// If the $offset is currently pointed at whitespace, bump it
|
||||||
|
// forward until it isn't; affects loosely targetted offsets
|
||||||
|
// for the 'xref' keyword
|
||||||
|
// See: https://github.com/smalot/pdfparser/issues/673
|
||||||
|
$bumpOffset = $offset;
|
||||||
|
while (preg_match('/\s/', substr($pdfData, $bumpOffset, 1))) {
|
||||||
|
++$bumpOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all startxref tables from this $offset forward
|
||||||
|
$startxrefPreg = preg_match_all(
|
||||||
|
'/(?<=[\r\n])startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i',
|
||||||
|
$pdfData,
|
||||||
|
$startxrefMatches,
|
||||||
|
\PREG_SET_ORDER,
|
||||||
|
$offset
|
||||||
|
);
|
||||||
|
|
||||||
|
if (0 == $startxrefPreg) {
|
||||||
|
// No startxref tables were found
|
||||||
|
throw new \Exception('Unable to find startxref');
|
||||||
|
} elseif (0 == $offset) {
|
||||||
|
// Use the last startxref in the document
|
||||||
|
$startxref = (int) $startxrefMatches[\count($startxrefMatches) - 1][1];
|
||||||
|
} elseif (strpos($pdfData, 'xref', $bumpOffset) == $bumpOffset) {
|
||||||
|
// Already pointing at the xref table
|
||||||
|
$startxref = $bumpOffset;
|
||||||
|
} elseif (preg_match('/([0-9]+[\s][0-9]+[\s]obj)/i', $pdfData, $matches, 0, $bumpOffset)) {
|
||||||
|
// Cross-Reference Stream object
|
||||||
|
$startxref = $bumpOffset;
|
||||||
|
} else {
|
||||||
|
// Use the next startxref from this $offset
|
||||||
|
$startxref = (int) $startxrefMatches[0][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startxref > \strlen($pdfData)) {
|
||||||
|
throw new \Exception('Unable to find xref (PDF corrupted?)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// check xref position
|
||||||
|
if (strpos($pdfData, 'xref', $startxref) == $startxref) {
|
||||||
|
// Cross-Reference
|
||||||
|
$xref = $this->decodeXref($pdfData, $startxref, $xref, $visitedOffsets);
|
||||||
|
} else {
|
||||||
|
// Check if the $pdfData might have the wrong line-endings
|
||||||
|
$pdfDataUnix = str_replace("\r\n", "\n", $pdfData);
|
||||||
|
if ($startxref < \strlen($pdfDataUnix) && strpos($pdfDataUnix, 'xref', $startxref) == $startxref) {
|
||||||
|
// Return Unix-line-ending flag
|
||||||
|
$xref = ['Unix' => true];
|
||||||
|
} else {
|
||||||
|
// Cross-Reference Stream
|
||||||
|
$xref = $this->decodeXrefStream($pdfData, $startxref, $xref, $visitedOffsets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($xref)) {
|
||||||
|
throw new \Exception('Unable to find xref');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $xref;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses PDF data and returns extracted data as array.
|
||||||
|
*
|
||||||
|
* @param string $data PDF data to parse
|
||||||
|
*
|
||||||
|
* @return array array of parsed PDF document objects
|
||||||
|
*
|
||||||
|
* @throws EmptyPdfException if empty PDF data given
|
||||||
|
* @throws MissingPdfHeaderException if PDF data missing `%PDF-` header
|
||||||
|
*/
|
||||||
|
public function parseData(string $data): array
|
||||||
|
{
|
||||||
|
if (empty($data)) {
|
||||||
|
throw new EmptyPdfException('Empty PDF data given.');
|
||||||
|
}
|
||||||
|
// find the pdf header starting position
|
||||||
|
if (false === ($trimpos = strpos($data, '%PDF-'))) {
|
||||||
|
throw new MissingPdfHeaderException('Invalid PDF data: Missing `%PDF-` header.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// get PDF content string
|
||||||
|
$pdfData = $trimpos > 0 ? substr($data, $trimpos) : $data;
|
||||||
|
|
||||||
|
// get xref and trailer data
|
||||||
|
$xref = $this->getXrefData($pdfData);
|
||||||
|
|
||||||
|
// If we found Unix line-endings
|
||||||
|
if (isset($xref['Unix'])) {
|
||||||
|
$pdfData = str_replace("\r\n", "\n", $pdfData);
|
||||||
|
$xref = $this->getXrefData($pdfData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse all document objects
|
||||||
|
$objects = [];
|
||||||
|
foreach ($xref['xref'] as $obj => $offset) {
|
||||||
|
if (!isset($objects[$obj]) && ($offset > 0)) {
|
||||||
|
// decode objects with positive offset
|
||||||
|
$objects[$obj] = $this->getIndirectObject($pdfData, $xref, $obj, $offset, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$xref, $objects];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\XObject;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Header;
|
||||||
|
use Smalot\PdfParser\Page;
|
||||||
|
use Smalot\PdfParser\PDFObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Form
|
||||||
|
*/
|
||||||
|
class Form extends Page
|
||||||
|
{
|
||||||
|
public function getText(?Page $page = null): string
|
||||||
|
{
|
||||||
|
$header = new Header([], $this->document);
|
||||||
|
$contents = new PDFObject($this->document, $header, $this->content, $this->config);
|
||||||
|
|
||||||
|
return $contents->getText($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file is part of the PdfParser library.
|
||||||
|
*
|
||||||
|
* @author Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* @date 2017-01-03
|
||||||
|
*
|
||||||
|
* @license LGPLv3
|
||||||
|
*
|
||||||
|
* @url <https://github.com/smalot/pdfparser>
|
||||||
|
*
|
||||||
|
* PdfParser is a pdf library written in PHP, extraction oriented.
|
||||||
|
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
|
||||||
|
*
|
||||||
|
* This program 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 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 program.
|
||||||
|
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Smalot\PdfParser\XObject;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Page;
|
||||||
|
use Smalot\PdfParser\PDFObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Image
|
||||||
|
*/
|
||||||
|
class Image extends PDFObject
|
||||||
|
{
|
||||||
|
public function getText(?Page $page = null): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2015-present Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
+1138
File diff suppressed because it is too large.
Load diff
+13
@@ -0,0 +1,13 @@
|
|||||||
|
Symfony Polyfill / Mbstring
|
||||||
|
===========================
|
||||||
|
|
||||||
|
This component provides a partial, native PHP implementation for the
|
||||||
|
[Mbstring](https://php.net/mbstring) extension.
|
||||||
|
|
||||||
|
More information can be found in the
|
||||||
|
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
This library is released under the [MIT license](LICENSE).
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'İ' => 'i̇',
|
||||||
|
'µ' => 'μ',
|
||||||
|
'ſ' => 's',
|
||||||
|
'ͅ' => 'ι',
|
||||||
|
'ς' => 'σ',
|
||||||
|
'ϐ' => 'β',
|
||||||
|
'ϑ' => 'θ',
|
||||||
|
'ϕ' => 'φ',
|
||||||
|
'ϖ' => 'π',
|
||||||
|
'ϰ' => 'κ',
|
||||||
|
'ϱ' => 'ρ',
|
||||||
|
'ϵ' => 'ε',
|
||||||
|
'ẛ' => 'ṡ',
|
||||||
|
'ι' => 'ι',
|
||||||
|
'ß' => 'ss',
|
||||||
|
'ʼn' => 'ʼn',
|
||||||
|
'ǰ' => 'ǰ',
|
||||||
|
'ΐ' => 'ΐ',
|
||||||
|
'ΰ' => 'ΰ',
|
||||||
|
'և' => 'եւ',
|
||||||
|
'ẖ' => 'ẖ',
|
||||||
|
'ẗ' => 'ẗ',
|
||||||
|
'ẘ' => 'ẘ',
|
||||||
|
'ẙ' => 'ẙ',
|
||||||
|
'ẚ' => 'aʾ',
|
||||||
|
'ẞ' => 'ss',
|
||||||
|
'ὐ' => 'ὐ',
|
||||||
|
'ὒ' => 'ὒ',
|
||||||
|
'ὔ' => 'ὔ',
|
||||||
|
'ὖ' => 'ὖ',
|
||||||
|
'ᾀ' => 'ἀι',
|
||||||
|
'ᾁ' => 'ἁι',
|
||||||
|
'ᾂ' => 'ἂι',
|
||||||
|
'ᾃ' => 'ἃι',
|
||||||
|
'ᾄ' => 'ἄι',
|
||||||
|
'ᾅ' => 'ἅι',
|
||||||
|
'ᾆ' => 'ἆι',
|
||||||
|
'ᾇ' => 'ἇι',
|
||||||
|
'ᾈ' => 'ἀι',
|
||||||
|
'ᾉ' => 'ἁι',
|
||||||
|
'ᾊ' => 'ἂι',
|
||||||
|
'ᾋ' => 'ἃι',
|
||||||
|
'ᾌ' => 'ἄι',
|
||||||
|
'ᾍ' => 'ἅι',
|
||||||
|
'ᾎ' => 'ἆι',
|
||||||
|
'ᾏ' => 'ἇι',
|
||||||
|
'ᾐ' => 'ἠι',
|
||||||
|
'ᾑ' => 'ἡι',
|
||||||
|
'ᾒ' => 'ἢι',
|
||||||
|
'ᾓ' => 'ἣι',
|
||||||
|
'ᾔ' => 'ἤι',
|
||||||
|
'ᾕ' => 'ἥι',
|
||||||
|
'ᾖ' => 'ἦι',
|
||||||
|
'ᾗ' => 'ἧι',
|
||||||
|
'ᾘ' => 'ἠι',
|
||||||
|
'ᾙ' => 'ἡι',
|
||||||
|
'ᾚ' => 'ἢι',
|
||||||
|
'ᾛ' => 'ἣι',
|
||||||
|
'ᾜ' => 'ἤι',
|
||||||
|
'ᾝ' => 'ἥι',
|
||||||
|
'ᾞ' => 'ἦι',
|
||||||
|
'ᾟ' => 'ἧι',
|
||||||
|
'ᾠ' => 'ὠι',
|
||||||
|
'ᾡ' => 'ὡι',
|
||||||
|
'ᾢ' => 'ὢι',
|
||||||
|
'ᾣ' => 'ὣι',
|
||||||
|
'ᾤ' => 'ὤι',
|
||||||
|
'ᾥ' => 'ὥι',
|
||||||
|
'ᾦ' => 'ὦι',
|
||||||
|
'ᾧ' => 'ὧι',
|
||||||
|
'ᾨ' => 'ὠι',
|
||||||
|
'ᾩ' => 'ὡι',
|
||||||
|
'ᾪ' => 'ὢι',
|
||||||
|
'ᾫ' => 'ὣι',
|
||||||
|
'ᾬ' => 'ὤι',
|
||||||
|
'ᾭ' => 'ὥι',
|
||||||
|
'ᾮ' => 'ὦι',
|
||||||
|
'ᾯ' => 'ὧι',
|
||||||
|
'ᾲ' => 'ὰι',
|
||||||
|
'ᾳ' => 'αι',
|
||||||
|
'ᾴ' => 'άι',
|
||||||
|
'ᾶ' => 'ᾶ',
|
||||||
|
'ᾷ' => 'ᾶι',
|
||||||
|
'ᾼ' => 'αι',
|
||||||
|
'ῂ' => 'ὴι',
|
||||||
|
'ῃ' => 'ηι',
|
||||||
|
'ῄ' => 'ήι',
|
||||||
|
'ῆ' => 'ῆ',
|
||||||
|
'ῇ' => 'ῆι',
|
||||||
|
'ῌ' => 'ηι',
|
||||||
|
'ῒ' => 'ῒ',
|
||||||
|
'ῖ' => 'ῖ',
|
||||||
|
'ῗ' => 'ῗ',
|
||||||
|
'ῢ' => 'ῢ',
|
||||||
|
'ῤ' => 'ῤ',
|
||||||
|
'ῦ' => 'ῦ',
|
||||||
|
'ῧ' => 'ῧ',
|
||||||
|
'ῲ' => 'ὼι',
|
||||||
|
'ῳ' => 'ωι',
|
||||||
|
'ῴ' => 'ώι',
|
||||||
|
'ῶ' => 'ῶ',
|
||||||
|
'ῷ' => 'ῶι',
|
||||||
|
'ῼ' => 'ωι',
|
||||||
|
'ff' => 'ff',
|
||||||
|
'fi' => 'fi',
|
||||||
|
'fl' => 'fl',
|
||||||
|
'ffi' => 'ffi',
|
||||||
|
'ffl' => 'ffl',
|
||||||
|
'ſt' => 'st',
|
||||||
|
'st' => 'st',
|
||||||
|
'ﬓ' => 'մն',
|
||||||
|
'ﬔ' => 'մե',
|
||||||
|
'ﬕ' => 'մի',
|
||||||
|
'ﬖ' => 'վն',
|
||||||
|
'ﬗ' => 'մխ',
|
||||||
|
];
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// from Case_Ignorable in https://unicode.org/Public/UNIDATA/DerivedCoreProperties.txt
|
||||||
|
|
||||||
|
return '/(?<![\x{0027}\x{002E}\x{003A}\x{005E}\x{0060}\x{00A8}\x{00AD}\x{00AF}\x{00B4}\x{00B7}\x{00B8}\x{02B0}-\x{02C1}\x{02C2}-\x{02C5}\x{02C6}-\x{02D1}\x{02D2}-\x{02DF}\x{02E0}-\x{02E4}\x{02E5}-\x{02EB}\x{02EC}\x{02ED}\x{02EE}\x{02EF}-\x{02FF}\x{0300}-\x{036F}\x{0374}\x{0375}\x{037A}\x{0384}-\x{0385}\x{0387}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0559}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{05F4}\x{0600}-\x{0605}\x{0610}-\x{061A}\x{061C}\x{0640}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DD}\x{06DF}-\x{06E4}\x{06E5}-\x{06E6}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{070F}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07F4}-\x{07F5}\x{07FA}\x{07FD}\x{0816}-\x{0819}\x{081A}\x{081B}-\x{0823}\x{0824}\x{0825}-\x{0827}\x{0828}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E2}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0971}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0CBC}\x{0CBF}\x{0CC6}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E46}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EB9}\x{0EBB}-\x{0EBC}\x{0EC6}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{10FC}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17D7}\x{17DD}\x{180B}-\x{180D}\x{180E}\x{1843}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AA7}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1C78}-\x{1C7D}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1D2C}-\x{1D6A}\x{1D78}\x{1D9B}-\x{1DBF}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{1FBD}\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}\x{1FFD}-\x{1FFE}\x{200B}-\x{200F}\x{2018}\x{2019}\x{2024}\x{2027}\x{202A}-\x{202E}\x{2060}-\x{2064}\x{2066}-\x{206F}\x{2071}\x{207F}\x{2090}-\x{209C}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2C7C}-\x{2C7D}\x{2CEF}-\x{2CF1}\x{2D6F}\x{2D7F}\x{2DE0}-\x{2DFF}\x{2E2F}\x{3005}\x{302A}-\x{302D}\x{3031}-\x{3035}\x{303B}\x{3099}-\x{309A}\x{309B}-\x{309C}\x{309D}-\x{309E}\x{30FC}-\x{30FE}\x{A015}\x{A4F8}-\x{A4FD}\x{A60C}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A67F}\x{A69C}-\x{A69D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A700}-\x{A716}\x{A717}-\x{A71F}\x{A720}-\x{A721}\x{A770}\x{A788}\x{A789}-\x{A78A}\x{A7F8}-\x{A7F9}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}\x{A9CF}\x{A9E5}\x{A9E6}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA70}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AADD}\x{AAEC}-\x{AAED}\x{AAF3}-\x{AAF4}\x{AAF6}\x{AB5B}\x{AB5C}-\x{AB5F}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1E}\x{FBB2}-\x{FBC1}\x{FE00}-\x{FE0F}\x{FE13}\x{FE20}-\x{FE2F}\x{FE52}\x{FE55}\x{FEFF}\x{FF07}\x{FF0E}\x{FF1A}\x{FF3E}\x{FF40}\x{FF70}\x{FF9E}-\x{FF9F}\x{FFE3}\x{FFF9}-\x{FFFB}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10F46}-\x{10F50}\x{11001}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{110BD}\x{110CD}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{116ABLine truncated
|
||||||
File diff suppressed because it is too large.
Load diff
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (\PHP_VERSION_ID >= 80000) {
|
||||||
|
return require __DIR__.'/bootstrap80.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
return require __DIR__.'/bootstrap72.php';
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Symfony\Polyfill\Mbstring as p;
|
||||||
|
|
||||||
|
if (!function_exists('mb_convert_encoding')) {
|
||||||
|
function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_decode_mimeheader')) {
|
||||||
|
function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_encode_mimeheader')) {
|
||||||
|
function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_decode_numericentity')) {
|
||||||
|
function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_encode_numericentity')) {
|
||||||
|
function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_convert_case')) {
|
||||||
|
function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_internal_encoding')) {
|
||||||
|
function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_language')) {
|
||||||
|
function mb_language($language = null) { return p\Mbstring::mb_language($language); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_list_encodings')) {
|
||||||
|
function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_encoding_aliases')) {
|
||||||
|
function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_check_encoding')) {
|
||||||
|
function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_detect_encoding')) {
|
||||||
|
function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_detect_order')) {
|
||||||
|
function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_parse_str')) {
|
||||||
|
function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strlen')) {
|
||||||
|
function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strpos')) {
|
||||||
|
function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strtolower')) {
|
||||||
|
function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strtoupper')) {
|
||||||
|
function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_substitute_character')) {
|
||||||
|
function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_substr')) {
|
||||||
|
function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_stripos')) {
|
||||||
|
function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_stristr')) {
|
||||||
|
function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strrchr')) {
|
||||||
|
function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strrichr')) {
|
||||||
|
function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strripos')) {
|
||||||
|
function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strrpos')) {
|
||||||
|
function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strstr')) {
|
||||||
|
function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_get_info')) {
|
||||||
|
function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_http_output')) {
|
||||||
|
function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_strwidth')) {
|
||||||
|
function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_substr_count')) {
|
||||||
|
function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_output_handler')) {
|
||||||
|
function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_http_input')) {
|
||||||
|
function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_convert_variables')) {
|
||||||
|
function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_ord')) {
|
||||||
|
function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_chr')) {
|
||||||
|
function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_scrub')) {
|
||||||
|
function mb_scrub($string, $encoding = null) { return p\Mbstring::mb_scrub($string, $encoding); }
|
||||||
|
}
|
||||||
|
if (!function_exists('mb_str_split')) {
|
||||||
|
function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_str_pad')) {
|
||||||
|
/** @return string|false */
|
||||||
|
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null) { return p\Mbstring::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_ucfirst')) {
|
||||||
|
/** @return string|false */
|
||||||
|
function mb_ucfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_ucfirst((string) $string, $encoding); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_lcfirst')) {
|
||||||
|
/** @return string|false */
|
||||||
|
function mb_lcfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_lcfirst((string) $string, $encoding); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_trim')) {
|
||||||
|
/** @return string|false */
|
||||||
|
function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_trim((string) $string, $characters, $encoding); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_ltrim')) {
|
||||||
|
/** @return string|false */
|
||||||
|
function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_ltrim((string) $string, $characters, $encoding); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_rtrim')) {
|
||||||
|
/** @return string|false */
|
||||||
|
function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_rtrim((string) $string, $characters, $encoding); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension_loaded('mbstring')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('MB_CASE_UPPER')) {
|
||||||
|
define('MB_CASE_UPPER', 0);
|
||||||
|
}
|
||||||
|
if (!defined('MB_CASE_LOWER')) {
|
||||||
|
define('MB_CASE_LOWER', 1);
|
||||||
|
}
|
||||||
|
if (!defined('MB_CASE_TITLE')) {
|
||||||
|
define('MB_CASE_TITLE', 2);
|
||||||
|
}
|
||||||
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user