commit 72701ad58c66df412acc645d000504cea0e10521 Author: pongpon pitisuk Date: Thu Jun 25 16:49:13 2026 +0700 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4021d6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store + +# Local environment / secrets +/config.php +.env + +# Runtime uploads and generated files +uploads/*.pdf +uploads/*.txt +uploads/*.csv diff --git a/api/add_manual_course.php b/api/add_manual_course.php new file mode 100644 index 0000000..6848078 --- /dev/null +++ b/api/add_manual_course.php @@ -0,0 +1,65 @@ + 0) { + // Check if course already exists under selected group + $stmt = $db->prepare("SELECT id FROM courses WHERE group_id = ? AND code = ?"); + $stmt->execute([$group_id, $course_code]); + $existing = $stmt->fetch(); + + if ($existing) { + $course_id = $existing['id']; + } else { + // Create a new course record under the selected group + $stmt = $db->prepare(" + INSERT INTO courses (group_id, code, name_th, credits, lecture_hours, practice_hours, self_study_hours, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?, 0) + "); + $stmt->execute([$group_id, $course_code, $course_name, $credits, $lecture, $practice, $self_study]); + $course_id = $db->lastInsertId(); + } + } + + $stmt = $db->prepare("INSERT INTO student_courses (student_id, course_id, course_code, course_name_th, credits, lecture_hours, practice_hours, self_study_hours, grade, course_type, semester, academic_year, source_type, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'manual', ?)"); + $stmt->execute([$student_id, $course_id, $course_code, $course_name, $credits, $lecture, $practice, $self_study, $grade ?: null, $course_type ?: null, $semester ?: null, $academic_year ?: null, $notes]); + header('Location: ' . BASE_URL . '/students/view.php?id=' . $student_id . '&added=1'); +} catch (PDOException $e) { + header('Location: ' . BASE_URL . '/students/view.php?id=' . $student_id . '&error=' . urlencode($e->getMessage())); +} +exit; diff --git a/api/delete_course.php b/api/delete_course.php new file mode 100644 index 0000000..b35c6e2 --- /dev/null +++ b/api/delete_course.php @@ -0,0 +1,20 @@ + 0) { + $stmt = getDB()->prepare("DELETE FROM student_courses WHERE id = ?"); + $stmt->execute([$id]); +} + +header('Location: ' . BASE_URL . '/students/view.php?id=' . $student_id); +exit; diff --git a/api/update_grade.php b/api/update_grade.php new file mode 100644 index 0000000..f292346 --- /dev/null +++ b/api/update_grade.php @@ -0,0 +1,22 @@ + 0) { + $stmt = getDB()->prepare("UPDATE student_courses SET grade = ? WHERE id = ?"); + $stmt->execute([$grade, $id]); +} + +header('Location: ' . BASE_URL . '/students/view.php?id=' . $student_id); +exit; diff --git a/api/upload_pdf.php b/api/upload_pdf.php new file mode 100644 index 0000000..040229b --- /dev/null +++ b/api/upload_pdf.php @@ -0,0 +1,105 @@ +prepare(" + SELECT c.id, c.code, c.name_th, c.credits, cg.name_th AS group_name + FROM courses c + JOIN course_groups cg ON c.group_id = cg.id + WHERE cg.curriculum_id = ? +"); +$curriculum_courses->execute([$curriculum_id]); +$curriculum_map = []; +foreach ($curriculum_courses->fetchAll() as $cc) { + $curriculum_map[$cc['code']] = $cc; +} + +$added = 0; +$skipped = 0; +$check_stmt = $db->prepare("SELECT COUNT(*) FROM student_courses WHERE student_id = ? AND course_code = ? AND source_type = 'transfer'"); + +foreach ($parsed_courses as $course) { + $code = $course['code']; + + // Only add courses that match the curriculum + if (!isset($curriculum_map[$code])) { + $skipped++; + continue; + } + + $cc = $curriculum_map[$code]; + + // Skip if already added + $check_stmt->execute([$student_id, $code]); + if ($check_stmt->fetchColumn() > 0) { + $skipped++; + continue; + } + + // Add the course as transfer credit + $stmt = $db->prepare("INSERT INTO student_courses (student_id, course_id, course_code, course_name_th, credits, lecture_hours, practice_hours, self_study_hours, grade, course_type, source_type, source_file) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'S', 'เทียบโอนรายวิชา', 'transfer', ?)"); + $stmt->execute([ + $student_id, + $cc['id'], + $code, + $cc['name_th'], + $cc['credits'], + $course['lecture'], + $course['practice'], + $course['self_study'], + $filename + ]); + $added++; +} + +$msg = $added > 0 ? "added=$added" : "added=0"; +header('Location: ' . BASE_URL . '/students/view.php?id=' . $student_id . '&' . $msg); +exit; diff --git a/api/upload_text.php b/api/upload_text.php new file mode 100644 index 0000000..3826188 --- /dev/null +++ b/api/upload_text.php @@ -0,0 +1,87 @@ +prepare(" + SELECT c.id, c.code, c.name_th, c.credits, cg.name_th AS group_name + FROM courses c + JOIN course_groups cg ON c.group_id = cg.id + WHERE cg.curriculum_id = ? +"); +$curriculum_courses->execute([$curriculum_id]); +$curriculum_map = []; +foreach ($curriculum_courses->fetchAll() as $cc) { + $curriculum_map[$cc['code']] = $cc; +} + +$added = 0; +$skipped = 0; +$check_stmt = $db->prepare("SELECT COUNT(*) FROM student_courses WHERE student_id = ? AND course_code = ? AND source_type = 'transfer'"); + +foreach ($parsed_courses as $course) { + $code = $course['code']; + + // Only add courses that match the curriculum + if (!isset($curriculum_map[$code])) { + $skipped++; + continue; + } + + $cc = $curriculum_map[$code]; + + // Skip if already added + $check_stmt->execute([$student_id, $code]); + if ($check_stmt->fetchColumn() > 0) { + $skipped++; + continue; + } + + // Add the course as transfer credit + $stmt = $db->prepare("INSERT INTO student_courses (student_id, course_id, course_code, course_name_th, credits, lecture_hours, practice_hours, self_study_hours, grade, course_type, source_type, source_file) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'S', 'เทียบโอนรายวิชา', 'transfer', 'pasted_text')"); + $stmt->execute([ + $student_id, + $cc['id'], + $code, + $cc['name_th'], + $cc['credits'], + $course['lecture'], + $course['practice'], + $course['self_study'] + ]); + $added++; +} + +$msg = $added > 0 ? "added=$added" : "added=0"; +header('Location: ' . BASE_URL . '/students/view.php?id=' . $student_id . '&' . $msg); +exit; diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..f70b323 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,380 @@ +:root { + --app-bg: #f4f7fb; + --app-surface: #ffffff; + --app-surface-soft: #f8fafc; + --app-border: #dbe4ef; + --app-text: #172033; + --app-muted: #64748b; + --app-primary: #1455a3; + --app-primary-dark: #0d376d; + --app-primary-soft: #e8f1fb; + --app-success: #16845b; + --app-warning: #c47a08; + --app-shadow: 0 12px 30px rgba(15, 23, 42, 0.08); + --app-radius: 8px; +} + +body { + font-family: 'Sarabun', 'Segoe UI', Tahoma, sans-serif; + background: + radial-gradient(circle at top left, rgba(20, 85, 163, 0.08), transparent 34rem), + linear-gradient(180deg, #f7faff 0%, var(--app-bg) 42%, #eef3f8 100%); + color: var(--app-text); + min-height: 100vh; +} + +.app-navbar { + background: linear-gradient(90deg, var(--app-primary-dark), var(--app-primary)); + box-shadow: 0 10px 28px rgba(13, 55, 109, 0.24); + padding-block: 0.65rem; +} + +.app-navbar .navbar-brand, +.app-navbar .nav-link { + color: rgba(255, 255, 255, 0.88); +} + +.app-navbar .nav-link { + border-radius: 6px; + font-weight: 500; + padding-inline: 0.85rem; +} + +.app-navbar .nav-link:hover, +.app-navbar .nav-link:focus { + background: rgba(255, 255, 255, 0.12); + color: #fff; +} + +.navbar-brand { + font-weight: 800; +} + +.user-chip { + background: rgba(255, 255, 255, 0.12); + color: #fff !important; +} + +.app-shell { + padding: 1.5rem; +} + +.page-heading { + align-items: center; + display: flex; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.page-heading h4, +.page-heading h5 { + color: var(--app-primary); + font-weight: 800; + margin: 0; +} + +.page-heading .text-muted { + color: rgba(20, 85, 163, 0.75) !important; + font-weight: 500; +} + +.card { + background: var(--app-surface); + border: 1px solid rgba(219, 228, 239, 0.9); + border-radius: var(--app-radius); + box-shadow: var(--app-shadow); + margin-bottom: 20px; +} + +.card-header { + background: var(--app-surface) !important; + border-bottom: 1px solid var(--app-border); + border-radius: var(--app-radius) var(--app-radius) 0 0 !important; + color: var(--app-text) !important; + font-weight: 700; + padding: 0.95rem 1.1rem; +} + +.card-header h4, +.card-header h5 { + color: inherit; + font-weight: 800; +} + +.card-header.bg-primary, +.card-header.bg-info, +.card-header.bg-success, +.card-header.bg-secondary, +.card-header.bg-warning { + border-left: 4px solid var(--app-primary); +} + +.card-header.bg-success { + border-left-color: var(--app-success); +} + +.card-header.bg-warning { + border-left-color: var(--app-warning); +} + +.dashboard-hero { + background: linear-gradient(135deg, #0d376d, #1455a3 56%, #1570b8); + border: 0; + color: #fff; + overflow: hidden; + position: relative; +} + +.dashboard-hero .card-header, +.dashboard-hero .card-body { + background: transparent !important; + border: 0; + color: #fff !important; + position: relative; + z-index: 1; +} + +.stat-card { + border: none; + border-radius: 16px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05); + overflow: hidden; + position: relative; + transition: transform 0.25s ease, box-shadow 0.25s ease; + min-height: 120px; +} + +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.1); +} + +.stat-card .card-body { + padding: 1.25rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.stat-card.card-curriculum { + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + color: #ffffff; +} + +.stat-card.card-dt { + background: linear-gradient(135deg, #1d3557 0%, #457b9d 100%); + color: #ffffff; +} + +.stat-card.card-dbc { + background: linear-gradient(135deg, #134e5e 0%, #71b280 100%); + color: #ffffff; +} + +.stat-card.card-all { + background: linear-gradient(135deg, #4b6cb7 0%, #182848 100%); + color: #ffffff; +} + +.stat-card.card-transfer { + background: linear-gradient(135deg, #d35400 0%, #f1c40f 100%); + color: #ffffff; +} + +.stat-card .stat-label { + font-size: 0.95rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.stat-card.card-curriculum .stat-label, +.stat-card.card-dt .stat-label, +.stat-card.card-dbc .stat-label, +.stat-card.card-all .stat-label, +.stat-card.card-transfer .stat-label { + color: rgba(255, 255, 255, 0.85); +} + +.stat-card .stat-value { + font-size: 2.25rem; + font-weight: 800; + line-height: 1.1; +} + +.stat-card.card-curriculum .stat-value, +.stat-card.card-dt .stat-value, +.stat-card.card-dbc .stat-value, +.stat-card.card-all .stat-value, +.stat-card.card-transfer .stat-value { + color: #ffffff; +} + +.stat-img-wrapper { + align-items: center; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border-radius: 18px; + display: flex; + height: 4.8rem; + justify-content: center; + width: 4.8rem; + padding: 0.5rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + flex-shrink: 0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.stat-card:hover .stat-img-wrapper { + transform: scale(1.1) rotate(5deg); +} + +.stat-img { + height: 100%; + object-fit: contain; + width: 100%; +} + +.summary-card { + background: var(--app-surface); + color: var(--app-text); +} + +.table { + vertical-align: middle; +} + +.table th { + background-color: var(--app-surface-soft); + color: #334155; + font-size: 0.88rem; + font-weight: 700; + white-space: nowrap; +} + +.table td { + color: #243044; +} + +.table-hover tbody tr:hover { + background-color: #f2f7fd; +} + +.table-responsive { + border: 1px solid var(--app-border); + border-radius: var(--app-radius); +} + +.table-responsive .table { + margin-bottom: 0; +} + +.btn { + border-radius: 7px; + font-weight: 600; +} + +.btn-primary, +.bg-primary { + background-color: var(--app-primary) !important; + border-color: var(--app-primary) !important; +} + +.btn-success, +.bg-success { + background-color: var(--app-success) !important; + border-color: var(--app-success) !important; +} + +.btn-warning, +.bg-warning { + background-color: #f6c453 !important; + border-color: #f6c453 !important; +} + +.btn-info, +.bg-info { + background-color: #2c7fb8 !important; + border-color: #2c7fb8 !important; +} + +.form-control, +.form-select { + border-color: #cfd9e6; + border-radius: 7px; +} + +.form-control:focus, +.form-select:focus { + border-color: var(--app-primary); + box-shadow: 0 0 0 0.22rem rgba(20, 85, 163, 0.14); +} + +.badge { + border-radius: 999px; + font-weight: 700; +} + +.badge-credit { + background-color: var(--app-primary-soft); + color: var(--app-primary); + padding: 4px 10px; + border-radius: 999px; + font-size: 0.85rem; + font-weight: 700; +} + +.curriculum-tree { + padding-left: 20px; + border-left: 3px solid var(--app-primary); +} + +.course-item { + border-left: 3px solid transparent; + border-radius: 6px; + transition: background-color 0.2s, border-color 0.2s; +} + +.course-item:hover { + background-color: #eef5ff; + border-left-color: var(--app-primary); +} + +.status-badge { + font-size: 0.8rem; +} + +.action-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.soft-panel { + background: var(--app-surface-soft); + border: 1px solid var(--app-border); + border-radius: var(--app-radius); + padding: 1rem; +} + +@media (max-width: 767.98px) { + .app-shell { + padding: 1rem; + } + + .page-heading { + align-items: stretch; + flex-direction: column; + } + + .card-header.d-flex { + align-items: flex-start !important; + flex-direction: column; + gap: 0.75rem; + } + + .stat-value { + font-size: 1.8rem; + } +} diff --git a/assets/js/script.js b/assets/js/script.js new file mode 100644 index 0000000..9d05e54 --- /dev/null +++ b/assets/js/script.js @@ -0,0 +1,21 @@ +$(function() { + $('.table-responsive').each(function() { + if ($(this).find('table').width() > $(this).width()) { + $(this).append('
เลื่อนดูตารางด้านข้างได้
'); + } + }); + + // Fail-safe tab switcher for import tabs + $('#importTab button').on('click', function(e) { + e.preventDefault(); + + // Switch active tab class + $('#importTab button').removeClass('active').attr('aria-selected', 'false'); + $(this).addClass('active').attr('aria-selected', 'true'); + + // Switch active pane class + var target = $(this).attr('data-bs-target'); + $('#importTabContent .tab-pane').removeClass('show active'); + $(target).addClass('show active'); + }); +}); diff --git a/config.example.php b/config.example.php new file mode 100644 index 0000000..aed7b8a --- /dev/null +++ b/config.example.php @@ -0,0 +1,59 @@ +prepare("SELECT * FROM curricula WHERE code = 'DBC'"); +$curriculum->execute(); +$curriculum = $curriculum->fetch(); + +if (!$curriculum) { + echo '
ไม่พบข้อมูลหลักสูตร กรุณา import ข้อมูลก่อน
'; + require_once __DIR__ . '/../includes/footer.php'; + exit; +} + +$tree = buildGroupTree($curriculum['id']); +$is_dbc = true; +?> +
+
+

หลักสูตรบริหารธุรกิจบัณฑิต สาขาวิชาคอมพิวเตอร์ธุรกิจดิจิทัล (DBC)

+
+
+
+
จำนวนหน่วยกิตรวม: หน่วยกิต
+
+
+ +
+
+ + +'; + echo ''; + echo htmlspecialchars($g['name_th']); + if ($g['min_credits'] > 0) { + echo ' (ไม่น้อยกว่า ' . $g['min_credits'] . ' หน่วยกิต)'; + } + echo ''; + + if ($has_courses) { + echo '
'; + echo ''; + echo ''; + echo ''; + foreach ($node['courses'] as $c) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
รหัสวิชาชื่อวิชาชื่อวิชา (EN)หน่วยกิต
' . htmlspecialchars($c['code']) . '' . htmlspecialchars($c['name_th']) . '' . htmlspecialchars($c['name_en']) . '' . $c['credits'] . '(' . $c['lecture_hours'] . '-' . $c['practice_hours'] . '-' . $c['self_study_hours'] . ')
'; + } + + if ($has_children) { + echo '
'; + renderCurriculumTree($node['children'], $is_dbc, $level + 1); + echo '
'; + } + + echo ''; + } +} diff --git a/curriculum/dt.php b/curriculum/dt.php new file mode 100644 index 0000000..4ae02db --- /dev/null +++ b/curriculum/dt.php @@ -0,0 +1,79 @@ +prepare("SELECT * FROM curricula WHERE code = 'DT'"); +$curriculum->execute(); +$curriculum = $curriculum->fetch(); + +if (!$curriculum) { + echo '
ไม่พบข้อมูลหลักสูตร กรุณา import ข้อมูลก่อน
'; + require_once __DIR__ . '/../includes/footer.php'; + exit; +} + +$tree = buildGroupTree($curriculum['id']); +$is_dbc = false; +?> +
+
+

หลักสูตรวิทยาศาสตรบัณฑิต สาขาวิชาเทคโนโลยีดิจิทัล (DT)

+
+
+
+
จำนวนหน่วยกิตรวม: หน่วยกิต
+
+
+ +
+
+ + +'; + echo ''; + echo htmlspecialchars($g['name_th']); + if ($g['min_credits'] > 0) { + echo ' (ไม่น้อยกว่า ' . $g['min_credits'] . ' หน่วยกิต)'; + } + echo ''; + + if ($has_courses) { + echo '
'; + echo ''; + echo ''; + echo ''; + foreach ($node['courses'] as $c) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
รหัสวิชาชื่อวิชาชื่อวิชา (EN)หน่วยกิต
' . htmlspecialchars($c['code']) . '' . htmlspecialchars($c['name_th']) . '' . htmlspecialchars($c['name_en']) . '' . $c['credits'] . '(' . $c['lecture_hours'] . '-' . $c['practice_hours'] . '-' . $c['self_study_hours'] . ')
'; + } + + if ($has_children) { + echo '
'; + renderCurriculumTree($node['children'], $is_dbc, $level + 1); + echo '
'; + } + + echo ''; + } +} diff --git a/db.php b/db.php new file mode 100644 index 0000000..fb3667a --- /dev/null +++ b/db.php @@ -0,0 +1,23 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ] + ); + } catch (PDOException $e) { + die("Database connection failed: " . $e->getMessage()); + } + } + return $pdo; +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..637e2ca --- /dev/null +++ b/deploy.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# 🚀 Script for deploying transfer credit files to Tapi University Server + +HOST="110.164.69.50" +USER="pongpon" +PASS="P@ngP@n29012520" +REMOTE_STAGING="/home/pongpon/transfer_deploy" +REMOTE_LIVE="/var/www/html/transfer" + +echo "--------------------------------------------------------" +echo "📦 1. Packaging modified files..." +echo "--------------------------------------------------------" +tar -czf deploy.tar.gz \ + index.php \ + includes/header.php \ + includes/functions.php \ + students/list.php \ + students/add.php \ + students/delete.php \ + students/import_csv.php \ + students/view.php \ + students/edit.php \ + curriculum/dt.php \ + curriculum/dbc.php \ + users/profile.php \ + users/profile_edit.php \ + api/add_manual_course.php \ + api/delete_course.php \ + api/update_grade.php \ + api/upload_pdf.php \ + api/upload_text.php \ + pic/dashboard_curriculum.png \ + pic/dashboard_student_dt.png \ + pic/dashboard_student_dbc.png \ + pic/dashboard_student_all.png \ + pic/dashboard_transfer_credits.png + +if [ $? -ne 0 ]; then + echo "❌ Error: Failed to create tarball." + exit 1 +fi +echo "✓ Tarball deploy.tar.gz created successfully." + +echo "" +echo "--------------------------------------------------------" +echo "📤 2. Uploading tarball to server..." +echo "🔑 Please enter password: $PASS" +echo "--------------------------------------------------------" +scrap_file="deploy.tar.gz" +scp $scrap_file $USER@$HOST:$REMOTE_STAGING.tar.gz + +if [ $? -ne 0 ]; then + echo "❌ Error: SCP upload failed." + rm -f deploy.tar.gz + exit 1 +fi +echo "✓ Upload completed." + +echo "" +echo "--------------------------------------------------------" +echo "⚙ 3. Extracting and deploying to live webroot..." +echo "🔑 Password for ssh and sudo is: $PASS" +echo "--------------------------------------------------------" +ssh $USER@$HOST " + echo 'Extracting staging files...' && \ + rm -rf $REMOTE_STAGING && \ + mkdir -p $REMOTE_STAGING && \ + tar -xzf $REMOTE_STAGING.tar.gz -C $REMOTE_STAGING && \ + rm -f $REMOTE_STAGING.tar.gz && \ + \ + echo 'Copying files and setting permissions...' && \ + echo '$PASS' | sudo -S bash -c \" + mkdir -p $REMOTE_LIVE/includes $REMOTE_LIVE/students $REMOTE_LIVE/curriculum $REMOTE_LIVE/users $REMOTE_LIVE/api $REMOTE_LIVE/pic && \ + cp $REMOTE_STAGING/index.php $REMOTE_LIVE/index.php && \ + cp $REMOTE_STAGING/includes/header.php $REMOTE_LIVE/includes/header.php && \ + cp $REMOTE_STAGING/includes/functions.php $REMOTE_LIVE/includes/functions.php && \ + cp $REMOTE_STAGING/students/list.php $REMOTE_LIVE/students/list.php && \ + cp $REMOTE_STAGING/students/add.php $REMOTE_LIVE/students/add.php && \ + cp $REMOTE_STAGING/students/delete.php $REMOTE_LIVE/students/delete.php && \ + cp $REMOTE_STAGING/students/import_csv.php $REMOTE_LIVE/students/import_csv.php && \ + cp $REMOTE_STAGING/students/view.php $REMOTE_LIVE/students/view.php && \ + cp $REMOTE_STAGING/students/edit.php $REMOTE_LIVE/students/edit.php && \ + cp $REMOTE_STAGING/curriculum/dt.php $REMOTE_LIVE/curriculum/dt.php && \ + cp $REMOTE_STAGING/curriculum/dbc.php $REMOTE_LIVE/curriculum/dbc.php && \ + cp $REMOTE_STAGING/users/profile.php $REMOTE_LIVE/users/profile.php && \ + cp $REMOTE_STAGING/users/profile_edit.php $REMOTE_LIVE/users/profile_edit.php && \ + cp $REMOTE_STAGING/api/add_manual_course.php $REMOTE_LIVE/api/add_manual_course.php && \ + cp $REMOTE_STAGING/api/delete_course.php $REMOTE_LIVE/api/delete_course.php && \ + cp $REMOTE_STAGING/api/update_grade.php $REMOTE_LIVE/api/update_grade.php && \ + cp $REMOTE_STAGING/api/upload_pdf.php $REMOTE_LIVE/api/upload_pdf.php && \ + cp $REMOTE_STAGING/api/upload_text.php $REMOTE_LIVE/api/upload_text.php && \ + cp $REMOTE_STAGING/pic/dashboard_curriculum.png $REMOTE_LIVE/pic/dashboard_curriculum.png && \ + cp $REMOTE_STAGING/pic/dashboard_student_dt.png $REMOTE_LIVE/pic/dashboard_student_dt.png && \ + cp $REMOTE_STAGING/pic/dashboard_student_dbc.png $REMOTE_LIVE/pic/dashboard_student_dbc.png && \ + cp $REMOTE_STAGING/pic/dashboard_student_all.png $REMOTE_LIVE/pic/dashboard_student_all.png && \ + cp $REMOTE_STAGING/pic/dashboard_transfer_credits.png $REMOTE_LIVE/pic/dashboard_transfer_credits.png && \ + chown -R www-data:www-data $REMOTE_LIVE/index.php $REMOTE_LIVE/includes/header.php $REMOTE_LIVE/includes/functions.php $REMOTE_LIVE/students $REMOTE_LIVE/curriculum $REMOTE_LIVE/users $REMOTE_LIVE/api $REMOTE_LIVE/pic && \ + chmod 644 $REMOTE_LIVE/index.php $REMOTE_LIVE/includes/header.php $REMOTE_LIVE/includes/functions.php $REMOTE_LIVE/students/*.php $REMOTE_LIVE/curriculum/*.php $REMOTE_LIVE/users/*.php $REMOTE_LIVE/api/*.php $REMOTE_LIVE/pic/*.png && \ + chmod 755 $REMOTE_LIVE/students $REMOTE_LIVE/curriculum $REMOTE_LIVE/users $REMOTE_LIVE/api $REMOTE_LIVE/pic + \" && \ + \ + echo '✓ Deployment completed successfully!' +" + +# Clean up local tarball +rm -f deploy.tar.gz +echo "" +echo "🎉 Done! All changes are live on transfer.tapee.ac.th!" diff --git a/identify_images.html b/identify_images.html new file mode 100644 index 0000000..3ae4d5c --- /dev/null +++ b/identify_images.html @@ -0,0 +1,31 @@ + + + + + Identify Images + + + +
+ Image 1 +

media__1781942624014.png

+
+
+ Image 2 +

media__1781942624039.png

+
+
+ Image 3 +

media__1781942624659.png

+
+
+ Image 4 +

media__1781942624664.png

