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
|
||||
$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';
|
||||
if (!in_array($tab, $allowedTabs)) {
|
||||
$tab = 'suivi';
|
||||
}
|
||||
|
||||
$pageTitle = tr('budget_page_title');
|
||||
$activePage = "budget";
|
||||
@@ -30,7 +45,6 @@ require __DIR__ . '/header.php';
|
||||
<div class="pf-hero budget-hero">
|
||||
<div class="budget-title-group">
|
||||
<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') ?>">
|
||||
<span class="gear-icon-anim">⚙️</span>
|
||||
<?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?>
|
||||
@@ -45,26 +59,31 @@ require __DIR__ . '/header.php';
|
||||
<span><?= tr('budget_tab_tracking') ?></span>
|
||||
</a>
|
||||
|
||||
<?php if (in_array('budget_prev', $allowedTabs)): ?>
|
||||
<a href="?tab=budget_prev" class="tab-item <?= $tab == 'budget_prev' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">🎯</span>
|
||||
<span><?= tr('budget_tab_prev') ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (in_array('epargne', $allowedTabs)): ?>
|
||||
<a href="?tab=epargne" class="tab-item <?= $tab == 'epargne' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">🐷</span>
|
||||
<span><?= tr('budget_tab_savings') ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="?tab=recap" class="tab-item <?= $tab == 'recap' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">📊</span>
|
||||
<span><?= tr('budget_tab_recap') ?></span>
|
||||
</a>
|
||||
|
||||
<?php if (in_array('provisions', $allowedTabs)): ?>
|
||||
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">🧮</span>
|
||||
<span><?= tr('budget_tab_provisions') ?></span>
|
||||
</a>
|
||||
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</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 {
|
||||
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,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
enabled_modules JSON DEFAULT NULL,
|
||||
enabled_views JSON DEFAULT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
@@ -5,8 +5,26 @@ require __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
require_login('/login.php');
|
||||
|
||||
// Gestion de l'onglet actif
|
||||
$tab = $_GET['tab'] ?? 'meals';
|
||||
// 1. Détection des vues actives
|
||||
$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');
|
||||
$activePage = "food";
|
||||
@@ -21,30 +39,44 @@ require __DIR__ . '/header.php';
|
||||
<h1>🍳 <?= tr('mod_food_name') ?></h1>
|
||||
</div>
|
||||
|
||||
<?php if ($tab !== 'empty'): ?>
|
||||
<nav class="budget-tabs-container">
|
||||
<?php if (in_array('meals', $allowedTabs)): ?>
|
||||
<a href="?tab=meals" class="tab-item <?= $tab == 'meals' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">🍽️</span>
|
||||
<span><?= tr('food_tab_meals') ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (in_array('liste', $allowedTabs)): ?>
|
||||
<a href="?tab=liste" class="tab-item <?= $tab == 'liste' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">🛒</span>
|
||||
<span><?= tr('food_tab_list') ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<?php
|
||||
$allowedTabs = ['meals', 'liste'];
|
||||
if (in_array($tab, $allowedTabs)) {
|
||||
if ($tab === 'empty') {
|
||||
// É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";
|
||||
if (file_exists($viewPath)) {
|
||||
require $viewPath;
|
||||
} else {
|
||||
echo "<p>Vue introuvable : " . htmlspecialchars($tab) . "</p>";
|
||||
}
|
||||
} else {
|
||||
echo "<p>" . tr('error_occured') . "</p>";
|
||||
}
|
||||
?>
|
||||
</section>
|
||||
|
||||
+112
-13
@@ -410,25 +410,124 @@ body.pf-home .pf-mobile-close-btn {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Tuile module (checkbox) — état coché via :has(), compatible thème */
|
||||
.pf-module-tile {
|
||||
/* --- MODULES & SOUS-VUES UNIFIÉS (Settings) --- */
|
||||
.pf-module-card-wrapper {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
padding: 12px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
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;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: all 0.2s 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);
|
||||
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 {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
+102
-22
@@ -23,6 +23,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="apple-touch-icon" href="/favicon.png">
|
||||
<link rel="stylesheet" href="/global.css">
|
||||
@@ -122,19 +123,13 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
<main class="pf-main<?= ($mainClass ?? '') ? ' '.htmlspecialchars($mainClass) : '' ?>">
|
||||
|
||||
<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') ?>";
|
||||
|
||||
/**
|
||||
* 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); ?>;
|
||||
|
||||
// ✅ Injection sécurisée
|
||||
window.CONFIG = {
|
||||
// Liste complète des profils de la famille (Membres, Nounou, Enfants)
|
||||
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' ?>,
|
||||
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
|
||||
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
|
||||
@@ -144,14 +139,20 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
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(){
|
||||
const dark=document.documentElement.getAttribute('data-theme')==='dark';
|
||||
document.documentElement.setAttribute('data-theme',dark?'light':'dark');
|
||||
localStorage.setItem('hh-theme',dark?'light':'dark');
|
||||
}
|
||||
|
||||
// Gestion du menu mobile (Off-Canvas Sidebar)
|
||||
<?php if (isset($_SESSION['user'])): ?>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const burgerBtn = document.querySelector('.pf-burger-btn');
|
||||
@@ -174,14 +175,13 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
if(burgerBtn && mobileMenu && overlay && closeBtn) {
|
||||
burgerBtn.addEventListener('click', openMenu);
|
||||
closeBtn.addEventListener('click', closeMenu);
|
||||
// Fermer le menu si on clique en dehors (sur l'overlay sombre)
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
}
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
/**
|
||||
* pachaFetch : Utilitaire de requête robuste
|
||||
* pachaFetch : Utilitaire de requête robuste avec gestion Offline Cross-Browser
|
||||
*/
|
||||
async function pachaFetch(url, options = {}) {
|
||||
const finalUrl = url.startsWith('/') ? url.substring(1) : url;
|
||||
@@ -200,36 +200,116 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
try {
|
||||
return JSON.parse(rawText);
|
||||
} 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.");
|
||||
}
|
||||
} 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);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Utility : Toasts (Notifications)
|
||||
* Utilitaire PWA : Ajout à la file d'attente (IndexedDB)
|
||||
*/
|
||||
function showToast(message, type = 'success') {
|
||||
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') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `pf-toast pf-toast--${type}`;
|
||||
toast.innerHTML = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animation et suppression
|
||||
setTimeout(() => toast.classList.add('is-visible'), 100);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('is-visible');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Utility : Confirmation stylisée (Remplace confirm())
|
||||
*/
|
||||
async function pachaConfirm(title, message) {
|
||||
async function pachaConfirm(title, message) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'pf-modal open';
|
||||
@@ -248,5 +328,5 @@ async function pachaConfirm(title, message) {
|
||||
document.getElementById('confirm-cancel').onclick = () => { modal.remove(); resolve(false); };
|
||||
document.getElementById('confirm-ok').onclick = () => { modal.remove(); resolve(true); };
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
</script>
|
||||
+2
-1
@@ -35,7 +35,7 @@ function require_login(?string $loginPage = '/login.php'): void
|
||||
|
||||
$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,
|
||||
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
|
||||
LEFT JOIN families f ON f.id = u.family_id
|
||||
WHERE u.id = ?
|
||||
@@ -57,6 +57,7 @@ function require_login(?string $loginPage = '/login.php'): void
|
||||
$_SESSION['family_db'] = $user['db_name'];
|
||||
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
||||
$_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_planka_name' => '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_explore' => 'Explorar',
|
||||
@@ -586,7 +587,6 @@ return [
|
||||
'bs_currency_label' => 'Divisa de la llar',
|
||||
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
|
||||
'bs_currency_updated' => 'Divisa actualitzada correctament!',
|
||||
'bud_settings_dyn_title' => 'Estimacions Dinàmiques (Nens)',
|
||||
'bud_dyn_nanny_fixed' => 'Sou fix Cangur (€)',
|
||||
'bud_dyn_nanny_daily' => 'Despeses diàries de manteniment (€)',
|
||||
'bud_dyn_nanny_aid' => 'Ajuda del govern (€)',
|
||||
@@ -600,7 +600,16 @@ return [
|
||||
'bud_audit_child' => '👦 %s :',
|
||||
'bud_audit_school_fee' => 'Menjador (%dd) + Acollida (%dd) + Casal (%dd) = %.2f€',
|
||||
'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
|
||||
'bs_csv_title' => 'Format de la importació CSV',
|
||||
@@ -1012,9 +1021,9 @@ return [
|
||||
'meal_saved' => 'Menjar guardat!',
|
||||
'meal_btn_import_pdf' => 'Import Menu PDF',
|
||||
'meal_import_title' => 'Import canteen menu',
|
||||
'meal_import_child' => 'For which child?',
|
||||
'meal_select_child' => 'Select child',
|
||||
'meal_pdf_file' => 'PDF File (Convivio)',
|
||||
'meal_import_child' => 'Per quina persona?',
|
||||
'meal_select_child' => 'Sélectionner un membre :',
|
||||
'meal_pdf_file' => 'PDF File',
|
||||
'meal_drop_pdf' => 'Click to add PDF',
|
||||
'meal_import_success' => 'Menus imported successfully!',
|
||||
'mod_food_name' => 'Cuina i Compres',
|
||||
@@ -1023,4 +1032,13 @@ return [
|
||||
'menu_food' => 'Menjar',
|
||||
'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.',
|
||||
'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_server' => 'Server error: ',
|
||||
'bud_err_delete' => 'Error while deleting.',
|
||||
'budget_mandatory_note' => 'Tracking and Summary tabs are mandatory and always active.',
|
||||
|
||||
// Categories (Budget)
|
||||
'cat_income' => 'Income',
|
||||
@@ -588,7 +589,6 @@ return [
|
||||
'bs_currency_label' => 'Household Currency',
|
||||
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
|
||||
'bs_currency_updated' => 'Currency updated successfully!',
|
||||
'bud_settings_dyn_title' => 'Dynamic Estimates (Children)',
|
||||
'bud_dyn_nanny_fixed' => 'Nanny fixed salary (€)',
|
||||
'bud_dyn_nanny_daily' => 'Daily maintenance fee (€)',
|
||||
'bud_dyn_nanny_aid' => 'Government aid (€)',
|
||||
@@ -602,7 +602,16 @@ return [
|
||||
'bud_audit_child' => '👦 %s :',
|
||||
'bud_audit_school_fee' => 'Canteen (%dd) + Aftercare (%dd) + Center (%dd) = %.2f€',
|
||||
'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)
|
||||
'bs_csv_title' => 'CSV Import Format',
|
||||
@@ -1015,9 +1024,9 @@ return [
|
||||
'meal_saved' => 'Meal saved!',
|
||||
'meal_btn_import_pdf' => 'Import Menu PDF',
|
||||
'meal_import_title' => 'Import canteen menu',
|
||||
'meal_import_child' => 'For which child?',
|
||||
'meal_select_child' => 'Select child',
|
||||
'meal_pdf_file' => 'PDF File (Convivio)',
|
||||
'meal_import_child' => 'For which person?',
|
||||
'meal_select_child' => 'Sélectionner un membre :',
|
||||
'meal_pdf_file' => 'PDF File',
|
||||
'meal_drop_pdf' => 'Click to add PDF',
|
||||
'meal_import_success' => 'Menus imported successfully!',
|
||||
'mod_food_name' => 'Kitchen & Groceries',
|
||||
@@ -1026,4 +1035,14 @@ return [
|
||||
'menu_food' => 'Food',
|
||||
'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.',
|
||||
'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_planka_name' => '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_explore' => 'Explorer',
|
||||
@@ -588,7 +589,6 @@ return [
|
||||
'bs_currency_label' => 'Devise du Foyer',
|
||||
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
|
||||
'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_daily' => 'Frais d\'entretien journaliers (€)',
|
||||
'bud_dyn_nanny_aid' => 'Aide de l\'État (CMG) (€)',
|
||||
@@ -602,7 +602,16 @@ return [
|
||||
'bud_audit_child' => '👦 %s :',
|
||||
'bud_audit_school_fee' => 'Cantine (%dj) + Garderie (%dj) + Centre (%dj) = %.2f€',
|
||||
'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)
|
||||
'bs_csv_title' => 'Format de l\'import CSV',
|
||||
@@ -1011,9 +1020,9 @@ return [
|
||||
'meal_saved' => 'Repas enregistré !',
|
||||
'meal_btn_import_pdf' => 'Importer Menu PDF',
|
||||
'meal_import_title' => 'Importer un menu de cantine',
|
||||
'meal_import_child' => 'Pour quel enfant ?',
|
||||
'meal_select_child' => 'Sélectionner l\'enfant',
|
||||
'meal_pdf_file' => 'Fichier PDF (Convivio)',
|
||||
'meal_import_child' => 'Pour quel membre ?',
|
||||
'meal_select_child' => 'Sélectionner un membre :',
|
||||
'meal_pdf_file' => 'Fichier PDF',
|
||||
'meal_drop_pdf' => 'Cliquez pour ajouter le PDF',
|
||||
'meal_import_success' => 'Menus importés avec succès !',
|
||||
'mod_food_name' => 'Cuisine & Courses',
|
||||
@@ -1022,4 +1031,14 @@ return [
|
||||
'menu_food' => 'Food',
|
||||
'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.',
|
||||
'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');
|
||||
} else {
|
||||
$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,
|
||||
f.db_name, f.is_active as family_active, f.enabled_modules
|
||||
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.enabled_views
|
||||
FROM users u
|
||||
LEFT JOIN families f ON f.id = u.family_id
|
||||
WHERE u.username = ?
|
||||
@@ -51,6 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$_SESSION['family_db'] = $user['db_name'];
|
||||
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
||||
$_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" ---
|
||||
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
|
||||
// migrate.php
|
||||
require __DIR__ . '/includes/auth.php';
|
||||
|
||||
// 1. Initialisation autonome de la connexion Meta
|
||||
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();
|
||||
|
||||
// 4. Sécurisation de la page
|
||||
if (empty($_SESSION['user']['is_admin'])) {
|
||||
die("Accès refusé. Droits administrateur requis.");
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $meta_pdo->query("SELECT db_name FROM families");
|
||||
$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());
|
||||
}
|
||||
echo "<h1>Migration Meta</h1>";
|
||||
echo "🎉 L'infrastructure est à jour et la session a pu démarrer correctement.";
|
||||
-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;
|
||||
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;
|
||||
}
|
||||
|
||||
private function getSettingString(string $key, string $default = ''): string {
|
||||
return isset($this->settings[$key]) ? (string)$this->settings[$key] : $default;
|
||||
}
|
||||
|
||||
private function loadPeople(): void {
|
||||
// 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");
|
||||
@@ -85,7 +89,13 @@ class DynamicBudgetCalculator {
|
||||
|
||||
$zone = $this->foyerSettings['zone_scolaire'] ?? 'C';
|
||||
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);
|
||||
if ($jsonVacances) {
|
||||
$data = json_decode($jsonVacances, true);
|
||||
@@ -93,7 +103,9 @@ class DynamicBudgetCalculator {
|
||||
foreach ($data['results'] as $r) {
|
||||
$start = new DateTime(substr($r['start_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');
|
||||
|
||||
while ($start < $end) {
|
||||
$offDays['vacances'][] = $start->format('Y-m-d');
|
||||
$start->modify('+1 day');
|
||||
@@ -120,7 +132,16 @@ class DynamicBudgetCalculator {
|
||||
$nannyFixed = $this->getSetting('budget_nanny_fixed', 700);
|
||||
$nannyDaily = $this->getSetting('budget_nanny_daily', 4);
|
||||
$nannyAid = $this->getSetting('budget_nanny_aid', 120);
|
||||
|
||||
$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);
|
||||
$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
|
||||
for ($d = 1; $d <= $daysInMonth; $d++) {
|
||||
@@ -161,6 +182,8 @@ class DynamicBudgetCalculator {
|
||||
if (in_array($dateStr, $offDays['feries'])) continue;
|
||||
|
||||
$isVacances = in_array($dateStr, $offDays['vacances']);
|
||||
|
||||
|
||||
$events = $this->calendarEvents[$dateStr] ?? [];
|
||||
|
||||
$nannyOffToday = false;
|
||||
@@ -209,13 +232,15 @@ class DynamicBudgetCalculator {
|
||||
}
|
||||
|
||||
$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
|
||||
if ($hasNanny) {
|
||||
$totalCost += max(0, $nannyGlobalCost);
|
||||
// On passe $cesuLabelText au sprintf
|
||||
$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) {
|
||||
$details .= sprintf(tr('bud_audit_nanny_abs') . "\n", $this->helperName, $nannyOffDays);
|
||||
|
||||
@@ -205,7 +205,8 @@ try {
|
||||
if ($action === 'save_dynamic_estimates') {
|
||||
$keys = [
|
||||
'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();
|
||||
|
||||
@@ -224,10 +224,11 @@ if (!defined('CURRENCY')) {
|
||||
<!-- NOUVEAU PANE ENFANTS -->
|
||||
<div id="pane-children" class="bs-pane">
|
||||
<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)">
|
||||
<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>
|
||||
<label class="pf-label"><?= tr('bud_dyn_nanny_fixed') ?></label>
|
||||
@@ -247,7 +248,17 @@ if (!defined('CURRENCY')) {
|
||||
</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>
|
||||
<label class="pf-label"><?= tr('bud_dyn_school_meal') ?></label>
|
||||
@@ -263,8 +274,8 @@ if (!defined('CURRENCY')) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bs-text-right" style="margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save') ?></button>
|
||||
<div class="bs-text-right pf-mt-md">
|
||||
<button type="submit" class="pf-btn btn-secondary">💾 <?= tr('btn_save') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</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_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) {
|
||||
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>`;
|
||||
@@ -657,4 +672,79 @@ async function saveDynamicEstimates(e) {
|
||||
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>
|
||||
+117
-43
@@ -116,11 +116,35 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') {
|
||||
// ============================================================================
|
||||
$csvData = [];
|
||||
$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'];
|
||||
|
||||
// 🔥 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");
|
||||
|
||||
// 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 = [];
|
||||
try {
|
||||
$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){}
|
||||
|
||||
$existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){}
|
||||
fgetcsv($handle, 1000, ";", "\"", "\\");
|
||||
$existingRefs = [];
|
||||
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) {
|
||||
$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;
|
||||
fgetcsv($handle, 1000, $delimiter, "\"", "\\"); // Skip header
|
||||
|
||||
$dateParts = explode('/', $data[0]);
|
||||
$dateSql = (count($dateParts) == 3) ? $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0] : date('Y-m-d');
|
||||
$label = trim($data[1]) ?: trim($data[2]);
|
||||
$refCSV = trim($data[3]);
|
||||
while (($data = fgetcsv($handle, 1000, $delimiter, "\"", "\\")) !== FALSE) {
|
||||
// Si la ligne est vide on passe
|
||||
if (empty(array_filter($data))) continue;
|
||||
|
||||
$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);
|
||||
$isDuplicate = in_array($uniqueKey, $existingRefs);
|
||||
|
||||
@@ -160,6 +223,10 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
||||
}
|
||||
fclose($handle);
|
||||
$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)
|
||||
// ============================================================================
|
||||
// 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
|
||||
|
||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$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;
|
||||
$name = trim($item['name']);
|
||||
$catCode = $item['category'];
|
||||
@@ -280,39 +364,23 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
}
|
||||
if (!$isPaid) {
|
||||
$reste_a_venir_calc += $absAmount;
|
||||
// Ajouté dans le tableau avec un tooltip vide
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => 'Charge fixe en attente'];
|
||||
// On inclut les détails de l'interception dans le tooltip natif du suivi
|
||||
$tooltip = !empty($calcDetails) ? $calcDetails : 'Charge fixe en attente';
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => $tooltip];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. C'est une estimation (Variable)
|
||||
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[] = [
|
||||
'name' => $name,
|
||||
'amount' => $amt,
|
||||
'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
|
||||
// Pour ne pas tout casser, on donne tout le plafond visuel à la 1ère catégorie de la liste
|
||||
if (!empty($catCode)) {
|
||||
$catCodesArray = array_map('trim', explode(',', $catCode));
|
||||
$firstCat = $catCodesArray[0];
|
||||
@@ -369,9 +437,11 @@ foreach ($estimatesList as $est) {
|
||||
$spentForEstimate = 0;
|
||||
$detailsHover = [];
|
||||
|
||||
// On additionne les dépenses de toutes les catégories liées à cette estimation
|
||||
foreach ($est['categories'] as $cCode) {
|
||||
if (!empty($cCode) && isset($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])) {
|
||||
$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);
|
||||
if ($rem > 0) {
|
||||
$reste_a_venir_calc += $rem;
|
||||
$tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
||||
|
||||
// --- INJECTION DU DÉTAIL DYNAMIQUE ---
|
||||
if (!empty($est['details'])) {
|
||||
$tooltip .= "\n\n📊 Détail du calcul :\n" . $est['details'];
|
||||
}
|
||||
// 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';
|
||||
$tooltip = !empty($est['calc_details']) ? $est['calc_details'] . " | " . $baseTooltip : $baseTooltip;
|
||||
|
||||
$pending_charges[] = [
|
||||
'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>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($importError)): ?>
|
||||
<div class="pf-alert pf-alert--error" style="margin-bottom: 15px;">
|
||||
<?= htmlspecialchars($importError) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?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 style="font-size: 2.5rem; margin-bottom: 10px;">📄</div>
|
||||
<label class="pf-label"><?= tr('bud_csv_file') ?></label>
|
||||
@@ -763,7 +837,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
</div>
|
||||
</form>
|
||||
<?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">
|
||||
|
||||
<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.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-leave-snapshots.php",
|
||||
@@ -1132,8 +1132,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!zone || zone === "Autre") return [];
|
||||
try {
|
||||
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 data = await res.json();
|
||||
@@ -1196,7 +1197,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
try {
|
||||
const year = this.modalSelectedYear || this.currentSchoolYearStart;
|
||||
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 data = await res.json();
|
||||
|
||||
@@ -1127,3 +1127,16 @@
|
||||
display: block;
|
||||
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
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require dirname(__DIR__, 4) . '/vendor/autoload.php';
|
||||
require_login();
|
||||
|
||||
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 {
|
||||
// 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) {
|
||||
$content = file_get_contents($filename);
|
||||
$texts = [];
|
||||
|
||||
// 1. Décompression et Tokenization Mathématique
|
||||
if (preg_match_all('/stream(.*?)endstream/is', $content, $matches)) {
|
||||
foreach ($matches[1] as $stream) {
|
||||
$stream = ltrim($stream, "\r\n");
|
||||
$decoded = @gzuncompress($stream);
|
||||
if (!$decoded) $decoded = $stream;
|
||||
$parser = new \Smalot\PdfParser\Parser();
|
||||
$pdf = $parser->parseFile($filename);
|
||||
|
||||
// On éclate le flux par opérateurs PDF clés (Position et Texte)
|
||||
$parts = preg_split('/(Tm|Td|Tj|TJ)/', $decoded, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
$x = 0; $y = 0;
|
||||
|
||||
for ($i = 0; $i < count($parts); $i++) {
|
||||
$token = $parts[$i];
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($pdf->getPages() as $page) {
|
||||
foreach ($page->getDataTm() as $entry) {
|
||||
[$tm, $rawText] = $entry;
|
||||
$textVal = trim((string) $rawText);
|
||||
if ($textVal === '') continue;
|
||||
self::addText($texts, $textVal, (float) $tm[4], (float) $tm[5]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +51,7 @@ class NativePdfParser {
|
||||
$dayLabels = ['LUNDI', 'MARDI', 'MERCREDI', 'JEUDI', 'VENDREDI'];
|
||||
$colXs = [];
|
||||
foreach ($texts as $t) {
|
||||
$clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
|
||||
$clean = self::cleanLabel($t['text']);
|
||||
if (in_array($clean, $dayLabels)) {
|
||||
$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)
|
||||
$rowHeaders = [];
|
||||
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'])) {
|
||||
// On s'assure de ne capter que la colonne tout à gauche
|
||||
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)
|
||||
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) {
|
||||
if (!in_array('PLATCHAUD', array_column($rowHeaders, 'label')) || !in_array('GARNITURE', array_column($rowHeaders, 'label'))) {
|
||||
throw new Exception("Structure du menu non reconnue (Mots-clés 'PLAT CHAUD' et 'GARNITURE' introuvables).");
|
||||
}
|
||||
|
||||
// 4. Affectation Chirurgicale par Intersection Grille (X, Y)
|
||||
$days = [[], [], [], [], []];
|
||||
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"
|
||||
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
|
||||
$textVal = trim(preg_replace('/\b(AB|HVE|MSC|Label Rouge)\b/i', '', $t['text']));
|
||||
$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;
|
||||
|
||||
// Filtre Vertical : Est-on dans la tranche Plat Chaud ou Garniture ? (Tolérance +/- 5 unités)
|
||||
$inPlat = ($t['y'] <= $yPlatTop + 5 && $t['y'] >= $yPlatBot + 5);
|
||||
$inGar = ($t['y'] <= $yGarTop + 5 && $t['y'] >= $yGarBot + 5);
|
||||
// Filtre Vertical : de quelle rangée (en-tête) ce texte est-il le plus proche ?
|
||||
// Le texte d'une rangée déborde souvent au-dessus de son propre label (lignes
|
||||
// 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 ?
|
||||
$bestCol = 0;
|
||||
$minDiff = PHP_FLOAT_MAX;
|
||||
@@ -164,7 +132,7 @@ class NativePdfParser {
|
||||
$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) {
|
||||
// Décodage de l'octal \123
|
||||
$m = preg_replace_callback('/\\\\([0-7]{1,3})/', function($cb) { return chr(octdec($cb[1])); }, $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);
|
||||
// smalot/pdfparser décode déjà correctement les polices Type0/CID + ToUnicode :
|
||||
// il ne reste plus qu'à nettoyer les caractères parasites résiduels.
|
||||
$m = str_replace("\0", "", $rawText);
|
||||
$m = trim(preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\+&\'’\-]/u', '', $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)
|
||||
$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'];
|
||||
$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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,10 +167,10 @@ while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('meal_pdf_file') ?></label>
|
||||
<div class="meal-dropzone" onclick="document.getElementById('canteenPdf').click()">
|
||||
<span class="meal-dropzone-icon">📥</span>
|
||||
<div class="meal-dropzone" id="mealDropzone" onclick="document.getElementById('canteenPdf').click()">
|
||||
<span class="meal-dropzone-icon" id="mealDropzoneIcon">📥</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>
|
||||
|
||||
@@ -184,7 +184,7 @@ while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Sauvegarde automatique AJAX à chaque modification
|
||||
// 1. Sauvegarde automatique AJAX à chaque modification
|
||||
document.querySelectorAll('.js-meal-input').forEach(input => {
|
||||
input.addEventListener('change', async function() {
|
||||
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);
|
||||
});
|
||||
|
||||
// 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]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Focus Context Bar
|
||||
// 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: '' };
|
||||
|
||||
// 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) => {
|
||||
if(e.target.classList.contains('js-meal-input')) {
|
||||
copyState = {
|
||||
@@ -218,13 +293,20 @@ document.addEventListener('focusin', (e) => {
|
||||
fullDayName: ''
|
||||
};
|
||||
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) => {
|
||||
setTimeout(() => {
|
||||
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);
|
||||
});
|
||||
@@ -312,7 +394,7 @@ window.executeCopy = async function(targetDate, targetService) {
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_bulk');
|
||||
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);
|
||||
}
|
||||
|
||||
// NOUVEAU : Fonction Javascript d'Import PDF
|
||||
// Fonction Javascript d'Import PDF mise à jour
|
||||
window.importPdfMenu = async function(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
@@ -351,7 +433,80 @@ window.importPdfMenu = async function(e) {
|
||||
btn.disabled = false;
|
||||
btn.innerText = oldText;
|
||||
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>
|
||||
+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'] ?? '';
|
||||
|
||||
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])));
|
||||
$enabledViews = array_values(array_filter($allViews, fn($v) => isset($_POST['view_' . $v])));
|
||||
|
||||
if (empty($enabled)) {
|
||||
$error = tr('set_err_min_module');
|
||||
} else {
|
||||
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
|
||||
->execute([json_encode($enabled), $family_id]);
|
||||
$meta_pdo->prepare("UPDATE families SET enabled_modules = ?, enabled_views = ? WHERE id = ?")
|
||||
->execute([json_encode($enabled), json_encode($enabledViews), $family_id]);
|
||||
$_SESSION['enabled_modules'] = $enabled;
|
||||
$_SESSION['enabled_views'] = $enabledViews;
|
||||
$success = tr('set_success_modules');
|
||||
}
|
||||
}
|
||||
@@ -396,15 +401,34 @@ require __DIR__ . '/header.php';
|
||||
<p class="pf-muted-note"><?= tr('set_desc_modules') ?></p>
|
||||
<?php
|
||||
$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 = [
|
||||
'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')],
|
||||
'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')],
|
||||
'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')],
|
||||
'memo' => ['icon' => '📝', 'label' => tr('menu_memo')],
|
||||
'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')],
|
||||
'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')],
|
||||
'planka' => ['icon' => '📋', 'label' => tr('menu_planka')],
|
||||
@@ -415,11 +439,34 @@ require __DIR__ . '/header.php';
|
||||
<input type="hidden" name="action" value="set_modules">
|
||||
<div class="pf-stack-md">
|
||||
<?php foreach ($allModules as $key => $mod): $active = in_array($key, $enabledMods); ?>
|
||||
<label class="pf-module-tile">
|
||||
<input type="checkbox" name="mod_<?= $key ?>" <?= $active ? 'checked' : '' ?> class="pf-checkbox-lg">
|
||||
<div class="pf-module-card-wrapper">
|
||||
|
||||
<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-label"><?= $mod['label'] ?></span>
|
||||
</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; ?>
|
||||
</div>
|
||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
@@ -427,26 +474,6 @@ require __DIR__ . '/header.php';
|
||||
</section>
|
||||
<?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">
|
||||
<h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_ios') ?></h2>
|
||||
<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