+
+ + diff --git a/import_curriculum.php b/import_curriculum.php new file mode 100644 index 0000000..9c70740 --- /dev/null +++ b/import_curriculum.php @@ -0,0 +1,434 @@ +exec("INSERT INTO curricula (code, name_th, name_en, total_credits) VALUES +('DT', 'เทคโนโลยีดิจิทัล', 'Digital Technology', 121)"); +$dt_id = $pdo->lastInsertId(); + +// --- หมวดวิชาศึกษาทั่วไป DT --- +$pdo->exec("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES +($dt_id, NULL, 'DT_GEN', 'หมวดวิชาศึกษาทั่วไป', 24, 1)"); +$dt_gen_id = $pdo->lastInsertId(); + +$groups_ge = [ + ['DT_GEN1', 'กลุ่มสาระที่ 1 การคิดและการแก้ปัญหา', 6, 1], + ['DT_GEN2', 'กลุ่มสาระที่ 2 การใช้ชีวิตอยู่ร่วมกับผู้อื่น', 6, 2], + ['DT_GEN3', 'กลุ่มสาระที่ 3 การสื่อสารระหว่างบุคคล', 6, 3], + ['DT_GEN4', 'กลุ่มสาระที่ 4 การใช้เทคโนโลยีดิจิทัลและปัญญาประดิษฐ์', 6, 4], +]; +foreach ($groups_ge as $g) { + $pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dt_id, $dt_gen_id, $g[0], $g[1], $g[2], $g[3]]); +} + +// --- หมวดวิชาเฉพาะ DT --- +$pdo->exec("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES +($dt_id, NULL, 'DT_SPEC', 'หมวดวิชาเฉพาะ', 91, 2)"); +$dt_spec_id = $pdo->lastInsertId(); + +// กลุ่มวิชาแกน +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dt_id, $dt_spec_id, 'DT_CORE', 'กลุ่มวิชาแกน', 9, 1]); +$dt_core_id = $pdo->lastInsertId(); + +// กลุ่มวิชาเฉพาะด้าน +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dt_id, $dt_spec_id, 'DT_MAJOR', 'กลุ่มวิชาเฉพาะด้าน', 75, 2]); +$dt_major_id = $pdo->lastInsertId(); + +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dt_id, $dt_major_id, 'DT_MAJOR_REQ', 'กลุ่มวิชาบังคับ', 60, 1]); +$dt_major_req_id = $pdo->lastInsertId(); + +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dt_id, $dt_major_id, 'DT_MAJOR_ELEC', 'กลุ่มวิชาเลือกทางเทคโนโลยีดิจิทัล', 15, 2]); +$dt_major_elec_id = $pdo->lastInsertId(); + +// กลุ่มวิชาพื้นฐานวิชาชีพและวิชาชีพ +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dt_id, $dt_spec_id, 'DT_PROF', 'กลุ่มวิชาพื้นฐานวิชาชีพและวิชาชีพ', 7, 3]); +$dt_prof_id = $pdo->lastInsertId(); + +// --- หมวดวิชาเลือกเสรี DT --- +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dt_id, NULL, 'DT_FREE', 'หมวดวิชาเลือกเสรี', 6, 3]); +$dt_free_id = $pdo->lastInsertId(); + +// ============================================================ +// 2. หลักสูตรคอมพิวเตอร์ธุรกิจดิจิทัล (DBC) +// ============================================================ +$pdo->exec("INSERT INTO curricula (code, name_th, name_en, total_credits) VALUES +('DBC', 'คอมพิวเตอร์ธุรกิจดิจิทัล', 'Digital Business Computer', 121)"); +$dbc_id = $pdo->lastInsertId(); + +// --- หมวดวิชาศึกษาทั่วไป DBC --- +$pdo->exec("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES +($dbc_id, NULL, 'DBC_GEN', 'หมวดวิชาศึกษาทั่วไป', 24, 1)"); +$dbc_gen_id = $pdo->lastInsertId(); + +foreach ($groups_ge as $g) { + $pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dbc_id, $dbc_gen_id, 'DBC_' . $g[0], $g[1], $g[2], $g[3]]); +} + +// --- หมวดวิชาเฉพาะ DBC --- +$pdo->exec("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES +($dbc_id, NULL, 'DBC_SPEC', 'หมวดวิชาเฉพาะ', 91, 2)"); +$dbc_spec_id = $pdo->lastInsertId(); + +// กลุ่มวิชาแกนธุรกิจ +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dbc_id, $dbc_spec_id, 'DBC_CORE', 'กลุ่มวิชาแกนธุรกิจ', 30, 1]); +$dbc_core_id = $pdo->lastInsertId(); + +// กลุ่มวิชาเฉพาะด้าน +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dbc_id, $dbc_spec_id, 'DBC_MAJOR', 'กลุ่มวิชาเฉพาะด้าน', 54, 2]); +$dbc_major_id = $pdo->lastInsertId(); + +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dbc_id, $dbc_major_id, 'DBC_MAJOR_REQ', 'กลุ่มวิชาบังคับ', 45, 1]); +$dbc_major_req_id = $pdo->lastInsertId(); + +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dbc_id, $dbc_major_id, 'DBC_MAJOR_ELEC', 'กลุ่มวิชาเลือก', 9, 2]); +$dbc_major_elec_id = $pdo->lastInsertId(); + +// กลุ่มวิชาพื้นฐานวิชาชีพและวิชาชีพ +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dbc_id, $dbc_spec_id, 'DBC_PROF', 'กลุ่มวิชาพื้นฐานวิชาชีพและวิชาชีพ', 7, 3]); +$dbc_prof_id = $pdo->lastInsertId(); + +// --- หมวดวิชาเลือกเสรี DBC --- +$pdo->prepare("INSERT INTO course_groups (curriculum_id, parent_id, code, name_th, min_credits, sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$dbc_id, NULL, 'DBC_FREE', 'หมวดวิชาเลือกเสรี', 6, 3]); + +// ============================================================ +// 3. เพิ่มรายวิชา DT +// ============================================================ + +// Helper function +function add_course($pdo, $group_id, $code, $name_th, $name_en, $credits, $lecture, $practice, $self, $order) { + $stmt = $pdo->prepare("INSERT INTO courses (group_id, code, name_th, name_en, credits, lecture_hours, practice_hours, self_study_hours, sort_order) VALUES (?,?,?,?,?,?,?,?,?)"); + $stmt->execute([$group_id, $code, $name_th, $name_en, $credits, $lecture, $practice, $self, $order]); +} + +// DT Get group IDs +$dt_ge_groups = []; +$stmt = $pdo->query("SELECT id, code FROM course_groups WHERE parent_id = $dt_gen_id ORDER BY sort_order"); +foreach ($stmt->fetchAll() as $r) { + $dt_ge_groups[$r['code']] = $r['id']; +} + +// DT GEN1 - การคิดและการแก้ปัญหา +$gid = $dt_ge_groups['DT_GEN1']; +$courses = [ + ['0001101','จิตวิทยาทั่วไป','General Psychology',3,3,0,6], + ['0001102','สังคม เศรษฐกิจและการเมือง','Society Economic and Politics',3,3,0,6], + ['0001103','ภูมิปัญญาท้องถิ่น','Local Wisdom',3,3,0,6], + ['0001104','ไทยในพลวัตอาเซียน','The Thai in ASEAN Dynamics',3,3,0,6], + ['0001105','บัณฑิตในอุดมคติไทย','Graduate Thai Ideal',3,3,0,6], + ['0001106','กฎหมายในชีวิตประจำวัน','Law in Everyday Life',3,3,0,6], + ['0001107','ฉลาดคิด','Smart Thinking',3,3,0,6], + ['0001108','ศาสตร์พระราชาเพื่อการบูรณาการที่ยั่งยืน','King Philosophy for Sustainable Integration',3,3,0,6], + ['0001109','นวัตกรสังคมเพื่อการพัฒนาท้องถิ่น','Social Innovators for Local Development',3,2,2,5], + ['0001110','คณิตศาสตร์และสถิติในชีวิตประจำวัน','Mathematics and Statistics for Daily Life',3,3,0,6], + ['0001111','ชีวิตกับสิ่งแวดล้อม','Life and Environment',3,3,0,6], + ['0001112','วิทยาศาสตร์และเทคโนโลยีเพื่อคุณภาพชีวิตและสังคม','Science and Technology for Quality of Life and Society',3,3,0,6], + ['0001113','ความรู้พื้นฐานทางคณิตศาสตร์ วิทยาศาสตร์ และเทคโนโลยีเพื่อการดำรงชีวิต','Fundamental Knowledge in Mathematics Science and Technology for Everyday Life',6,6,0,12], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DT GEN2 - การใช้ชีวิตอยู่ร่วมกับผู้อื่น +$gid = $dt_ge_groups['DT_GEN2']; +$courses = [ + ['0002101','องค์รวมแห่งชีวิต','Holistic Approaches to Life',3,3,0,6], + ['0002102','มนุษย์สัมพันธ์และการพัฒนาบุคลิกภาพ','Human Relations and Personality Development',3,3,0,6], + ['0002103','สารสนเทศเพื่อการศึกษาและค้นคว้า','Information for Education and Research',3,3,0,6], + ['0002104','ความเป็นพลเมืองและจิตสาธารณะ','Citizenship and Public Consciousness',3,3,0,6], + ['0002105','สันติศึกษา','Peace Studies',3,3,0,6], + ['0002106','ทักษะชีวิต','Life Skills',3,3,0,6], + ['0002107','การเรียนรู้ในระดับอุดมศึกษา','University Study',3,3,0,6], + ['0002108','รู้ทันสุขภาพ','Health Literacy',3,2,2,5], + ['0002109','พลเมืองโลก','Global Citizens',3,3,0,6], + ['0002110','วิถีชีวิตที่ยั่งยืน','Sustainable Lifestyles',3,3,0,6], + ['0002111','ใส่ใจภัยพิบัติในโลกสมัยใหม่','Disaster Intentions in the Modern World',3,3,0,6], + ['0002112','งานช่างในชีวิตประจำวัน','Engineering Work in Daily Life',3,2,2,5], + ['0002113','สมาธิเพื่อพัฒนาชีวิต','Meditation for Life Development',3,3,0,6], + ['0002114','ทักษะชีวิตเพื่อการอยู่ร่วมกันอย่างยั่งยืน','Life Skills for Sustainable Living and Coexistence',3,3,0,6], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DT GEN3 - การสื่อสารระหว่างบุคคล +$gid = $dt_ge_groups['DT_GEN3']; +$courses = [ + ['0003101','ภาษาอังกฤษพื้นฐาน','Fundamental English',3,3,0,6], + ['0003102','การใช้ภาษาอังกฤษ','English Usage',3,3,0,6], + ['0003103','การพัฒนาทักษะการอ่านและเขียนภาษาอังกฤษ','Development of Reading and Writing Skills in English',3,3,0,6], + ['0003104','ภาษาอังกฤษเพื่อการสื่อสารในบริบทสากล','Communicative English in Global Context',3,3,0,6], + ['0003105','ภาษาอังกฤษในบริบทการทำงานอย่างมีประสิทธิภาพ','Effective English in Professional Contexts',3,3,0,6], + ['0003106','ภาษาจีนเพื่อการสื่อสาร','Chinese for Communication',3,3,0,6], + ['0003107','ภาษาฝรั่งเศสเบื้องต้น','Introduction to French',3,3,0,6], + ['0003108','ภาษาญี่ปุ่นเบื้องต้น','Introduction to Japanese',3,3,0,6], + ['0003109','ภาษาญี่ปุ่นเพื่อการสื่อสาร','Japanese for Communication',3,3,0,6], + ['0003110','ภาษามาเลย์เพื่อการสื่อสาร','Malay for Communication',3,3,0,6], + ['0003111','ภาษาอินโดนีเซียเพื่อการสื่อสาร','Indonesian for Communication',3,3,0,6], + ['0003112','การพัฒนาทักษะทางภาษาไทย','Development of Thai Language Skills',3,3,0,6], + ['0003113','ภาษาไทยเพื่อการสื่อสาร','Thai for Communication',3,3,0,6], + ['0003114','ภาษาไทยถิ่นและภาษาเฉพาะถิ่นในสังคมไทย','Thai Dialects and Ethnic Languages in Thai Society',3,3,0,6], + ['0003115','ทักษะภาษาเพื่อการสื่อสารในชีวิตประจำวัน','Language Skills for Everyday Communication',6,6,0,12], + ['0003116','ภาษาเพื่อวิชาชีพและการทำงาน','Professional and Workplace Communication',6,6,0,12], + ['0003117','ภาษาเพื่อวัฒนธรรมและความเข้าใจระหว่างประเทศ','Language and Intercultural Understanding',6,6,0,12], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DT GEN4 - การใช้เทคโนโลยีดิจิทัลและปัญญาประดิษฐ์ +$gid = $dt_ge_groups['DT_GEN4']; +$courses = [ + ['0004101','เทคโนโลยีการสื่อสารกับมนุษย์','Communication Technology and Human',3,3,0,6], + ['0004102','เทคโนโลยีดิจิทัลและปัญญาประดิษฐ์เพื่อชีวิตวิถีใหม่','Digital Technology and AI for the New Normal',3,2,2,6], + ['0004103','ความมั่นคงปลอดภัยทางไซเบอร์','Cyber Security',3,3,0,6], + ['0004104','สารสนเทศดิจิทัลและปัญญาประดิษฐ์','Digital Information and Artificial Intelligence',3,3,0,6], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DT กลุ่มวิชาแกน +$courses = [ + ['1211101','คณิตศาสตร์สำหรับเทคโนโลยีดิจิทัล','Mathematics for Digital Technology',3,2,2,5], + ['1211102','สถิติสำหรับเทคโนโลยีดิจิทัล','Statistics for Digital Technology',3,2,2,5], + ['1211203','เทคโนโลยีดิจิทัลพื้นฐาน','Basic Digital Technology',3,2,2,5], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dt_core_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DT กลุ่มวิชาบังคับ +$courses = [ + ['1212101','การเขียนโปรแกรมคอมพิวเตอร์','Computer Programming',3,2,2,5], + ['1212102','ปัญญาประดิษฐ์เพื่อการวิเคราะห์ข้อมูล','Artificial Intelligence for Data Analytics',3,2,2,5], + ['1212103','เทคโนโลยีเว็บและแอปพลิเคชัน','Web and Application Technology',3,2,2,5], + ['1212104','เครือข่ายคอมพิวเตอร์และความมั่นคงปลอดภัย','Computer Networks and Security',3,2,2,5], + ['1212205','การจัดการระบบฐานข้อมูล','Database System Management',3,2,2,5], + ['1212206','วิทยาการข้อมูลเบื้องต้น','Introduction to Data Science',3,2,2,5], + ['1212207','เทคโนโลยีดิจิทัลและนวัตกรรมธุรกิจ','Digital Technology and Business Innovation',3,2,2,5], + ['1212208','การวิเคราะห์และออกแบบระบบนวัตกรรมดิจิทัล','Digital Innovation Design and Analysis',3,2,2,5], + ['1212309','การออกแบบระบบอัจฉริยะ','Intelligent Systems Design',3,2,2,5], + ['1212310','การสร้างเนื้อหาดิจิทัลและมัลติมิเดีย','Digital Content and Multimedia Creation',3,2,2,5], + ['1212311','การบริหารโครงการเทคโนโลยีดิจิทัล','Digital Project Management',3,2,2,5], + ['1212312','เทคโนโลยีบนคลาวด์','Cloud Technology',3,2,2,5], + ['1212313','เทคโนโลยีธุรกิจอิเล็กทรอนิกส์ดิจิทัล','Digital Electronic Business Technology',3,2,2,5], + ['1212314','การสื่อสารทางวิชาชีพเทคโนโลยีดิจิทัล','Digital Technology Professional Communication',3,2,2,5], + ['1212315','การประยุกต์ใช้งานปัญญาประดิษฐ์ด้านธุรกิจดิจิทัล','AI Applications in Digital Business',3,2,2,5], + ['1212416','อินเทอร์เน็ตสรรพสิ่งสำหรับธุรกิจดิจิทัล','Internet of Things for Business Digital',3,2,2,5], + ['1212417','กฎหมายและจริยธรรมทางเทคโนโลยีดิจิทัล','Digital Technology Law and Ethics',3,2,2,5], + ['1212418','การเป็นผู้ประกอบการดิจิทัล','Digital Entrepreneurship',3,2,2,5], + ['1212419','ภาษาอังกฤษสำหรับเทคโนโลยีดิจิทัล','English for Digital Technology',3,2,2,5], + ['1212420','สัมมนาเทคโนโลยีดิจิทัล','Digital Technology Seminar',3,1,4,9], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dt_major_req_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DT กลุ่มวิชาเลือกทางเทคโนโลยีดิจิทัล +$courses = [ + ['1212421','การพัฒนาโปรแกรมประยุกต์สำหรับอุปกรณ์เคลื่อนที่','Application Development for Mobile Devices',3,2,2,5], + ['1212422','ปฏิบัติการโปรแกรมภาษาจาวา','Java Programming Workshop',3,2,2,5], + ['1212423','การออกแบบกราฟิกบนคอมพิวเตอร์','Computer Graphic Design',3,2,2,5], + ['1212424','การสร้างสื่อดิจิทัล','Digital Media Production',3,2,2,5], + ['1212425','เทคโนโลยีเว็บเซอร์วิส','Web Services Technology',3,2,2,5], + ['1212426','การบริหารบริการเทคโนโลยีดิจิทัล','Digital Technology Service Management',3,2,2,5], + ['1212427','ปฏิบัติการการโปรแกรมฐานข้อมูล','Database Programming Workshop',3,2,2,5], + ['1212428','ปฏิบัติการพัฒนาโปรแกรมประยุกต์ฐานข้อมูลบนเว็บ','Web Database Application Development Workshop',3,2,2,5], + ['1212429','ปฏิบัติการสถาปัตยกรรมและการบริหารฐานข้อมูล','Database Architecture and Administration Workshop',3,2,2,5], + ['1212430','ปฏิบัติการเครือข่ายในสำนักงาน','Office Networking Workshop',3,2,2,5], + ['1212431','ปฏิบัติการการใช้ซอฟต์แวร์สำเร็จรูปในสำนักงาน','Office Package Workshop',3,2,2,5], + ['1212432','สถาปัตยกรรมคอมพิวเตอร์และระบบปฏิบัติการ','Computer Architecture and Operating System',3,2,2,5], + ['1212433','ระเบียบวิธีวิจัยทางเทคโนโลยีดิจิทัล','Digital Technology Research Methodology',3,2,2,5], + ['1212434','การศึกษาเฉพาะเรื่องทางเทคโนโลยีดิจิทัล 1','Specialized Study in Digital Technology 1',3,2,2,5], + ['1212435','การศึกษาเฉพาะเรื่องทางเทคโนโลยีดิจิทัล 2','Specialized Study in Digital Technology 2',3,2,2,5], + ['1212436','การศึกษาเฉพาะเรื่องทางเทคโนโลยีดิจิทัล 3','Specialized Study in Digital Technology 3',3,2,2,5], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dt_major_elec_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DT กลุ่มวิชาพื้นฐานวิชาชีพและวิชาชีพ +$courses = [ + ['1213401','โครงงานเทคโนโลยีดิจิทัล','Digital Technology Project',3,0,6,12], + ['1213402','การเตรียมความพร้อมก่อนฝึกประสบการณ์วิชาชีพ','Pre-Professional Internship Preparation',1,0,2,4], + ['1213403','การฝึกประสบการณ์วิชาชีพทางเทคโนโลยีดิจิทัล','Internship in Digital Technology',3,3,0,0], + ['1213404','การเตรียมสหกิจศึกษาทางเทคโนโลยีดิจิทัล','Pre-Cooperative Education in Digital Technology',1,0,2,4], + ['1213405','สหกิจศึกษาทางด้านเทคโนโลยีดิจิทัล','Digital Technology Cooperative Education',6,5,60,0], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dt_prof_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// ============================================================ +// 4. เพิ่มรายวิชา DBC +// ============================================================ + +$dbc_ge_groups = []; +$stmt = $pdo->query("SELECT id, code FROM course_groups WHERE parent_id = $dbc_gen_id ORDER BY sort_order"); +foreach ($stmt->fetchAll() as $r) { + $dbc_ge_groups[$r['code']] = $r['id']; +} + +// DBC GEN1 - การคิดและการแก้ปัญหา +$gid = $dbc_ge_groups['DBC_DT_GEN1']; +$courses = [ + ['0001101','จิตวิทยาทั่วไป','General Psychology',3,3,0,6], + ['0001102','สังคม เศรษฐกิจและการเมือง','Society Economic and Politics',3,3,0,6], + ['0001103','ภูมิปัญญาท้องถิ่น','Local Wisdom',3,3,0,6], + ['0001104','ไทยในพลวัตอาเซียน','The Thai in ASEAN Dynamics',3,3,0,6], + ['0001105','บัณฑิตในอุดมคติไทย','Graduate Thai Ideal',3,3,0,6], + ['0001106','กฎหมายในชีวิตประจำวัน','Law in Everyday Life',3,3,0,6], + ['0001107','ฉลาดคิด','Smart Thinking',3,3,0,6], + ['0001108','ศาสตร์พระราชาเพื่อการบูรณาการที่ยั่งยืน','King Philosophy for Sustainable Integration',3,3,0,6], + ['0001109','นวัตกรสังคมเพื่อการพัฒนาท้องถิ่น','Social Innovators for Local Development',3,2,2,5], + ['0001110','คณิตศาสตร์และสถิติในชีวิตประจำวัน','Mathematics and Statistics for Daily Life',3,3,0,6], + ['0001111','ชีวิตกับสิ่งแวดล้อม','Life and Environment',3,3,0,6], + ['0001112','วิทยาศาสตร์และเทคโนโลยีเพื่อคุณภาพชีวิตและสังคม','Science and Technology for Quality of Life and Society',3,3,0,6], + ['0001113','ความรู้พื้นฐานทางคณิตศาสตร์ วิทยาศาสตร์ และเทคโนโลยีเพื่อการดำรงชีวิต','Fundamental Knowledge in Mathematics Science and Technology for Everyday Life',6,6,0,12], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, 'DBC_'.$c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DBC GEN2 +$gid = $dbc_ge_groups['DBC_DT_GEN2']; +$courses = [ + ['0002101','องค์รวมแห่งชีวิต','Holistic Approaches to Life',3,3,0,6], + ['0002102','มนุษย์สัมพันธ์และการพัฒนาบุคลิกภาพ','Human Relations and Personality Development',3,3,0,6], + ['0002103','สารสนเทศเพื่อการศึกษาและค้นคว้า','Information for Education and Research',3,3,0,6], + ['0002104','ความเป็นพลเมืองและจิตสาธารณะ','Citizenship and Public Consciousness',3,3,0,6], + ['0002105','สันติศึกษา','Peace Studies',3,3,0,6], + ['0002106','ทักษะชีวิต','Life Skills',3,3,0,6], + ['0002107','การเรียนรู้ในระดับอุดมศึกษา','University Study',3,3,0,6], + ['0002108','รู้ทันสุขภาพ','Health Literacy',3,2,2,5], + ['0002109','พลเมืองโลก','Global Citizens',3,3,0,6], + ['0002110','วิถีชีวิตที่ยั่งยืน','Sustainable Lifestyles',3,3,0,6], + ['0002111','ใส่ใจภัยพิบัติในโลกสมัยใหม่','Disaster Intentions in the Modern World',3,3,0,6], + ['0002112','งานช่างในชีวิตประจำวัน','Engineering Work in Daily Life',3,2,2,5], + ['0002113','สมาธิเพื่อพัฒนาชีวิต','Meditation for Life Development',3,3,0,6], + ['0002114','ทักษะชีวิตเพื่อการอยู่ร่วมกันอย่างยั่งยืน','Life Skills for Sustainable Living and Coexistence',3,3,0,6], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, 'DBC_'.$c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DBC GEN3 +$gid = $dbc_ge_groups['DBC_DT_GEN3']; +$courses = [ + ['0003101','ภาษาอังกฤษพื้นฐาน','Fundamental English',3,3,0,6], + ['0003102','การใช้ภาษาอังกฤษ','English Usage',3,3,0,6], + ['0003103','การพัฒนาทักษะการอ่านและเขียนภาษาอังกฤษ','Development of Reading and Writing Skills in English',3,3,0,6], + ['0003104','ภาษาอังกฤษเพื่อการสื่อสารในบริบทสากล','Communicative English in Global Context',3,3,0,6], + ['0003105','ภาษาอังกฤษในบริบทการทำงานอย่างมีประสิทธิภาพ','Effective English in Professional Contexts',3,3,0,6], + ['0003106','ภาษาจีนเพื่อการสื่อสาร','Chinese for Communication',3,3,0,6], + ['0003107','ภาษาฝรั่งเศสเบื้องต้น','Introduction to French',3,3,0,6], + ['0003108','ภาษาญี่ปุ่นเบื้องต้น','Introduction to Japanese',3,3,0,6], + ['0003109','ภาษาญี่ปุ่นเพื่อการสื่อสาร','Japanese for Communication',3,3,0,6], + ['0003110','ภาษามาเลย์เพื่อการสื่อสาร','Malay for Communication',3,3,0,6], + ['0003111','ภาษาอินโดนีเซียเพื่อการสื่อสาร','Indonesian for Communication',3,3,0,6], + ['0003112','การพัฒนาทักษะทางภาษาไทย','Development of Thai Language Skills',3,3,0,6], + ['0003113','ภาษาไทยเพื่อการสื่อสาร','Thai for Communication',3,3,0,6], + ['0003114','ภาษาไทยถิ่นและภาษาเฉพาะถิ่นในสังคมไทย','Thai Dialects and Ethnic Languages in Thai Society',3,3,0,6], + ['0003115','ทักษะภาษาเพื่อการสื่อสารในชีวิตประจำวัน','Language Skills for Everyday Communication',6,6,0,12], + ['0003116','ภาษาเพื่อวิชาชีพและการทำงาน','Professional and Workplace Communication',6,6,0,12], + ['0003117','ภาษาเพื่อวัฒนธรรมและความเข้าใจระหว่างประเทศ','Language and Intercultural Understanding',6,6,0,12], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, 'DBC_'.$c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DBC GEN4 +$gid = $dbc_ge_groups['DBC_DT_GEN4']; +$courses = [ + ['0004101','เทคโนโลยีการสื่อสารกับมนุษย์','Communication Technology and Human',3,3,0,6], + ['0004102','เทคโนโลยีดิจิทัลและปัญญาประดิษฐ์เพื่อชีวิตวิถีใหม่','Digital Technology and AI for the New Normal',3,2,2,6], + ['0004103','ความมั่นคงปลอดภัยทางไซเบอร์','Cyber Security',3,3,0,6], + ['0004104','สารสนเทศดิจิทัลและปัญญาประดิษฐ์','Digital Information and Artificial Intelligence',3,3,0,6], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $gid, 'DBC_'.$c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DBC กลุ่มวิชาแกนธุรกิจ +$courses = [ + ['1111101','หลักการตลาดในยุคดิจิทัล','Principles of Marketing in Digital Era',3,2,2,5], + ['1121101','องค์การและการจัดการ','Organization and Management',3,2,2,5], + ['1121201','เศรษฐศาสตร์ธุรกิจ','Business Economics',3,2,2,5], + ['1121202','กฎหมายธุรกิจ','Business Law',3,2,2,5], + ['1121203','การบัญชีขั้นต้น','Fundamentals of Accounting',3,2,2,5], + ['1121204','การเป็นผู้ประกอบการและการสร้างธุรกิจใหม่','Entrepreneurship and New Venture Creation',3,2,2,5], + ['1221301','ภาษาอังกฤษธุรกิจ','English for Business',3,2,2,5], + ['1121303','การภาษีอากร','Taxation',3,2,2,5], + ['1121403','การจัดการเชิงกลยุทธ์','Strategic Management',3,2,2,5], + ['1221201','โปรแกรมสำเร็จรูปทางธุรกิจ','Package Program in Business',3,2,2,5], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dbc_core_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DBC กลุ่มวิชาบังคับ +$courses = [ + ['1222101','คอมพิวเตอร์ธุรกิจดิจิทัล','Digital Business Computer',3,2,2,5], + ['1222102','ปัญญาประดิษฐ์เพื่องานคอมพิวเตอร์ธุรกิจดิจิทัล','AI for Digital Business Computer',3,2,2,5], + ['1222103','การโปรแกรมคอมพิวเตอร์ด้วยภาษาสมัยใหม่','Computer Programming with Modern Language',3,2,2,5], + ['1222104','สถิติการวิเคราะห์ข้อมูลสำหรับคอมพิวเตอร์ธุรกิจดิจิทัล','Statistics Data Analytics for Digital Business Computer',3,2,2,5], + ['1222205','โครงสร้างข้อมูลสำหรับคอมพิวเตอร์ธุรกิจดิจิทัล','Data Structure for Digital Business Computer',3,2,2,5], + ['1222206','การบริหารเครือข่ายคอมพิวเตอร์','Network Administration Computer',3,2,2,5], + ['1222307','การวิเคราะห์และออกแบบระบบสารสนเทศ','Information System Analysis and Design',3,2,2,5], + ['1222308','ระบบจัดการฐานข้อมูล','Database Management System',3,2,2,5], + ['1222309','การออกแบบและพัฒนาเว็บไซต์','Web Design and Development',3,2,2,5], + ['1222310','การบริหารโครงการ','Project Management',3,2,2,5], + ['1222311','ระบบสารสนเทศทางคอมพิวเตอร์ธุรกิจดิจิทัล','Business Information Systems Computer',3,2,2,5], + ['1222312','การออกแบบงานกราฟฟิกและมัลติมีเดีย','Graphic Design and Multimedia',3,2,2,5], + ['1222313','การใช้สื่อสังคมออนไลน์เพื่อธุรกิจ','Social Media Usages for Business',3,2,2,5], + ['1222414','การจัดการธุรกิจอิเล็กทรอนิกส์','Electronic Business Management',3,2,2,5], + ['1222415','สัมมนาคอมพิวเตอร์ธุรกิจดิจิทัล','Seminar in Digital Business Computer',3,1,4,9], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dbc_major_req_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DBC กลุ่มวิชาเลือก +$courses = [ + ['1222316','การรักษาความปลอดภัยทางไซเบอร์และระบบสารสนเทศ','Cyber and Information System Security',3,2,2,5], + ['1222317','การพัฒนาอินเทอร์เน็ตสรรพสิ่ง','Internet of Things Development',3,2,2,5], + ['1222318','การพัฒนาโปรแกรมประยุกต์สำหรับอุปกรณ์เคลื่อนที่','Application Development for Mobile Devices',3,2,2,5], + ['1222419','ธุรกิจอัจฉริยะ','Business Intelligence',3,2,2,5], + ['1222420','หัวข้อพิเศษทางคอมพิวเตอร์ธุรกิจดิจิทัล 1','Special Topic in Digital Business Computer 1',3,2,2,5], + ['1222421','หัวข้อพิเศษทางคอมพิวเตอร์ธุรกิจดิจิทัล 2','Special Topic in Digital Business Computer 2',3,2,2,5], + ['1222422','หัวข้อพิเศษทางคอมพิวเตอร์ธุรกิจดิจิทัล 3','Special Topic in Digital Business Computer 3',3,2,2,5], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dbc_major_elec_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +// DBC กลุ่มวิชาพื้นฐานวิชาชีพและวิชาชีพ +$courses = [ + ['1223401','โครงงานทางคอมพิวเตอร์ธุรกิจดิจิทัล','Project in Digital Business Computer',3,0,6,12], + ['1223402','การเตรียมการฝึกประสบการณ์วิชาชีพทางคอมพิวเตอร์ธุรกิจดิจิทัล','Pre-Cooperative Education in Digital Business Computer',1,1,1,2], + ['1223403','การฝึกประสบการณ์วิชาชีพทางคอมพิวเตอร์ธุรกิจดิจิทัล','Internship in Digital Business Computer',3,3,0,0], + ['1223404','การเตรียมสหกิจศึกษาทางคอมพิวเตอร์ธุรกิจดิจิทัล','Pre-Cooperative Education in Digital Business Computer',1,1,1,2], + ['1223405','สหกิจศึกษาทางคอมพิวเตอร์ธุรกิจดิจิทัล','Cooperative Education in Digital Business Computer',6,5,60,0], +]; +foreach ($courses as $i => $c) { + add_course($pdo, $dbc_prof_id, $c[0], $c[1], $c[2], $c[3], $c[4], $c[5], $c[6], $i+1); +} + +echo "✅ Curriculum data imported successfully!\n"; diff --git a/includes/footer.php b/includes/footer.php new file mode 100644 index 0000000..40a6c71 --- /dev/null +++ b/includes/footer.php @@ -0,0 +1,6 @@ + + + + + + diff --git a/includes/functions.php b/includes/functions.php new file mode 100644 index 0000000..bdf8567 --- /dev/null +++ b/includes/functions.php @@ -0,0 +1,741 @@ +query("SELECT * FROM curricula ORDER BY code")->fetchAll(); +} + +function getCurriculum($id) { + $stmt = getDB()->prepare("SELECT * FROM curricula WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(); +} + +function getCourseGroups($curriculum_id, $parent_id = null) { + if ($parent_id === null) { + $stmt = getDB()->prepare("SELECT * FROM course_groups WHERE curriculum_id = ? AND parent_id IS NULL ORDER BY sort_order"); + $stmt->execute([$curriculum_id]); + } else { + $stmt = getDB()->prepare("SELECT * FROM course_groups WHERE parent_id = ? ORDER BY sort_order"); + $stmt->execute([$parent_id]); + } + return $stmt->fetchAll(); +} + +function getCoursesByGroup($group_id) { + $stmt = getDB()->prepare("SELECT * FROM courses WHERE group_id = ? ORDER BY sort_order"); + $stmt->execute([$group_id]); + return $stmt->fetchAll(); +} + +function getStudents($curriculum_id = null) { + if ($curriculum_id === null && isStudent()) { + $stmt = getDB()->prepare(" + SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name + FROM students s + JOIN curricula c ON s.curriculum_id = c.id + WHERE s.id = ? + ORDER BY s.student_code + "); + $stmt->execute([getCurrentUserId()]); + return $stmt->fetchAll(); + } + if ($curriculum_id === null && isAdvisor()) { + $stmt = getDB()->prepare(" + SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name + FROM students s + JOIN curricula c ON s.curriculum_id = c.id + WHERE s.advisor_id = ? + ORDER BY s.student_code + "); + $stmt->execute([getCurrentUserId()]); + return $stmt->fetchAll(); + } + if ($curriculum_id) { + $stmt = getDB()->prepare(" + SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name + FROM students s + JOIN curricula c ON s.curriculum_id = c.id + WHERE s.curriculum_id = ? + ORDER BY s.student_code + "); + $stmt->execute([$curriculum_id]); + return $stmt->fetchAll(); + } + return getDB()->query(" + SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name + FROM students s + JOIN curricula c ON s.curriculum_id = c.id + ORDER BY s.student_code + ")->fetchAll(); +} + +function getStudent($id) { + $stmt = getDB()->prepare(" + SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name, c.total_credits + FROM students s + JOIN curricula c ON s.curriculum_id = c.id + WHERE s.id = ? + "); + $stmt->execute([$id]); + return $stmt->fetch(); +} + +function getFacultyList() { + return [ + '12' => 'คณะนวัตกรรมดิจิทัลเทคโนโลยี', + '11' => 'คณะบริหารธุรกิจ', + '13' => 'คณะศึกษาศาสตร์และศิลปศาสตร์', + '14' => 'คณะบัญชี', + '15' => 'คณะนิติศาสตร์และรัฐศาสตร์', + ]; +} + +function getTransferCreditsTotal($student_id) { + $stmt = getDB()->prepare("SELECT COALESCE(SUM(credits), 0) FROM student_courses WHERE student_id = ? AND source_type = 'transfer'"); + $stmt->execute([$student_id]); + return floatval($stmt->fetchColumn()); +} + +function getCompletedCreditsTotal($student_id) { + $stmt = getDB()->prepare("SELECT COALESCE(SUM(credits), 0) FROM student_courses WHERE student_id = ?"); + $stmt->execute([$student_id]); + return floatval($stmt->fetchColumn()); +} + +function getStudentCourses($student_id) { + $stmt = getDB()->prepare(" + SELECT sc.*, c.code AS curriculum_course_code + FROM student_courses sc + LEFT JOIN courses c ON sc.course_id = c.id + WHERE sc.student_id = ? + ORDER BY sc.course_code + "); + $stmt->execute([$student_id]); + return $stmt->fetchAll(); +} + +function getStudentCoursesByGroup($student_id, $group_id) { + $stmt = getDB()->prepare(" + SELECT sc.* FROM student_courses sc + JOIN courses c ON sc.course_id = c.id + WHERE sc.student_id = ? AND c.group_id = ? + "); + $stmt->execute([$student_id, $group_id]); + return $stmt->fetchAll(); +} + +/** + * Build recursive tree of course groups for a curriculum + */ +function buildGroupTree($curriculum_id, $parent_id = null) { + $groups = getCourseGroups($curriculum_id, $parent_id); + $tree = []; + foreach ($groups as $g) { + $children = buildGroupTree($curriculum_id, $g['id']); + $courses = getCoursesByGroup($g['id']); + $node = [ + 'group' => $g, + 'children' => $children, + 'courses' => $courses, + 'total_credits' => 0, + 'completed_credits' => 0, + ]; + // Calculate total credits from courses + foreach ($courses as $c) { + $node['total_credits'] += $c['credits']; + } + // Add children totals + foreach ($children as $ch) { + $node['total_credits'] += $ch['total_credits']; + } + $tree[] = $node; + } + return $tree; +} + +function getStudentSummary($student_id) { + $student = getStudent($student_id); + if (!$student) return null; + + $tree = buildGroupTree($student['curriculum_id']); + + $student_courses = getStudentCourses($student_id); + $completed_map = []; + foreach ($student_courses as $sc) { + $completed_map[$sc['course_code']] = $sc; + } + + // Mark completed courses recursively + $total_completed = 0; + foreach ($student_courses as $sc) { + $total_completed += $sc['credits']; + } + + $markCompleted = function(&$nodes) use (&$markCompleted, $completed_map) { + foreach ($nodes as &$node) { + foreach ($node['courses'] as &$course) { + if (isset($completed_map[$course['code']])) { + $course['_completed'] = $completed_map[$course['code']]; + $node['completed_credits'] += $course['credits']; + } + } + if (!empty($node['children'])) { + $markCompleted($node['children']); + // Sum children completed credits + foreach ($node['children'] as $ch) { + $node['completed_credits'] += $ch['completed_credits']; + } + } + } + }; + $markCompleted($tree); + + return [ + 'student' => $student, + 'groups' => $tree, + 'total_completed' => $total_completed, + 'student_courses' => $student_courses, + 'completed_map' => $completed_map + ]; +} + +function findCourseInCurriculum($curriculum_id, $code) { + $stmt = getDB()->prepare(" + SELECT c.* FROM courses c + JOIN course_groups cg ON c.group_id = cg.id + WHERE cg.curriculum_id = ? AND c.code = ? + "); + $stmt->execute([$curriculum_id, $code]); + return $stmt->fetch(); +} + +function getGroupSelectOptions($curriculum_id, $selected_id = null, $parent_id = null, $prefix = '') { + $groups = getCourseGroups($curriculum_id, $parent_id); + $html = ''; + foreach ($groups as $g) { + $sel = $g['id'] == $selected_id ? ' selected' : ''; + $html .= ''; + $html .= getGroupSelectOptions($curriculum_id, $selected_id, $g['id'], $prefix . '— '); + } + return $html; +} + +function getGradeOptions($selected = null) { + $grades = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'F', 'S', 'U', 'กำลังเรียน']; + $html = ''; + foreach ($grades as $g) { + $sel = $g === $selected ? ' selected' : ''; + $html .= ''; + } + return $html; +} + +function getCourseTypeOptions($selected = null) { + $types = [ + 'เรียนปกติ' => 'เรียนปกติ', + 'เทียบโอนรายวิชา' => 'เทียบโอนรายวิชา', + 'เทียบโอนกลุ่มวิชา' => 'เทียบโอนกลุ่มวิชา', + 'กิจกรรม/สหกิจศึกษา' => 'กิจกรรม/สหกิจศึกษา', + 'ฝึกอบรม' => 'ฝึกอบรม', + ]; + $html = ''; + foreach ($types as $val => $label) { + $sel = $val === $selected ? ' selected' : ''; + $html .= ''; + } + return $html; +} + +function getTotalCurriculumCredits($curriculum_id) { + $stmt = getDB()->prepare("SELECT total_credits FROM curricula WHERE id = ?"); + $stmt->execute([$curriculum_id]); + $r = $stmt->fetch(); + return $r ? $r['total_credits'] : 0; +} + +// ============================================================ +// Access Control Helpers +// ============================================================ + +function requireStudentAccess($student_id) { + if (isAdmin()) return; + if (isStudent() && getCurrentUserId() == $student_id) return; + $db = getDB(); + $stmt = $db->prepare("SELECT advisor_id, curriculum_id FROM students WHERE id = ?"); + $stmt->execute([$student_id]); + $student = $stmt->fetch(); + if (!$student || $student['advisor_id'] != getCurrentUserId()) { + header('Location: ' . BASE_URL . '/index.php'); + exit; + } +} + +// ============================================================ +// User Management Functions +// ============================================================ + +function getUsers() { + return getDB()->query("SELECT u.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM users u LEFT JOIN curricula c ON u.curriculum_id = c.id ORDER BY u.staff_code")->fetchAll(); +} + +function getAdvisors() { + return getDB()->query("SELECT u.id, u.name, u.staff_code, c.code AS curriculum_code FROM users u LEFT JOIN curricula c ON u.curriculum_id = c.id WHERE u.role = 'advisor' ORDER BY u.name")->fetchAll(); +} + +function getUser($id) { + $stmt = getDB()->prepare("SELECT u.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM users u LEFT JOIN curricula c ON u.curriculum_id = c.id WHERE u.id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(); +} + +function authenticateUser($staff_code, $password) { + $stmt = getDB()->prepare("SELECT * FROM users WHERE staff_code = ?"); + $stmt->execute([$staff_code]); + $user = $stmt->fetch(); + if ($user && password_verify($password, $user['password'])) { + return $user; + } + return false; +} + +// ============================================================ +// PDF Parsing Functions +// ============================================================ + +function extractTextFromPDF($filepath) { + $text = trySmalotParser($filepath); + if (!empty(trim($text))) return $text; + + $text = tryPdftotext($filepath); + if (!empty(trim($text))) return $text; + + $text = tryBasicPdfParse($filepath); + if (!empty(trim($text))) return $text; + + return tryRawPdfRead($filepath); +} + +function trySmalotParser($filepath) { + if (!class_exists('Smalot\\PdfParser\\Parser')) { + spl_autoload_register(function ($class) { + $prefix = 'Smalot\\PdfParser\\'; + $base_dir = __DIR__ . '/../vendor/smalot/pdfparser/src/'; + if (strncmp($prefix, $class, strlen($prefix)) === 0) { + $file = $base_dir . str_replace('\\', '/', $class) . '.php'; + if (file_exists($file)) { require $file; } + } + }); + } + try { + $parser = new Smalot\PdfParser\Parser(); + $pdf = $parser->parseFile($filepath); + return $pdf->getText(); + } catch (Throwable $e) { + return ''; + } +} + +function tryPdftotext($filepath) { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $exe_paths = [ + 'C:\xampp\htdocs\Transfer\vendor\pdftotext.exe', + 'pdftotext', + 'C:\Program Files\pdftotext\pdftotext.exe', + ]; + } else { + $exe_paths = [ + 'pdftotext', + '/usr/bin/pdftotext' + ]; + } + + foreach ($exe_paths as $exe) { + $output = []; + $cmd = escapeshellcmd($exe) . ' ' . escapeshellarg($filepath) . ' -'; + @exec($cmd, $output, $return_var); + if ($return_var === 0 && !empty(implode('', $output))) { + return implode("\n", $output); + } + } + return ''; +} + +function tryBasicPdfParse($filepath) { + $content = file_get_contents($filepath); + if ($content === false) return ''; + + $text = ''; + $offset = 0; + + while (($start = strpos($content, 'stream', $offset)) !== false) { + $stream_start = $start + 6; + // Skip leading whitespace (usually CR/LF or LF) after 'stream' + while ($stream_start < strlen($content) && ctype_space($content[$stream_start])) { + $stream_start++; + } + + $end = strpos($content, 'endstream', $stream_start); + if ($end === false) { + break; + } + + $stream = substr($content, $stream_start, $end - $stream_start); + $stream = rtrim($stream); + + $decoded = @gzuncompress($stream); + if ($decoded === false) { + $decoded = @gzuncompress(trim($stream)); + } + + if ($decoded !== false) { + if (strlen($decoded) > 100000) continue; + preg_match_all('/\((.*?)\)\s*Tj/s', $decoded, $text_matches); + $text .= implode(' ', $text_matches[1]) . "\n"; + + preg_match_all('/\[(.*?)\]\s*TJ/s', $decoded, $tj_matches); + foreach ($tj_matches[1] as $tj) { + preg_match_all('/\((.*?)\)/', $tj, $tj_text); + $text .= implode('', $tj_text[1]) . ' '; + } + } + + $offset = $end + 9; + } + + return $text; +} + +function tryRawPdfRead($filepath) { + $content = file_get_contents($filepath); + if ($content === false) return ''; + + // Cap binary parsing to prevent CPU exhaustion on very large or corrupt files + if (strlen($content) > 500000) { + $content = substr($content, 0, 500000); + } + + $text = ''; + $len = strlen($content); + $in_paren = false; + $buffer = ''; + + for ($i = 0; $i < $len; $i++) { + $char = $content[$i]; + if ($char === '(') { + $in_paren = true; + $buffer = ''; + } else if ($char === ')') { + if ($in_paren) { + if (strlen($buffer) > 0 && strlen($buffer) <= 200) { + $text .= $buffer . "\n"; + } + $in_paren = false; + } + } else if ($in_paren) { + $buffer .= $char; + if (strlen($buffer) > 200) { + $in_paren = false; + } + } + } + return $text; +} + +function parseCoursesFromText($text) { + $courses = []; + + // Try Pattern A: 9-digit code (transfer PDF format with multi-line text) + // Strip first 2 digits for 7-digit curriculum code + $courses = parseTransferPdfCourses($text); + + // Fallback: Standard 7-digit code format + if (empty($courses)) { + $courses = parseStandardCourses($text); + } + + return $courses; +} + +function parseTransferPdfCourses($text) { + $courses = []; + $lines = explode("\n", $text); + + $lines = array_map(function($l) { + return str_replace("\t", " ", trim($l)); + }, $lines); + $lines = array_values(array_filter($lines, function($l) { + return $l !== ''; + })); + + $current_code = ''; + $current_name_parts = []; + + $flushCourse = function() use (&$current_code, &$current_name_parts, &$courses) { + if ($current_code && !empty($current_name_parts)) { + $courses[] = finalizeTransferCourse($current_code, $current_name_parts); + $current_code = ''; + $current_name_parts = []; + } + }; + + for ($i = 0; $i < count($lines); $i++) { + $line = $lines[$i]; + + // Single regex: optional seq# (1-2 digits with optional trailing space) + 9-digit code + rest + $m = []; + if (preg_match('/^(\d{1,2})?\s*(\d{9})(.*)$/', $line, $m)) { + $pdf_code = $m[2]; + $after_code = trim($m[3]); + + if ($current_code) { + $courses[] = finalizeTransferCourse($current_code, $current_name_parts); + $current_code = ''; + $current_name_parts = []; + } + + $current_code = $pdf_code; + $current_name_parts = []; + + if ($after_code !== '') { + if (preg_match('/^(.+?)\s+(\d+\.\d)\s*$/', $after_code, $nm)) { + $current_name_parts[] = trim($nm[1]); + $courses[] = finalizeTransferCourse($current_code, $current_name_parts, floatval($nm[2])); + $current_code = ''; + $current_name_parts = []; + } else { + $current_name_parts[] = $after_code; + } + } + continue; + } + + if ($current_code) { + if (preg_match('/^(\d+\.\d)\s*$/', $line, $cm)) { + $courses[] = finalizeTransferCourse($current_code, $current_name_parts, floatval($cm[1])); + $current_code = ''; + $current_name_parts = []; + continue; + } + + if (preg_match('/^(.+?)\s+(\d+\.\d)\s*$/', $line, $nm)) { + $current_name_parts[] = trim($nm[1]); + $courses[] = finalizeTransferCourse($current_code, $current_name_parts, floatval($nm[2])); + $current_code = ''; + $current_name_parts = []; + continue; + } + + $current_name_parts[] = $line; + } + } + + if ($current_code && !empty($current_name_parts)) { + $courses[] = finalizeTransferCourse($current_code, $current_name_parts); + } + + return $courses; +} + +function finalizeTransferCourse($pdf_code, $name_parts, $credits = null) { + // Clean the name: remove extra whitespace, collapsed newlines + $name = preg_replace('/\s+/', ' ', implode(' ', $name_parts)); + $name = trim($name); + // Remove trailing orphan characters like "-" + $name = rtrim($name, ' -'); + + // Strip first 2 digits from 9-digit code to get 7-digit curriculum code + $curriculum_code = substr($pdf_code, 2); + + // If credits not specified, try to find a number in the name + if ($credits === null) { + if (preg_match('/\b(\d+\.\d)\b/', $name, $cm)) { + $credits = floatval($cm[1]); + $name = trim(str_replace($cm[0], '', $name)); + } else { + $credits = 3.0; // default + } + } + + return [ + 'code' => $curriculum_code, + 'name_th' => $name, + 'credits' => $credits, + 'lecture' => 0, + 'practice' => 0, + 'self_study' => 0, + ]; +} + +function parseStandardCourses($text) { + $courses = []; + + // Pattern: Standard format "0001101 จิตวิทยาทั่วไป 3(3-0-6)" + preg_match_all('/(\d{7})\s+([^\d]+?)\s+(\d+(?:\.\d)?)\s*\(\s*(\d+)\s*[-–]\s*(\d+)\s*[-–]\s*(\d+)\s*\)/', $text, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $courses[] = [ + 'code' => trim($m[1]), + 'name_th' => trim($m[2]), + 'credits' => floatval($m[3]), + 'lecture' => intval($m[4]), + 'practice' => intval($m[5]), + 'self_study' => intval($m[6]), + ]; + } + + // Pattern: Simple format "0001101 จิตวิทยาทั่วไป 3" + if (empty($courses)) { + preg_match_all('/(\d{7})\s+([^\d]{2,80}?)\s+(\d+(?:\.\d)?)(?:\s|$)/', $text, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $courses[] = [ + 'code' => trim($m[1]), + 'name_th' => trim($m[2]), + 'credits' => floatval($m[3]), + 'lecture' => 0, 'practice' => 0, 'self_study' => 0, + ]; + } + } + + return $courses; +} + +function getStudentTransferPdfPath($student_id) { + $stmt = getDB()->prepare("SELECT DISTINCT source_file FROM student_courses WHERE student_id = ? AND source_type = 'transfer' AND source_file IS NOT NULL LIMIT 1"); + $stmt->execute([$student_id]); + $filename = $stmt->fetchColumn(); + if ($filename) { + return __DIR__ . '/../uploads/' . $filename; + } + return null; +} + +function parseTransferReportFromPdf($filepath) { + if (!$filepath || !file_exists($filepath)) return null; + + // Register autoloader for Smalot Parser just in case it is not registered yet + spl_autoload_register(function ($class) { + $prefix = 'Smalot\\PdfParser\\'; + $base_dir = __DIR__ . '/../vendor/smalot/pdfparser/src/'; + if (strncmp($prefix, $class, strlen($prefix)) === 0) { + $file = $base_dir . str_replace('\\', '/', $class) . '.php'; + if (file_exists($file)) { require $file; } + } + }); + + $text = extractTextFromPDF($filepath); + $lines = explode("\n", $text); + + $pattern = '/^(?:(\d+)\s+)?(\d*)(\d{9})\s*(.+?)\s+(\d+\.\d)\s+(\d{5}-\d{4})\s*(.+?)\s+(\d+(?:\.\d)?)\s+([A-F][+-]?|[0-4](?:\.[0-9])?|S|U)\s*(\d+(?:\.\d+)?%)/u'; + $source_pattern = '/^(\d{5}-\d{4})\s*(.+?)\s+(\d+(?:\.\d)?)\s+([A-F][+-]?|[0-4](?:\.[0-9])?|S|U)\s*(\d+(?:\.\d+)?%)/u'; + $target_only_pattern = '/^(?:(\d+)\s+)?(\d*)(\d{9})\s*(.+?)\s+(\d+\.\d)\s*$/u'; + $untransferable_pattern = '/^(?:(\d+)\s+)?(\d*)(\d{5}-\d{4})\s*(.+?)\s+(\d+(?:\.\d)?)\s+([A-F][+-]?|[0-4](?:\.[0-9])?|[ก-๙]\.?|S|U|W|I)\s*(.+)$/u'; + + $results = []; + $untransferable = []; + + $start_transfer = false; + $start_untransfer = false; + + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') continue; + + if (mb_strpos($line, 'รายละเอียดวิชาที่เทียบโอนได้') !== false) { + $start_transfer = true; + $start_untransfer = false; + continue; + } + if (mb_strpos($line, 'วิชาต้นทางที่ไม่สามารถเทียบโอนได้') !== false) { + $start_transfer = false; + $start_untransfer = true; + continue; + } + if (mb_strpos($line, 'ผู้ประเมิน') !== false || mb_strpos($line, '(............................................)') !== false) { + $start_transfer = false; + $start_untransfer = false; + break; + } + + if ($start_transfer) { + if (preg_match($pattern, $line, $m)) { + $seq = !empty($m[1]) ? $m[1] : (!empty($m[2]) ? $m[2] : (count($results) + 1)); + $target_code = $m[3]; + $target_name = trim($m[4]); + $target_credits = floatval($m[5]); + + $source_code = $m[6]; + $source_name = trim($m[7]); + $source_credits = floatval($m[8]); + $source_grade = $m[9]; + $similarity = $m[10]; + + $results[] = [ + 'seq' => $seq, + 'target_code' => $target_code, + 'target_name' => $target_name, + 'target_credits' => $target_credits, + 'sources' => [ + [ + 'code' => $source_code, + 'name' => $source_name, + 'credits' => $source_credits, + 'grade' => $source_grade, + 'similarity' => $similarity + ] + ] + ]; + } else if (preg_match($target_only_pattern, $line, $m)) { + $seq = !empty($m[1]) ? $m[1] : (!empty($m[2]) ? $m[2] : (count($results) + 1)); + $target_code = $m[3]; + $target_name = trim($m[4]); + $target_credits = floatval($m[5]); + + $results[] = [ + 'seq' => $seq, + 'target_code' => $target_code, + 'target_name' => $target_name, + 'target_credits' => $target_credits, + 'sources' => [] + ]; + } else if (preg_match($source_pattern, $line, $m) && !empty($results)) { + $source_code = $m[1]; + $source_name = trim($m[2]); + $source_credits = floatval($m[3]); + $source_grade = $m[4]; + $similarity = $m[5]; + + $results[count($results) - 1]['sources'][] = [ + 'code' => $source_code, + 'name' => $source_name, + 'credits' => $source_credits, + 'grade' => $source_grade, + 'similarity' => $similarity + ]; + } + } else if ($start_untransfer) { + if (preg_match($untransferable_pattern, $line, $m)) { + $seq = !empty($m[1]) ? $m[1] : (!empty($m[2]) ? $m[2] : (count($untransferable) + 1)); + $source_code = $m[3]; + $source_name = trim($m[4]); + $source_credits = floatval($m[5]); + $source_grade = $m[6]; + $reason = trim($m[7]); + + $untransferable[] = [ + 'seq' => $seq, + 'code' => $source_code, + 'name' => $source_name, + 'credits' => $source_credits, + 'grade' => $source_grade, + 'reason' => $reason + ]; + } + } + } + + return [ + 'transfers' => $results, + 'untransferable' => $untransferable + ]; +} + diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..9879f7b --- /dev/null +++ b/includes/header.php @@ -0,0 +1,81 @@ + + + + + + + <?= isset($page_title) ? htmlspecialchars($page_title) . ' - ' : '' ?>ระบบใบควบคุมผลการเรียน + + + + + + + + + +
diff --git a/index.php b/index.php new file mode 100644 index 0000000..59b1958 --- /dev/null +++ b/index.php @@ -0,0 +1,226 @@ + + +
+ +
+
+
+
+
หลักสูตร
+
+
+
+ หลักสูตร +
+
+
+
+
+
+
+
+
นักศึกษา DT
+
+
+
+ นักศึกษา DT +
+
+
+
+
+
+
+
+
นักศึกษา DBC
+
+
+
+ นักศึกษา DBC +
+
+
+
+ + +
+
+
+
+
หน่วยกิตที่เรียนแล้ว
+
+
+
+ หน่วยกิต +
+
+
+
+
+
+
+
+
หน่วยกิตเทียบโอน
+
+
+
+ หน่วยกิตเทียบโอน +
+
+
+
+ +
+
+
+
+
นักศึกษาทั้งหมด
+
+
+
+ นักศึกษาทั้งหมด +
+
+
+
+
+
+
+
+
หน่วยกิตเทียบโอนรวม
+
+
+
+ หน่วยกิตเทียบโอนรวม +
+
+
+
+ +
+ + +
+
+
+
+ ข้อมูลของฉัน +
+
+
+
+ รหัสนักศึกษา +
+
+
+ ชื่อ-นามสกุล +
+
+
+ สาขาวิชา +
-
+
+
+ ปีการศึกษา +
+
+
+
+ + ดูผลการเรียนของฉัน + +
+
+
+
+ +
+
+
+
+ หลักสูตร +
+ +
+
+
+
+
+ จัดการนักศึกษา +
+
+ + เพิ่มนักศึกษา + + + ดูรายชื่อนักศึกษา + +
+

เลือกนักศึกษาเพื่อดูข้อมูลและจัดการรายวิชา:

+ +
+
+
+ +
+
+
+ จัดการผู้ใช้ +
+
+ + รายชื่อผู้ใช้ทั้งหมด + + + เพิ่มผู้ใช้ + +
+

จัดการบัญชีผู้ใช้สำหรับเข้าใช้งานระบบ

+
+
+
+ +
+ + diff --git a/init_db.php b/init_db.php new file mode 100644 index 0000000..650246d --- /dev/null +++ b/init_db.php @@ -0,0 +1,102 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->exec("CREATE DATABASE IF NOT EXISTS `" . DB_NAME . "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); + $pdo->exec("USE `" . DB_NAME . "`"); + + // สร้างตาราง + $pdo->exec(" + CREATE TABLE IF NOT EXISTS curricula ( + id INT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(10) NOT NULL UNIQUE, + name_th VARCHAR(255) NOT NULL, + name_en VARCHAR(255) NOT NULL, + total_credits INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS course_groups ( + id INT AUTO_INCREMENT PRIMARY KEY, + curriculum_id INT NOT NULL, + parent_id INT DEFAULT NULL, + code VARCHAR(50) NOT NULL, + name_th VARCHAR(255) NOT NULL, + min_credits INT NOT NULL DEFAULT 0, + sort_order INT NOT NULL DEFAULT 0, + FOREIGN KEY (curriculum_id) REFERENCES curricula(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES course_groups(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS courses ( + id INT AUTO_INCREMENT PRIMARY KEY, + group_id INT NOT NULL, + code VARCHAR(20) NOT NULL UNIQUE, + name_th VARCHAR(255) NOT NULL, + name_en VARCHAR(255) DEFAULT '', + credits DECIMAL(3,1) NOT NULL, + lecture_hours INT NOT NULL DEFAULT 0, + practice_hours INT NOT NULL DEFAULT 0, + self_study_hours INT NOT NULL DEFAULT 0, + sort_order INT NOT NULL DEFAULT 0, + FOREIGN KEY (group_id) REFERENCES course_groups(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS students ( + id INT AUTO_INCREMENT PRIMARY KEY, + student_code VARCHAR(20) NOT NULL UNIQUE, + name_th VARCHAR(255) NOT NULL, + name_en VARCHAR(255) DEFAULT '', + faculty VARCHAR(255) DEFAULT '', + graduated_institution VARCHAR(255) DEFAULT '', + curriculum_id INT NOT NULL, + enrollment_year INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (curriculum_id) REFERENCES curricula(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS student_courses ( + id INT AUTO_INCREMENT PRIMARY KEY, + student_id INT NOT NULL, + course_id INT DEFAULT NULL, + course_code VARCHAR(20) NOT NULL, + course_name_th VARCHAR(255) NOT NULL, + credits DECIMAL(3,1) NOT NULL, + lecture_hours INT NOT NULL DEFAULT 0, + practice_hours INT NOT NULL DEFAULT 0, + self_study_hours INT NOT NULL DEFAULT 0, + source_type ENUM('transfer','manual') NOT NULL DEFAULT 'manual', + source_file VARCHAR(255) DEFAULT NULL, + notes TEXT DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE, + FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + "); + + echo "✅ Database and tables created successfully!\n"; + + // ตรวจสอบว่ามีข้อมูลหลักสูตรหรือยัง + $stmt = $pdo->query("SELECT COUNT(*) FROM curricula"); + if ($stmt->fetchColumn() == 0) { + echo "⏳ Importing curriculum data...\n"; + require_once __DIR__ . '/import_curriculum.php'; + echo "✅ Curriculum data imported!\n"; + } else { + echo "ℹ️ Curriculum data already exists, skipping import.\n"; + } + +} catch (PDOException $e) { + die("❌ Error: " . $e->getMessage() . "\n"); +} diff --git a/login-google-callback.php b/login-google-callback.php new file mode 100644 index 0000000..33a09a4 --- /dev/null +++ b/login-google-callback.php @@ -0,0 +1,100 @@ + 'https://oauth2.googleapis.com/token', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query([ + 'code' => $_GET['code'], + 'client_id' => GOOGLE_CLIENT_ID, + 'client_secret' => GOOGLE_CLIENT_SECRET, + 'redirect_uri' => GOOGLE_REDIRECT_URI, + 'grant_type' => 'authorization_code', + ]), + CURLOPT_RETURNTRANSFER => true, + ]); + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($http_code !== 200) { + $error = 'ไม่สามารถยืนยันตัวตนกับ Google ได้'; + } else { + $token_data = json_decode($response, true); + if (!isset($token_data['id_token'])) { + $error = 'ไม่ได้รับข้อมูลจาก Google'; + } else { + $parts = explode('.', $token_data['id_token']); + $encoded = strtr($parts[1], '-_', '+/'); + $encoded = str_pad($encoded, strlen($encoded) % 4 ? 4 - strlen($encoded) % 4 + strlen($encoded) : strlen($encoded), '=', STR_PAD_RIGHT); + $payload = json_decode(base64_decode($encoded), true); + + $email = $payload['email'] ?? ''; + $google_id = $payload['sub'] ?? ''; + + if (empty($email)) { + $error = 'ไม่ได้รับอีเมลจาก Google'; + } else { + $db = getDB(); + + $stmt = $db->prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user) { + if (empty($user['google_id'])) { + $stmt = $db->prepare("UPDATE users SET google_id = ? WHERE id = ?"); + $stmt->execute([$google_id, $user['id']]); + } + $_SESSION['logged_in'] = true; + $_SESSION['username'] = $user['name']; + $_SESSION['user_id'] = $user['id']; + $_SESSION['staff_code'] = $user['staff_code']; + $_SESSION['role'] = $user['role'] ?? 'advisor'; + $_SESSION['curriculum_id'] = $user['curriculum_id']; + header('Location: ' . BASE_URL . '/index.php'); + exit; + } + + $stmt = $db->prepare("SELECT * FROM students WHERE email = ?"); + $stmt->execute([$email]); + $student = $stmt->fetch(); + + if ($student) { + if (empty($student['google_id'])) { + try { + $stmt = $db->prepare("UPDATE students SET google_id = ? WHERE id = ?"); + $stmt->execute([$google_id, $student['id']]); + } catch (PDOException $e) { + // column may not exist yet + } + } + $_SESSION['logged_in'] = true; + $_SESSION['username'] = $student['name_th']; + $_SESSION['user_id'] = $student['id']; + $_SESSION['student_code'] = $student['student_code']; + $_SESSION['role'] = 'student'; + $_SESSION['curriculum_id'] = $student['curriculum_id']; + header('Location: ' . BASE_URL . '/index.php'); + exit; + } + + $error = 'ไม่พบอีเมล ' . htmlspecialchars($email) . ' ในระบบ กรุณาติดต่อผู้ดูแลระบบ'; + } + } + } +} + +$_SESSION['google_login_error'] = $error; +header('Location: ' . BASE_URL . '/login.php'); +exit; diff --git a/login-google.php b/login-google.php new file mode 100644 index 0000000..3af3b33 --- /dev/null +++ b/login-google.php @@ -0,0 +1,22 @@ + GOOGLE_CLIENT_ID, + 'redirect_uri' => GOOGLE_REDIRECT_URI, + 'response_type' => 'code', + 'scope' => 'openid email profile', + 'access_type' => 'online', + 'state' => $state, +]; + +header('Location: https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query($params)); +exit; diff --git a/login.php b/login.php new file mode 100644 index 0000000..69f07cd --- /dev/null +++ b/login.php @@ -0,0 +1,193 @@ + + + + + + + เข้าสู่ระบบ - ระบบใบควบคุมผลการเรียน + + + + + + + + + + + + +
+ +
+
+ +
+
+ + +
+

มหาวิทยาลัยตาปี

+

คณะนวัตกรรมดิจิทัลเทคโนโลยี

+
+
+ + ระบบใบควบคุมผลการเรียนนักศึกษา + +
+
+
+ + +
+

เข้าสู่ระบบ

+

กรุณาใส่ชื่อผู้ใช้และรหัสผ่าน

+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+
+
+ หรือ +
+
+ + + + + + + + + เข้าสู่ระบบด้วยอีเมลมหาวิทยาลัยตาปี + +
+ +

+ ระบบใบควบคุมผลการเรียน มหาวิทยาลัยตาปี +

+
+
+ + +
+
+
+
+
+ Logo +
+

คณะนวัตกรรมดิจิทัลเทคโนโลยี

+

มหาวิทยาลัยตาปี

+
+
+

© 2569 มหาวิทยาลัยตาปี

+
+
+
+ + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..7e229c1 --- /dev/null +++ b/logout.php @@ -0,0 +1,5 @@ +exec("ALTER TABLE users ADD COLUMN google_id VARCHAR(255) DEFAULT NULL AFTER email"); + echo "✅ Added google_id to users table\n"; + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'Duplicate column') !== false) { + echo "ℹ️ google_id already exists in users\n"; + } else { + throw $e; + } + } + + // Students table + $columns = [ + 'email' => "ALTER TABLE students ADD COLUMN email VARCHAR(255) DEFAULT NULL AFTER name_en", + 'password' => "ALTER TABLE students ADD COLUMN password VARCHAR(255) DEFAULT NULL AFTER email", + 'google_id' => "ALTER TABLE students ADD COLUMN google_id VARCHAR(255) DEFAULT NULL AFTER password", + 'advisor_id' => "ALTER TABLE students ADD COLUMN advisor_id INT DEFAULT NULL AFTER curriculum_id", + 'previous_qualification' => "ALTER TABLE students ADD COLUMN previous_qualification VARCHAR(255) DEFAULT NULL AFTER graduated_institution", + ]; + + foreach ($columns as $name => $sql) { + try { + $db->exec($sql); + echo "✅ Added $name to students table\n"; + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'Duplicate column') !== false) { + echo "ℹ️ $name already exists in students\n"; + } else { + throw $e; + } + } + } + + echo "\n✅ Migration completed successfully!\n"; + +} catch (Exception $e) { + die("❌ Error: " . $e->getMessage() . "\n"); +} diff --git a/pic/Logo DIT.png b/pic/Logo DIT.png new file mode 100644 index 0000000..b318e3a Binary files /dev/null and b/pic/Logo DIT.png differ diff --git a/pic/dashboard_curriculum.png b/pic/dashboard_curriculum.png new file mode 100644 index 0000000..aeee1c3 Binary files /dev/null and b/pic/dashboard_curriculum.png differ diff --git a/pic/dashboard_student_all.png b/pic/dashboard_student_all.png new file mode 100644 index 0000000..993e9fb Binary files /dev/null and b/pic/dashboard_student_all.png differ diff --git a/pic/dashboard_student_dbc.png b/pic/dashboard_student_dbc.png new file mode 100644 index 0000000..aeee1c3 Binary files /dev/null and b/pic/dashboard_student_dbc.png differ diff --git a/pic/dashboard_student_dt.png b/pic/dashboard_student_dt.png new file mode 100644 index 0000000..34aafbf Binary files /dev/null and b/pic/dashboard_student_dt.png differ diff --git a/pic/dashboard_transfer_credits.png b/pic/dashboard_transfer_credits.png new file mode 100644 index 0000000..26b0f86 Binary files /dev/null and b/pic/dashboard_transfer_credits.png differ diff --git a/pic/logo.png b/pic/logo.png new file mode 100644 index 0000000..695cd51 Binary files /dev/null and b/pic/logo.png differ diff --git a/reports/print_summary.php b/reports/print_summary.php new file mode 100644 index 0000000..9d337d1 --- /dev/null +++ b/reports/print_summary.php @@ -0,0 +1,260 @@ + + + + + +สรุปผลการเรียน - <?= htmlspecialchars($student['student_code']) ?> + + + + + +
+

ใบควบคุมผลการเรียน

+

มหาวิทยาลัยตาปี

+
+ +
+ + + + + + + +
รหัสนักศึกษา:
ชื่อ-นามสกุล: ()
สาขาวิชา: -
คณะ:
สถาบันที่จบ:
ปีการศึกษา:
+
+ +
+ 0 ? round(($total_completed / $total_curriculum_credits) * 100) : 0; ?> +
+ / หน่วยกิต (%) +
+
+ +
+
+
หน่วยกิตเทียบโอน +
+
+
หน่วยกิตเรียนผ่านแล้ว +
+
+
หน่วยกิตคงเหลือ +
+
+ + + +
+
สรุปรายวิชาที่เรียนผ่านแล้วทั้งหมด (แยกตามโครงสร้างหลักสูตร)
+ + + + + + +
รวมหน่วยกิตที่เรียนผ่านแล้วทั้งหมด หน่วยกิต
+
+ + +
+ + ✓ ครบตามหลักสูตรแล้ว ( / หน่วยกิต) + + หน่วยกิตคงเหลือที่ต้องเรียน: หน่วยกิต ( / หน่วยกิต) + +
+ + + + + +'; + echo '
' . htmlspecialchars($g['name_th']); + if ($g['min_credits'] > 0) echo ' (ไม่น้อยกว่า ' . $g['min_credits'] . ' หน่วยกิต)'; + echo '
'; + + if ($has_courses) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($node['courses'] as $course) { + $row_class = ''; + $status_text = 'ยังไม่เรียน'; + $sem = $yr = '-'; + if (isset($course['_completed'])) { + $row_class = $course['_completed']['source_type'] == 'transfer' ? 'transfer' : 'completed'; + $status_text = $course['_completed']['source_type'] == 'transfer' ? 'เทียบโอน' : 'เรียนแล้ว'; + $sem = $course['_completed']['semester'] ? 'ภาค ' . htmlspecialchars($course['_completed']['semester']) : '-'; + $yr = htmlspecialchars($course['_completed']['academic_year'] ?: '-'); + } + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
รหัสวิชาชื่อวิชาหน่วยกิต(ท-ป-อ)ภาคเรียนปีการศึกษาสถานะ
' . htmlspecialchars($course['code']) . '' . htmlspecialchars($course['name_th']) . '' . number_format($course['credits'], 1) . '(' . $course['lecture_hours'] . '-' . $course['practice_hours'] . '-' . $course['self_study_hours'] . ')' . $sem . '' . $yr . '' . $status_text . '
'; + } + + if ($has_children) { + renderPrintTree($node['children'], $is_dbc, $level + 1); + } + + echo ''; + } +} + +function renderGroupedSummary($nodes, $level = 0) { + foreach ($nodes as $node) { + $g = $node['group']; + $has_completed = $node['completed_credits'] > 0; + $has_courses = !empty($node['courses']); + $has_children = !empty($node['children']); + + if (!$has_completed && !$has_children) { + continue; + } + + $style = $level == 0 ? 'section-title' : ($level == 1 ? 'subsection-title' : 'subsubsection-title'); + echo '
'; + echo '
' . htmlspecialchars($g['name_th']); + if ($g['min_credits'] > 0) echo ' (ไม่น้อยกว่า ' . $g['min_credits'] . ' หน่วยกิต)'; + if ($level == 0 && $has_completed) { + echo ' — เรียนผ่านแล้ว ' . $node['completed_credits'] . ' หน่วยกิต'; + } + echo '
'; + + if ($has_courses) { + $group_total = 0; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($node['courses'] as $course) { + if (!isset($course['_completed'])) continue; + $sc = $course['_completed']; + $group_total += $sc['credits']; + $type = htmlspecialchars($sc['course_type'] ?: ($sc['source_type'] == 'transfer' ? 'เทียบโอน' : 'เรียน')); + $sem = $sc['semester'] ? 'ภาค ' . htmlspecialchars($sc['semester']) : '-'; + $yr = htmlspecialchars($sc['academic_year'] ?: '-'); + $row_class = $sc['source_type'] == 'transfer' ? 'transfer' : 'completed'; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
รหัสวิชาชื่อวิชาหน่วยกิตเกรดภาคเรียนปีการศึกษาประเภท
' . htmlspecialchars($sc['course_code']) . '' . htmlspecialchars($sc['course_name_th']) . '' . number_format($sc['credits'], 1) . '' . htmlspecialchars($sc['grade'] ?: '-') . '' . $sem . '' . $yr . '' . $type . '
รวมหน่วยกิต' . number_format($group_total, 1) . '
'; + } + + if ($has_children) { + renderGroupedSummary($node['children'], $level + 1); + } + + echo '
'; + } +} diff --git a/reports/summary_grouped_pdf.php b/reports/summary_grouped_pdf.php new file mode 100644 index 0000000..aa5c451 --- /dev/null +++ b/reports/summary_grouped_pdf.php @@ -0,0 +1,215 @@ + 0 ? round(($total_completed / $total_curriculum_credits) * 100) : 0; + +// ดึงข้อมูลชื่ออาจารย์ที่ปรึกษาจากระบบ +$advisor_name = '................................'; +if (!empty($student['advisor_id'])) { + $advisor = getUser($student['advisor_id']); + if ($advisor) { + $advisor_name = $advisor['name']; + } +} +?> + + + + +สรุปผลการเรียนแยกตามโครงสร้าง - <?= htmlspecialchars($student['student_code']) ?> + + + + + +
+

สรุปผลการเรียน

+

มหาวิทยาลัยตาปี

+
+ +
+ + + + + + + +
รหัสนักศึกษา:
ชื่อ-นามสกุล: ()
สาขาวิชา: -
คณะ:
สถาบันที่จบ:
ปีการศึกษา:
+
+ +
+
+
หน่วยกิตเทียบโอน +
+
+
หน่วยกิตเรียนผ่านแล้ว +
+
+
หน่วยกิตคงเหลือ +
+
+ +
+
สรุปรายวิชาที่เรียนผ่านแล้วทั้งหมด (แยกตามโครงสร้างหลักสูตร)
+ + + + + + +
รวมหน่วยกิตที่เรียนผ่านแล้วทั้งหมด หน่วยกิต
+
+ +
+ + ✓ ครบตามหลักสูตรแล้ว ( / หน่วยกิต) + + หน่วยกิตคงเหลือที่ต้องเรียน: หน่วยกิต ( / หน่วยกิต) + +
+ + + + + + + + +
+ ............................................................
+ ( )
+ อาจารย์ที่ปรึกษา
  +
+ ............................................................
+ ( ............................................................ )
+ ประธานหลักสูตรสาขาวิชา
+
+ ............................................................
+ ( ............................................................ )
+ คณบดี
+
+ + + + + + 0; + $has_courses = !empty($node['courses']); + $has_children = !empty($node['children']); + + if (!$has_completed && !$has_children) continue; + + $style = $level == 0 ? 'section-title' : ($level == 1 ? 'subsection-title' : 'subsubsection-title'); + echo '
'; + echo '
' . htmlspecialchars($g['name_th']); + if ($g['min_credits'] > 0) echo ' (ไม่น้อยกว่า ' . $g['min_credits'] . ' หน่วยกิต)'; + if ($level == 0 && $has_completed) { + echo ' — เรียนผ่านแล้ว ' . $node['completed_credits'] . ' หน่วยกิต'; + } + echo '
'; + + if ($has_courses) { + $group_total = 0; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($node['courses'] as $course) { + if (!isset($course['_completed'])) continue; + $sc = $course['_completed']; + $group_total += $sc['credits']; + $type = htmlspecialchars($sc['course_type'] ?: ($sc['source_type'] == 'transfer' ? 'เทียบโอน' : 'เรียน')); + $sem = $sc['semester'] ? 'ภาค ' . htmlspecialchars($sc['semester']) : '-'; + $yr = htmlspecialchars($sc['academic_year'] ?: '-'); + $row_class = $sc['source_type'] == 'transfer' ? 'transfer' : 'completed'; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
รหัสวิชาชื่อวิชาหน่วยกิตเกรดภาคเรียนปีการศึกษาประเภท
' . htmlspecialchars($sc['course_code']) . '' . htmlspecialchars($sc['course_name_th']) . '' . number_format($sc['credits'], 1) . '' . htmlspecialchars($sc['grade'] ?: '-') . '' . $sem . '' . $yr . '' . $type . '
รวมหน่วยกิต' . number_format($group_total, 1) . '
'; + } + + if ($has_children) { + renderGroupedSummary($node['children'], $level + 1); + } + + echo '
'; + } +} diff --git a/reports/summary_pdf.php b/reports/summary_pdf.php new file mode 100644 index 0000000..007616a --- /dev/null +++ b/reports/summary_pdf.php @@ -0,0 +1,167 @@ +SetFont('Courier', '', 10); + $this->Cell(0, 8, iconv('UTF-8','cp874','ใบควบคุมผลการเรียน'), 0, 1, 'C'); + $this->Ln(2); + } + function footer() { + $this->SetY(-15); + $this->SetFont('Courier', '', 8); + $this->Cell(0, 10, iconv('UTF-8','cp874','หน้าที่ ').$this->PageNo().'/{nb}', 0, 0, 'C'); + } +} + +$pdf = new SummaryPDF('P', 'mm', 'A4'); +$pdf->AliasNbPages(); +$pdf->SetMargins(8, 10, 8); +$pdf->AddPage(); + +// Student info +$pdf->SetFont('Courier', '', 9); +$info = [ + ['รหัสนักศึกษา:', $student['student_code']], + ['ชื่อ-นามสกุล:', $student['name_th'] . ' (' . $student['name_en'] . ')'], + ['สาขาวิชา:', $student['curriculum_code'] . ' - ' . $student['curriculum_name']], + ['คณะ:', $student['faculty'] ?: '-'], + ['สถาบันที่จบ:', $student['graduated_institution'] ?: '-'], + ['ปีการศึกษา:', $student['enrollment_year']], +]; +foreach ($info as $row) { + $pdf->Cell(45, 5, iconv('UTF-8','cp874',$row[0]), 0, 0); + $pdf->Cell(0, 5, iconv('UTF-8','cp874//IGNORE',$row[1]), 0, 1); +} + +$pdf->Ln(1); +$remaining = max(0, $total_curriculum_credits - $total_completed); +$pct = $total_curriculum_credits > 0 ? round(($total_completed / $total_curriculum_credits) * 100) : 0; +$pdf->SetFont('Courier', 'B', 9); +$pdf->Cell(0, 6, iconv('UTF-8','cp874','เทียบโอน: '.$transfer_credits_total.' | เรียนแล้ว: '.$total_completed.' | คงเหลือ: '.$remaining.' | คิดเป็น '.$pct.'%'), 0, 1); +$pdf->Ln(3); + +// Render tree recursively +$level = 0; +renderPdfTree($summary['groups'], $pdf, $level); + +// Summary table +$pdf->Ln(3); +$pdf->SetFont('Courier', 'B', 9); +$pdf->Cell(0, 6, iconv('UTF-8','cp874','--- สรุปรายวิชาที่เรียนผ่านแล้วทั้งหมด ---'), 0, 1); +$pdf->Ln(1); + +$pdf->SetFont('Courier', '', 8); +$pdf->Cell(28, 4, iconv('UTF-8','cp874','รหัสวิชา'), 1, 0); +$pdf->Cell(46, 4, iconv('UTF-8','cp874','ชื่อวิชา'), 1, 0); +$pdf->Cell(12, 4, iconv('UTF-8','cp874','หน่วยกิต'), 1, 0, 'C'); +$pdf->Cell(10, 4, iconv('UTF-8','cp874','เกรด'), 1, 0, 'C'); +$pdf->Cell(16, 4, iconv('UTF-8','cp874','ภาคเรียน'), 1, 0, 'C'); +$pdf->Cell(16, 4, iconv('UTF-8','cp874','ปีการศึกษา'), 1, 0, 'C'); +$pdf->Cell(19, 4, iconv('UTF-8','cp874','ประเภท'), 1, 1); + +$total = 0; +foreach ($student_courses as $sc) { + $total += $sc['credits']; + $name = mb_substr($sc['course_name_th'], 0, 24, 'UTF-8'); + $type = iconv('UTF-8','cp874//IGNORE', $sc['course_type'] ?: ($sc['source_type'] == 'transfer' ? 'เทียบโอน' : 'เรียน')); + $sem = $sc['semester'] ? 'ภาค '.$sc['semester'] : '-'; + $yr = $sc['academic_year'] ?: '-'; + $grade = $sc['grade'] ?: '-'; + $pdf->SetFont('Courier', '', 8); + $pdf->Cell(28, 4, iconv('UTF-8','cp874//IGNORE',$sc['course_code']), 1, 0); + $pdf->Cell(46, 4, iconv('UTF-8','cp874//IGNORE',$name), 1, 0); + $pdf->Cell(12, 4, $sc['credits'], 1, 0, 'C'); + $pdf->Cell(10, 4, $grade, 1, 0, 'C'); + $pdf->Cell(16, 4, iconv('UTF-8','cp874//IGNORE',$sem), 1, 0, 'C'); + $pdf->Cell(16, 4, iconv('UTF-8','cp874//IGNORE',$yr), 1, 0, 'C'); + $pdf->Cell(19, 4, $type, 1, 1); +} + +$pdf->SetFont('Courier', 'B', 8); +$pdf->Cell(147, 5, iconv('UTF-8','cp874','รวมหน่วยกิตที่เรียนผ่านแล้วทั้งหมด: '.$total.' หน่วยกิต'), 0, 1); +$pdf->Ln(3); + +if ($remaining <= 0) { + $pdf->SetFont('Courier', 'B', 10); + $pdf->Cell(0, 6, iconv('UTF-8','cp874','*** ครบตามหลักสูตรแล้ว ***'), 0, 1, 'C'); +} else { + $pdf->SetFont('Courier', '', 9); + $pdf->Cell(0, 6, iconv('UTF-8','cp874','หน่วยกิตคงเหลือที่ต้องเรียน: '.$remaining.' หน่วยกิต'), 0, 1); +} + +$filename = 'Summary_' . $student['student_code'] . '_' . date('Ymd') . '.pdf'; +$pdf->Output('I', $filename); + +// ============================================================ +function renderPdfTree($nodes, &$pdf, $level) { + foreach ($nodes as $node) { + $g = $node['group']; + $has_courses = !empty($node['courses']); + $has_children = !empty($node['children']); + + $indent = $level * 4; + $pdf->SetX(8 + $indent); + + if ($level == 0) { + $pdf->SetFont('Courier', 'B', 9); + } elseif ($level == 1) { + $pdf->SetFont('Courier', 'B', 8); + } else { + $pdf->SetFont('Courier', '', 8); + } + + $title = $g['name_th']; + if ($g['min_credits'] > 0) $title .= ' ('.iconv('UTF-8','cp874','ไม่น้อยกว่า ').$g['min_credits'].iconv('UTF-8','cp874',' หน่วยกิต').')'; + $pdf->Cell(0, 5, iconv('UTF-8','cp874//IGNORE',$title), 0, 1); + + if ($has_courses) { + $pdf->SetX(8 + $indent); + $pdf->SetFont('Courier', '', 7); + $pdf->Cell(30, 3, iconv('UTF-8','cp874','รหัสวิชา'), 0, 0); + $pdf->Cell(50, 3, iconv('UTF-8','cp874','ชื่อวิชา'), 0, 0); + $pdf->Cell(12, 3, iconv('UTF-8','cp874','หน่วยกิต'), 0, 0, 'C'); + $pdf->Cell(10, 3, iconv('UTF-8','cp874','เกรด'), 0, 0, 'C'); + $pdf->Cell(18, 3, iconv('UTF-8','cp874','ประเภท'), 0, 1); + + foreach ($node['courses'] as $course) { + $status_text = ''; + $grade_text = ''; + if (isset($course['_completed'])) { + $sc = $course['_completed']; + $status_text = iconv('UTF-8','cp874//IGNORE', $sc['course_type'] ?: ($sc['source_type'] == 'transfer' ? 'เทียบโอน' : 'เรียนแล้ว')); + $grade_text = $sc['grade'] ?: '-'; + } + $name = mb_substr($course['name_th'], 0, 28, 'UTF-8'); + $pdf->SetX(8 + $indent); + $pdf->SetFont('Courier', '', 8); + $pdf->Cell(30, 4, iconv('UTF-8','cp874//IGNORE',$course['code']), 0, 0); + $pdf->Cell(50, 4, iconv('UTF-8','cp874//IGNORE',$name), 0, 0); + $pdf->Cell(12, 4, $course['credits'], 0, 0, 'C'); + $pdf->Cell(10, 4, $grade_text, 0, 0, 'C'); + $pdf->Cell(18, 4, $status_text, 0, 1); + } + $pdf->Ln(1); + } + + if ($has_children) { + renderPdfTree($node['children'], $pdf, $level + 1); + } + } +} diff --git a/reports/transfer_report.php b/reports/transfer_report.php new file mode 100644 index 0000000..02d5830 --- /dev/null +++ b/reports/transfer_report.php @@ -0,0 +1,216 @@ +getMessage(); + } +} else { + $error_msg = 'ยังไม่ได้นำเข้าไฟล์ PDF สำหรับการเทียบโอนวิชานี้ หรือไม่พบไฟล์ PDF ในระบบ'; +} + +$is_dbc = ($student['curriculum_code'] == 'DBC'); +?> + + + + +รายงานผลการประเมินเทียบโอนรายวิชา - <?= htmlspecialchars($student['student_code']) ?> + + + +
+ + +
+

ผลการประเมินเทียบโอนรายวิชา

+

มหาวิทยาลัยตาปี (Tapee University)

+
+ +
+ + + + + + + + + + + + + + + + + + + +
รหัสนักศึกษา:ปีที่เข้าศึกษา:
ชื่อ-นามสกุล: ()คณะ:
สาขาวิชา: + - +
+ หน่วยกิตที่เทียบโอนได้: หน่วยกิต +
+
สถาบันที่จบเดิม:
+
+ + +
+ ⚠️ +
+ +
รายละเอียดวิชาที่เทียบโอนได้
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ลำดับวิชาปลายทาง (ตามหลักสูตร)วิชาต้นทางที่ใช้เทียบ (สถาบันเดิม)
รหัสชื่อวิชาหน่วยกิตรหัสชื่อวิชาหน่วยกิตเกรดความสอดคล้อง
ไม่พบข้อมูลรายวิชาที่เทียบโอนได้
+ + + + + +
+ + diff --git a/sql/migration_google_oauth.sql b/sql/migration_google_oauth.sql new file mode 100644 index 0000000..1590a34 --- /dev/null +++ b/sql/migration_google_oauth.sql @@ -0,0 +1,9 @@ +-- Add google_id column to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS google_id VARCHAR(255) DEFAULT NULL AFTER email; + +-- Add columns to students table (if not already exist) +ALTER TABLE students ADD COLUMN IF NOT EXISTS email VARCHAR(255) DEFAULT NULL AFTER name_en; +ALTER TABLE students ADD COLUMN IF NOT EXISTS password VARCHAR(255) DEFAULT NULL AFTER email; +ALTER TABLE students ADD COLUMN IF NOT EXISTS google_id VARCHAR(255) DEFAULT NULL AFTER password; +ALTER TABLE students ADD COLUMN IF NOT EXISTS advisor_id INT DEFAULT NULL AFTER curriculum_id; +ALTER TABLE students ADD COLUMN IF NOT EXISTS previous_qualification VARCHAR(255) DEFAULT NULL AFTER graduated_institution; diff --git a/sql/schema.sql b/sql/schema.sql new file mode 100644 index 0000000..eaf1142 --- /dev/null +++ b/sql/schema.sql @@ -0,0 +1,73 @@ +CREATE DATABASE IF NOT EXISTS transfer_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE transfer_db; + +-- หลักสูตร +CREATE TABLE curricula ( + id INT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(10) NOT NULL UNIQUE, + name_th VARCHAR(255) NOT NULL, + name_en VARCHAR(255) NOT NULL, + total_credits INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- กลุ่มวิชาในหลักสูตร (โครงสร้าง) +CREATE TABLE course_groups ( + id INT AUTO_INCREMENT PRIMARY KEY, + curriculum_id INT NOT NULL, + parent_id INT DEFAULT NULL, + code VARCHAR(50) NOT NULL, + name_th VARCHAR(255) NOT NULL, + min_credits INT NOT NULL DEFAULT 0, + sort_order INT NOT NULL DEFAULT 0, + FOREIGN KEY (curriculum_id) REFERENCES curricula(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES course_groups(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- รายวิชาในหลักสูตร +CREATE TABLE courses ( + id INT AUTO_INCREMENT PRIMARY KEY, + group_id INT NOT NULL, + code VARCHAR(20) NOT NULL UNIQUE, + name_th VARCHAR(255) NOT NULL, + name_en VARCHAR(255) DEFAULT '', + credits DECIMAL(3,1) NOT NULL, + lecture_hours INT NOT NULL DEFAULT 0, + practice_hours INT NOT NULL DEFAULT 0, + self_study_hours INT NOT NULL DEFAULT 0, + sort_order INT NOT NULL DEFAULT 0, + FOREIGN KEY (group_id) REFERENCES course_groups(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- นักศึกษา +CREATE TABLE students ( + id INT AUTO_INCREMENT PRIMARY KEY, + student_code VARCHAR(20) NOT NULL UNIQUE, + name_th VARCHAR(255) NOT NULL, + name_en VARCHAR(255) DEFAULT '', + faculty VARCHAR(255) DEFAULT '', + graduated_institution VARCHAR(255) DEFAULT '', + curriculum_id INT NOT NULL, + enrollment_year INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (curriculum_id) REFERENCES curricula(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- รายวิชาที่นักศึกษาเรียนผ่านแล้ว (ทั้งเทียบโอนและลงทะเบียน) +CREATE TABLE student_courses ( + id INT AUTO_INCREMENT PRIMARY KEY, + student_id INT NOT NULL, + course_id INT DEFAULT NULL, + course_code VARCHAR(20) NOT NULL, + course_name_th VARCHAR(255) NOT NULL, + credits DECIMAL(3,1) NOT NULL, + lecture_hours INT NOT NULL DEFAULT 0, + practice_hours INT NOT NULL DEFAULT 0, + self_study_hours INT NOT NULL DEFAULT 0, + source_type ENUM('transfer','manual') NOT NULL DEFAULT 'manual', + source_file VARCHAR(255) DEFAULT NULL, + notes TEXT DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE, + FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/students/add.php b/students/add.php new file mode 100644 index 0000000..8a3df06 --- /dev/null +++ b/students/add.php @@ -0,0 +1,145 @@ +prepare("INSERT INTO students (student_code, name_th, name_en, email, faculty, graduated_institution, curriculum_id, enrollment_year, advisor_id, previous_qualification) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$student_code, $name_th, $name_en, $email, $faculty, $graduated_institution, $curriculum_id, $enrollment_year, $advisor_id, $previous_qualification ?: null]); + $message = 'เพิ่มนักศึกษาสำเร็จ'; + echo ""; + exit; + } catch (PDOException $e) { + if ($e->getCode() == 23000) { + $error = 'รหัสนักศึกษานี้มีอยู่ในระบบแล้ว'; + } else { + $error = 'เกิดข้อผิดพลาด: ' . $e->getMessage(); + } + } + } +} +?> +
+
+

เพิ่มนักศึกษาใหม่

+
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + กลับ +
+
+
+
+ diff --git a/students/delete.php b/students/delete.php new file mode 100644 index 0000000..8ba81b4 --- /dev/null +++ b/students/delete.php @@ -0,0 +1,26 @@ +prepare("DELETE FROM students WHERE id = ?"); + $stmt->execute([$id]); +} catch (PDOException $e) { + // ignore +} + +header('Location: list.php'); +exit; diff --git a/students/edit.php b/students/edit.php new file mode 100644 index 0000000..45a71d2 --- /dev/null +++ b/students/edit.php @@ -0,0 +1,143 @@ +ไม่พบข้อมูลนักศึกษา'; + require_once __DIR__ . '/../includes/footer.php'; + exit; +} + +$curricula = getCurricula(); +$faculties = getFacultyList(); +$error = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isStudent()) { + $student_code = $student['student_code']; + $curriculum_id = intval($student['curriculum_id']); + $faculty = $student['faculty']; + $enrollment_year = intval($student['enrollment_year']); + $advisor_id = $student['advisor_id'] ? intval($student['advisor_id']) : null; + } else { + $student_code = trim($_POST['student_code'] ?? ''); + $curriculum_id = intval($_POST['curriculum_id'] ?? 0); + $faculty = trim($_POST['faculty'] ?? ''); + $enrollment_year = intval($_POST['enrollment_year'] ?? date('Y')); + $advisor_id = !empty($_POST['advisor_id']) ? intval($_POST['advisor_id']) : null; + } + $name_th = trim($_POST['name_th'] ?? ''); + $name_en = trim($_POST['name_en'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $graduated_institution = trim($_POST['graduated_institution'] ?? ''); + $previous_qualification = trim($_POST['previous_qualification'] ?? ''); + + if (empty($student_code) || empty($name_th) || empty($curriculum_id)) { + $error = 'กรุณากรอกข้อมูลให้ครบถ้วน'; + } else { + try { + $stmt = getDB()->prepare("UPDATE students SET student_code=?, name_th=?, name_en=?, email=?, faculty=?, graduated_institution=?, curriculum_id=?, enrollment_year=?, previous_qualification=?, advisor_id=? WHERE id=?"); + $stmt->execute([$student_code, $name_th, $name_en, $email, $faculty, $graduated_institution, $curriculum_id, $enrollment_year, $previous_qualification ?: null, $advisor_id, $id]); + echo ""; + exit; + } catch (PDOException $e) { + $error = 'เกิดข้อผิดพลาด: ' . $e->getMessage(); + } + } +} +?> +
+
+

แก้ไขข้อมูลนักศึกษา

+
+
+ +
+ +
+
+ + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + กลับ +
+
+
+
+ diff --git a/students/import_csv.php b/students/import_csv.php new file mode 100644 index 0000000..37bf9bd --- /dev/null +++ b/students/import_csv.php @@ -0,0 +1,213 @@ +ไม่พบนักศึกษา'; require_once __DIR__ . '/../includes/footer.php'; exit; } +} + +require_once __DIR__ . '/../includes/header.php'; + +$results = []; +$total_success = 0; +$total_error = 0; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] === UPLOAD_ERR_OK) { + $tmp = $_FILES['csv_file']['tmp_name']; + // Read all rows at once + $all_rows = []; + $handle = fopen($tmp, 'r'); + if ($handle) { + while (($r = fgetcsv($handle)) !== false) $all_rows[] = $r; + fclose($handle); + } + if (empty($all_rows)) { $results[] = 'ไฟล์ CSV ว่างเปล่า'; $total_error++; } + else { + $first = $all_rows[0]; + $first[0] = preg_replace('/^\xEF\xBB\xBF/', '', $first[0]); + $first = array_map(function($v) { return trim($v, " \t\n\r\0\x0B\""); }, $first); + // Auto-detect header: if first cell starts with a digit, it's data (no header) + $has_header = !preg_match('/^\d/', $first[0]); + if ($has_header) { + $header_norm = array_map(function($v) { + return preg_replace('/[^a-z0-9_]/', '', str_replace(' ', '_', strtolower(trim($v)))); + }, $first); + $results[] = "พบหัวคอลัมน์: " . implode(', ', $first); + $col_map = []; + $expected = $student_id ? ['course_code','course_name','credits','grade','course_type','semester','academic_year','lecture','practice','self_study','notes','group_id'] : ['student_code','course_code','course_name','credits','grade','course_type','semester','academic_year','lecture','practice','self_study','notes','group_id']; + foreach ($expected as $e) { + $idx = array_search($e, $header_norm); + if ($idx !== false) $col_map[$e] = $idx; + } + array_shift($all_rows); + } else { + $results[] = "ไม่พบแถวหัวคอลัมน์ — ใช้ตำแหน่งคอลัมน์ตามลำดับ"; + if ($student_id) { + $col_map = ['course_code' => 0, 'course_name' => 1, 'credits' => 2, 'grade' => 3, 'course_type' => 4, 'semester' => 5, 'academic_year' => 6, 'lecture' => 7, 'practice' => 8, 'self_study' => 9, 'notes' => 10, 'group_id' => 11]; + } else { + $col_map = ['student_code' => 0, 'course_code' => 1, 'course_name' => 2, 'credits' => 3, 'grade' => 4, 'course_type' => 5, 'semester' => 6, 'academic_year' => 7, 'lecture' => 8, 'practice' => 9, 'self_study' => 10, 'notes' => 11, 'group_id' => 12]; + } + } + $db = getDB(); + foreach ($all_rows as $idx => $row) { + $line = $idx + 1; + $c = fn($k) => $col_map[$k] ?? null; + if ($student_id) { + $student = getStudent($student_id); + $student_code = $student['student_code']; + } else { + $student_code = trim($row[$c('student_code')] ?? ''); + } + $course_code = trim($row[$c('course_code')] ?? ''); + $course_name = trim($row[$c('course_name')] ?? ''); + $credits = floatval($row[$c('credits')] ?? 0); + $grade = trim($row[$c('grade')] ?? ''); + $course_type_col = trim($row[$c('course_type')] ?? ''); + $semester = trim($row[$c('semester')] ?? ''); + $academic_year = trim($row[$c('academic_year')] ?? ''); + $lecture = intval($row[$c('lecture')] ?? 0); + $practice = intval($row[$c('practice')] ?? 0); + $self_study = intval($row[$c('self_study')] ?? 0); + $notes = trim($row[$c('notes')] ?? ''); + $group_id = intval($row[$c('group_id')] ?? 0); + + if (empty($course_code) || empty($course_name) || $credits <= 0) { + $results[] = "แถวที่ $line: ข้าม (ข้อมูลไม่ครบ: course_code, course_name, credits)"; + $total_error++; + continue; + } + + try { + if (!$student_id) { + $stmt = $db->prepare("SELECT id, curriculum_id, advisor_id FROM students WHERE student_code = ?"); + $stmt->execute([$student_code]); + $student = $stmt->fetch(); + if (!$student) { + $results[] = "แถวที่ $line: ไม่พบรหัสนักศึกษา '$student_code'"; + $total_error++; + continue; + } + if (isAdvisor() && $student['advisor_id'] != getCurrentUserId()) { + $results[] = "แถวที่ $line: นักศึกษา '$student_code' ไม่อยู่ในที่ปรึกษาของคุณ"; + $total_error++; + continue; + } + } + + $curriculum_course = findCourseInCurriculum($student['curriculum_id'], $course_code); + $course_id = null; + if ($curriculum_course) { + $course_id = $curriculum_course['id']; + } elseif ($group_id > 0) { + $stmt = $db->prepare("SELECT id FROM courses WHERE group_id = ? AND code = ?"); + $stmt->execute([$group_id, $course_code]); + $existing = $stmt->fetch(); + if ($existing) { + $course_id = $existing['id']; + } else { + $stmt = $db->prepare("INSERT INTO courses (group_id, code, name_th, credits, lecture_hours, practice_hours, self_study_hours, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, 0)"); + $stmt->execute([$group_id, $course_code, $course_name, $credits, $lecture, $practice, $self_study]); + $course_id = $db->lastInsertId(); + } + } + + $stmt = $db->prepare("INSERT INTO student_courses (student_id, course_id, course_code, course_name_th, credits, lecture_hours, practice_hours, self_study_hours, grade, course_type, semester, academic_year, source_type, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'manual', ?)"); + $stmt->execute([$student['id'], $course_id, $course_code, $course_name, $credits, $lecture, $practice, $self_study, $grade ?: null, $course_type_col ?: null, $semester ?: null, $academic_year ?: null, $notes]); + $results[] = "แถวที่ $line: ✓ $student_code - $course_code ($course_name)"; + $total_success++; + } catch (PDOException $e) { + $results[] = "แถวที่ $line: ผิดพลาด - " . $e->getMessage(); + $total_error++; + } + } + } +} +?> +
+
+

นำเข้ารายวิชาจากไฟล์ CSV

+
รองรับไฟล์ .csv ตามรูปแบบคอลัมน์ที่ระบบกำหนด
+
+ กลับ +
+ + +
กำลังนำเข้าสำหรับนักศึกษา: - (ไม่ต้องระบุ student_code ใน CSV)
+ + +
+
+
+
+
เลือกไฟล์สำหรับนำเข้า
+
+
+
+
+ + +
+
+ +

ไฟล์ CSV ต้องมีหัวคอลัมน์ด้านล่างนี้ (คั่นด้วย comma):

+ +

คอลัมน์ที่จำเป็น:

+
+ +
+
+
+
+
+

📥 ดาวน์โหลด Template

+ template.csv +
+
+
+ +
+ 0 || $total_error > 0): ?> +
+
+ ผลลัพธ์ + สำเร็จ | ล้มเหลว +
+
+
    + +
  • + +
+
+
+ +
+
+ + diff --git a/students/list.php b/students/list.php new file mode 100644 index 0000000..1c1f8e2 --- /dev/null +++ b/students/list.php @@ -0,0 +1,100 @@ +prepare(" + SELECT + student_id, + COALESCE(SUM(CASE WHEN source_type = 'transfer' THEN credits ELSE 0 END), 0) AS transfer_total, + COALESCE(SUM(CASE WHEN source_type = 'manual' THEN credits ELSE 0 END), 0) AS manual_total, + COALESCE(SUM(credits), 0) AS total + FROM student_courses + WHERE student_id IN ($placeholders) + GROUP BY student_id + "); + $stmt->execute($ids); + foreach ($stmt->fetchAll() as $row) { + $credits_map[$row['student_id']] = [ + 'transfer' => floatval($row['transfer_total']), + 'manual' => floatval($row['manual_total']), + 'total' => floatval($row['total']) + ]; + } +} +?> +
+
+

รายชื่อนักศึกษา

+
ทั้งหมด รายการ
+
+ เพิ่มนักศึกษา +
+ +
+
+

ตารางข้อมูลนักศึกษา

+ รายการ +
+
+ +
ยังไม่มีข้อมูลนักศึกษา คลิกเพิ่มนักศึกษา
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
รหัสนักศึกษาชื่อ-นามสกุลสาขาวิชาคณะสถาบันที่จบวุฒิเดิมปีการศึกษาหน่วยกิตที่เทียบโอนหน่วยกิตที่เรียนแล้วหน่วยกิตทั้งหมดที่เทียบโอนและเรียนแล้วการจัดการ
+ +
+
+ +
+
+ diff --git a/students/view.php b/students/view.php new file mode 100644 index 0000000..4342348 --- /dev/null +++ b/students/view.php @@ -0,0 +1,373 @@ +ไม่พบข้อมูลนักศึกษา'; + require_once __DIR__ . '/../includes/footer.php'; + exit; +} + +if (isset($_GET['added'])) { + $msg_class = 'success'; + $msg_text = 'เพิ่มรายวิชาเรียบร้อยแล้ว'; + if ($_GET['added'] === '0') $msg_text = 'ไม่พบรายวิชาใหม่จากไฟล์ (อาจซ้ำหรือไม่ตรงหลักสูตร)'; + echo "
$msg_text
"; +} +if (isset($_GET['error'])) { + $err = $_GET['error']; + $map = ['upload_failed' => 'อัปโหลดไฟล์ล้มเหลว', 'no_text' => 'ไม่พบข้อความในไฟล์ PDF', 'invalid_data' => 'ข้อมูลไม่ถูกต้อง']; + $txt = $map[$err] ?? $err; + echo "
$txt
"; +} + +$summary = getStudentSummary($id); +$student_courses = getStudentCourses($id); + +// เรียง: ลงเรียนปกติ+ยังไม่ออกเกรด → เทียบโอน/ออกเกรดแล้ว +usort($student_courses, function($a, $b) { + $isUngraded = fn($r) => $r['grade'] === null || $r['grade'] === '' || $r['grade'] === '-' || mb_strpos((string)$r['grade'], 'กำลัง') === 0; + $a_ungraded = $isUngraded($a); + $b_ungraded = $isUngraded($b); + if ($a_ungraded && !$b_ungraded) return -1; + if (!$a_ungraded && $b_ungraded) return 1; + return strcasecmp($a['course_code'], $b['course_code']); +}); + +$total_curriculum_credits = getTotalCurriculumCredits($student['curriculum_id']); +$total_completed = getCompletedCreditsTotal($id); +$transfer_credits = getTransferCreditsTotal($id); +$remaining_credits = max(0, $total_curriculum_credits - $total_completed); +$is_dbc = ($student['curriculum_code'] == 'DBC'); +?> + + +
+
+
+
+

ข้อมูลนักศึกษา

+
+
+ + + + + + + + + +
รหัสนักศึกษา:
ชื่อ-นามสกุล: ()
อีเมล:
สาขาวิชา: -
คณะ:
สถาบันที่จบ:
วุฒิเดิม:
ปีการศึกษาที่เข้าศึกษา:
+
+
+
+
+
+ +
หน่วยกิตที่เทียบโอนได้
+
+
+
+ +
หน่วยกิตที่เรียนผ่านแล้วทั้งหมด
+
+
+
+ +
หน่วยกิตที่ต้องเรียนอีก
+
/
+
+
+ 0 ? round(($total_completed / $total_curriculum_credits) * 100) : 0 ?>% +
+
+
+
+
+ +
+
+
+
+
รายวิชาตามหลักสูตร
+
+
+
+ เรียนแล้ว + เทียบโอน + ยังไม่เรียน +
+ +
+
+
+
+ +
+
+
นำเข้าข้อมูลเทียบโอน (PDF)
+
+
+
+ +
+ + +
+ +
+
+ ระบบจะอ่านรหัสวิชา ชื่อวิชา และหน่วยกิตจากไฟล์ PDF โดยอัตโนมัติ เฉพาะรายวิชาที่เทียบโอนได้จะถูกบันทึกลงแบบฟอร์มตามหลักสูตร +
+
+
+ +
+
+
เพิ่มรายวิชาที่กำลังเรียน
+ นำเข้า CSV +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ + +
+
+
รายวิชาเทียบโอนและกำลังเรียนรอออกเกรด
+ +
+ 0): ?> + รอออกเกรด วิชา + + รายการ | หน่วยกิต +
+
+
+ +
ยังไม่มีรายวิชาที่บันทึก
+ +
+ + + + + + + + + + + + + + + + + $r['grade'] === null || $r['grade'] === '' || $r['grade'] === '-' || mb_strpos((string)$r['grade'], 'กำลัง') === 0; + foreach ($student_courses as $sc_check) { + if ($isUngraded2($sc_check)) $ungraded_count++; + else $graded_count++; + } + $printed_ungraded_header = false; + $printed_separator = false; + foreach ($student_courses as $sc): + $t += $sc['credits']; + $is_ungraded = $isUngraded2($sc); + if ($is_ungraded && !$printed_ungraded_header) { + $printed_ungraded_header = true; + echo ''; + } + if (!$is_ungraded && !$printed_separator && $printed_ungraded_header) { + $printed_separator = true; + echo ''; + } + ?> + + + + + + + + + + + + + + + + + +
รหัสวิชาชื่อวิชาหน่วยกิตเกรดภาคเรียนปีการศึกษาประเภท
รายวิชาที่กำลังเรียน/รอออกเกรด
รายวิชาที่เทียบโอน/ออกเกรดแล้ว
+ + + + + + +
+ + + +
+ +
รวม
+
+ +
+
+
+
+ + +'; + echo '
' . htmlspecialchars($g['name_th']); + if ($g['min_credits'] > 0) { + echo ' (' . $g['min_credits'] . ' หน่วยกิต)'; + } + echo '
'; + + foreach ($node['courses'] as $course) { + $status_class = ''; + $status_badge = 'ยังไม่เรียน'; + if (isset($course['_completed'])) { + $sc = $course['_completed']; + $label = $sc['course_type'] ?: ($sc['source_type'] == 'transfer' ? 'เทียบโอน' : 'เรียนแล้ว'); + $grade_part = $sc['grade'] ? ' เกรด ' . htmlspecialchars($sc['grade']) : ''; + if ($sc['source_type'] == 'transfer') { + $status_class = 'bg-warning bg-opacity-10'; + $status_badge = ' ' . $label . $grade_part . ''; + } else { + $status_class = 'bg-success bg-opacity-10'; + $status_badge = ' ' . $label . $grade_part . ''; + } + } + echo '
'; + echo '
' . htmlspecialchars($course['code']) . ' ' . htmlspecialchars($course['name_th']) . '
'; + echo '
' . $course['credits'] . '(' . $course['lecture_hours'] . '-' . $course['practice_hours'] . '-' . $course['self_study_hours'] . ') ' . $status_badge . '
'; + echo '
'; + } + + if (!empty($node['children'])) { + renderStudentTree($node['children'], $level + 1); + } + + echo ''; + } +} diff --git a/students111/add.php b/students111/add.php new file mode 100644 index 0000000..9c67f9a --- /dev/null +++ b/students111/add.php @@ -0,0 +1,121 @@ +prepare("INSERT INTO students (student_code, name_th, name_en, email, faculty, graduated_institution, curriculum_id, enrollment_year, advisor_id, previous_qualification) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$student_code, $name_th, $name_en, $email, $faculty, $graduated_institution, $curriculum_id, $enrollment_year, $advisor_id, $previous_qualification ?: null]); + $message = 'เพิ่มนักศึกษาสำเร็จ'; + echo ""; + exit; + } catch (PDOException $e) { + if ($e->getCode() == 23000) { + $error = 'รหัสนักศึกษานี้มีอยู่ในระบบแล้ว'; + } else { + $error = 'เกิดข้อผิดพลาด: ' . $e->getMessage(); + } + } + } +} +?> +
+
+

เพิ่มนักศึกษาใหม่

+
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + กลับ +
+
+
+
+ diff --git a/students111/delete.php b/students111/delete.php new file mode 100644 index 0000000..4c42df1 --- /dev/null +++ b/students111/delete.php @@ -0,0 +1,20 @@ +prepare("DELETE FROM students WHERE id = ?"); + $stmt->execute([$id]); +} catch (PDOException $e) { + // ignore +} + +header('Location: list.php'); +exit; diff --git a/students111/edit.php b/students111/edit.php new file mode 100644 index 0000000..b1e0300 --- /dev/null +++ b/students111/edit.php @@ -0,0 +1,121 @@ +ไม่พบข้อมูลนักศึกษา'; + require_once __DIR__ . '/../includes/footer.php'; + exit; +} + +$curricula = getCurricula(); +$faculties = getFacultyList(); +$error = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $student_code = trim($_POST['student_code'] ?? ''); + $name_th = trim($_POST['name_th'] ?? ''); + $name_en = trim($_POST['name_en'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $curriculum_id = intval($_POST['curriculum_id'] ?? 0); + $faculty = trim($_POST['faculty'] ?? ''); + $graduated_institution = trim($_POST['graduated_institution'] ?? ''); + $previous_qualification = trim($_POST['previous_qualification'] ?? ''); + $enrollment_year = intval($_POST['enrollment_year'] ?? date('Y')); + + if (empty($student_code) || empty($name_th) || empty($curriculum_id)) { + $error = 'กรุณากรอกข้อมูลให้ครบถ้วน'; + } else { + try { + $stmt = getDB()->prepare("UPDATE students SET student_code=?, name_th=?, name_en=?, email=?, faculty=?, graduated_institution=?, curriculum_id=?, enrollment_year=?, previous_qualification=? WHERE id=?"); + $stmt->execute([$student_code, $name_th, $name_en, $email, $faculty, $graduated_institution, $curriculum_id, $enrollment_year, $previous_qualification ?: null, $id]); + echo ""; + exit; + } catch (PDOException $e) { + $error = 'เกิดข้อผิดพลาด: ' . $e->getMessage(); + } + } +} +?> +
+
+

แก้ไขข้อมูลนักศึกษา

+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + กลับ +
+
+
+
+ diff --git a/students111/import_csv.php b/students111/import_csv.php new file mode 100644 index 0000000..423c3c8 --- /dev/null +++ b/students111/import_csv.php @@ -0,0 +1,206 @@ +ไม่พบนักศึกษา'; require_once __DIR__ . '/../includes/footer.php'; exit; } +} + +require_once __DIR__ . '/../includes/header.php'; + +$results = []; +$total_success = 0; +$total_error = 0; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] === UPLOAD_ERR_OK) { + $tmp = $_FILES['csv_file']['tmp_name']; + // Read all rows at once + $all_rows = []; + $handle = fopen($tmp, 'r'); + if ($handle) { + while (($r = fgetcsv($handle)) !== false) $all_rows[] = $r; + fclose($handle); + } + if (empty($all_rows)) { $results[] = 'ไฟล์ CSV ว่างเปล่า'; $total_error++; } + else { + $first = $all_rows[0]; + $first[0] = preg_replace('/^\xEF\xBB\xBF/', '', $first[0]); + $first = array_map(function($v) { return trim($v, " \t\n\r\0\x0B\""); }, $first); + // Auto-detect header: if first cell starts with a digit, it's data (no header) + $has_header = !preg_match('/^\d/', $first[0]); + if ($has_header) { + $header_norm = array_map(function($v) { + return preg_replace('/[^a-z0-9_]/', '', str_replace(' ', '_', strtolower(trim($v)))); + }, $first); + $results[] = "พบหัวคอลัมน์: " . implode(', ', $first); + $col_map = []; + $expected = $student_id ? ['course_code','course_name','credits','grade','course_type','semester','academic_year','lecture','practice','self_study','notes','group_id'] : ['student_code','course_code','course_name','credits','grade','course_type','semester','academic_year','lecture','practice','self_study','notes','group_id']; + foreach ($expected as $e) { + $idx = array_search($e, $header_norm); + if ($idx !== false) $col_map[$e] = $idx; + } + array_shift($all_rows); + } else { + $results[] = "ไม่พบแถวหัวคอลัมน์ — ใช้ตำแหน่งคอลัมน์ตามลำดับ"; + if ($student_id) { + $col_map = ['course_code' => 0, 'course_name' => 1, 'credits' => 2, 'grade' => 3, 'course_type' => 4, 'semester' => 5, 'academic_year' => 6, 'lecture' => 7, 'practice' => 8, 'self_study' => 9, 'notes' => 10, 'group_id' => 11]; + } else { + $col_map = ['student_code' => 0, 'course_code' => 1, 'course_name' => 2, 'credits' => 3, 'grade' => 4, 'course_type' => 5, 'semester' => 6, 'academic_year' => 7, 'lecture' => 8, 'practice' => 9, 'self_study' => 10, 'notes' => 11, 'group_id' => 12]; + } + } + $db = getDB(); + foreach ($all_rows as $idx => $row) { + $line = $idx + 1; + $c = fn($k) => $col_map[$k] ?? null; + if ($student_id) { + $student = getStudent($student_id); + $student_code = $student['student_code']; + } else { + $student_code = trim($row[$c('student_code')] ?? ''); + } + $course_code = trim($row[$c('course_code')] ?? ''); + $course_name = trim($row[$c('course_name')] ?? ''); + $credits = floatval($row[$c('credits')] ?? 0); + $grade = trim($row[$c('grade')] ?? ''); + $course_type_col = trim($row[$c('course_type')] ?? ''); + $semester = trim($row[$c('semester')] ?? ''); + $academic_year = trim($row[$c('academic_year')] ?? ''); + $lecture = intval($row[$c('lecture')] ?? 0); + $practice = intval($row[$c('practice')] ?? 0); + $self_study = intval($row[$c('self_study')] ?? 0); + $notes = trim($row[$c('notes')] ?? ''); + $group_id = intval($row[$c('group_id')] ?? 0); + + if (empty($course_code) || empty($course_name) || $credits <= 0) { + $results[] = "แถวที่ $line: ข้าม (ข้อมูลไม่ครบ: course_code, course_name, credits)"; + $total_error++; + continue; + } + + try { + if (!$student_id) { + $stmt = $db->prepare("SELECT id, curriculum_id, advisor_id FROM students WHERE student_code = ?"); + $stmt->execute([$student_code]); + $student = $stmt->fetch(); + if (!$student) { + $results[] = "แถวที่ $line: ไม่พบรหัสนักศึกษา '$student_code'"; + $total_error++; + continue; + } + if (isAdvisor() && $student['advisor_id'] != getCurrentUserId()) { + $results[] = "แถวที่ $line: นักศึกษา '$student_code' ไม่อยู่ในที่ปรึกษาของคุณ"; + $total_error++; + continue; + } + } + + $curriculum_course = findCourseInCurriculum($student['curriculum_id'], $course_code); + $course_id = null; + if ($curriculum_course) { + $course_id = $curriculum_course['id']; + } elseif ($group_id > 0) { + $stmt = $db->prepare("SELECT id FROM courses WHERE group_id = ? AND code = ?"); + $stmt->execute([$group_id, $course_code]); + $existing = $stmt->fetch(); + if ($existing) { + $course_id = $existing['id']; + } else { + $stmt = $db->prepare("INSERT INTO courses (group_id, code, name_th, credits, lecture_hours, practice_hours, self_study_hours, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, 0)"); + $stmt->execute([$group_id, $course_code, $course_name, $credits, $lecture, $practice, $self_study]); + $course_id = $db->lastInsertId(); + } + } + + $stmt = $db->prepare("INSERT INTO student_courses (student_id, course_id, course_code, course_name_th, credits, lecture_hours, practice_hours, self_study_hours, grade, course_type, semester, academic_year, source_type, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'manual', ?)"); + $stmt->execute([$student['id'], $course_id, $course_code, $course_name, $credits, $lecture, $practice, $self_study, $grade ?: null, $course_type_col ?: null, $semester ?: null, $academic_year ?: null, $notes]); + $results[] = "แถวที่ $line: ✓ $student_code - $course_code ($course_name)"; + $total_success++; + } catch (PDOException $e) { + $results[] = "แถวที่ $line: ผิดพลาด - " . $e->getMessage(); + $total_error++; + } + } + } +} +?> +
+
+

นำเข้ารายวิชาจากไฟล์ CSV

+
รองรับไฟล์ .csv ตามรูปแบบคอลัมน์ที่ระบบกำหนด
+
+ กลับ +
+ + +
กำลังนำเข้าสำหรับนักศึกษา: - (ไม่ต้องระบุ student_code ใน CSV)
+ + +
+
+
+
+
เลือกไฟล์สำหรับนำเข้า
+
+
+
+
+ + +
+
+ +

ไฟล์ CSV ต้องมีหัวคอลัมน์ด้านล่างนี้ (คั่นด้วย comma):

+ +

คอลัมน์ที่จำเป็น:

+
+ +
+
+
+
+
+

📥 ดาวน์โหลด Template

+ template.csv +
+
+
+ +
+ 0 || $total_error > 0): ?> +
+
+ ผลลัพธ์ + สำเร็จ | ล้มเหลว +
+
+
    + +
  • + +
+
+
+ +
+
+ + diff --git a/students111/list.php b/students111/list.php new file mode 100644 index 0000000..3c7ef3b --- /dev/null +++ b/students111/list.php @@ -0,0 +1,94 @@ +prepare(" + SELECT + student_id, + COALESCE(SUM(CASE WHEN source_type = 'transfer' THEN credits ELSE 0 END), 0) AS transfer_total, + COALESCE(SUM(CASE WHEN source_type = 'manual' THEN credits ELSE 0 END), 0) AS manual_total, + COALESCE(SUM(credits), 0) AS total + FROM student_courses + WHERE student_id IN ($placeholders) + GROUP BY student_id + "); + $stmt->execute($ids); + foreach ($stmt->fetchAll() as $row) { + $credits_map[$row['student_id']] = [ + 'transfer' => floatval($row['transfer_total']), + 'manual' => floatval($row['manual_total']), + 'total' => floatval($row['total']) + ]; + } +} +?> +
+
+

รายชื่อนักศึกษา

+
ทั้งหมด รายการ
+
+ เพิ่มนักศึกษา +
+ +
+
+

ตารางข้อมูลนักศึกษา

+ รายการ +
+
+ +
ยังไม่มีข้อมูลนักศึกษา คลิกเพิ่มนักศึกษา
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
รหัสนักศึกษาชื่อ-นามสกุลสาขาวิชาคณะสถาบันที่จบวุฒิเดิมปีการศึกษาหน่วยกิตที่เทียบโอนหน่วยกิตที่เรียนแล้วหน่วยกิตทั้งหมดที่เทียบโอนและเรียนแล้วการจัดการ
+ +
+
+ +
+
+ diff --git a/students111/view.php b/students111/view.php new file mode 100644 index 0000000..78eafcf --- /dev/null +++ b/students111/view.php @@ -0,0 +1,389 @@ +ไม่พบข้อมูลนักศึกษา'; + require_once __DIR__ . '/../includes/footer.php'; + exit; +} + +if (isset($_GET['added'])) { + $msg_class = 'success'; + $msg_text = 'เพิ่มรายวิชาเรียบร้อยแล้ว'; + if ($_GET['added'] === '0') $msg_text = 'ไม่พบรายวิชาใหม่จากไฟล์ (อาจซ้ำหรือไม่ตรงหลักสูตร)'; + echo "
$msg_text
"; +} +if (isset($_GET['error'])) { + $err = $_GET['error']; + $map = ['upload_failed' => 'อัปโหลดไฟล์ล้มเหลว', 'no_text' => 'ไม่พบข้อความในไฟล์ PDF', 'invalid_data' => 'ข้อมูลไม่ถูกต้อง']; + $txt = $map[$err] ?? $err; + echo "
$txt
"; +} + +$summary = getStudentSummary($id); +$student_courses = getStudentCourses($id); + +// เรียง: ลงเรียนปกติ+ยังไม่ออกเกรด → เทียบโอน/ออกเกรดแล้ว +usort($student_courses, function($a, $b) { + $isUngraded = fn($r) => $r['grade'] === null || $r['grade'] === '' || $r['grade'] === '-' || mb_strpos((string)$r['grade'], 'กำลัง') === 0; + $a_ungraded = $isUngraded($a); + $b_ungraded = $isUngraded($b); + if ($a_ungraded && !$b_ungraded) return -1; + if (!$a_ungraded && $b_ungraded) return 1; + return strcasecmp($a['course_code'], $b['course_code']); +}); + +$total_curriculum_credits = getTotalCurriculumCredits($student['curriculum_id']); +$total_completed = getCompletedCreditsTotal($id); +$transfer_credits = getTransferCreditsTotal($id); +$remaining_credits = max(0, $total_curriculum_credits - $total_completed); +$is_dbc = ($student['curriculum_code'] == 'DBC'); +?> + + +
+
+
+
+

ข้อมูลนักศึกษา

+
+
+ + + + + + + + + +
รหัสนักศึกษา:
ชื่อ-นามสกุล: ()
อีเมล:
สาขาวิชา: -
คณะ:
สถาบันที่จบ:
วุฒิเดิม:
ปีการศึกษาที่เข้าศึกษา:
+
+
+
+
+
+ +
หน่วยกิตที่เทียบโอนได้
+
+
+
+ +
หน่วยกิตที่เรียนผ่านแล้วทั้งหมด
+
+
+
+ +
หน่วยกิตที่ต้องเรียนอีก
+
/
+
+
+ 0 ? round(($total_completed / $total_curriculum_credits) * 100) : 0 ?>% +
+
+
+
+
+ +
+
+
+
+
รายวิชาตามหลักสูตร
+
+
+
+ เรียนแล้ว + เทียบโอน + ยังไม่เรียน +
+ +
+
+
+
+
+
+
นำเข้าข้อมูลเทียบโอน
+
+
+ + + +
+ +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+ + +
+ +
+
+
+ +
+ ระบบจะอ่านรหัสวิชา ชื่อวิชา และหน่วยกิตโดยอัตโนมัติ เฉพาะรายวิชาที่ตรงตามหลักสูตรจะได้รับการเทียบโอน +
+
+
+ +
+
+
เพิ่มรายวิชาที่กำลังเรียน
+ นำเข้า CSV +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+
+
รายวิชาเทียบโอนและกำลังเรียนรอออกเกรด
+ +
+ 0): ?> + รอออกเกรด วิชา + + รายการ | หน่วยกิต +
+
+
+ +
ยังไม่มีรายวิชาที่บันทึก
+ +
+ + + + + + + + + + + + + + + $r['grade'] === null || $r['grade'] === '' || $r['grade'] === '-' || mb_strpos((string)$r['grade'], 'กำลัง') === 0; + foreach ($student_courses as $sc_check) { + if ($isUngraded2($sc_check)) $ungraded_count++; + else $graded_count++; + } + $printed_ungraded_header = false; + $printed_separator = false; + foreach ($student_courses as $sc): + $t += $sc['credits']; + $is_ungraded = $isUngraded2($sc); + if ($is_ungraded && !$printed_ungraded_header) { + $printed_ungraded_header = true; + echo ''; + } + if (!$is_ungraded && !$printed_separator && $printed_ungraded_header) { + $printed_separator = true; + echo ''; + } + ?> + + + + + + + + + + + + + + + +
รหัสวิชาชื่อวิชาหน่วยกิตเกรดภาคเรียนปีการศึกษาประเภท
รายวิชาที่กำลังเรียน/รอออกเกรด
รายวิชาที่เทียบโอน/ออกเกรดแล้ว
+ + + +
+ + + +
+
รวม
+
+ +
+
+
+
+ + +'; + echo '
' . htmlspecialchars($g['name_th']); + if ($g['min_credits'] > 0) { + echo ' (' . $g['min_credits'] . ' หน่วยกิต)'; + } + echo '
'; + + foreach ($node['courses'] as $course) { + $status_class = ''; + $status_badge = 'ยังไม่เรียน'; + if (isset($course['_completed'])) { + $sc = $course['_completed']; + $label = $sc['course_type'] ?: ($sc['source_type'] == 'transfer' ? 'เทียบโอน' : 'เรียนแล้ว'); + $grade_part = $sc['grade'] ? ' เกรด ' . htmlspecialchars($sc['grade']) : ''; + if ($sc['source_type'] == 'transfer') { + $status_class = 'bg-warning bg-opacity-10'; + $status_badge = ' ' . $label . $grade_part . ''; + } else { + $status_class = 'bg-success bg-opacity-10'; + $status_badge = ' ' . $label . $grade_part . ''; + } + } + echo '
'; + echo '
' . htmlspecialchars($course['code']) . ' ' . htmlspecialchars($course['name_th']) . '
'; + echo '
' . $course['credits'] . '(' . $course['lecture_hours'] . '-' . $course['practice_hours'] . '-' . $course['self_study_hours'] . ') ' . $status_badge . '
'; + echo '
'; + } + + if (!empty($node['children'])) { + renderStudentTree($node['children'], $level + 1); + } + + echo ''; + } +} diff --git a/users/add.php b/users/add.php new file mode 100644 index 0000000..32bf653 --- /dev/null +++ b/users/add.php @@ -0,0 +1,84 @@ +prepare("SELECT COUNT(*) FROM users WHERE staff_code = ?"); + $stmt->execute([$staff_code]); + if ($stmt->fetchColumn() > 0) { + $error = 'รหัสบุคลากรนี้มีอยู่ในระบบแล้ว'; + } else { + $stmt = $db->prepare("INSERT INTO users (staff_code, name, email, curriculum_id, role, password) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$staff_code, $name, $email, $curriculum_id, $role, password_hash($password, PASSWORD_DEFAULT)]); + $message = 'เพิ่มผู้ใช้สำเร็จ'; + echo ""; + exit; + } + } catch (PDOException $e) { + $error = 'เกิดข้อผิดพลาด: ' . $e->getMessage(); + } + } +} +?> +

เพิ่มผู้ใช้

+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + ยกเลิก +
+
+
+ diff --git a/users/delete.php b/users/delete.php new file mode 100644 index 0000000..888903a --- /dev/null +++ b/users/delete.php @@ -0,0 +1,11 @@ + 0) { + $db = getDB(); + $stmt = $db->prepare("DELETE FROM users WHERE id = ?"); + $stmt->execute([$id]); +} +header('Location: list.php?deleted=1'); +exit; diff --git a/users/edit.php b/users/edit.php new file mode 100644 index 0000000..b9e1ba1 --- /dev/null +++ b/users/edit.php @@ -0,0 +1,96 @@ +ไม่พบผู้ใช้'; + require_once __DIR__ . '/../includes/footer.php'; + exit; +} + +$curricula = getCurricula(); +$error = ''; +$message = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $staff_code = trim($_POST['staff_code'] ?? ''); + $name = trim($_POST['name'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $curriculum_id = !empty($_POST['curriculum_id']) ? intval($_POST['curriculum_id']) : null; + $role = $_POST['role'] ?? 'advisor'; + $password = trim($_POST['password'] ?? ''); + + if (empty($staff_code) || empty($name) || empty($email)) { + $error = 'กรุณากรอกข้อมูลให้ครบถ้วน'; + } else { + try { + $db = getDB(); + $stmt = $db->prepare("SELECT COUNT(*) FROM users WHERE staff_code = ? AND id != ?"); + $stmt->execute([$staff_code, $id]); + if ($stmt->fetchColumn() > 0) { + $error = 'รหัสบุคลากรนี้มีผู้ใช้อื่นใช้อยู่แล้ว'; + } else { + if (!empty($password)) { + $stmt = $db->prepare("UPDATE users SET staff_code=?, name=?, email=?, curriculum_id=?, role=?, password=? WHERE id=?"); + $stmt->execute([$staff_code, $name, $email, $curriculum_id, $role, password_hash($password, PASSWORD_DEFAULT), $id]); + } else { + $stmt = $db->prepare("UPDATE users SET staff_code=?, name=?, email=?, curriculum_id=?, role=? WHERE id=?"); + $stmt->execute([$staff_code, $name, $email, $curriculum_id, $role, $id]); + } + $message = 'บันทึกการแก้ไขเรียบร้อย'; + $user = getUser($id); + } + } catch (PDOException $e) { + $error = 'เกิดข้อผิดพลาด: ' . $e->getMessage(); + } + } +} +?> +

แก้ไขผู้ใช้:

+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + ยกเลิก +
+
+
+ diff --git a/users/list.php b/users/list.php new file mode 100644 index 0000000..67522c0 --- /dev/null +++ b/users/list.php @@ -0,0 +1,58 @@ +query("SELECT u.*, c.name_th AS curriculum_name FROM users u LEFT JOIN curricula c ON u.curriculum_id = c.id ORDER BY u.staff_code")->fetchAll(); + +if (isset($_GET['deleted'])): ?> +
ลบผู้ใช้เรียบร้อยแล้ว
+ + +
+
+

จัดการผู้ใช้

+
ทั้งหมด บัญชี
+
+ เพิ่มผู้ใช้ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
รหัสบุคลากรชื่อ-สกุลอีเมลสาขาวิชาบทบาทจัดการ
+
+ + +
+
+
+
+
+ + diff --git a/users/profile.php b/users/profile.php new file mode 100644 index 0000000..e03b711 --- /dev/null +++ b/users/profile.php @@ -0,0 +1,81 @@ +ไม่พบข้อมูลผู้ใช้'; + exit; +} + +require_once __DIR__ . '/../includes/header.php'; +?> + +
+
+

ข้อมูลส่วนตัว

+
ดูรายละเอียดข้อมูลบัญชีผู้ใช้ของคุณ
+
+ กลับหน้าหลัก +
+ +
+
+
+
+
รายละเอียดผู้ใช้งาน
+
+
+ + + + + + + + + + + + + + + + + + + + + +
รหัสบุคลากร:
ชื่อ-สกุล:
อีเมล:
บทบาทในระบบ: + + + +
สาขาวิชาที่ดูแล: + + + () + +
+
+ +
+
+
+
+ + diff --git a/users/profile_edit.php b/users/profile_edit.php new file mode 100644 index 0000000..d3f60ed --- /dev/null +++ b/users/profile_edit.php @@ -0,0 +1,115 @@ +ไม่พบข้อมูลผู้ใช้'; + exit; +} + +$error = ''; +$message = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $staff_code = trim($_POST['staff_code'] ?? ''); + $name = trim($_POST['name'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $password = trim($_POST['password'] ?? ''); + + if (empty($staff_code) || empty($name) || empty($email)) { + $error = 'กรุณากรอกข้อมูลให้ครบถ้วน'; + } else { + try { + $db = getDB(); + // Check if staff code is already in use by another user + $stmt = $db->prepare("SELECT COUNT(*) FROM users WHERE staff_code = ? AND id != ?"); + $stmt->execute([$staff_code, $id]); + if ($stmt->fetchColumn() > 0) { + $error = 'รหัสบุคลากรนี้มีผู้ใช้อื่นใช้อยู่แล้ว'; + } else { + if (!empty($password)) { + $stmt = $db->prepare("UPDATE users SET staff_code=?, name=?, email=?, password=? WHERE id=?"); + $stmt->execute([$staff_code, $name, $email, password_hash($password, PASSWORD_DEFAULT), $id]); + } else { + $stmt = $db->prepare("UPDATE users SET staff_code=?, name=?, email=? WHERE id=?"); + $stmt->execute([$staff_code, $name, $email, $id]); + } + + // Update session username to match new name + $_SESSION['username'] = $name; + + $message = 'บันทึกการแก้ไขเรียบร้อย'; + $user = getUser($id); + } + } catch (PDOException $e) { + $error = 'เกิดข้อผิดพลาด: ' . $e->getMessage(); + } + } +} + +require_once __DIR__ . '/../includes/header.php'; +?> + +
+
+

แก้ไขข้อมูลส่วนตัว

+
แก้ไขข้อมูลส่วนตัวและเปลี่ยนรหัสผ่านของคุณ
+
+ กลับ +
+ +
+
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + ไม่สามารถแก้ไขบทบาทด้วยตัวเองได้ +
+
+ + +
+
+
+ + ยกเลิก +
+
+
+
+
+
+ + diff --git a/vendor/font/courier.php b/vendor/font/courier.php new file mode 100644 index 0000000..c01f812 --- /dev/null +++ b/vendor/font/courier.php @@ -0,0 +1,16 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> diff --git a/vendor/font/courierb.php b/vendor/font/courierb.php new file mode 100644 index 0000000..8d2a223 --- /dev/null +++ b/vendor/font/courierb.php @@ -0,0 +1,16 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/courierbi.php b/vendor/font/courierbi.php new file mode 100644 index 0000000..1919a90 --- /dev/null +++ b/vendor/font/courierbi.php @@ -0,0 +1,16 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/courieri.php b/vendor/font/courieri.php new file mode 100644 index 0000000..d21092c --- /dev/null +++ b/vendor/font/courieri.php @@ -0,0 +1,16 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/helvetica.php b/vendor/font/helvetica.php new file mode 100644 index 0000000..4ff9391 --- /dev/null +++ b/vendor/font/helvetica.php @@ -0,0 +1,27 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/helveticab.php b/vendor/font/helveticab.php new file mode 100644 index 0000000..158bbcb --- /dev/null +++ b/vendor/font/helveticab.php @@ -0,0 +1,27 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/helveticabi.php b/vendor/font/helveticabi.php new file mode 100644 index 0000000..7dcf5f6 --- /dev/null +++ b/vendor/font/helveticabi.php @@ -0,0 +1,27 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/helveticai.php b/vendor/font/helveticai.php new file mode 100644 index 0000000..2b41700 --- /dev/null +++ b/vendor/font/helveticai.php @@ -0,0 +1,27 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/symbol.php b/vendor/font/symbol.php new file mode 100644 index 0000000..2b896d4 --- /dev/null +++ b/vendor/font/symbol.php @@ -0,0 +1,26 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549, + ','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722, + 'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768, + 'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576, + 'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0, + chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603, + chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768, + chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042, + chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329, + chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0); +$uv = array(32=>160,33=>33,34=>8704,35=>35,36=>8707,37=>array(37,2),39=>8715,40=>array(40,2),42=>8727,43=>array(43,2),45=>8722,46=>array(46,18),64=>8773,65=>array(913,2),67=>935,68=>array(916,2),70=>934,71=>915,72=>919,73=>921,74=>977,75=>array(922,4),79=>array(927,2),81=>920,82=>929,83=>array(931,3),86=>962,87=>937,88=>926,89=>936,90=>918,91=>91,92=>8756,93=>93,94=>8869,95=>95,96=>63717,97=>array(945,2),99=>967,100=>array(948,2),102=>966,103=>947,104=>951,105=>953,106=>981,107=>array(954,4),111=>array(959,2),113=>952,114=>961,115=>array(963,3),118=>982,119=>969,120=>958,121=>968,122=>950,123=>array(123,3),126=>8764,160=>8364,161=>978,162=>8242,163=>8804,164=>8725,165=>8734,166=>402,167=>9827,168=>9830,169=>9829,170=>9824,171=>8596,172=>array(8592,4),176=>array(176,2),178=>8243,179=>8805,180=>215,181=>8733,182=>8706,183=>8226,184=>247,185=>array(8800,2),187=>8776,188=>8230,189=>array(63718,2),191=>8629,192=>8501,193=>8465,194=>8476,195=>8472,196=>8855,197=>8853,198=>8709,199=>array(8745,2),201=>8835,202=>8839,203=>8836,204=>8834,205=>8838,206=>array(8712,2),208=>8736,209=>8711,210=>63194,211=>63193,212=>63195,213=>8719,214=>8730,215=>8901,216=>172,217=>array(8743,2),219=>8660,220=>array(8656,4),224=>9674,225=>9001,226=>array(63720,3),229=>8721,230=>array(63723,10),241=>9002,242=>8747,243=>8992,244=>63733,245=>8993,246=>array(63734,9)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/times.php b/vendor/font/times.php new file mode 100644 index 0000000..f0fc0ea --- /dev/null +++ b/vendor/font/times.php @@ -0,0 +1,27 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722, + 'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944, + 'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/timesb.php b/vendor/font/timesb.php new file mode 100644 index 0000000..02cf3a5 --- /dev/null +++ b/vendor/font/timesb.php @@ -0,0 +1,27 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000, + 'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833, + 'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/timesbi.php b/vendor/font/timesbi.php new file mode 100644 index 0000000..5304a5a --- /dev/null +++ b/vendor/font/timesbi.php @@ -0,0 +1,27 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667, + 'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889, + 'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/timesi.php b/vendor/font/timesi.php new file mode 100644 index 0000000..eff02c5 --- /dev/null +++ b/vendor/font/timesi.php @@ -0,0 +1,27 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611, + 'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833, + 'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/font/zapfdingbats.php b/vendor/font/zapfdingbats.php new file mode 100644 index 0000000..ab7d7f3 --- /dev/null +++ b/vendor/font/zapfdingbats.php @@ -0,0 +1,26 @@ +0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0, + chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939, + ','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692, + 'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776, + 'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873, + 'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317, + chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788, + chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788, + chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918, + chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874, + chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0); +$uv = array(32=>32,33=>array(9985,4),37=>9742,38=>array(9990,4),42=>9755,43=>9758,44=>array(9996,28),72=>9733,73=>array(10025,35),108=>9679,109=>10061,110=>9632,111=>array(10063,4),115=>9650,116=>9660,117=>9670,118=>10070,119=>9687,120=>array(10072,7),128=>array(10088,14),161=>array(10081,7),168=>9827,169=>9830,170=>9829,171=>9824,172=>array(9312,10),182=>array(10102,31),213=>8594,214=>array(8596,2),216=>array(10136,24),241=>array(10161,14)); +$desc = ''; +$diff = ''; +$file = ''; +$size1 = 0; +$size2 = 0; +$originalsize = 0; +?> \ No newline at end of file diff --git a/vendor/fpdf.php b/vendor/fpdf.php new file mode 100644 index 0000000..060ffab --- /dev/null +++ b/vendor/fpdf.php @@ -0,0 +1,1551 @@ +_dochecks(); + // Initialization of properties + $this->state = 0; + $this->page = 0; + $this->n = 2; + $this->buffer = ''; + $this->pages = array(); + $this->PageInfo = array(); + $this->fonts = array(); + $this->FontFiles = array(); + $this->encodings = array(); + $this->cmaps = array(); + $this->images = array(); + $this->links = array(); + $this->InHeader = false; + $this->InFooter = false; + $this->lasth = 0; + $this->FontFamily = ''; + $this->FontStyle = ''; + $this->FontSizePt = 12; + $this->underline = false; + $this->DrawColor = '0 G'; + $this->FillColor = '0 g'; + $this->TextColor = '0 g'; + $this->ColorFlag = false; + $this->WithAlpha = false; + $this->ws = 0; + // Font path + if(defined('FPDF_FONTPATH')) + { + $this->fontpath = FPDF_FONTPATH; + } + else + { + $this->fontpath = __DIR__ . '/font/'; + } + // Core fonts + $this->CoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats'); + // Scale factor + if($unit=='pt') + $this->k = 1; + elseif($unit=='mm') + $this->k = 72/25.4; + elseif($unit=='cm') + $this->k = 72/2.54; + elseif($unit=='in') + $this->k = 72; + else + $this->Error('Incorrect unit: '.$unit); + // Page sizes + $this->StdPageSizes = array('a3'=>array(841.89,1190.55), 'a4'=>array(595.28,841.89), 'a5'=>array(420.94,595.28), + 'letter'=>array(612,792), 'legal'=>array(612,1008)); + $size = $this->_getpagesize($size); + $this->DefPageSize = $size; + $this->CurPageSize = $size; + // Page orientation + $orientation = strtolower($orientation); + if($orientation=='p' || $orientation=='portrait') + { + $this->DefOrientation = 'P'; + $this->w = $size[0]; + $this->h = $size[1]; + } + elseif($orientation=='l' || $orientation=='landscape') + { + $this->DefOrientation = 'L'; + $this->w = $size[1]; + $this->h = $size[0]; + } + else + $this->Error('Incorrect orientation: '.$orientation); + $this->CurOrientation = $this->DefOrientation; + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + // Page margins (1 cm) + $margin = 28.35/$this->k; + $this->SetMargins($margin,$margin); + // Interior cell margin (1 mm) + $this->cMargin = $margin/10; + // Line width (0.2 mm) + $this->LineWidth = .567/$this->k; + // Automatic page break + $this->SetAutoPageBreak(true,2*$margin); + // Default display mode + $this->SetDisplayMode('default'); + // Enable compression + $this->SetCompression(true); + // Set default PDF version number + $this->PDFVersion = '1.3'; +} + +function SetMargins($left, $top, $right=null) +{ + // Set left, top and right margins + $this->lMargin = $left; + $this->tMargin = $top; + if($right===null) + $right = $left; + $this->rMargin = $right; +} + +function SetLeftMargin($margin) +{ + // Set left margin + $this->lMargin = $margin; + if($this->page>0 && $this->x<$margin) + $this->x = $margin; +} + +function SetTopMargin($margin) +{ + // Set top margin + $this->tMargin = $margin; +} + +function SetRightMargin($margin) +{ + // Set right margin + $this->rMargin = $margin; +} + +function SetAutoPageBreak($auto, $margin=0) +{ + // Set auto page break mode and bottom margin + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h-$margin; +} + +function SetDisplayMode($zoom, $layout='default') +{ + // Set display mode in viewer + if($zoom=='fullpage' || $zoom=='fullwidth' || $zoom=='real' || $zoom=='default' || !is_string($zoom)) + $this->ZoomMode = $zoom; + else + $this->Error('Incorrect zoom display mode: '.$zoom); + if($layout=='single' || $layout=='continuous' || $layout=='two' || $layout=='default') + $this->LayoutMode = $layout; + else + $this->Error('Incorrect layout display mode: '.$layout); +} + +function SetCompression($compress) +{ + // Set page compression + if(function_exists('gzcompress')) + $this->compress = $compress; + else + $this->compress = false; +} + +function SetTitle($title, $isUTF8=false) +{ + // Title of document + $this->metadata['Title'] = $isUTF8 ? $title : utf8_encode($title); +} + +function SetAuthor($author, $isUTF8=false) +{ + // Author of document + $this->metadata['Author'] = $isUTF8 ? $author : utf8_encode($author); +} + +function SetSubject($subject, $isUTF8=false) +{ + // Subject of document + $this->metadata['Subject'] = $isUTF8 ? $subject : utf8_encode($subject); +} + +function SetKeywords($keywords, $isUTF8=false) +{ + // Keywords of document + $this->metadata['Keywords'] = $isUTF8 ? $keywords : utf8_encode($keywords); +} + +function SetCreator($creator, $isUTF8=false) +{ + $this->metadata['Creator'] = $isUTF8 ? $creator : utf8_encode($creator); +} + +function AliasNbPages($alias='{nb}') +{ + // Define an alias for total number of pages + $this->AliasNbPages = $alias; +} + +function Error($msg) +{ + // Fatal error + throw new Exception('FPDF error: '.$msg); +} + +function Close() +{ + // Terminate document + if($this->state==3) + return; + if($this->page==0) + $this->AddPage(); + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + // Close document + $this->_enddoc(); +} + +function AddPage($orientation='', $size='', $rotation=0) +{ + // Start a new page + if($this->state==3) + $this->Error('The document is closed'); + $family = $this->FontFamily; + $style = $this->FontStyle.($this->underline ? 'U' : ''); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + if($this->page>0) + { + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + } + // Start new page + $this->_beginpage($orientation,$size,$rotation); + // Set line cap style to square + $this->_out('2 J'); + // Set line width + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w',$lw*$this->k)); + // Set font + if($family) + $this->SetFont($family,$style,$fontsize); + // Set colors + $this->DrawColor = $dc; + if($dc!='0 G') + $this->_out($dc); + $this->FillColor = $fc; + if($fc!='0 g') + $this->_out($fc); + $this->TextColor = $tc; + $this->ColorFlag = $cf; + // Page header + $this->InHeader = true; + $this->Header(); + $this->InHeader = false; + // Restore line width + if($this->LineWidth!=$lw) + { + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w',$lw*$this->k)); + } + // Restore font + if($family) + $this->SetFont($family,$style,$fontsize); + // Restore colors + if($this->DrawColor!=$dc) + { + $this->DrawColor = $dc; + $this->_out($dc); + } + if($this->FillColor!=$fc) + { + $this->FillColor = $fc; + $this->_out($fc); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; +} + +function Header() +{ + // To be implemented in your own inherited class +} + +function Footer() +{ + // To be implemented in your own inherited class +} + +function PageNo() +{ + // Get current page number + return $this->page; +} + +function SetDrawColor($r, $g=null, $b=null) +{ + // Set color for all stroking operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->DrawColor = sprintf('%.3F G',$r/255); + else + $this->DrawColor = sprintf('%.3F %.3F %.3F RG',$r/255,$g/255,$b/255); + if($this->page>0) + $this->_out($this->DrawColor); +} + +function SetFillColor($r, $g=null, $b=null) +{ + // Set color for all filling operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->FillColor = sprintf('%.3F g',$r/255); + else + $this->FillColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); + if($this->page>0) + $this->_out($this->FillColor); +} + +function SetTextColor($r, $g=null, $b=null) +{ + // Set color for text + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->TextColor = sprintf('%.3F g',$r/255); + else + $this->TextColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); +} + +function GetStringWidth($s) +{ + // Get width of a string in the current font + if(!is_string($s)) $s = (string)$s; + $cw = $this->CurrentFont['cw']; + if(!is_array($cw)) + return 0; + $w = 0; + $l = strlen($s); + for($i=0;$i<$l;$i++) { + $c = $s[$i]; + if($c === '') + continue; + $w += $cw[$c]; + } + return $w*$this->FontSize/1000; +} + +function SetLineWidth($width) +{ + // Set line width + $this->LineWidth = $width; + if($this->page>0) + $this->_out(sprintf('%.2F w',$width*$this->k)); +} + +function Line($x1, $y1, $x2, $y2) +{ + // Draw a line + $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S',$x1*$this->k,($this->h-$y1)*$this->k,$x2*$this->k,($this->h-$y2)*$this->k)); +} + +function Rect($x, $y, $w, $h, $style='') +{ + // Draw a rectangle + if($style=='F') + $op = 'f'; + elseif($style=='FD' || $style=='D') + $op = 'B'; + else + $op = 'S'; + $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s',$x*$this->k,($this->h-$y)*$this->k,$w*$this->k,-$h*$this->k,$op)); +} + +function AddFont($family, $style='', $file='', $dir='') +{ + // Add a TrueType, OpenType or Type1 font + $family = strtolower($family); + if($file=='') + $file = str_replace(' ','',$family).strtolower($style).'.php'; + $style = strtoupper($style); + if($style=='IB') + $style = 'BI'; + $fontkey = $family.$style; + if(isset($this->fonts[$fontkey])) + return; + if($dir=='') + $dir = $this->fontpath; + if(strpos($file,'/')!==false || strpos($file,"\\")!==false) + $this->Error('Incorrect font definition file name: '.$file); + include $dir.$file; + if(!isset($name)) + $this->Error('Could not include font definition file'); + $i = count($this->fonts)+1; + $this->fonts[$fontkey] = array('i'=>$i, 'type'=>$type, 'name'=>$name, 'desc'=>$desc, 'up'=>$up, 'ut'=>$ut, 'cw'=>$cw, 'enc'=>$enc, 'file'=>$file); + if($diff) + { + // Search existing encodings + $d = 0; + $nb = count($this->encodings); + for($i=1;$i<=$nb;$i++) + { + if($this->encodings[$i]==$diff) + { + $d = $i; + break; + } + } + if($d==0) + { + $d = $nb+1; + $this->encodings[$d] = $diff; + } + $this->fonts[$fontkey]['diff'] = $d; + } + if($file) + { + if($type=='TrueType') + $this->FontFiles[$fontkey] = array('length1'=>$originalsize, 'type'=>"TTF", 'file'=>preg_replace('/\.php$/','',$file)); + else + $this->FontFiles[$fontkey] = array('length1'=>$size1, 'length2'=>$size2); + } +} + +function SetFont($family, $style='', $size=0) +{ + // Select a font; size given in points + if($family=='') + $family = $this->FontFamily; + else + $family = strtolower($family); + $style = strtoupper($style); + if(strpos($style,'U')!==false) + { + $this->underline = true; + $style = str_replace('U','',$style); + } + else + $this->underline = false; + if($style=='IB') + $style = 'BI'; + $fontkey = $family.$style; + if(!isset($this->fonts[$fontkey])) + { + if($family=='arial') + $family = 'helvetica'; + if(in_array($family,$this->CoreFonts)) + { + if($family=='symbol' || $family=='zapfdingbats') + $style = ''; + $fontkey = $family.$style; + if(!isset($this->fonts[$fontkey])) + $this->AddFont($family,$style); + } + else + $this->Error('Undefined font: '.$family.' '.$style); + } + // Select it + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + $this->CurrentFont = $this->fonts[$fontkey]; + if($this->page>0) + $this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt)); +} + +function SetFontSize($size) +{ + // Set font size in points + if($this->FontSizePt==$size) + return; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + if($this->page>0 && isset($this->CurrentFont)) + $this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt)); +} + +function AddLink() +{ + // Create a new internal link + $n = count($this->links)+1; + $this->links[$n] = array(0, 0); + return $n; +} + +function SetLink($link, $y=0, $page=-1) +{ + // Set destination of internal link + if($y==-1) + $y = $this->y; + if($page==-1) + $page = $this->page; + $this->links[$link] = array($page, $y); +} + +function Link($x, $y, $w, $h, $link) +{ + // Put a link on the page + $this->PageLinks[$this->page][] = array($x*$this->k, $this->hPt-$y*$this->k, $w*$this->k, $h*$this->k, $link); +} + +function Text($x, $y, $txt) +{ + // Output a string + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $s = sprintf('BT %.2F %.2F Td (%s) Tj ET',$x*$this->k,($this->h-$y)*$this->k,$this->_escape($txt)); + if($this->underline && $txt!='') + $s .= ' '.$this->_dounderline($x,$y,$txt); + if($this->ColorFlag) + $s = 'q '.$this->TextColor.' '.$s.' Q'; + $this->_out($s); +} + +function AcceptPageBreak() +{ + // Accept automatic page break or not + return $this->AutoPageBreak; +} + +function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='') +{ + // Output a cell + $k = $this->k; + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + // Automatic page break + $x = $this->x; + $ws = $this->ws; + if($ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation); + $this->x = $x; + if($ws>0) + { + $this->ws = $ws; + $this->_out(sprintf('%.3F Tw',$ws*$k)); + } + } + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $s = ''; + if($fill || $border==1) + { + if($fill) + $op = ($border==1) ? 'B' : 'f'; + else + $op = 'S'; + $s = sprintf('%.2F %.2F %.2F %.2F re %s ',$this->x*$k,($this->h-$this->y)*$k,$w*$k,-$h*$k,$op); + } + if(is_string($border)) + { + $x = $this->x; + $y = $this->y; + if(strpos($border,'L')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,$x*$k,($this->h-($y+$h))*$k); + if(strpos($border,'T')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-$y)*$k); + if(strpos($border,'R')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',($x+$w)*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-($y+$h))*$k); + if(strpos($border,'B')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-($y+$h))*$k,($x+$w)*$k,($this->h-($y+$h))*$k); + } + if($txt!=='') + { + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + if($align=='R') + $dx = $w-$this->cMargin-$this->GetStringWidth($txt); + elseif($align=='C') + $dx = ($w-$this->GetStringWidth($txt))/2; + else + $dx = $this->cMargin; + if($this->ColorFlag) + $s .= 'q '.$this->TextColor.' '; + $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET',($this->x+$dx)*$k,($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k,$this->_escape($txt)); + if($this->underline) + $s .= ' '.$this->_dounderline($this->x+$dx,$this->y+.5*$h+.3*$this->FontSize,$txt); + if($this->ColorFlag) + $s .= ' Q'; + if($link) + $this->Link($this->x+$dx,$this->y+.5*$h-.5*$this->FontSize,$this->GetStringWidth($txt),$this->FontSize,$link); + } + if($s) + $this->_out($s); + $this->lasth = $h; + if($ln>0) + { + // Go to next line + $this->y += $h; + if($ln==1) + $this->x = $this->lMargin; + } + else + $this->x += $w; +} + +function MultiCell($w, $h, $txt, $border=0, $align='J', $fill=false) +{ + // Output text with automatic or explicit line breaks + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $s = str_replace("\r",'',$txt); + $nb = strlen($s); + if($nb>0 && $s[$nb-1]=="\n") + $nb--; + $b = 0; + if($border) + { + if($border==1) + { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } + else + { + $b2 = ''; + if(strpos($border,'L')!==false) + $b2 .= 'L'; + if(strpos($border,'R')!==false) + $b2 .= 'R'; + $b = (strpos($border,'T')!==false) ? $b2.'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + $c = $s[$i]; + if($c=="\n") + { + // Explicit line break + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + $i++; + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + continue; + } + if($c==' ') + { + $sep = $i; + $ls = $l; + $ns++; + } + $l += $cw[$c]; + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($i==$j) + $i++; + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + } + else + { + if($align=='J') + { + $this->ws = ($ns>1) ? ($wmax-$ls)/1000*$this->FontSize/($ns-1) : 0; + $this->_out(sprintf('%.3F Tw',$this->ws*$this->k)); + } + $this->Cell($w,$h,substr($s,$j,$sep-$j),$b,2,$align,$fill); + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + } + else + $i++; + } + // Last chunk + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + if($border && strpos($border,'B')!==false) + $b .= 'B'; + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + $this->x = $this->lMargin; +} + +function Write($h, $txt, $link='') +{ + // Output text in flowing mode + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $s = str_replace("\r",'',$txt); + $nb = strlen($s); + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + $c = $s[$i]; + if($c=="\n") + { + // Explicit line break + $this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link); + $i++; + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + } + $nl++; + continue; + } + if($c==' ') + $sep = $i; + $l += $cw[$c]; + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($this->x>$this->lMargin) + { + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $i++; + $nl++; + continue; + } + if($i==$j) + $i++; + $this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link); + } + else + { + $this->Cell($w,$h,substr($s,$j,$sep-$j),0,2,'',false,$link); + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + } + $nl++; + } + else + $i++; + } + // Last chunk + if($i!=$j) + $this->Cell($w/1,$h,substr($s,$j),0,0,'',false,$link); +} + +function Ln($h=null) +{ + // Line feed; default value is the last cell height + $this->x = $this->lMargin; + if($h===null) + $this->y += $this->lasth; + else + $this->y += $h; +} + +function Image($file, $x=null, $y=null, $w=0, $h=0, $type='', $link='') +{ + // Put an image on the page + if($file=='') + return; + if(!isset($this->images[$file])) + { + $data = getimagesize($file); + if(!$data) + $this->Error('Image file not found: '.$file); + $type = image_type_to_mime_type($data[2]); + $info = array( + 'w'=>$data[0], + 'h'=>$data[1], + 'type'=>$type, + 'data'=>file_get_contents($file) + ); + if(($info['type']=='image/gif')) + $info['data'] = $this->_gdImageCreateFromGif($file); + $this->images[$file] = $info; + } + $info = $this->images[$file]; + // Automatic width and height calculation if needed + if($w==0 && $h==0) + { + // Put image at 96 dpi + $w = -96; + $h = -96; + } + if($w<0) + $w = -$info['w']*72/$w/$this->k; + if($h<0) + $h = -$info['h']*72/$h/$this->k; + if($w==0) + $w = $h*$info['w']/$info['h']; + if($h==0) + $h = $w*$info['h']/$info['w']; + // Flowing mode + if($y===null) + { + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + $this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation); + $x = $this->x; + } + else + $x = $this->x; + $y = $this->y; + } + $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q',$w*$this->k,$h*$this->k,$x*$this->k,($this->h-($y+$h))*$this->k,$this->i)); + if($y===null) + $this->y += $h; + if($link) + $this->Link($x,$y,$w,$h,$link); +} + +function GetPageWidth() +{ + // Get current page width + return $this->w; +} + +function GetPageHeight() +{ + // Get current page height + return $this->h; +} + +function GetX() +{ + // Get x position + return $this->x; +} + +function SetX($x) +{ + // Set x position + if($x>=0) + $this->x = $x; + else + $this->x = $this->w+$x; +} + +function GetY() +{ + // Get y position + return $this->y; +} + +function SetY($y, $resetx=true) +{ + // Set y position and optionally reset x + if($y>=0) + $this->y = $y; + else + $this->y = $this->h+$y; + if($resetx) + $this->x = $this->lMargin; +} + +function SetXY($x, $y) +{ + // Set x and y positions + $this->SetX($x); + $this->SetY($y,false); +} + +function Output($dest='', $name='', $isUTF8=false) +{ + // Output PDF to some destination + $this->Close(); + if(strlen($name)==1 && strlen($dest)!=1) + { + // Fix parameter order + $tmp = $dest; + $dest = $name; + $name = $tmp; + } + if($dest=='') + $dest = 'I'; + if($name=='') + $name = 'doc.pdf'; + switch(strtoupper($dest)) + { + case 'I': + // Send to standard output + $this->_checkoutput(); + if(PHP_SAPI!='cli') + { + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="'.$name.'"'); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + } + echo $this->buffer; + break; + case 'D': + // Download file + $this->_checkoutput(); + header('Content-Type: application/x-download'); + header('Content-Disposition: attachment; filename="'.$name.'"'); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + echo $this->buffer; + break; + case 'F': + // Save to local file + if(!file_put_contents($name, $this->buffer)) + $this->Error('Unable to create output file: '.$name); + break; + case 'S': + // Return as a string + return $this->buffer; + default: + $this->Error('Incorrect output destination: '.$dest); + } + return ''; +} + +/******************************************************************************* +* Protected methods * +*******************************************************************************/ + +protected function _dochecks() +{ + // Check mbstring overloading + if(ini_get('mbstring.func_overload') & 2) + $this->Error('mbstring overloading must be disabled'); +} + +protected function _checkoutput() +{ + if(PHP_SAPI!='cli') + { + if(headers_sent($file,$line)) + $this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)"); + } + if(ob_get_contents()) + { + $this->Error('Some data has already been output, can\'t send PDF file'); + } +} + +protected function _getpagesize($size) +{ + if(is_string($size)) + { + $size = strtolower($size); + if(!isset($this->StdPageSizes[$size])) + $this->Error('Unknown page size: '.$size); + $a = $this->StdPageSizes[$size]; + return array($a[0]/$this->k, $a[1]/$this->k); + } + else + { + if($size[0]>$size[1]) + return array($size[1], $size[0]); + else + return $size; + } +} + +protected function _beginpage($orientation, $size, $rotation) +{ + $this->page++; + if($this->state==0) + $this->_begin_doc(); + $this->pages[$this->page] = ''; + $this->PageLinks[$this->page] = array(); + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + // Check page size and orientation + if($orientation=='') + $orientation = $this->DefOrientation; + else + $orientation = strtoupper($orientation[0]); + if($size=='') + $size = $this->DefPageSize; + else + $size = $this->_getpagesize($size); + if($orientation!=$this->CurOrientation || $size[0]!=$this->CurPageSize[0] || $size[1]!=$this->CurPageSize[1]) + { + // New size or orientation + if($orientation=='P') + { + $this->w = $size[0]; + $this->h = $size[1]; + } + else + { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + $this->PageBreakTrigger = $this->h-$this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + } + if($orientation!=$this->DefOrientation || $size[0]!=$this->DefPageSize[0] || $size[1]!=$this->DefPageSize[1]) + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + if($rotation!=0) + { + if($rotation%90!=0) + $this->Error('Incorrect rotation value: '.$rotation); + $this->PageInfo[$this->page]['rotation'] = $rotation; + } + $this->CurRotation = $rotation; +} + +protected function _endpage() +{ + $this->state = 1; +} + +protected function _loadfont($font) +{ + // Load a font definition file from the font directory + if(strpos($font,'/')!==false || strpos($font,"\\")!==false) + $this->Error('Incorrect font definition file name: '.$font); + include $this->fontpath.$font; + if(!isset($name)) + $this->Error('Could not include font definition file'); + return $a; +} + +protected function _escape($s) +{ + // Escape special characters + if(strpos($s,'(')!==false || strpos($s,')')!==false || strpos($s,'\\')!==false || strpos($s,"\r")!==false) + return str_replace(array('\\','(',')',"\r"), array('\\\\','\\(','\\)','\\r'), $s); + else + return $s; +} + +protected function _textstring($s) +{ + // Format a text string + if(!$this->_isascii($s)) + $s = $this->_UTF8toUTF16($s); + return '('.$this->_escape($s).')'; +} + +protected function _isascii($s) +{ + // Test if string is ASCII + $nb = strlen($s); + for($i=0;$i<$nb;$i++) + { + if(ord($s[$i])>127) + return false; + } + return true; +} + +protected function _UTF8toUTF16($s) +{ + // Convert UTF-8 to UTF-16BE with BOM + $res = "\xFE\xFF"; + $nb = strlen($s); + $i = 0; + while($i<$nb) + { + $c1 = ord($s[$i++]); + if($c1>=224) + { + // 3-byte character + $c2 = ord($s[$i++]); + $c3 = ord($s[$i++]); + $res .= chr((($c1 & 0x0F)<<4) + (($c2 & 0x3C)>>2)); + $res .= chr((($c2 & 0x03)<<6) + ($c3 & 0x3F)); + } + elseif($c1>=192) + { + // 2-byte character + $c2 = ord($s[$i++]); + $res .= chr(($c1 & 0x1C)>>2); + $res .= chr((($c1 & 0x03)<<6) + ($c2 & 0x3F)); + } + else + { + // Single-byte character + $res .= "\0".chr($c1); + } + } + return $res; +} + +protected function _dounderline($x, $y, $txt) +{ + // Underline text + $up = $this->CurrentFont['up']; + $ut = $this->CurrentFont['ut']; + $w = $this->GetStringWidth($txt)+$this->ws*substr_count($txt,' '); + return sprintf('%.2F %.2F %.2F %.2F re f',$x*$this->k,($this->h-($y-$up/1000*$this->FontSize))*$this->k,$w*$this->k,-$ut/1000*$this->FontSizePt); +} + +protected function _out($s) +{ + // Add a line to the document + if($this->state==2) + $this->pages[$this->page] .= $s."\n"; + elseif($this->state==1) + $this->_put($s); + elseif($this->state==0) + $this->Error('No page has been added'); + elseif($this->state==3) + $this->Error('The document is closed'); +} + +protected function _put($s) +{ + $this->buffer .= $s."\n"; +} + +protected function _getoffset() +{ + return strlen($this->buffer); +} + +protected function _newobj($n=null) +{ + // Begin a new object + if($n===null) + $n = ++$this->n; + $this->offsets[$n] = $this->_getoffset(); + $this->_put($n.' 0 obj'); +} + +protected function _putstream($data) +{ + $this->_put('stream'); + $this->_put($data); + $this->_put('endstream'); +} + +protected function _putstreamobject($data) +{ + if($this->compress) + { + $entries = '/Filter /FlateDecode '; + $data = gzcompress($data); + } + else + $entries = ''; + $entries .= '/Length '.strlen($data); + $this->_newobj(); + $this->_put('<<'.$entries.'>>'); + $this->_putstream($data); + $this->_put('endobj'); +} + +protected function _putpage($n) +{ + $this->_newobj(); + $this->_put('<_put('/Parent 1 0 R'); + if(isset($this->PageInfo[$n]['size'])) + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]',$this->PageInfo[$n]['size'][0],$this->PageInfo[$n]['size'][1])); + if(isset($this->PageInfo[$n]['rotation'])) + $this->_put('/Rotate '.$this->PageInfo[$n]['rotation']); + $this->_put('/Resources 2 0 R'); + if(!empty($this->PageLinks[$n])) + { + $s = '/Annots ['; + foreach($this->PageLinks[$n] as $pl) + $s .= $pl[5].' 0 R '; + $s .= ']'; + $this->_put($s); + } + $this->_put('/Contents '.($this->n+1).' 0 R>>'); + $this->_put('endobj'); + // Page content + if(!empty($this->AliasNbPages)) + $this->pages[$n] = str_replace($this->AliasNbPages,$this->page,$this->pages[$n]); + $this->_putstreamobject($this->pages[$n]); + // Annotations + foreach($this->PageLinks[$n] as $pl) + { + $this->_putannot($pl); + } +} + +protected function _putpages() +{ + $nb = $this->page; + for($n=1;$n<=$nb;$n++) + $this->_putpage($n); +} + +protected function _putresources() +{ + $this->_putfonts(); + $this->_putimages(); + // Resource dictionary + $this->_newobj(2); + $this->_put('<<'); + $this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + $this->_put('/Font <<'); + foreach($this->fonts as $font) + $this->_put('/F'.$font['i'].' '.$font['n'].' 0 R'); + $this->_put('>>'); + if(count($this->images)) + { + $this->_put('/XObject <<'); + foreach($this->images as $image) + $this->_put('/I'.$image['i'].' '.$image['n'].' 0 R'); + $this->_put('>>'); + } + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putfonts() +{ + foreach($this->FontFiles as $kk=>$font) + { + $this->_putfontfile($kk, $font); + } + foreach($this->fonts as $k=>$font) + { + $this->_putfont($font,$k); + } +} + +protected function _putfont($font, $key) +{ + // Reference + $font['n'] = $this->n+1; + $this->fonts[$key]['n'] = $font['n']; + $this->_newobj(); + $this->_put('<_put('/BaseFont /'.$font['name']); + if($font['type']=='Type1' && $font['enc'] && $font['enc']!='cp1252') + $this->_put('/Encoding /WinAnsiEncoding'); + $this->_put('/Subtype /'.$font['type']); + $this->_put('/FirstChar 32'); + $this->_put('/LastChar 255'); + $this->_put('/Widths '.($this->n+1).' 0 R'); + $this->_put('/FontDescriptor '.($this->n+2).' 0 R'); + if(isset($font['diff'])) + $this->_put('/Encoding '.($this->n+3).' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + // Widths + $this->_newobj(); + $cw = $font['cw']; + $s = '['; + for($i=32;$i<=255;$i++) + $s .= $cw[chr($i)].' '; + $this->_put($s.']'); + $this->_put('endobj'); + // Descriptor + $this->_newobj(); + $s = '<$v) + $s .= ' /'.$k.' '.$v; + if(!empty($font['file'])) + $s .= ' /FontFile'.($font['type']=='Type1' ? '' : '2').' '.$this->FontFiles[$key]['n'].' 0 R'; + $this->_put($s.'>>'); + $this->_put('endobj'); + // Encoding / Differences + if(isset($font['diff'])) + { + $this->_newobj(); + $this->_put('<>'); + $this->_put('endobj'); + } +} + +protected function _putfontfile($fontkey, $fontinfo) +{ + $this->_newobj(); + $this->FontFiles[$fontkey]['n'] = $this->n; + $fontfile = $this->fontpath.$fontinfo['file']; + if(!file_exists($fontfile)) + return; + $size = filesize($fontfile); + $this->_put('<_put('/Length1 '.$size); + $this->_put('>>'); + $this->_putstream(file_get_contents($fontfile)); + $this->_put('endobj'); +} + +protected function _putimages() +{ + foreach($this->images as $file=>$info) + { + $this->_putimage($info); + unset($this->images[$file]['data']); + unset($this->images[$file]['gd']); + } +} + +protected function _putimage(&$info) +{ + $info['n'] = $this->n+1; + $this->_newobj(); + $info['type'] = str_replace('image/','',$info['type']); + $this->_put('<_put('/Subtype /Image'); + $this->_put('/Width '.$info['w']); + $this->_put('/Height '.$info['h']); + if($info['type']=='jpg' || $info['type']=='jpeg') + $this->_put('/Filter /DCTDecode'); + elseif($info['type']=='png') + $this->_put('/Filter /FlateDecode'); + $this->_put('/ColorSpace /DeviceRGB'); + $this->_put('/BitsPerComponent 8'); + if(strlen($info['data'])) + { + if($info['type']=='png') + { + $data = gzcompress($info['data']); + $this->_put('/Length '.strlen($data)); + } + else + $this->_put('/Length '.strlen($info['data'])); + $this->_put('>>'); + $this->_putstream($info['data']); + } + else + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putannot($pl) +{ + $this->_newobj(); + $this->_put('<< /Type /Annot /Subtype /Link /Rect ['.$pl[0].' '.$pl[1].' '.$pl[2].' '.$pl[3].'] /Border [0 0 0] '); + if(is_string($pl[4])) + $this->_put('/A <_textstring($pl[4]).'>>>>'); + else + { + $l = $this->links[$pl[4]]; + if(isset($this->PageInfo[$l[0]]['size'])) + $h = $this->PageInfo[$l[0]]['size'][1]; + else + $h = ($this->DefOrientation=='P') ? $this->DefPageSize[1]*$this->k : $this->DefPageSize[0]*$this->k; + $this->_put(sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]',1+2*$l[0],$h-$l[1]*$this->k*$this->k)); + } + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putcatalog() +{ + $this->_newobj(); + $this->_put('<< /Type /Catalog'); + $this->_put('/Pages 1 0 R'); + if($this->ZoomMode=='fullpage') + $this->_put('/OpenAction [3 0 R /Fit]'); + elseif($this->ZoomMode=='fullwidth') + $this->_put('/OpenAction [3 0 R /FitH null]'); + elseif($this->ZoomMode=='real') + $this->_put('/OpenAction [3 0 R /XYZ null null 1]'); + elseif(!is_string($this->ZoomMode)) + $this->_put('/OpenAction [3 0 R /XYZ null null '.sprintf('%.2F',$this->ZoomMode/100).']'); + if($this->LayoutMode=='single') + $this->_put('/PageLayout /SinglePage'); + elseif($this->LayoutMode=='continuous') + $this->_put('/PageLayout /OneColumn'); + elseif($this->LayoutMode=='two') + $this->_put('/PageLayout /TwoColumnLeft'); + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putinfo() +{ + $this->metadata['Producer'] = 'FPDF '.FPDF_VERSION; + $this->metadata['CreationDate'] = 'D:'.substr(date('c'),0,10).substr(date('O'),0,3).substr(date('O'),3,2); + $this->_newobj(); + $this->_put('<<'); + foreach($this->metadata as $k=>$v) { + $this->_put('/'.$k.' '.$this->_textstring($v)); + } + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putheader() +{ + $this->_put('%PDF-'.$this->PDFVersion); +} + +protected function _begin_doc() +{ + // Start document + $this->_putheader(); + $this->_put($this->_getobjinfo()); +} + +protected function _enddoc() +{ + // Close document + $this->_putinfo(); + $this->_putcatalog(); + $this->_putpages(); + $this->_putresources(); + // Cross-reference + $o = $this->_getoffset(); + $this->_put('xref'); + $this->_put('0 '.($this->n+1)); + $this->_put('0000000000 65535 f '); + for($i=1;$i<=$this->n;$i++) + $this->_put(sprintf('%010d 00000 n ', $this->offsets[$i] ?? 0)); + // Trailer + $this->_put('trailer'); + $this->_put('<< /Size '.($this->n+1).' /Root '.$this->n.' 0 R /Info '.($this->n-1).' 0 R>>'); + $this->_put('startxref'); + $this->_put($o); + $this->_put('%%EOF'); + $this->state = 3; +} + +protected function _getobjinfo() +{ + // Object hierarchy + $this->_newobj(1); + $this->_put('<page;$i++) + $this->_put((3+2*$i).' 0 R '); + $this->_put('] /Count '.$this->page.'>>'); + $this->_put('endobj'); + return ''; +} +} diff --git a/vendor/smalot/pdfparser/LICENSE.txt b/vendor/smalot/pdfparser/LICENSE.txt new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/vendor/smalot/pdfparser/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. diff --git a/vendor/smalot/pdfparser/README.md b/vendor/smalot/pdfparser/README.md new file mode 100644 index 0000000..febcf30 --- /dev/null +++ b/vendor/smalot/pdfparser/README.md @@ -0,0 +1,59 @@ +# PDF parser + +[![Version](https://poser.pugx.org/smalot/pdfparser/v)](//packagist.org/packages/smalot/pdfparser) +![CI](https://github.com/smalot/pdfparser/workflows/CI/badge.svg) +![CS](https://github.com/smalot/pdfparser/workflows/CS/badge.svg) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/smalot/pdfparser/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/smalot/pdfparser/?branch=master) +[![Downloads](https://poser.pugx.org/smalot/pdfparser/downloads)](//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 +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. diff --git a/vendor/smalot/pdfparser/composer.json b/vendor/smalot/pdfparser/composer.json new file mode 100644 index 0000000..e9f425f --- /dev/null +++ b/vendor/smalot/pdfparser/composer.json @@ -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 + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Config.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Config.php new file mode 100644 index 0000000..e44b164 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Config.php @@ -0,0 +1,175 @@ + + * + * @date 2020-11-22 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Document.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Document.php new file mode 100644 index 0000000..1fad8b1 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Document.php @@ -0,0 +1,470 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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 + */ + 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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element.php new file mode 100644 index 0000000..8066030 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element.php @@ -0,0 +1,156 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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\/[A-Z#0-9\._]+)(?P.*)/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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementArray.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementArray.php new file mode 100644 index 0000000..b54bf84 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementArray.php @@ -0,0 +1,139 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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.*)/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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php new file mode 100644 index 0000000..55fb463 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementBoolean.php @@ -0,0 +1,75 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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*(?Ptrue|false)/is', $content, $match)) { + $value = $match['value']; + $offset += strpos($content, $value) + \strlen($value); + + return new self($value); + } + + return false; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementDate.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementDate.php new file mode 100644 index 0000000..f1f2df6 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementDate.php @@ -0,0 +1,139 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHPi, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Element; + +use Smalot\PdfParser\Document; + +/** + * Class ElementDate + */ +class ElementDate extends ElementString +{ + /** + * @var array + */ + 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.*?)\)/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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php new file mode 100644 index 0000000..3fc3413 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementHexa.php @@ -0,0 +1,91 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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[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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementMissing.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementMissing.php new file mode 100644 index 0000000..d2fc000 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementMissing.php @@ -0,0 +1,66 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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 ''; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementName.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementName.php new file mode 100644 index 0000000..6e8d97a --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementName.php @@ -0,0 +1,69 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementNull.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementNull.php new file mode 100644 index 0000000..9af8843 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementNull.php @@ -0,0 +1,71 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php new file mode 100644 index 0000000..5454acc --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementNumeric.php @@ -0,0 +1,62 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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\-?[0-9\.]+)/s', $content, $match)) { + $value = $match['value']; + $offset += strpos($content, $value) + \strlen($value); + + return new self($value); + } + + return false; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementString.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementString.php new file mode 100644 index 0000000..011bcf4 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementString.php @@ -0,0 +1,93 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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.*)/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[\\\]*)$/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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php new file mode 100644 index 0000000..c37b6da --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementStruct.php @@ -0,0 +1,75 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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.*)/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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php new file mode 100644 index 0000000..ebba71a --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Element/ElementXRef.php @@ -0,0 +1,98 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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[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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding.php new file mode 100644 index 0000000..511411b --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding.php @@ -0,0 +1,162 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php new file mode 100644 index 0000000..aea9c02 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php @@ -0,0 +1,8 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +// 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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php new file mode 100644 index 0000000..616a0f5 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php @@ -0,0 +1,76 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +// 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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php new file mode 100644 index 0000000..c47131c --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php @@ -0,0 +1,80 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +// 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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/PDFDocEncoding.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/PDFDocEncoding.php new file mode 100644 index 0000000..70bc48c --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/PDFDocEncoding.php @@ -0,0 +1,189 @@ + + * + * @date 2023-06-28 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +// 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()); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php new file mode 100644 index 0000000..fbe1af4 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php @@ -0,0 +1,1099 @@ + + * + * @date 2019-09-17 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Encoding; + +/** + * Class PostScriptGlyphs + */ +class PostScriptGlyphs +{ + /** + * The mapping tables have been converted from https://github.com/OpenPrinting/cups-filters/blob/master/fontembed/aglfn13.c, + * part of the OpenPrinting/cups-filters package, which itself is licensed under the MIT license and lists this specific code part as: + * Copyright 2008,2012 Tobias Hoffmann under the Expat license (https://www.gnu.org/licenses/license-list.html#Expat) + */ + public static function getGlyphs(): array + { + return [ + 'space' => '0x00a0', + 'exclam' => '0x0021', + 'quotedbl' => '0x0022', + 'numbersign' => '0x0023', + 'dollar' => '0x0024', + 'percent' => '0x0025', + 'ampersand' => '0x0026', + 'quotesingle' => '0x0027', + 'parenleft' => '0x0028', + 'parenright' => '0x0029', + 'asterisk' => '0x002a', + 'plus' => '0x002b', + 'comma' => '0x002c', + 'hyphen' => '0x002d', + 'period' => '0x002e', + 'slash' => '0x002f', + 'zero' => '0x0030', + 'one' => '0x0031', + 'two' => '0x0032', + 'three' => '0x0033', + 'four' => '0x0034', + 'five' => '0x0035', + 'six' => '0x0036', + 'seven' => '0x0037', + 'eight' => '0x0038', + 'nine' => '0x0039', + 'colon' => '0x003a', + 'semicolon' => '0x003b', + 'less' => '0x003c', + 'equal' => '0x003d', + 'greater' => '0x003e', + 'question' => '0x003f', + 'at' => '0x0040', + 'A' => '0x0041', + 'B' => '0x0042', + 'C' => '0x0043', + 'D' => '0x0044', + 'E' => '0x0045', + 'F' => '0x0046', + 'G' => '0x0047', + 'H' => '0x0048', + 'I' => '0x0049', + 'J' => '0x004a', + 'K' => '0x004b', + 'L' => '0x004c', + 'M' => '0x004d', + 'N' => '0x004e', + 'O' => '0x004f', + 'P' => '0x0050', + 'Q' => '0x0051', + 'R' => '0x0052', + 'S' => '0x0053', + 'T' => '0x0054', + 'U' => '0x0055', + 'V' => '0x0056', + 'W' => '0x0057', + 'X' => '0x0058', + 'Y' => '0x0059', + 'Z' => '0x005a', + 'bracketleft' => '0x005b', + 'backslash' => '0x005c', + 'bracketright' => '0x005d', + 'asciicircum' => '0x005e', + 'underscore' => '0x005f', + 'grave' => '0x0060', + 'a' => '0x0061', + 'b' => '0x0062', + 'c' => '0x0063', + 'd' => '0x0064', + 'e' => '0x0065', + 'f' => '0x0066', + 'g' => '0x0067', + 'h' => '0x0068', + 'i' => '0x0069', + 'j' => '0x006a', + 'k' => '0x006b', + 'l' => '0x006c', + 'm' => '0x006d', + 'n' => '0x006e', + 'o' => '0x006f', + 'p' => '0x0070', + 'q' => '0x0071', + 'r' => '0x0072', + 's' => '0x0073', + 't' => '0x0074', + 'u' => '0x0075', + 'v' => '0x0076', + 'w' => '0x0077', + 'x' => '0x0078', + 'y' => '0x0079', + 'z' => '0x007a', + 'braceleft' => '0x007b', + 'bar' => '0x007c', + 'braceright' => '0x007d', + 'asciitilde' => '0x007e', + 'exclamdown' => '0x00a1', + 'cent' => '0x00a2', + 'sterling' => '0x00a3', + 'currency' => '0x00a4', + 'yen' => '0x00a5', + 'brokenbar' => '0x00a6', + 'section' => '0x00a7', + 'dieresis' => '0x00a8', + 'copyright' => '0x00a9', + 'ordfeminine' => '0x00aa', + 'guillemotleft' => '0x00ab', + 'logicalnot' => '0x00ac', + 'minus' => '0x2212', + 'registered' => '0x00ae', + 'macron' => '0x02c9', + 'degree' => '0x00b0', + 'plusminus' => '0x00b1', + 'twosuperior' => '0x00b2', + 'threesuperior' => '0x00b3', + 'acute' => '0x00b4', + 'mu' => '0x03bc', + 'paragraph' => '0x00b6', + 'periodcentered' => '0x2219', + 'cedilla' => '0x00b8', + 'onesuperior' => '0x00b9', + 'ordmasculine' => '0x00ba', + 'guillemotright' => '0x00bb', + 'onequarter' => '0x00bc', + 'onehalf' => '0x00bd', + 'threequarters' => '0x00be', + 'questiondown' => '0x00bf', + 'Agrave' => '0x00c0', + 'Aacute' => '0x00c1', + 'Acircumflex' => '0x00c2', + 'Atilde' => '0x00c3', + 'Adieresis' => '0x00c4', + 'Aring' => '0x00c5', + 'AE' => '0x00c6', + 'Ccedilla' => '0x00c7', + 'Egrave' => '0x00c8', + 'Eacute' => '0x00c9', + 'Ecircumflex' => '0x00ca', + 'Edieresis' => '0x00cb', + 'Igrave' => '0x00cc', + 'Iacute' => '0x00cd', + 'Icircumflex' => '0x00ce', + 'Idieresis' => '0x00cf', + 'Eth' => '0x00d0', + 'Ntilde' => '0x00d1', + 'Ograve' => '0x00d2', + 'Oacute' => '0x00d3', + 'Ocircumflex' => '0x00d4', + 'Otilde' => '0x00d5', + 'Odieresis' => '0x00d6', + 'multiply' => '0x00d7', + 'Oslash' => '0x00d8', + 'Ugrave' => '0x00d9', + 'Uacute' => '0x00da', + 'Ucircumflex' => '0x00db', + 'Udieresis' => '0x00dc', + 'Yacute' => '0x00dd', + 'Thorn' => '0x00de', + 'germandbls' => '0x00df', + 'agrave' => '0x00e0', + 'aacute' => '0x00e1', + 'acircumflex' => '0x00e2', + 'atilde' => '0x00e3', + 'adieresis' => '0x00e4', + 'aring' => '0x00e5', + 'ae' => '0x00e6', + 'ccedilla' => '0x00e7', + 'egrave' => '0x00e8', + 'eacute' => '0x00e9', + 'ecircumflex' => '0x00ea', + 'edieresis' => '0x00eb', + 'igrave' => '0x00ec', + 'iacute' => '0x00ed', + 'icircumflex' => '0x00ee', + 'idieresis' => '0x00ef', + 'eth' => '0x00f0', + 'ntilde' => '0x00f1', + 'ograve' => '0x00f2', + 'oacute' => '0x00f3', + 'ocircumflex' => '0x00f4', + 'otilde' => '0x00f5', + 'odieresis' => '0x00f6', + 'divide' => '0x00f7', + 'oslash' => '0x00f8', + 'ugrave' => '0x00f9', + 'uacute' => '0x00fa', + 'ucircumflex' => '0x00fb', + 'udieresis' => '0x00fc', + 'yacute' => '0x00fd', + 'thorn' => '0x00fe', + 'ydieresis' => '0x00ff', + 'Amacron' => '0x0100', + 'amacron' => '0x0101', + 'Abreve' => '0x0102', + 'abreve' => '0x0103', + 'Aogonek' => '0x0104', + 'aogonek' => '0x0105', + 'Cacute' => '0x0106', + 'cacute' => '0x0107', + 'Ccircumflex' => '0x0108', + 'ccircumflex' => '0x0109', + 'Cdotaccent' => '0x010a', + 'cdotaccent' => '0x010b', + 'Ccaron' => '0x010c', + 'ccaron' => '0x010d', + 'Dcaron' => '0x010e', + 'dcaron' => '0x010f', + 'Dcroat' => '0x0110', + 'dcroat' => '0x0111', + 'Emacron' => '0x0112', + 'emacron' => '0x0113', + 'Ebreve' => '0x0114', + 'ebreve' => '0x0115', + 'Edotaccent' => '0x0116', + 'edotaccent' => '0x0117', + 'Eogonek' => '0x0118', + 'eogonek' => '0x0119', + 'Ecaron' => '0x011a', + 'ecaron' => '0x011b', + 'Gcircumflex' => '0x011c', + 'gcircumflex' => '0x011d', + 'Gbreve' => '0x011e', + 'gbreve' => '0x011f', + 'Gdotaccent' => '0x0120', + 'gdotaccent' => '0x0121', + 'Gcommaaccent' => '0x0122', + 'gcommaaccent' => '0x0123', + 'Hcircumflex' => '0x0124', + 'hcircumflex' => '0x0125', + 'Hbar' => '0x0126', + 'hbar' => '0x0127', + 'Itilde' => '0x0128', + 'itilde' => '0x0129', + 'Imacron' => '0x012a', + 'imacron' => '0x012b', + 'Ibreve' => '0x012c', + 'ibreve' => '0x012d', + 'Iogonek' => '0x012e', + 'iogonek' => '0x012f', + 'Idotaccent' => '0x0130', + 'dotlessi' => '0x0131', + 'IJ' => '0x0132', + 'ij' => '0x0133', + 'Jcircumflex' => '0x0134', + 'jcircumflex' => '0x0135', + 'Kcommaaccent' => '0x0136', + 'kcommaaccent' => '0x0137', + 'kgreenlandic' => '0x0138', + 'Lacute' => '0x0139', + 'lacute' => '0x013a', + 'Lcommaaccent' => '0x013b', + 'lcommaaccent' => '0x013c', + 'Lcaron' => '0x013d', + 'lcaron' => '0x013e', + 'Ldot' => '0x013f', + 'ldot' => '0x0140', + 'Lslash' => '0x0141', + 'lslash' => '0x0142', + 'Nacute' => '0x0143', + 'nacute' => '0x0144', + 'Ncommaaccent' => '0x0145', + 'ncommaaccent' => '0x0146', + 'Ncaron' => '0x0147', + 'ncaron' => '0x0148', + 'napostrophe' => '0x0149', + 'Eng' => '0x014a', + 'eng' => '0x014b', + 'Omacron' => '0x014c', + 'omacron' => '0x014d', + 'Obreve' => '0x014e', + 'obreve' => '0x014f', + 'Ohungarumlaut' => '0x0150', + 'ohungarumlaut' => '0x0151', + 'OE' => '0x0152', + 'oe' => '0x0153', + 'Racute' => '0x0154', + 'racute' => '0x0155', + 'Rcommaaccent' => '0x0156', + 'rcommaaccent' => '0x0157', + 'Rcaron' => '0x0158', + 'rcaron' => '0x0159', + 'Sacute' => '0x015a', + 'sacute' => '0x015b', + 'Scircumflex' => '0x015c', + 'scircumflex' => '0x015d', + 'Scedilla' => '0xf6c1', + 'scedilla' => '0xf6c2', + 'Scaron' => '0x0160', + 'scaron' => '0x0161', + 'Tcommaaccent' => '0x021a', + 'tcommaaccent' => '0x021b', + 'Tcaron' => '0x0164', + 'tcaron' => '0x0165', + 'Tbar' => '0x0166', + 'tbar' => '0x0167', + 'Utilde' => '0x0168', + 'utilde' => '0x0169', + 'Umacron' => '0x016a', + 'umacron' => '0x016b', + 'Ubreve' => '0x016c', + 'ubreve' => '0x016d', + 'Uring' => '0x016e', + 'uring' => '0x016f', + 'Uhungarumlaut' => '0x0170', + 'uhungarumlaut' => '0x0171', + 'Uogonek' => '0x0172', + 'uogonek' => '0x0173', + 'Wcircumflex' => '0x0174', + 'wcircumflex' => '0x0175', + 'Ycircumflex' => '0x0176', + 'ycircumflex' => '0x0177', + 'Ydieresis' => '0x0178', + 'Zacute' => '0x0179', + 'zacute' => '0x017a', + 'Zdotaccent' => '0x017b', + 'zdotaccent' => '0x017c', + 'Zcaron' => '0x017d', + 'zcaron' => '0x017e', + 'longs' => '0x017f', + 'florin' => '0x0192', + 'Ohorn' => '0x01a0', + 'ohorn' => '0x01a1', + 'Uhorn' => '0x01af', + 'uhorn' => '0x01b0', + 'Gcaron' => '0x01e6', + 'gcaron' => '0x01e7', + 'Aringacute' => '0x01fa', + 'aringacute' => '0x01fb', + 'AEacute' => '0x01fc', + 'aeacute' => '0x01fd', + 'Oslashacute' => '0x01fe', + 'oslashacute' => '0x01ff', + 'Scommaaccent' => '0x0218', + 'scommaaccent' => '0x0219', + 'afii57929' => '0x02bc', + 'afii64937' => '0x02bd', + 'circumflex' => '0x02c6', + 'caron' => '0x02c7', + 'breve' => '0x02d8', + 'dotaccent' => '0x02d9', + 'ring' => '0x02da', + 'ogonek' => '0x02db', + 'tilde' => '0x02dc', + 'hungarumlaut' => '0x02dd', + 'gravecomb' => '0x0300', + 'acutecomb' => '0x0301', + 'tildecomb' => '0x0303', + 'hookabovecomb' => '0x0309', + 'dotbelowcomb' => '0x0323', + 'tonos' => '0x0384', + 'dieresistonos' => '0x0385', + 'Alphatonos' => '0x0386', + 'anoteleia' => '0x0387', + 'Epsilontonos' => '0x0388', + 'Etatonos' => '0x0389', + 'Iotatonos' => '0x038a', + 'Omicrontonos' => '0x038c', + 'Upsilontonos' => '0x038e', + 'Omegatonos' => '0x038f', + 'iotadieresistonos' => '0x0390', + 'Alpha' => '0x0391', + 'Beta' => '0x0392', + 'Gamma' => '0x0393', + 'Delta' => '0x2206', + 'Epsilon' => '0x0395', + 'Zeta' => '0x0396', + 'Eta' => '0x0397', + 'Theta' => '0x0398', + 'Iota' => '0x0399', + 'Kappa' => '0x039a', + 'Lambda' => '0x039b', + 'Mu' => '0x039c', + 'Nu' => '0x039d', + 'Xi' => '0x039e', + 'Omicron' => '0x039f', + 'Pi' => '0x03a0', + 'Rho' => '0x03a1', + 'Sigma' => '0x03a3', + 'Tau' => '0x03a4', + 'Upsilon' => '0x03a5', + 'Phi' => '0x03a6', + 'Chi' => '0x03a7', + 'Psi' => '0x03a8', + 'Omega' => '0x2126', + 'Iotadieresis' => '0x03aa', + 'Upsilondieresis' => '0x03ab', + 'alphatonos' => '0x03ac', + 'epsilontonos' => '0x03ad', + 'etatonos' => '0x03ae', + 'iotatonos' => '0x03af', + 'upsilondieresistonos' => '0x03b0', + 'alpha' => '0x03b1', + 'beta' => '0x03b2', + 'gamma' => '0x03b3', + 'delta' => '0x03b4', + 'epsilon' => '0x03b5', + 'zeta' => '0x03b6', + 'eta' => '0x03b7', + 'theta' => '0x03b8', + 'iota' => '0x03b9', + 'kappa' => '0x03ba', + 'lambda' => '0x03bb', + 'nu' => '0x03bd', + 'xi' => '0x03be', + 'omicron' => '0x03bf', + 'pi' => '0x03c0', + 'rho' => '0x03c1', + 'sigma1' => '0x03c2', + 'sigma' => '0x03c3', + 'tau' => '0x03c4', + 'upsilon' => '0x03c5', + 'phi' => '0x03c6', + 'chi' => '0x03c7', + 'psi' => '0x03c8', + 'omega' => '0x03c9', + 'iotadieresis' => '0x03ca', + 'upsilondieresis' => '0x03cb', + 'omicrontonos' => '0x03cc', + 'upsilontonos' => '0x03cd', + 'omegatonos' => '0x03ce', + 'theta1' => '0x03d1', + 'Upsilon1' => '0x03d2', + 'phi1' => '0x03d5', + 'omega1' => '0x03d6', + 'afii10023' => '0x0401', + 'afii10051' => '0x0402', + 'afii10052' => '0x0403', + 'afii10053' => '0x0404', + 'afii10054' => '0x0405', + 'afii10055' => '0x0406', + 'afii10056' => '0x0407', + 'afii10057' => '0x0408', + 'afii10058' => '0x0409', + 'afii10059' => '0x040a', + 'afii10060' => '0x040b', + 'afii10061' => '0x040c', + 'afii10062' => '0x040e', + 'afii10145' => '0x040f', + 'afii10017' => '0x0410', + 'afii10018' => '0x0411', + 'afii10019' => '0x0412', + 'afii10020' => '0x0413', + 'afii10021' => '0x0414', + 'afii10022' => '0x0415', + 'afii10024' => '0x0416', + 'afii10025' => '0x0417', + 'afii10026' => '0x0418', + 'afii10027' => '0x0419', + 'afii10028' => '0x041a', + 'afii10029' => '0x041b', + 'afii10030' => '0x041c', + 'afii10031' => '0x041d', + 'afii10032' => '0x041e', + 'afii10033' => '0x041f', + 'afii10034' => '0x0420', + 'afii10035' => '0x0421', + 'afii10036' => '0x0422', + 'afii10037' => '0x0423', + 'afii10038' => '0x0424', + 'afii10039' => '0x0425', + 'afii10040' => '0x0426', + 'afii10041' => '0x0427', + 'afii10042' => '0x0428', + 'afii10043' => '0x0429', + 'afii10044' => '0x042a', + 'afii10045' => '0x042b', + 'afii10046' => '0x042c', + 'afii10047' => '0x042d', + 'afii10048' => '0x042e', + 'afii10049' => '0x042f', + 'afii10065' => '0x0430', + 'afii10066' => '0x0431', + 'afii10067' => '0x0432', + 'afii10068' => '0x0433', + 'afii10069' => '0x0434', + 'afii10070' => '0x0435', + 'afii10072' => '0x0436', + 'afii10073' => '0x0437', + 'afii10074' => '0x0438', + 'afii10075' => '0x0439', + 'afii10076' => '0x043a', + 'afii10077' => '0x043b', + 'afii10078' => '0x043c', + 'afii10079' => '0x043d', + 'afii10080' => '0x043e', + 'afii10081' => '0x043f', + 'afii10082' => '0x0440', + 'afii10083' => '0x0441', + 'afii10084' => '0x0442', + 'afii10085' => '0x0443', + 'afii10086' => '0x0444', + 'afii10087' => '0x0445', + 'afii10088' => '0x0446', + 'afii10089' => '0x0447', + 'afii10090' => '0x0448', + 'afii10091' => '0x0449', + 'afii10092' => '0x044a', + 'afii10093' => '0x044b', + 'afii10094' => '0x044c', + 'afii10095' => '0x044d', + 'afii10096' => '0x044e', + 'afii10097' => '0x044f', + 'afii10071' => '0x0451', + 'afii10099' => '0x0452', + 'afii10100' => '0x0453', + 'afii10101' => '0x0454', + 'afii10102' => '0x0455', + 'afii10103' => '0x0456', + 'afii10104' => '0x0457', + 'afii10105' => '0x0458', + 'afii10106' => '0x0459', + 'afii10107' => '0x045a', + 'afii10108' => '0x045b', + 'afii10109' => '0x045c', + 'afii10110' => '0x045e', + 'afii10193' => '0x045f', + 'afii10146' => '0x0462', + 'afii10194' => '0x0463', + 'afii10147' => '0x0472', + 'afii10195' => '0x0473', + 'afii10148' => '0x0474', + 'afii10196' => '0x0475', + 'afii10050' => '0x0490', + 'afii10098' => '0x0491', + 'afii10846' => '0x04d9', + 'afii57799' => '0x05b0', + 'afii57801' => '0x05b1', + 'afii57800' => '0x05b2', + 'afii57802' => '0x05b3', + 'afii57793' => '0x05b4', + 'afii57794' => '0x05b5', + 'afii57795' => '0x05b6', + 'afii57798' => '0x05b7', + 'afii57797' => '0x05b8', + 'afii57806' => '0x05b9', + 'afii57796' => '0x05bb', + 'afii57807' => '0x05bc', + 'afii57839' => '0x05bd', + 'afii57645' => '0x05be', + 'afii57841' => '0x05bf', + 'afii57842' => '0x05c0', + 'afii57804' => '0x05c1', + 'afii57803' => '0x05c2', + 'afii57658' => '0x05c3', + 'afii57664' => '0x05d0', + 'afii57665' => '0x05d1', + 'afii57666' => '0x05d2', + 'afii57667' => '0x05d3', + 'afii57668' => '0x05d4', + 'afii57669' => '0x05d5', + 'afii57670' => '0x05d6', + 'afii57671' => '0x05d7', + 'afii57672' => '0x05d8', + 'afii57673' => '0x05d9', + 'afii57674' => '0x05da', + 'afii57675' => '0x05db', + 'afii57676' => '0x05dc', + 'afii57677' => '0x05dd', + 'afii57678' => '0x05de', + 'afii57679' => '0x05df', + 'afii57680' => '0x05e0', + 'afii57681' => '0x05e1', + 'afii57682' => '0x05e2', + 'afii57683' => '0x05e3', + 'afii57684' => '0x05e4', + 'afii57685' => '0x05e5', + 'afii57686' => '0x05e6', + 'afii57687' => '0x05e7', + 'afii57688' => '0x05e8', + 'afii57689' => '0x05e9', + 'afii57690' => '0x05ea', + 'afii57716' => '0x05f0', + 'afii57717' => '0x05f1', + 'afii57718' => '0x05f2', + 'afii57388' => '0x060c', + 'afii57403' => '0x061b', + 'afii57407' => '0x061f', + 'afii57409' => '0x0621', + 'afii57410' => '0x0622', + 'afii57411' => '0x0623', + 'afii57412' => '0x0624', + 'afii57413' => '0x0625', + 'afii57414' => '0x0626', + 'afii57415' => '0x0627', + 'afii57416' => '0x0628', + 'afii57417' => '0x0629', + 'afii57418' => '0x062a', + 'afii57419' => '0x062b', + 'afii57420' => '0x062c', + 'afii57421' => '0x062d', + 'afii57422' => '0x062e', + 'afii57423' => '0x062f', + 'afii57424' => '0x0630', + 'afii57425' => '0x0631', + 'afii57426' => '0x0632', + 'afii57427' => '0x0633', + 'afii57428' => '0x0634', + 'afii57429' => '0x0635', + 'afii57430' => '0x0636', + 'afii57431' => '0x0637', + 'afii57432' => '0x0638', + 'afii57433' => '0x0639', + 'afii57434' => '0x063a', + 'afii57440' => '0x0640', + 'afii57441' => '0x0641', + 'afii57442' => '0x0642', + 'afii57443' => '0x0643', + 'afii57444' => '0x0644', + 'afii57445' => '0x0645', + 'afii57446' => '0x0646', + 'afii57470' => '0x0647', + 'afii57448' => '0x0648', + 'afii57449' => '0x0649', + 'afii57450' => '0x064a', + 'afii57451' => '0x064b', + 'afii57452' => '0x064c', + 'afii57453' => '0x064d', + 'afii57454' => '0x064e', + 'afii57455' => '0x064f', + 'afii57456' => '0x0650', + 'afii57457' => '0x0651', + 'afii57458' => '0x0652', + 'afii57392' => '0x0660', + 'afii57393' => '0x0661', + 'afii57394' => '0x0662', + 'afii57395' => '0x0663', + 'afii57396' => '0x0664', + 'afii57397' => '0x0665', + 'afii57398' => '0x0666', + 'afii57399' => '0x0667', + 'afii57400' => '0x0668', + 'afii57401' => '0x0669', + 'afii57381' => '0x066a', + 'afii63167' => '0x066d', + 'afii57511' => '0x0679', + 'afii57506' => '0x067e', + 'afii57507' => '0x0686', + 'afii57512' => '0x0688', + 'afii57513' => '0x0691', + 'afii57508' => '0x0698', + 'afii57505' => '0x06a4', + 'afii57509' => '0x06af', + 'afii57514' => '0x06ba', + 'afii57519' => '0x06d2', + 'afii57534' => '0x06d5', + 'Wgrave' => '0x1e80', + 'wgrave' => '0x1e81', + 'Wacute' => '0x1e82', + 'wacute' => '0x1e83', + 'Wdieresis' => '0x1e84', + 'wdieresis' => '0x1e85', + 'Ygrave' => '0x1ef2', + 'ygrave' => '0x1ef3', + 'afii61664' => '0x200c', + 'afii301' => '0x200d', + 'afii299' => '0x200e', + 'afii300' => '0x200f', + 'figuredash' => '0x2012', + 'endash' => '0x2013', + 'emdash' => '0x2014', + 'afii00208' => '0x2015', + 'underscoredbl' => '0x2017', + 'quoteleft' => '0x2018', + 'quoteright' => '0x2019', + 'quotesinglbase' => '0x201a', + 'quotereversed' => '0x201b', + 'quotedblleft' => '0x201c', + 'quotedblright' => '0x201d', + 'quotedblbase' => '0x201e', + 'dagger' => '0x2020', + 'daggerdbl' => '0x2021', + 'bullet' => '0x2022', + 'onedotenleader' => '0x2024', + 'twodotenleader' => '0x2025', + 'ellipsis' => '0x2026', + 'afii61573' => '0x202c', + 'afii61574' => '0x202d', + 'afii61575' => '0x202e', + 'perthousand' => '0x2030', + 'minute' => '0x2032', + 'second' => '0x2033', + 'guilsinglleft' => '0x2039', + 'guilsinglright' => '0x203a', + 'exclamdbl' => '0x203c', + 'fraction' => '0x2215', + 'zerosuperior' => '0x2070', + 'foursuperior' => '0x2074', + 'fivesuperior' => '0x2075', + 'sixsuperior' => '0x2076', + 'sevensuperior' => '0x2077', + 'eightsuperior' => '0x2078', + 'ninesuperior' => '0x2079', + 'parenleftsuperior' => '0x207d', + 'parenrightsuperior' => '0x207e', + 'nsuperior' => '0x207f', + 'zeroinferior' => '0x2080', + 'oneinferior' => '0x2081', + 'twoinferior' => '0x2082', + 'threeinferior' => '0x2083', + 'fourinferior' => '0x2084', + 'fiveinferior' => '0x2085', + 'sixinferior' => '0x2086', + 'seveninferior' => '0x2087', + 'eightinferior' => '0x2088', + 'nineinferior' => '0x2089', + 'parenleftinferior' => '0x208d', + 'parenrightinferior' => '0x208e', + 'colonmonetary' => '0x20a1', + 'franc' => '0x20a3', + 'lira' => '0x20a4', + 'peseta' => '0x20a7', + 'afii57636' => '0x20aa', + 'dong' => '0x20ab', + 'Euro' => '0x20ac', + 'afii61248' => '0x2105', + 'Ifraktur' => '0x2111', + 'afii61289' => '0x2113', + 'afii61352' => '0x2116', + 'weierstrass' => '0x2118', + 'Rfraktur' => '0x211c', + 'prescription' => '0x211e', + 'trademark' => '0x2122', + 'estimated' => '0x212e', + 'aleph' => '0x2135', + 'onethird' => '0x2153', + 'twothirds' => '0x2154', + 'oneeighth' => '0x215b', + 'threeeighths' => '0x215c', + 'fiveeighths' => '0x215d', + 'seveneighths' => '0x215e', + 'arrowleft' => '0x2190', + 'arrowup' => '0x2191', + 'arrowright' => '0x2192', + 'arrowdown' => '0x2193', + 'arrowboth' => '0x2194', + 'arrowupdn' => '0x2195', + 'arrowupdnbse' => '0x21a8', + 'carriagereturn' => '0x21b5', + 'arrowdblleft' => '0x21d0', + 'arrowdblup' => '0x21d1', + 'arrowdblright' => '0x21d2', + 'arrowdbldown' => '0x21d3', + 'arrowdblboth' => '0x21d4', + 'universal' => '0x2200', + 'partialdiff' => '0x2202', + 'existential' => '0x2203', + 'emptyset' => '0x2205', + 'gradient' => '0x2207', + 'element' => '0x2208', + 'notelement' => '0x2209', + 'suchthat' => '0x220b', + 'product' => '0x220f', + 'summation' => '0x2211', + 'asteriskmath' => '0x2217', + 'radical' => '0x221a', + 'proportional' => '0x221d', + 'infinity' => '0x221e', + 'orthogonal' => '0x221f', + 'angle' => '0x2220', + 'logicaland' => '0x2227', + 'logicalor' => '0x2228', + 'intersection' => '0x2229', + 'union' => '0x222a', + 'integral' => '0x222b', + 'therefore' => '0x2234', + 'similar' => '0x223c', + 'congruent' => '0x2245', + 'approxequal' => '0x2248', + 'notequal' => '0x2260', + 'equivalence' => '0x2261', + 'lessequal' => '0x2264', + 'greaterequal' => '0x2265', + 'propersubset' => '0x2282', + 'propersuperset' => '0x2283', + 'notsubset' => '0x2284', + 'reflexsubset' => '0x2286', + 'reflexsuperset' => '0x2287', + 'circleplus' => '0x2295', + 'circlemultiply' => '0x2297', + 'perpendicular' => '0x22a5', + 'dotmath' => '0x22c5', + 'house' => '0x2302', + 'revlogicalnot' => '0x2310', + 'integraltp' => '0x2320', + 'integralbt' => '0x2321', + 'angleleft' => '0x2329', + 'angleright' => '0x232a', + 'SF100000' => '0x2500', + 'SF110000' => '0x2502', + 'SF010000' => '0x250c', + 'SF030000' => '0x2510', + 'SF020000' => '0x2514', + 'SF040000' => '0x2518', + 'SF080000' => '0x251c', + 'SF090000' => '0x2524', + 'SF060000' => '0x252c', + 'SF070000' => '0x2534', + 'SF050000' => '0x253c', + 'SF430000' => '0x2550', + 'SF240000' => '0x2551', + 'SF510000' => '0x2552', + 'SF520000' => '0x2553', + 'SF390000' => '0x2554', + 'SF220000' => '0x2555', + 'SF210000' => '0x2556', + 'SF250000' => '0x2557', + 'SF500000' => '0x2558', + 'SF490000' => '0x2559', + 'SF380000' => '0x255a', + 'SF280000' => '0x255b', + 'SF270000' => '0x255c', + 'SF260000' => '0x255d', + 'SF360000' => '0x255e', + 'SF370000' => '0x255f', + 'SF420000' => '0x2560', + 'SF190000' => '0x2561', + 'SF200000' => '0x2562', + 'SF230000' => '0x2563', + 'SF470000' => '0x2564', + 'SF480000' => '0x2565', + 'SF410000' => '0x2566', + 'SF450000' => '0x2567', + 'SF460000' => '0x2568', + 'SF400000' => '0x2569', + 'SF540000' => '0x256a', + 'SF530000' => '0x256b', + 'SF440000' => '0x256c', + 'upblock' => '0x2580', + 'dnblock' => '0x2584', + 'block' => '0x2588', + 'lfblock' => '0x258c', + 'rtblock' => '0x2590', + 'ltshade' => '0x2591', + 'shade' => '0x2592', + 'dkshade' => '0x2593', + 'filledbox' => '0x25a0', + 'H22073' => '0x25a1', + 'H18543' => '0x25aa', + 'H18551' => '0x25ab', + 'filledrect' => '0x25ac', + 'triagup' => '0x25b2', + 'triagrt' => '0x25ba', + 'triagdn' => '0x25bc', + 'triaglf' => '0x25c4', + 'lozenge' => '0x25ca', + 'circle' => '0x25cb', + 'H18533' => '0x25cf', + 'invbullet' => '0x25d8', + 'invcircle' => '0x25d9', + 'openbullet' => '0x25e6', + 'smileface' => '0x263a', + 'invsmileface' => '0x263b', + 'sun' => '0x263c', + 'female' => '0x2640', + 'male' => '0x2642', + 'spade' => '0x2660', + 'club' => '0x2663', + 'heart' => '0x2665', + 'diamond' => '0x2666', + 'musicalnote' => '0x266a', + 'musicalnotedbl' => '0x266b', + 'dotlessj' => '0xf6be', + 'LL' => '0xf6bf', + 'll' => '0xf6c0', + 'commaaccent' => '0xf6c3', + 'afii10063' => '0xf6c4', + 'afii10064' => '0xf6c5', + 'afii10192' => '0xf6c6', + 'afii10831' => '0xf6c7', + 'afii10832' => '0xf6c8', + 'Acute' => '0xf6c9', + 'Caron' => '0xf6ca', + 'Dieresis' => '0xf6cb', + 'DieresisAcute' => '0xf6cc', + 'DieresisGrave' => '0xf6cd', + 'Grave' => '0xf6ce', + 'Hungarumlaut' => '0xf6cf', + 'Macron' => '0xf6d0', + 'cyrBreve' => '0xf6d1', + 'cyrFlex' => '0xf6d2', + 'dblGrave' => '0xf6d3', + 'cyrbreve' => '0xf6d4', + 'cyrflex' => '0xf6d5', + 'dblgrave' => '0xf6d6', + 'dieresisacute' => '0xf6d7', + 'dieresisgrave' => '0xf6d8', + 'copyrightserif' => '0xf6d9', + 'registerserif' => '0xf6da', + 'trademarkserif' => '0xf6db', + 'onefitted' => '0xf6dc', + 'rupiah' => '0xf6dd', + 'threequartersemdash' => '0xf6de', + 'centinferior' => '0xf6df', + 'centsuperior' => '0xf6e0', + 'commainferior' => '0xf6e1', + 'commasuperior' => '0xf6e2', + 'dollarinferior' => '0xf6e3', + 'dollarsuperior' => '0xf6e4', + 'hypheninferior' => '0xf6e5', + 'hyphensuperior' => '0xf6e6', + 'periodinferior' => '0xf6e7', + 'periodsuperior' => '0xf6e8', + 'asuperior' => '0xf6e9', + 'bsuperior' => '0xf6ea', + 'dsuperior' => '0xf6eb', + 'esuperior' => '0xf6ec', + 'isuperior' => '0xf6ed', + 'lsuperior' => '0xf6ee', + 'msuperior' => '0xf6ef', + 'osuperior' => '0xf6f0', + 'rsuperior' => '0xf6f1', + 'ssuperior' => '0xf6f2', + 'tsuperior' => '0xf6f3', + 'Brevesmall' => '0xf6f4', + 'Caronsmall' => '0xf6f5', + 'Circumflexsmall' => '0xf6f6', + 'Dotaccentsmall' => '0xf6f7', + 'Hungarumlautsmall' => '0xf6f8', + 'Lslashsmall' => '0xf6f9', + 'OEsmall' => '0xf6fa', + 'Ogoneksmall' => '0xf6fb', + 'Ringsmall' => '0xf6fc', + 'Scaronsmall' => '0xf6fd', + 'Tildesmall' => '0xf6fe', + 'Zcaronsmall' => '0xf6ff', + 'exclamsmall' => '0xf721', + 'dollaroldstyle' => '0xf724', + 'ampersandsmall' => '0xf726', + 'zerooldstyle' => '0xf730', + 'oneoldstyle' => '0xf731', + 'twooldstyle' => '0xf732', + 'threeoldstyle' => '0xf733', + 'fouroldstyle' => '0xf734', + 'fiveoldstyle' => '0xf735', + 'sixoldstyle' => '0xf736', + 'sevenoldstyle' => '0xf737', + 'eightoldstyle' => '0xf738', + 'nineoldstyle' => '0xf739', + 'questionsmall' => '0xf73f', + 'Gravesmall' => '0xf760', + 'Asmall' => '0xf761', + 'Bsmall' => '0xf762', + 'Csmall' => '0xf763', + 'Dsmall' => '0xf764', + 'Esmall' => '0xf765', + 'Fsmall' => '0xf766', + 'Gsmall' => '0xf767', + 'Hsmall' => '0xf768', + 'Ismall' => '0xf769', + 'Jsmall' => '0xf76a', + 'Ksmall' => '0xf76b', + 'Lsmall' => '0xf76c', + 'Msmall' => '0xf76d', + 'Nsmall' => '0xf76e', + 'Osmall' => '0xf76f', + 'Psmall' => '0xf770', + 'Qsmall' => '0xf771', + 'Rsmall' => '0xf772', + 'Ssmall' => '0xf773', + 'Tsmall' => '0xf774', + 'Usmall' => '0xf775', + 'Vsmall' => '0xf776', + 'Wsmall' => '0xf777', + 'Xsmall' => '0xf778', + 'Ysmall' => '0xf779', + 'Zsmall' => '0xf77a', + 'exclamdownsmall' => '0xf7a1', + 'centoldstyle' => '0xf7a2', + 'Dieresissmall' => '0xf7a8', + 'Macronsmall' => '0xf7af', + 'Acutesmall' => '0xf7b4', + 'Cedillasmall' => '0xf7b8', + 'questiondownsmall' => '0xf7bf', + 'Agravesmall' => '0xf7e0', + 'Aacutesmall' => '0xf7e1', + 'Acircumflexsmall' => '0xf7e2', + 'Atildesmall' => '0xf7e3', + 'Adieresissmall' => '0xf7e4', + 'Aringsmall' => '0xf7e5', + 'AEsmall' => '0xf7e6', + 'Ccedillasmall' => '0xf7e7', + 'Egravesmall' => '0xf7e8', + 'Eacutesmall' => '0xf7e9', + 'Ecircumflexsmall' => '0xf7ea', + 'Edieresissmall' => '0xf7eb', + 'Igravesmall' => '0xf7ec', + 'Iacutesmall' => '0xf7ed', + 'Icircumflexsmall' => '0xf7ee', + 'Idieresissmall' => '0xf7ef', + 'Ethsmall' => '0xf7f0', + 'Ntildesmall' => '0xf7f1', + 'Ogravesmall' => '0xf7f2', + 'Oacutesmall' => '0xf7f3', + 'Ocircumflexsmall' => '0xf7f4', + 'Otildesmall' => '0xf7f5', + 'Odieresissmall' => '0xf7f6', + 'Oslashsmall' => '0xf7f8', + 'Ugravesmall' => '0xf7f9', + 'Uacutesmall' => '0xf7fa', + 'Ucircumflexsmall' => '0xf7fb', + 'Udieresissmall' => '0xf7fc', + 'Yacutesmall' => '0xf7fd', + 'Thornsmall' => '0xf7fe', + 'Ydieresissmall' => '0xf7ff', + 'radicalex' => '0xf8e5', + 'arrowvertex' => '0xf8e6', + 'arrowhorizex' => '0xf8e7', + 'registersans' => '0xf8e8', + 'copyrightsans' => '0xf8e9', + 'trademarksans' => '0xf8ea', + 'parenlefttp' => '0xf8eb', + 'parenleftex' => '0xf8ec', + 'parenleftbt' => '0xf8ed', + 'bracketlefttp' => '0xf8ee', + 'bracketleftex' => '0xf8ef', + 'bracketleftbt' => '0xf8f0', + 'bracelefttp' => '0xf8f1', + 'braceleftmid' => '0xf8f2', + 'braceleftbt' => '0xf8f3', + 'braceex' => '0xf8f4', + 'integralex' => '0xf8f5', + 'parenrighttp' => '0xf8f6', + 'parenrightex' => '0xf8f7', + 'parenrightbt' => '0xf8f8', + 'bracketrighttp' => '0xf8f9', + 'bracketrightex' => '0xf8fa', + 'bracketrightbt' => '0xf8fb', + 'bracerighttp' => '0xf8fc', + 'bracerightmid' => '0xf8fd', + 'bracerightbt' => '0xf8fe', + 'ff' => '0xfb00', + 'fi' => '0xfb01', + 'fl' => '0xfb02', + 'ffi' => '0xfb03', + 'ffl' => '0xfb04', + 'afii57705' => '0xfb1f', + 'afii57694' => '0xfb2a', + 'afii57695' => '0xfb2b', + 'afii57723' => '0xfb35', + 'afii57700' => '0xfb4b', + ]; + } + + public static function getCodePoint($glyph): ?int + { + $glyphsMap = static::getGlyphs(); + + if (isset($glyphsMap[$glyph])) { + return hexdec($glyphsMap[$glyph]); + } + + return null; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php new file mode 100644 index 0000000..01d0a1c --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/StandardEncoding.php @@ -0,0 +1,76 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +// 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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php new file mode 100644 index 0000000..1938f55 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php @@ -0,0 +1,76 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +// 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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Exception/EmptyPdfException.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Exception/EmptyPdfException.php new file mode 100644 index 0000000..9eda9ce --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Exception/EmptyPdfException.php @@ -0,0 +1,12 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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.*?)endcodespacerange/s', $content, $matches)) { + foreach ($matches['sections'] as $section) { + $regexp = '/<(?P[0-9A-F]+)> *<(?P[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.*?)endbfchar/s', $content, $matches)) { + foreach ($matches['sections'] as $section) { + $regexp = '/<(?P[0-9A-F]+)> *<(?P[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.*?)endbfrange/s', $content, $matches)) { + foreach ($matches['sections'] as $section) { + /** + * Regexp to capture , , and either or [...] items. + * - (?P...) Source range's start + * - (?P...) Source range's end + * - (?P...) Destination range's offset or each char code + * Some PDF file has 2-byte Unicode values on new lines > added \r\n + */ + $regexp = '/<(?P[0-9A-F]+)> *<(?P[0-9A-F]+)> *(?P<[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[0-9A-F]+)>$/i', $dest, $offset_matches)) { + // Support for : + $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 : [ ... ] + $strings = []; + $matched = preg_match_all('/<(?P[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, ')/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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php new file mode 100644 index 0000000..310c44c --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType0.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontCIDFontType0 + */ +class FontCIDFontType0 extends Font +{ +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php new file mode 100644 index 0000000..077d6e7 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontCIDFontType2.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontCIDFontType2 + */ +class FontCIDFontType2 extends Font +{ +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php new file mode 100644 index 0000000..8a55c00 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontTrueType.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontTrueType + */ +class FontTrueType extends Font +{ +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType0.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType0.php new file mode 100644 index 0000000..4e5cc6d --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType0.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontType0 + */ +class FontType0 extends Font +{ +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType1.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType1.php new file mode 100644 index 0000000..ee93e69 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType1.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontType1 + */ +class FontType1 extends Font +{ +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType3.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType3.php new file mode 100644 index 0000000..08f8da0 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Font/FontType3.php @@ -0,0 +1,42 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser\Font; + +use Smalot\PdfParser\Font; + +/** + * Class FontType3 + */ +class FontType3 extends Font +{ +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Header.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Header.php new file mode 100644 index 0000000..b58773a --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Header.php @@ -0,0 +1,194 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/PDFObject.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/PDFObject.php new file mode 100644 index 0000000..378ae15 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/PDFObject.php @@ -0,0 +1,1215 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Exception\InvalidDictionaryObjectException; +use Smalot\PdfParser\XObject\Form; +use Smalot\PdfParser\XObject\Image; + +/** + * Class PDFObject + */ +class PDFObject +{ + public const TYPE = 't'; + + public const OPERATOR = 'o'; + + public const COMMAND = 'c'; + + /** + * The recursion stack. + * + * @var array + */ + public static $recursionStack = []; + + /** + * @var Document|null + */ + protected $document; + + /** + * @var Header + */ + protected $header; + + /** + * @var string + */ + protected $content; + + /** + * @var Config|null + */ + protected $config; + + /** + * @var bool + */ + protected $addPositionWhitespace = false; + + public function __construct( + Document $document, + ?Header $header = null, + ?string $content = null, + ?Config $config = null + ) { + $this->document = $document; + $this->header = $header ?? new Header(); + $this->content = $content; + $this->config = $config; + } + + public function init() + { + } + + public function getDocument(): Document + { + return $this->document; + } + + public function getHeader(): ?Header + { + return $this->header; + } + + public function getConfig(): ?Config + { + return $this->config; + } + + /** + * @return Element|PDFObject|Header + */ + public function get(string $name) + { + return $this->header->get($name); + } + + public function has(string $name): bool + { + return $this->header->has($name); + } + + public function getDetails(bool $deep = true): array + { + return $this->header->getDetails($deep); + } + + public function getContent(): ?string + { + return $this->content; + } + + /** + * Creates a duplicate of the document stream with + * strings and other items replaced by $char. Formerly + * getSectionsText() used this output to more easily gather offset + * values to extract text from the *actual* document stream. + * + * @deprecated function is no longer used and will be removed in a future release + * + * @internal + */ + public function cleanContent(string $content, string $char = 'X') + { + $char = $char[0]; + $content = str_replace(['\\\\', '\\)', '\\('], $char.$char, $content); + + // Remove image bloc with binary content + preg_match_all('/\s(BI\s.*?(\sID\s).*?(\sEI))\s/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + // Clean content in square brackets [.....] + preg_match_all('/\[((\(.*?\)|[0-9\.\-\s]*)*)\]/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + // Clean content in round brackets (.....) + preg_match_all('/\((.*?)\)/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + // Clean structure + if ($parts = preg_split('/(<|>)/s', $content, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE)) { + $content = ''; + $level = 0; + foreach ($parts as $part) { + if ('<' == $part) { + ++$level; + } + + $content .= (0 == $level ? $part : str_repeat($char, \strlen($part))); + + if ('>' == $part) { + --$level; + } + } + } + + // Clean BDC and EMC markup + preg_match_all( + '/(\/[A-Za-z0-9\_]*\s*'.preg_quote($char).'*BDC)/s', + $content, + $matches, + \PREG_OFFSET_CAPTURE + ); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + preg_match_all('/\s(EMC)\s/s', $content, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[1] as $part) { + $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0])); + } + + return $content; + } + + /** + * Takes a string of PDF document stream text and formats + * it into a multi-line string with one PDF command on each line, + * separated by \r\n. If the given string is null, or binary data + * is detected instead of a document stream then return an empty + * string. + */ + private function formatContent(?string $content): string + { + if (null === $content) { + return ''; + } + + // Outside of (String) and inline image content in PDF document + // streams, all text should conform to UTF-8. Test for binary + // content by deleting everything after the first open- + // parenthesis ( which indicates the beginning of a string, or + // the first ID command which indicates the beginning of binary + // inline image content. Then test what remains for valid + // UTF-8. If it's not UTF-8, return an empty string as this + // $content is most likely binary. Unfortunately, using + // mb_check_encoding(..., 'UTF-8') is not strict enough, so the + // following regexp, adapted from the W3, is used. See: + // https://www.w3.org/International/questions/qa-forms-utf-8.en + // We use preg_replace() instead of preg_match() to avoid "JIT + // stack limit exhausted" errors on larger files. + $utf8Filter = preg_replace('/( + [\x09\x0A\x0D\x20-\x7E] | # ASCII + [\xC2-\xDF][\x80-\xBF] | # non-overlong 2-byte + \xE0[\xA0-\xBF][\x80-\xBF] | # excluding overlongs + [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} | # straight 3-byte + \xED[\x80-\x9F][\x80-\xBF] | # excluding surrogates + \xF0[\x90-\xBF][\x80-\xBF]{2} | # planes 1-3 + [\xF1-\xF3][\x80-\xBF]{3} | # planes 4-15 + \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 + )/xs', '', preg_replace('/(\(|ID\s).*$/s', '', $content)); + + if ('' !== $utf8Filter) { + return ''; + } + + // Find all inline image content and replace them so they aren't + // affected by the next steps + $pdfInlineImages = []; + $offsetBI = 0; + while (preg_match('/\sBI\s(\/.+?)\sID\s(.+?)\sEI(?=\s|$)/s', $content, $text, \PREG_OFFSET_CAPTURE, $offsetBI)) { + // Attempt to detemine if this instance of the 'BI' command + // actually occured within a (string) using the following + // steps: + + // Step 1: Remove any escaped slashes and parentheses from + // the alleged image characteristics data + $para = str_replace(['\\\\', '\\(', '\\)'], '', $text[1][0]); + + // Step 2: Remove all correctly ordered and balanced + // parentheses from (strings) + do { + $paraTest = $para; + $para = preg_replace('/\(([^()]*)\)/', '$1', $paraTest); + } while ($para != $paraTest); + + $paraOpen = strpos($para, '('); + $paraClose = strpos($para, ')'); + + // Check: If the remaining text contains a close parenthesis + // ')' AND it occurs before any open parenthesis, then we + // are almost certain to be inside a (string) + if (0 < $paraClose && (false === $paraOpen || $paraClose < $paraOpen)) { + // Bump the search offset forward and match again + $offsetBI = (int) $text[1][1]; + continue; + } + + // Step 3: Double check that this is actually inline image + // data by parsing the alleged image characteristics as a + // dictionary + $dict = $this->parseDictionary('<<'.$text[1][0].'>>'); + + // Check if an image Width and Height are set in the dict + if ((isset($dict['W']) || isset($dict['Width'])) + && (isset($dict['H']) || isset($dict['Height']))) { + $id = uniqid('IMAGE_', true); + $pdfInlineImages[$id] = [ + preg_replace(['/\r\n/', '/\r/', '/\n/'], ' ', $text[1][0]), + preg_replace(['/\r\n/', '/\r/', '/\n/'], '', $text[2][0]), + ]; + $content = preg_replace( + '/'.preg_quote($text[0][0], '/').'/', + '^^^'.$id.'^^^', + $content, + 1 + ); + } else { + // If there was no valid dictionary, or a height and width + // weren't specified, then we don't know what this is, so + // just leave it alone; bump the search offset forward and + // match again + $offsetBI = (int) $text[1][1]; + } + } + + // Find all strings () and replace them so they aren't affected + // by the next steps + $pdfstrings = []; + $attempt = '('; + while (preg_match('/'.preg_quote($attempt, '/').'.*?\)/s', $content, $text)) { + // Remove all escaped slashes and parentheses from the target text + $para = str_replace(['\\\\', '\\(', '\\)'], '', $text[0]); + + // PDF strings can contain unescaped parentheses as long as + // they're balanced, so check for balanced parentheses + $left = preg_match_all('/\(/', $para); + $right = preg_match_all('/\)/', $para); + + if (')' == $para[-1] && $left == $right) { + // Replace the string with a unique placeholder + $id = uniqid('STRING_', true); + $pdfstrings[$id] = $text[0]; + $content = preg_replace( + '/'.preg_quote($text[0], '/').'/', + '@@@'.$id.'@@@', + $content, + 1 + ); + + // Reset to search for the next string + $attempt = '('; + } else { + // We had unbalanced parentheses, so use the current + // match as a base to find a longer string + $attempt = $text[0]; + } + } + + // Remove all carriage returns and line-feeds from the document stream + $content = str_replace(["\r", "\n"], ' ', trim($content)); + + // Find all dictionary << >> commands and replace them so they + // aren't affected by the next steps + $dictstore = []; + while (preg_match('/(<<.*?>> *)(BDC|BMC|DP|MP)/s', $content, $dicttext)) { + $dictid = uniqid('DICT_', true); + $dictstore[$dictid] = $dicttext[1]; + $content = preg_replace( + '/'.preg_quote($dicttext[0], '/').'/', + ' ###'.$dictid.'###'.$dicttext[2], + $content, + 1 + ); + } + + // Normalize white-space in the document stream + $content = preg_replace('/\s{2,}/', ' ', $content); + + // Find all valid PDF operators and add \r\n after each; this + // ensures there is just one command on every line + // Source: https://ia801001.us.archive.org/1/items/pdf1.7/pdf_reference_1-7.pdf - Appendix A + // Source: https://archive.org/download/pdf320002008/PDF32000_2008.pdf - Annex A + // Note: PDF Reference 1.7 lists 'I' and 'rI' as valid commands, while + // PDF 32000:2008 lists them as 'i' and 'ri' respectively. Both versions + // appear here in the list for completeness. + $operators = [ + 'b*', 'b', 'BDC', 'BMC', 'B*', 'BI', 'BT', 'BX', 'B', 'cm', 'cs', 'c', 'CS', + 'd0', 'd1', 'd', 'Do', 'DP', 'EMC', 'EI', 'ET', 'EX', 'f*', 'f', 'F', 'gs', + 'g', 'G', 'h', 'i', 'ID', 'I', 'j', 'J', 'k', 'K', 'l', 'm', 'MP', 'M', 'n', + 'q', 'Q', 're', 'rg', 'ri', 'rI', 'RG', 'scn', 'sc', 'sh', 's', 'SCN', 'SC', + 'S', 'T*', 'Tc', 'Td', 'TD', 'Tf', 'TJ', 'Tj', 'TL', 'Tm', 'Tr', 'Ts', 'Tw', + 'Tz', 'v', 'w', 'W*', 'W', 'y', '\'', '"', + ]; + foreach ($operators as $operator) { + $content = preg_replace( + '/(?> commands + $dictstore = array_reverse($dictstore, true); + foreach ($dictstore as $id => $dict) { + $content = str_replace('###'.$id.'###', $dict, $content); + } + + // Restore the original string content + $pdfstrings = array_reverse($pdfstrings, true); + foreach ($pdfstrings as $id => $text) { + // Strings may contain escaped newlines, or literal newlines + // and we should clean these up before replacing the string + // back into the content stream; this ensures no strings are + // split between two lines (every command must be on one line) + $text = str_replace( + ["\\\r\n", "\\\r", "\\\n", "\r", "\n"], + ['', '', '', '\r', '\n'], + $text + ); + + $content = str_replace('@@@'.$id.'@@@', $text, $content); + } + + // Restore the original content of any inline images + $pdfInlineImages = array_reverse($pdfInlineImages, true); + foreach ($pdfInlineImages as $id => $image) { + $content = str_replace( + '^^^'.$id.'^^^', + "\r\nBI\r\n".$image[0]." ID\r\n".$image[1]." EI\r\n", + $content + ); + } + + $content = trim(preg_replace(['/(\r\n){2,}/', '/\r\n +/'], "\r\n", $content)); + + return $content; + } + + /** + * getSectionsText() now takes an entire, unformatted + * document stream as a string, cleans it, then filters out + * commands that aren't needed for text positioning/extraction. It + * returns an array of unprocessed PDF commands, one command per + * element. + * + * @internal + */ + public function getSectionsText(?string $content): array + { + $sections = []; + + // A cleaned stream has one command on every line, so split the + // cleaned stream content on \r\n into an array + $textCleaned = preg_split( + '/(\r\n|\n|\r)/', + $this->formatContent($content), + -1, + \PREG_SPLIT_NO_EMPTY + ); + + $inTextBlock = false; + foreach ($textCleaned as $line) { + $line = trim($line); + + // Skip empty lines + if ('' === $line) { + continue; + } + + // If a 'BT' is encountered, set the $inTextBlock flag + if (preg_match('/BT$/', $line)) { + $inTextBlock = true; + $sections[] = $line; + + // If an 'ET' is encountered, unset the $inTextBlock flag + } elseif ('ET' == $line) { + $inTextBlock = false; + $sections[] = $line; + } elseif ($inTextBlock) { + // If we are inside a BT ... ET text block, save all lines + $sections[] = trim($line); + } else { + // Otherwise, if we are outside of a text block, only + // save specific, necessary lines. Care should be taken + // to ensure a command being checked for *only* matches + // that command. For instance, a simple search for 'c' + // may also match the 'sc' command. See the command + // list in the formatContent() method above. + // Add more commands to save here as you find them in + // weird PDFs! + if ('q' == $line[-1] || 'Q' == $line[-1]) { + // Save and restore graphics state commands + $sections[] = $line; + } elseif (preg_match('/(?getFonts(); + } + + $firstFont = $this->document->getFirstFont(); + if (null !== $firstFont) { + $fonts[] = $firstFont; + } + + if (\count($fonts) > 0) { + return reset($fonts); + } + + return new Font($this->document, null, null, $this->config); + } + + /** + * Decode a '[]TJ' command and attempt to use alternate + * fonts if the current font results in output that contains + * Unicode control characters. + * + * @internal + * + * @param array> $command + */ + private function getTJUsingFontFallback(Font $font, array $command, ?Page $page = null, float $fontFactor = 4): string + { + $orig_text = $font->decodeText($command, $fontFactor); + $text = $orig_text; + + // If we make this a Config option, we can add a check if it's + // enabled here. + if (null !== $page) { + $font_ids = array_keys($page->getFonts()); + + // If the decoded text contains UTF-8 control characters + // then the font page being used is probably the wrong one. + // Loop through the rest of the fonts to see if we can get + // a good decode. Allow x09 to x0d which are whitespace. + while (preg_match('/[\x00-\x08\x0e-\x1f\x7f]/u', $text) || false !== strpos(bin2hex($text), '00')) { + // If we're out of font IDs, then give up and use the + // original string + if (0 == \count($font_ids)) { + return $orig_text; + } + + // Try the next font ID + $font = $page->getFont(array_shift($font_ids)); + $text = $font->decodeText($command, $fontFactor); + } + } + + return $text; + } + + /** + * Expects a string that is a full PDF dictionary object, + * including the outer enclosing << >> angle brackets + * + * @internal + * + * @throws InvalidDictionaryObjectException + */ + public function parseDictionary(string $dictionary): array + { + // Normalize whitespace + $dictionary = preg_replace(['/\r/', '/\n/', '/\s{2,}/'], ' ', trim($dictionary)); + + if ('<<' != substr($dictionary, 0, 2)) { + throw new InvalidDictionaryObjectException('Not a valid dictionary object.'); + } + + $parsed = []; + $stack = []; + $currentName = ''; + $arrayTypeNumeric = false; + + // Remove outer layer of dictionary, and split on tokens + $split = preg_split( + '/(<<|>>|\[|\]|\/[^\s\/\[\]\(\)<>]*)/', + trim(preg_replace('/^<<|>>$/', '', $dictionary)), + -1, + \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE + ); + + foreach ($split as $token) { + $token = trim($token); + switch ($token) { + case '': + break; + + // Open numeric array + case '[': + $parsed[$currentName] = []; + $arrayTypeNumeric = true; + + // Move up one level in the stack + $stack[\count($stack)] = &$parsed; + $parsed = &$parsed[$currentName]; + $currentName = ''; + break; + + // Open hashed array + case '<<': + $parsed[$currentName] = []; + $arrayTypeNumeric = false; + + // Move up one level in the stack + $stack[\count($stack)] = &$parsed; + $parsed = &$parsed[$currentName]; + $currentName = ''; + break; + + // Close numeric array + case ']': + // Revert string type arrays back to a single element + if (\is_array($parsed) && 1 == \count($parsed) + && isset($parsed[0]) && \is_string($parsed[0]) + && '' !== $parsed[0] && '/' != $parsed[0][0]) { + $parsed = '['.$parsed[0].']'; + } + // Close hashed array + // no break + case '>>': + $arrayTypeNumeric = false; + + // Move down one level in the stack + $parsed = &$stack[\count($stack) - 1]; + unset($stack[\count($stack) - 1]); + break; + + default: + // If value begins with a slash, then this is a name + // Add it to the appropriate array + if ('/' == substr($token, 0, 1)) { + $currentName = substr($token, 1); + if (true == $arrayTypeNumeric) { + $parsed[] = $currentName; + $currentName = ''; + } + } elseif ('' != $currentName) { + if (false == $arrayTypeNumeric) { + $parsed[$currentName] = $token; + } + $currentName = ''; + } elseif ('' == $currentName) { + $parsed[] = $token; + } + } + } + + return $parsed; + } + + /** + * Returns the text content of a PDF as a string. Attempts to add + * whitespace for spacing and line-breaks where appropriate. + * + * getText() leverages getTextArray() to get the content + * of the document, setting the addPositionWhitespace flag to true + * so whitespace is inserted in a logical way for reading by + * humans. + */ + public function getText(?Page $page = null): string + { + $this->addPositionWhitespace = true; + $result = $this->getTextArray($page); + $this->addPositionWhitespace = false; + + return implode('', $result).' '; + } + + /** + * Returns the text content of a PDF as an array of strings. No + * extra whitespace is inserted besides what is actually encoded in + * the PDF text. + * + * @throws \Exception + */ + public function getTextArray(?Page $page = null): array + { + $result = []; + $text = []; + + $marked_stack = []; + $last_written_position = false; + + $sections = $this->getSectionsText($this->content); + $current_font = $this->getDefaultFont($page); + $current_font_size = 1; + $current_text_leading = 0; + + $current_position = ['x' => false, 'y' => false]; + $current_position_tm = [ + 'a' => 1, 'b' => 0, 'c' => 0, + 'i' => 0, 'j' => 1, 'k' => 0, + 'x' => 0, 'y' => 0, 'z' => 1, + ]; + $current_position_td = ['x' => 0, 'y' => 0]; + $current_position_cm = [ + 'a' => 1, 'b' => 0, 'c' => 0, + 'i' => 0, 'j' => 1, 'k' => 0, + 'x' => 0, 'y' => 0, 'z' => 1, + ]; + + $clipped_font = []; + $clipped_position_cm = []; + + self::$recursionStack[] = $this->getUniqueId(); + + foreach ($sections as $section) { + $commands = $this->getCommandsText($section); + foreach ($commands as $command) { + switch ($command[self::OPERATOR]) { + // Begin text object + case 'BT': + // Reset text positioning matrices + $current_position_tm = [ + 'a' => 1, 'b' => 0, 'c' => 0, + 'i' => 0, 'j' => 1, 'k' => 0, + 'x' => 0, 'y' => 0, 'z' => 1, + ]; + $current_position_td = ['x' => 0, 'y' => 0]; + $current_text_leading = 0; + break; + + // Begin marked content sequence with property list + case 'BDC': + if (preg_match('/(<<.*>>)$/', $command[self::COMMAND], $match)) { + $dict = $this->parseDictionary($match[1]); + + // Check for ActualText block + if (isset($dict['ActualText']) && \is_string($dict['ActualText']) && '' !== $dict['ActualText']) { + if ('[' == $dict['ActualText'][0]) { + // Simulate a 'TJ' command on the stack + $marked_stack[] = [ + 'ActualText' => $this->getCommandsText($dict['ActualText'].'TJ')[0], + ]; + } elseif ('<' == $dict['ActualText'][0] || '(' == $dict['ActualText'][0]) { + // Simulate a 'Tj' command on the stack + $marked_stack[] = [ + 'ActualText' => $this->getCommandsText($dict['ActualText'].'Tj')[0], + ]; + } + } + } + break; + + // Begin marked content sequence + case 'BMC': + if ('ReversedChars' == $command[self::COMMAND]) { + // Upon encountering a ReversedChars command, + // add the characters we've built up so far to + // the result array + $result = array_merge($result, $text); + + // Start a fresh $text array that will contain + // reversed characters + $text = []; + + // Add the reversed text flag to the stack + $marked_stack[] = ['ReversedChars' => true]; + } + break; + + // set graphics position matrix + case 'cm': + $args = preg_split('/\s+/s', $command[self::COMMAND]); + $current_position_cm = [ + 'a' => (float) $args[0], 'b' => (float) $args[1], 'c' => 0, + 'i' => (float) $args[2], 'j' => (float) $args[3], 'k' => 0, + 'x' => (float) $args[4], 'y' => (float) $args[5], 'z' => 1, + ]; + break; + + case 'Do': + if (is_null($page)) { + break; + } + + $args = preg_split('/\s/s', $command[self::COMMAND]); + $id = trim(array_pop($args), '/ '); + $xobject = $page->getXObject($id); + + // Check we got a PDFObject back. + if (!$xobject instanceof self) { + break; + } + + // If the PDFObject is an Image, do nothing as images + // aren't text. + if ($xobject instanceof Image) { + break; + } + + // Check this is not a circular reference. + if (\in_array($xobject->getUniqueId(), self::$recursionStack, true)) { + break; + } + + $objectText = $xobject->getText($page); + + // If the PDFObject is a Form and doesn't have any text, + // skip it. + if ($xobject instanceof Form && $objectText === ' ') { + break; + } + + $text[] = $objectText; + break; + + // Marked content point with (DP) & without (MP) property list + case 'DP': + case 'MP': + break; + + // End text object + case 'ET': + break; + + // Store current selected font and graphics matrix + case 'q': + $clipped_font[] = [$current_font, $current_font_size]; + $clipped_position_cm[] = $current_position_cm; + break; + + // Restore previous selected font and graphics matrix + case 'Q': + list($current_font, $current_font_size) = array_pop($clipped_font); + $current_position_cm = array_pop($clipped_position_cm); + break; + + // End marked content sequence + case 'EMC': + $data = false; + if (\count($marked_stack)) { + $marked = array_pop($marked_stack); + $action = key($marked); + $data = $marked[$action]; + + switch ($action) { + // If we are in ReversedChars mode... + case 'ReversedChars': + // Reverse the characters we've built up so far + foreach ($text as $key => $t) { + $text[$key] = implode('', array_reverse( + mb_str_split($t, 1, mb_internal_encoding()) + )); + } + + // Add these characters to the result array + $result = array_merge($result, $text); + + // Start a fresh $text array that will contain + // non-reversed characters + $text = []; + break; + + case 'ActualText': + // Use the content of the ActualText as a command + $command = $data; + break; + } + } + + // If this EMC command has been transformed into a 'Tj' + // or 'TJ' command because of being ActualText, then bypass + // the break to proceed to the writing section below. + if ('Tj' != $command[self::OPERATOR] && 'TJ' != $command[self::OPERATOR]) { + break; + } + + // no break + case "'": + case '"': + if ("'" == $command[self::OPERATOR] || '"' == $command[self::OPERATOR]) { + // Move to next line and write text + $current_position['x'] = 0; + $current_position_td['x'] = 0; + $current_position_td['y'] += $current_text_leading; + } + // no break + case 'Tj': + $command[self::COMMAND] = [$command]; + // no break + case 'TJ': + // Check the marked content stack for flags + $actual_text = false; + $reverse_text = false; + foreach ($marked_stack as $marked) { + if (isset($marked['ActualText'])) { + $actual_text = true; + } + if (isset($marked['ReversedChars'])) { + $reverse_text = true; + } + } + + // Account for text position ONLY just before we write text + if (false === $actual_text && \is_array($last_written_position)) { + // If $last_written_position is an array, that + // means we have stored text position coordinates + // for placing an ActualText + $currentX = $last_written_position[0]; + $currentY = $last_written_position[1]; + $last_written_position = false; + } else { + $currentX = $current_position_cm['x'] + $current_position_tm['x'] + $current_position_td['x']; + $currentY = $current_position_cm['y'] + $current_position_tm['y'] + $current_position_td['y']; + } + $whiteSpace = ''; + + $factorX = -$current_font_size * $current_position_tm['a'] - $current_font_size * $current_position_tm['i']; + $factorY = $current_font_size * $current_position_tm['b'] + $current_font_size * $current_position_tm['j']; + + if (true === $this->addPositionWhitespace && false !== $current_position['x']) { + $curY = $currentY - $current_position['y']; + if (abs($curY) >= abs($factorY) / 4) { + $whiteSpace = "\n"; + } else { + if (true === $reverse_text) { + $curX = $current_position['x'] - $currentX; + } else { + $curX = $currentX - $current_position['x']; + } + + // In abs($factorX * 7) below, the 7 is chosen arbitrarily + // as the number of apparent "spaces" in a document we + // would need before considering them a "tab". In the + // future, we might offer this value to users as a config + // option. + if ($curX >= abs($factorX * 7)) { + $whiteSpace = "\t"; + } elseif ($curX >= abs($factorX * 2)) { + $whiteSpace = ' '; + } + } + } + + $newtext = $this->getTJUsingFontFallback( + $current_font, + $command[self::COMMAND], + $page, + $factorX + ); + + // If there is no ActualText pending then write + if (false === $actual_text) { + $newtext = str_replace(["\r", "\n"], '', $newtext); + if (false !== $reverse_text) { + // If we are in ReversedChars mode, add the whitespace last + $text[] = preg_replace('/ $/', ' ', $newtext.$whiteSpace); + } else { + // Otherwise add the whitespace first + if (' ' === $whiteSpace && isset($text[\count($text) - 1])) { + $text[\count($text) - 1] = preg_replace('/ $/', '', $text[\count($text) - 1]); + } + $text[] = preg_replace('/^[ \t]{2}/', ' ', $whiteSpace.$newtext); + } + + // Record the position of this inserted text for comparison + // with the next text block. + // Provide a 'fudge' factor guess on how wide this text block + // is based on the number of characters. This helps limit the + // number of tabs inserted, but isn't perfect. + $factor = $factorX / 2; + $current_position = [ + 'x' => $currentX - mb_strlen($newtext) * $factor, + 'y' => $currentY, + ]; + } elseif (false === $last_written_position) { + // If there is an ActualText in the pipeline + // store the position this undisplayed text + // *would* have been written to, so the + // ActualText is displayed in the right spot + $last_written_position = [$currentX, $currentY]; + $current_position['x'] = $currentX; + } + break; + + // move to start of next line + case 'T*': + $current_position['x'] = 0; + $current_position_td['x'] = 0; + $current_position_td['y'] += $current_text_leading; + break; + + // set character spacing + case 'Tc': + break; + + // move text current point and set leading + case 'Td': + case 'TD': + // move text current point + $args = preg_split('/\s+/s', $command[self::COMMAND]); + $y = (float) array_pop($args); + $x = (float) array_pop($args); + + if ('TD' == $command[self::OPERATOR]) { + $current_text_leading = -$y * $current_position_tm['b'] - $y * $current_position_tm['j']; + } + + $current_position_td = [ + 'x' => $current_position_td['x'] + $x * $current_position_tm['a'] + $x * $current_position_tm['i'], + 'y' => $current_position_td['y'] + $y * $current_position_tm['b'] + $y * $current_position_tm['j'], + ]; + break; + + case 'Tf': + $args = preg_split('/\s/s', $command[self::COMMAND]); + $size = (float) array_pop($args); + $id = trim(array_pop($args), '/'); + if (null !== $page) { + $new_font = $page->getFont($id); + // If an invalid font ID is given, do not update the font. + // This should theoretically never happen, as the PDF spec states for the Tf operator: + // "The specified font value shall match a resource name in the Font entry of the default resource dictionary" + // (https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf, page 435) + // But we want to make sure that malformed PDFs do not simply crash. + if (null !== $new_font) { + $current_font = $new_font; + $current_font_size = $size; + } + } + break; + + // set leading + case 'TL': + $y = (float) $command[self::COMMAND]; + $current_text_leading = -$y * $current_position_tm['b'] + -$y * $current_position_tm['j']; + break; + + // set text position matrix + case 'Tm': + $args = preg_split('/\s+/s', $command[self::COMMAND]); + $current_position_tm = [ + 'a' => (float) $args[0], 'b' => (float) $args[1], 'c' => 0, + 'i' => (float) $args[2], 'j' => (float) $args[3], 'k' => 0, + 'x' => (float) $args[4], 'y' => (float) $args[5], 'z' => 1, + ]; + break; + + // set text rendering mode + case 'Ts': + break; + + // set super/subscripting text rise + case 'Ts': + break; + + // set word spacing + case 'Tw': + break; + + // set horizontal scaling + case 'Tz': + break; + + default: + } + } + } + + $result = array_merge($result, $text); + + return $result; + } + + /** + * getCommandsText() expects the content of $text_part to be an + * already formatted, single-line command from a document stream. + * The companion function getSectionsText() returns a document + * stream as an array of single commands for just this purpose. + * Because of this, the argument $offset is no longer used, and + * may be removed in a future PdfParser release. + * + * A better name for this function would be getCommandText() + * since it now always works on just one command. + */ + public function getCommandsText(string $text_part, int &$offset = 0): array + { + $commands = $matches = []; + + preg_match('/^(([\/\[\(<])?.*)(? '(', + self::OPERATOR => 'TJ', + self::COMMAND => $tjmatch[1], + ]; + if (isset($tjmatch[2]) && trim($tjmatch[2])) { + $subcommand[] = [ + self::TYPE => 'n', + self::OPERATOR => '', + self::COMMAND => $tjmatch[2], + ]; + } + $command = substr($command, \strlen($tjmatch[0])); + } + + // Search for hexadecimal <> format + if (preg_match('/^ *<([0-9a-f\s]*)> *(-?[\d.]+)?/i', $command, $tjmatch)) { + $tjmatch[1] = preg_replace('/\s/', '', $tjmatch[1]); + $subcommand[] = [ + self::TYPE => '<', + self::OPERATOR => 'TJ', + self::COMMAND => $tjmatch[1], + ]; + if (isset($tjmatch[2]) && trim($tjmatch[2])) { + $subcommand[] = [ + self::TYPE => 'n', + self::OPERATOR => '', + self::COMMAND => $tjmatch[2], + ]; + } + $command = substr($command, \strlen($tjmatch[0])); + } + } while ($command != $oldCommand); + + $command = $subcommand; + } elseif ('Tj' == $operator || "'" == $operator || '"' == $operator) { + // Depending on the string type, trim the data of the + // appropriate delimiters + if ('(' == $type) { + // Don't use trim() here since a () string may end with + // a balanced or escaped right parentheses, and trim() + // will delete both. Both strings below are valid: + // eg. (String()) + // eg. (String\)) + $command = preg_replace('/^\(|\)$/', '', $command); + } elseif ('<' == $type) { + $command = trim($command, '<>'); + } + } elseif ('/' == $type) { + $command = substr($command, 1); + } + + $commands[] = [ + self::TYPE => $type, + self::OPERATOR => $operator, + self::COMMAND => $command, + ]; + + return $commands; + } + + public static function factory( + Document $document, + Header $header, + ?string $content, + ?Config $config = null + ): self { + switch ($header->get('Type')->getContent()) { + case 'XObject': + switch ($header->get('Subtype')->getContent()) { + case 'Image': + return new Image($document, $header, $config->getRetainImageContent() ? $content : null, $config); + + case 'Form': + return new Form($document, $header, $content, $config); + } + + return new self($document, $header, $content, $config); + + case 'Pages': + return new Pages($document, $header, $content, $config); + + case 'Page': + return new Page($document, $header, $content, $config); + + case 'Encoding': + return new Encoding($document, $header, $content, $config); + + case 'Font': + $subtype = $header->get('Subtype')->getContent(); + $classname = '\Smalot\PdfParser\Font\Font'.$subtype; + + if (class_exists($classname)) { + return new $classname($document, $header, $content, $config); + } + + return new Font($document, $header, $content, $config); + + default: + return new self($document, $header, $content, $config); + } + } + + /** + * Returns unique id identifying the object. + */ + protected function getUniqueId(): string + { + return spl_object_hash($this); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Page.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Page.php new file mode 100644 index 0000000..1bd29e1 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Page.php @@ -0,0 +1,1014 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +namespace Smalot\PdfParser; + +use Smalot\PdfParser\Element\ElementArray; +use Smalot\PdfParser\Element\ElementMissing; +use Smalot\PdfParser\Element\ElementNull; +use Smalot\PdfParser\Element\ElementXRef; + +class Page extends PDFObject +{ + /** + * @var Font[] + */ + protected $fonts; + + /** + * @var PDFObject[] + */ + protected $xobjects; + + /** + * @var array + */ + protected $dataTm; + + /** + * @param array<\Smalot\PdfParser\Font> $fonts + * + * @internal + */ + public function setFonts($fonts) + { + if (empty($this->fonts)) { + $this->fonts = $fonts; + } + } + + /** + * @return Font[] + */ + public function getFonts() + { + if (null !== $this->fonts) { + return $this->fonts; + } + + $resources = $this->get('Resources'); + + if (method_exists($resources, 'has') && $resources->has('Font')) { + if ($resources->get('Font') instanceof 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; + } + } + } + + return $this->fonts = $table; + } + + return []; + } + + public function getFont(string $id): ?Font + { + $fonts = $this->getFonts(); + + if (isset($fonts[$id])) { + return $fonts[$id]; + } + + // According to the PDF specs (https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf, page 238) + // "The font resource name presented to the Tf operator is arbitrary, as are the names for all kinds of resources" + // Instead, we search for the unfiltered name first and then do this cleaning as a fallback, so all tests still pass. + + if (isset($fonts[$id])) { + return $fonts[$id]; + } else { + $id = preg_replace('/[^0-9\.\-_]/', '', $id); + if (isset($fonts[$id])) { + return $fonts[$id]; + } + } + + return null; + } + + /** + * Support for XObject + * + * @return PDFObject[] + */ + public function getXObjects() + { + if (null !== $this->xobjects) { + return $this->xobjects; + } + + $resources = $this->get('Resources'); + + if (method_exists($resources, 'has') && $resources->has('XObject')) { + if ($resources->get('XObject') instanceof Header) { + $xobjects = $resources->get('XObject')->getElements(); + } else { + $xobjects = $resources->get('XObject')->getHeader()->getElements(); + } + + $table = []; + + foreach ($xobjects as $id => $xobject) { + $table[$id] = $xobject; + + // Store too on cleaned id value (only numeric) + $id = preg_replace('/[^0-9\.\-_]/', '', $id); + if ('' != $id) { + $table[$id] = $xobject; + } + } + + return $this->xobjects = $table; + } + + return []; + } + + public function getXObject(string $id): ?PDFObject + { + $xobjects = $this->getXObjects(); + + if (isset($xobjects[$id])) { + return $xobjects[$id]; + } + + return null; + /*$id = preg_replace('/[^0-9\.\-_]/', '', $id); + + if (isset($xobjects[$id])) { + return $xobjects[$id]; + } else { + return null; + }*/ + } + + public function getText(?self $page = null): string + { + if ($contents = $this->get('Contents')) { + if ($contents instanceof ElementMissing) { + return ''; + } elseif ($contents instanceof ElementNull) { + return ''; + } elseif ($contents instanceof PDFObject) { + $elements = $contents->getHeader()->getElements(); + + if (is_numeric(key($elements))) { + $new_content = ''; + + foreach ($elements as $element) { + if ($element instanceof ElementXRef) { + $new_content .= $element->getObject()->getContent(); + } else { + $new_content .= $element->getContent(); + } + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } + } elseif ($contents instanceof ElementArray) { + // Create a virtual global content. + $new_content = ''; + + foreach ($contents->getContent() as $content) { + $new_content .= $content->getContent()."\n"; + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } + + /* + * Elements referencing each other on the same page can cause endless loops during text parsing. + * To combat this we keep a recursionStack containing already parsed elements on the page. + * The stack is only emptied here after getting text from a page. + */ + $contentsText = $contents->getText($this); + PDFObject::$recursionStack = []; + + return $contentsText; + } + + return ''; + } + + /** + * Return true if the current page is a (setasign\Fpdi\Fpdi) FPDI/FPDF document + * + * The metadata 'Producer' should have the value of "FPDF" . FPDF_VERSION if the + * pdf file was generated by FPDF/Fpfi. + * + * @return bool true is the current page is a FPDI/FPDF document + */ + public function isFpdf(): bool + { + if (\array_key_exists('Producer', $this->document->getDetails()) + && \is_string($this->document->getDetails()['Producer']) + && 0 === strncmp($this->document->getDetails()['Producer'], 'FPDF', 4)) { + return true; + } + + return false; + } + + /** + * Return the page number of the PDF document of the page object + * + * @return int the page number + */ + public function getPageNumber(): int + { + $pages = $this->document->getPages(); + $numOfPages = \count($pages); + for ($pageNum = 0; $pageNum < $numOfPages; ++$pageNum) { + if ($pages[$pageNum] === $this) { + break; + } + } + + return $pageNum; + } + + /** + * Return the Object of the page if the document is a FPDF/FPDI document + * + * If the document was generated by FPDF/FPDI it returns the + * PDFObject of the given page + * + * @return PDFObject The PDFObject for the page + */ + public function getPDFObjectForFpdf(): PDFObject + { + $pageNum = $this->getPageNumber(); + $xObjects = $this->getXObjects(); + + return $xObjects[$pageNum]; + } + + /** + * Return a new PDFObject of the document created with FPDF/FPDI + * + * For a document generated by FPDF/FPDI, it generates a + * new PDFObject for that document + * + * @return PDFObject The PDFObject + */ + public function createPDFObjectForFpdf(): PDFObject + { + $pdfObject = $this->getPDFObjectForFpdf(); + $new_content = $pdfObject->getContent(); + $header = $pdfObject->getHeader(); + $config = $pdfObject->config; + + return new PDFObject($pdfObject->document, $header, $new_content, $config); + } + + /** + * Return page if document is a FPDF/FPDI document + * + * @return Page The page + */ + public function createPageForFpdf(): self + { + $pdfObject = $this->getPDFObjectForFpdf(); + $new_content = $pdfObject->getContent(); + $header = $pdfObject->getHeader(); + $config = $pdfObject->config; + + return new self($pdfObject->document, $header, $new_content, $config); + } + + public function getTextArray(?self $page = null): array + { + if ($this->isFpdf()) { + $pdfObject = $this->getPDFObjectForFpdf(); + $newPdfObject = $this->createPDFObjectForFpdf(); + + return $newPdfObject->getTextArray($pdfObject); + } else { + if ($contents = $this->get('Contents')) { + if ($contents instanceof ElementMissing) { + return []; + } elseif ($contents instanceof ElementNull) { + return []; + } elseif ($contents instanceof PDFObject) { + $elements = $contents->getHeader()->getElements(); + + if (is_numeric(key($elements))) { + $new_content = ''; + + /** @var PDFObject $element */ + foreach ($elements as $element) { + if ($element instanceof ElementXRef) { + $new_content .= $element->getObject()->getContent(); + } else { + $new_content .= $element->getContent(); + } + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } else { + try { + $contents->getTextArray($this); + } catch (\Throwable $e) { + return $contents->getTextArray(); + } + } + } elseif ($contents instanceof ElementArray) { + // Create a virtual global content. + $new_content = ''; + + /** @var PDFObject $content */ + foreach ($contents->getContent() as $content) { + $new_content .= $content->getContent()."\n"; + } + + $header = new Header([], $this->document); + $contents = new PDFObject($this->document, $header, $new_content, $this->config); + } + + return $contents->getTextArray($this); + } + + return []; + } + } + + /** + * Gets all the text data with its internal representation of the page. + * + * Returns an array with the data and the internal representation + */ + public function extractRawData(): array + { + /* + * Now you can get the complete content of the object with the text on it + */ + $extractedData = []; + $content = $this->get('Contents'); + $values = $content->getContent(); + if (isset($values) && \is_array($values)) { + $text = ''; + foreach ($values as $section) { + $text .= $section->getContent(); + } + $sectionsText = $this->getSectionsText($text); + foreach ($sectionsText as $sectionText) { + $commandsText = $this->getCommandsText($sectionText); + foreach ($commandsText as $command) { + $extractedData[] = $command; + } + } + } else { + if ($this->isFpdf()) { + $content = $this->getPDFObjectForFpdf(); + } + $sectionsText = $content->getSectionsText($content->getContent()); + foreach ($sectionsText as $sectionText) { + $commandsText = $content->getCommandsText($sectionText); + foreach ($commandsText as $command) { + $extractedData[] = $command; + } + } + } + + return $extractedData; + } + + /** + * Gets all the decoded text data with it internal representation from a page. + * + * @param array $extractedRawData the extracted data return by extractRawData or + * null if extractRawData should be called + * + * @return array An array with the data and the internal representation + */ + public function extractDecodedRawData(?array $extractedRawData = null): array + { + if (!isset($extractedRawData) || !$extractedRawData) { + $extractedRawData = $this->extractRawData(); + } + $currentFont = null; /** @var Font $currentFont */ + $clippedFont = null; + $fpdfPage = null; + if ($this->isFpdf()) { + $fpdfPage = $this->createPageForFpdf(); + } + foreach ($extractedRawData as &$command) { + if ('Tj' == $command['o'] || 'TJ' == $command['o']) { + $data = $command['c']; + if (!\is_array($data)) { + $tmpText = ''; + if (isset($currentFont)) { + $tmpText = $currentFont->decodeOctal($data); + // $tmpText = $currentFont->decodeHexadecimal($tmpText, false); + } + $tmpText = str_replace( + ['\\\\', '\(', '\)', '\n', '\r', '\t', '\ '], + ['\\', '(', ')', "\n", "\r", "\t", ' '], + $tmpText + ); + $tmpText = mb_convert_encoding($tmpText, 'UTF-8', 'ISO-8859-1'); + if (isset($currentFont)) { + $tmpText = $currentFont->decodeContent($tmpText); + } + $command['c'] = $tmpText; + continue; + } + $numText = \count($data); + for ($i = 0; $i < $numText; ++$i) { + if (0 != ($i % 2)) { + continue; + } + $tmpText = $data[$i]['c']; + $decodedText = isset($currentFont) ? $currentFont->decodeOctal($tmpText) : $tmpText; + $decodedText = str_replace( + ['\\\\', '\(', '\)', '\n', '\r', '\t', '\ '], + ['\\', '(', ')', "\n", "\r", "\t", ' '], + $decodedText + ); + + $decodedText = mb_convert_encoding($decodedText, 'UTF-8', 'ISO-8859-1'); + + if (isset($currentFont)) { + $decodedText = $currentFont->decodeContent($decodedText); + } + $command['c'][$i]['c'] = $decodedText; + continue; + } + } elseif ('Tf' == $command['o'] || 'TF' == $command['o']) { + $fontId = explode(' ', $command['c'])[0]; + // If document is a FPDI/FPDF the $page has the correct font + $currentFont = isset($fpdfPage) ? $fpdfPage->getFont($fontId) : $this->getFont($fontId); + continue; + } elseif ('Q' == $command['o']) { + $currentFont = $clippedFont; + } elseif ('q' == $command['o']) { + $clippedFont = $currentFont; + } + } + + return $extractedRawData; + } + + /** + * Gets just the Text commands that are involved in text positions and + * Text Matrix (Tm) + * + * It extract just the PDF commands that are involved with text positions, and + * the Text Matrix (Tm). These are: BT, ET, TL, Td, TD, Tm, T*, Tj, ', ", and TJ + * + * @param array $extractedDecodedRawData The data extracted by extractDecodeRawData. + * If it is null, the method extractDecodeRawData is called. + * + * @return array An array with the text command of the page + */ + public function getDataCommands(?array $extractedDecodedRawData = null): array + { + if (!isset($extractedDecodedRawData) || !$extractedDecodedRawData) { + $extractedDecodedRawData = $this->extractDecodedRawData(); + } + $extractedData = []; + foreach ($extractedDecodedRawData as $command) { + switch ($command['o']) { + /* + * BT + * Begin a text object, inicializind the Tm and Tlm to identity matrix + */ + case 'BT': + $extractedData[] = $command; + break; + /* + * cm + * Concatenation Matrix that will transform all following Tm + */ + case 'cm': + $extractedData[] = $command; + break; + /* + * ET + * End a text object, discarding the text matrix + */ + case 'ET': + $extractedData[] = $command; + break; + + /* + * leading TL + * Set the text leading, Tl, to leading. Tl is used by the T*, ' and " operators. + * Initial value: 0 + */ + case 'TL': + $extractedData[] = $command; + break; + + /* + * tx ty Td + * Move to the start of the next line, offset form the start of the + * current line by tx, ty. + */ + case 'Td': + $extractedData[] = $command; + break; + + /* + * tx ty TD + * Move to the start of the next line, offset form the start of the + * current line by tx, ty. As a side effect, this operator set the leading + * parameter in the text state. This operator has the same effect as the + * code: + * -ty TL + * tx ty Td + */ + case 'TD': + $extractedData[] = $command; + break; + + /* + * a b c d e f Tm + * Set the text matrix, Tm, and the text line matrix, Tlm. The operands are + * all numbers, and the initial value for Tm and Tlm is the identity matrix + * [1 0 0 1 0 0] + */ + case 'Tm': + $extractedData[] = $command; + break; + + /* + * T* + * Move to the start of the next line. This operator has the same effect + * as the code: + * 0 Tl Td + * Where Tl is the current leading parameter in the text state. + */ + case 'T*': + $extractedData[] = $command; + break; + + /* + * string Tj + * Show a Text String + */ + case 'Tj': + $extractedData[] = $command; + break; + + /* + * string ' + * Move to the next line and show a text string. This operator has the + * same effect as the code: + * T* + * string Tj + */ + case "'": + $extractedData[] = $command; + break; + + /* + * aw ac string " + * Move to the next lkine and show a text string, using aw as the word + * spacing and ac as the character spacing. This operator has the same + * effect as the code: + * aw Tw + * ac Tc + * string ' + * Tw set the word spacing, Tw, to wordSpace. + * Tc Set the character spacing, Tc, to charsSpace. + */ + case '"': + $extractedData[] = $command; + break; + + case 'Tf': + case 'TF': + $extractedData[] = $command; + break; + + /* + * array TJ + * Show one or more text strings allow individual glyph positioning. + * Each lement of array con be a string or a number. If the element is + * a string, this operator shows the string. If it is a number, the + * operator adjust the text position by that amount; that is, it translates + * the text matrix, Tm. This amount is substracted form the current + * horizontal or vertical coordinate, depending on the writing mode. + * in the default coordinate system, a positive adjustment has the effect + * of moving the next glyph painted either to the left or down by the given + * amount. + */ + case 'TJ': + $extractedData[] = $command; + break; + /* + * q + * Save current graphics state to stack + */ + case 'q': + /* + * Q + * Load last saved graphics state from stack + */ + case 'Q': + $extractedData[] = $command; + break; + default: + } + } + + return $extractedData; + } + + /** + * Gets the Text Matrix of the text in the page + * + * Return an array where every item is an array where the first item is the + * Text Matrix (Tm) and the second is a string with the text data. The Text matrix + * is an array of 6 numbers. The last 2 numbers are the coordinates X and Y of the + * text. The first 4 numbers has to be with Scalation, Rotation and Skew of the text. + * + * @param array $dataCommands the data extracted by getDataCommands + * if null getDataCommands is called + * + * @return array an array with the data of the page including the Tm information + * of any text in the page + */ + public function getDataTm(?array $dataCommands = null): array + { + if (!isset($dataCommands) || !$dataCommands) { + $dataCommands = $this->getDataCommands(); + } + + /* + * At the beginning of a text object Tm is the identity matrix + */ + $defaultTm = ['1', '0', '0', '1', '0', '0']; + $concatTm = ['1', '0', '0', '1', '0', '0']; + $graphicsStatesStack = []; + /* + * Set the text leading used by T*, ' and " operators + */ + $defaultTl = 0; + + /* + * Set default values for font data + */ + $defaultFontId = -1; + $defaultFontSize = 1; + + /* + * Indexes of horizontal/vertical scaling and X,Y-coordinates in the matrix (Tm) + */ + $hSc = 0; // horizontal scaling + /** + * index of vertical scaling in the array that encodes the text matrix. + * for more information: https://github.com/smalot/pdfparser/pull/559#discussion_r1053415500 + */ + $vSc = 3; + $x = 4; + $y = 5; + + /* + * x,y-coordinates of text space origin in user units + * + * These will be assigned the value of the currently printed string + */ + $Tx = 0; + $Ty = 0; + + $Tm = $defaultTm; + $Tl = $defaultTl; + $fontId = $defaultFontId; + $fontSize = $defaultFontSize; // reflects fontSize set by Tf or Tfs + + $extractedTexts = $this->getTextArray(); + $extractedData = []; + foreach ($dataCommands as $command) { + // If we've used up all the texts from getTextArray(), exit + // so we aren't accessing non-existent array indices + // Fixes 'undefined array key' errors in Issues #575, #576 + if (\count($extractedTexts) <= \count($extractedData)) { + break; + } + $currentText = $extractedTexts[\count($extractedData)]; + switch ($command['o']) { + /* + * BT + * Begin a text object, initializing the Tm and Tlm to identity matrix + */ + case 'BT': + $Tm = $defaultTm; + $Tl = $defaultTl; + $Tx = 0; + $Ty = 0; + break; + + case 'cm': + $newConcatTm = (array) explode(' ', $command['c']); + $TempMatrix = []; + // Multiply with previous concatTm + $TempMatrix[0] = (float) $concatTm[0] * (float) $newConcatTm[0] + (float) $concatTm[1] * (float) $newConcatTm[2]; + $TempMatrix[1] = (float) $concatTm[0] * (float) $newConcatTm[1] + (float) $concatTm[1] * (float) $newConcatTm[3]; + $TempMatrix[2] = (float) $concatTm[2] * (float) $newConcatTm[0] + (float) $concatTm[3] * (float) $newConcatTm[2]; + $TempMatrix[3] = (float) $concatTm[2] * (float) $newConcatTm[1] + (float) $concatTm[3] * (float) $newConcatTm[3]; + $TempMatrix[4] = (float) $concatTm[4] * (float) $newConcatTm[0] + (float) $concatTm[5] * (float) $newConcatTm[2] + (float) $newConcatTm[4]; + $TempMatrix[5] = (float) $concatTm[4] * (float) $newConcatTm[1] + (float) $concatTm[5] * (float) $newConcatTm[3] + (float) $newConcatTm[5]; + $concatTm = $TempMatrix; + break; + /* + * ET + * End a text object + */ + case 'ET': + break; + + /* + * text leading TL + * Set the text leading, Tl, to leading. Tl is used by the T*, ' and " operators. + * Initial value: 0 + */ + case 'TL': + // scaled text leading + $Tl = (float) $command['c'] * (float) $Tm[$vSc]; + break; + + /* + * tx ty Td + * Move to the start of the next line, offset from the start of the + * current line by tx, ty. + */ + case 'Td': + $coord = explode(' ', $command['c']); + $Tx += (float) $coord[0] * (float) $Tm[$hSc]; + $Ty += (float) $coord[1] * (float) $Tm[$vSc]; + $Tm[$x] = (string) $Tx; + $Tm[$y] = (string) $Ty; + break; + + /* + * tx ty TD + * Move to the start of the next line, offset form the start of the + * current line by tx, ty. As a side effect, this operator set the leading + * parameter in the text state. This operator has the same effect as the + * code: + * -ty TL + * tx ty Td + */ + case 'TD': + $coord = explode(' ', $command['c']); + $Tl = -((float) $coord[1] * (float) $Tm[$vSc]); + $Tx += (float) $coord[0] * (float) $Tm[$hSc]; + $Ty += (float) $coord[1] * (float) $Tm[$vSc]; + $Tm[$x] = (string) $Tx; + $Tm[$y] = (string) $Ty; + break; + + /* + * a b c d e f Tm + * Set the text matrix, Tm, and the text line matrix, Tlm. The operands are + * all numbers, and the initial value for Tm and Tlm is the identity matrix + * [1 0 0 1 0 0] + */ + case 'Tm': + $Tm = explode(' ', $command['c']); + $TempMatrix = []; + $TempMatrix[0] = (float) $Tm[0] * (float) $concatTm[0] + (float) $Tm[1] * (float) $concatTm[2]; + $TempMatrix[1] = (float) $Tm[0] * (float) $concatTm[1] + (float) $Tm[1] * (float) $concatTm[3]; + $TempMatrix[2] = (float) $Tm[2] * (float) $concatTm[0] + (float) $Tm[3] * (float) $concatTm[2]; + $TempMatrix[3] = (float) $Tm[2] * (float) $concatTm[1] + (float) $Tm[3] * (float) $concatTm[3]; + $TempMatrix[4] = (float) $Tm[4] * (float) $concatTm[0] + (float) $Tm[5] * (float) $concatTm[2] + (float) $concatTm[4]; + $TempMatrix[5] = (float) $Tm[4] * (float) $concatTm[1] + (float) $Tm[5] * (float) $concatTm[3] + (float) $concatTm[5]; + $Tm = $TempMatrix; + $Tx = (float) $Tm[$x]; + $Ty = (float) $Tm[$y]; + break; + + /* + * T* + * Move to the start of the next line. This operator has the same effect + * as the code: + * 0 Tl Td + * Where Tl is the current leading parameter in the text state. + */ + case 'T*': + $Ty -= $Tl; + $Tm[$y] = (string) $Ty; + break; + + /* + * string Tj + * Show a Text String + */ + case 'Tj': + $data = [$Tm, $currentText]; + if ($this->config->getDataTmFontInfoHasToBeIncluded()) { + $data[] = $fontId; + $data[] = $fontSize; + } + $extractedData[] = $data; + break; + + /* + * string ' + * Move to the next line and show a text string. This operator has the + * same effect as the code: + * T* + * string Tj + */ + case "'": + $Ty -= $Tl; + $Tm[$y] = (string) $Ty; + $extractedData[] = [$Tm, $currentText]; + break; + + /* + * aw ac string " + * Move to the next line and show a text string, using aw as the word + * spacing and ac as the character spacing. This operator has the same + * effect as the code: + * aw Tw + * ac Tc + * string ' + * Tw set the word spacing, Tw, to wordSpace. + * Tc Set the character spacing, Tc, to charsSpace. + */ + case '"': + $data = explode(' ', $currentText); + $Ty -= $Tl; + $Tm[$y] = (string) $Ty; + $extractedData[] = [$Tm, $data[2]]; // Verify + break; + + case 'Tf': + /* + * From PDF 1.0 specification, page 106: + * fontname size Tf Set font and size + * Sets the text font and text size in the graphics state. There is no default value for + * either fontname or size; they must be selected using Tf before drawing any text. + * fontname is a resource name. size is a number expressed in text space units. + * + * Source: https://ia902503.us.archive.org/10/items/pdfy-0vt8s-egqFwDl7L2/PDF%20Reference%201.0.pdf + * Introduced with https://github.com/smalot/pdfparser/pull/516 + */ + list($fontId, $fontSize) = explode(' ', $command['c'], 2); + break; + + /* + * array TJ + * Show one or more text strings allow individual glyph positioning. + * Each lement of array con be a string or a number. If the element is + * a string, this operator shows the string. If it is a number, the + * operator adjust the text position by that amount; that is, it translates + * the text matrix, Tm. This amount is substracted form the current + * horizontal or vertical coordinate, depending on the writing mode. + * in the default coordinate system, a positive adjustment has the effect + * of moving the next glyph painted either to the left or down by the given + * amount. + */ + case 'TJ': + $data = [$Tm, $currentText]; + if ($this->config->getDataTmFontInfoHasToBeIncluded()) { + $data[] = $fontId; + $data[] = $fontSize; + } + $extractedData[] = $data; + break; + /* + * q + * Save current graphics state to stack + */ + case 'q': + $graphicsStatesStack[] = $concatTm; + break; + /* + * Q + * Load last saved graphics state from stack + */ + case 'Q': + $concatTm = array_pop($graphicsStatesStack); + break; + default: + } + } + $this->dataTm = $extractedData; + + return $extractedData; + } + + /** + * Gets text data that are around the given coordinates (X,Y) + * + * If the text is in near the given coordinates (X,Y) (or the TM info), + * the text is returned. The extractedData return by getDataTm, could be use to see + * where is the coordinates of a given text, using the TM info for it. + * + * @param float $x The X value of the coordinate to search for. if null + * just the Y value is considered (same Row) + * @param float $y The Y value of the coordinate to search for + * just the X value is considered (same column) + * @param float $xError The value less or more to consider an X to be "near" + * @param float $yError The value less or more to consider an Y to be "near" + * + * @return array An array of text that are near the given coordinates. If no text + * "near" the x,y coordinate, an empty array is returned. If Both, x + * and y coordinates are null, null is returned. + */ + public function getTextXY(?float $x = null, ?float $y = null, float $xError = 0, float $yError = 0): array + { + if (!isset($this->dataTm) || !$this->dataTm) { + $this->getDataTm(); + } + + if (null !== $x) { + $x = (float) $x; + } + + if (null !== $y) { + $y = (float) $y; + } + + if (null === $x && null === $y) { + return []; + } + + $xError = (float) $xError; + $yError = (float) $yError; + + $extractedData = []; + foreach ($this->dataTm as $item) { + $tm = $item[0]; + $xTm = (float) $tm[4]; + $yTm = (float) $tm[5]; + $text = $item[1]; + if (null === $y) { + if (($xTm >= ($x - $xError)) + && ($xTm <= ($x + $xError))) { + $extractedData[] = [$tm, $text]; + continue; + } + } + if (null === $x) { + if (($yTm >= ($y - $yError)) + && ($yTm <= ($y + $yError))) { + $extractedData[] = [$tm, $text]; + continue; + } + } + if (($xTm >= ($x - $xError)) + && ($xTm <= ($x + $xError)) + && ($yTm >= ($y - $yError)) + && ($yTm <= ($y + $yError))) { + $extractedData[] = [$tm, $text]; + continue; + } + } + + return $extractedData; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Pages.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Pages.php new file mode 100644 index 0000000..f95134b --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Pages.php @@ -0,0 +1,131 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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 = []; + } + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/Parser.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Parser.php new file mode 100644 index 0000000..b051f11 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/Parser.php @@ -0,0 +1,331 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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.'".'); + } + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php new file mode 100644 index 0000000..87f5524 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/RawData/FilterHelper.php @@ -0,0 +1,427 @@ + + * + * @date 2020-01-06 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php new file mode 100644 index 0000000..ec8d01e --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/RawData/RawDataParser.php @@ -0,0 +1,990 @@ + + * + * @date 2020-01-06 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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 + */ + 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 $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 $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 */ + $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 $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]; + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/XObject/Form.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/XObject/Form.php new file mode 100644 index 0000000..8e60647 --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/XObject/Form.php @@ -0,0 +1,51 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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); + } +} diff --git a/vendor/smalot/pdfparser/src/Smalot/PdfParser/XObject/Image.php b/vendor/smalot/pdfparser/src/Smalot/PdfParser/XObject/Image.php new file mode 100644 index 0000000..6dc6b0a --- /dev/null +++ b/vendor/smalot/pdfparser/src/Smalot/PdfParser/XObject/Image.php @@ -0,0 +1,47 @@ + + * + * @date 2017-01-03 + * + * @license LGPLv3 + * + * @url + * + * PdfParser is a pdf library written in PHP, extraction oriented. + * Copyright (C) 2017 - Sébastien MALOT + * + * 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 . + */ + +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 ''; + } +}