versión entregada a cliente

This commit is contained in:
alexis.palestina 2025-06-17 11:40:27 -06:00
parent 45368a331f
commit f3fe65cd47
313 changed files with 35094 additions and 684 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -14,96 +14,123 @@ $profesorId = $_SESSION['profesor']['id'] ?? null;
try {
switch ($method) {
case 'GET':
$alumnoId = $_GET['alumno_id'] ?? null;
$alumnoId = $_GET['alumno_id'] ?? null;
$tipoCurso = $_GET['tipo'] ?? null;
$estadoCurso = $_GET['estado'] ?? null;
if ($alumnoId) {
// Obtener cursos de un alumno específico
$query = "SELECT c.* FROM cursos c
JOIN alumnos_cursos ac ON c.id = ac.curso_id
WHERE ac.alumno_id = ? AND c.profesor_id = ?";
// Obtener cursos de un alumno específico (incluyendo competencias si aplica)
$query = "SELECT
c.id,
c.nombre,
c.tipo,
c.estado,
c.competencias
FROM cursos c
JOIN alumnos_cursos ac ON c.id = ac.curso_id
WHERE ac.alumno_id = ? AND c.profesor_id = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$alumnoId, $profesorId]);
} else {
// Obtener todos los alumnos
$query = "SELECT * FROM alumnos";
$stmt = $pdo->prepare($query);
$stmt->execute();
}
echo json_encode([
'success' => true,
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)
]);
break;
$query = "
SELECT
ac.id,
ac.alumno_id,
ac.curso_id,
c.nombre AS curso_nombre,
c.tipo,
c.estado,
c.competencias
FROM alumnos_cursos ac
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?
";
$params = [$profesorId];
if ($tipoCurso) {
$query .= " AND c.tipo = ?";
$params[] = $tipoCurso;
}
if ($estadoCurso) {
$query .= " AND c.estado = ?";
$params[] = $estadoCurso;
}
$stmt = $pdo->prepare($query);
$stmt->execute($params);?
}
echo json_encode([
'success' => true,
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)
]);
break;
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
$cursoId = $data['curso_id'] ?? null;
$alumnos = $data['alumnos'] ?? [];
$data = json_decode(file_get_contents('php://input'), true);
$cursoId = $data['curso_id'] ?? null;
$alumnos = $data['alumnos'] ?? [];
if (empty($cursoId) || empty($alumnos)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Datos incompletos']);
exit;
}
// Verificar que el curso pertenece al profesor
$stmt = $pdo->prepare("SELECT id FROM cursos WHERE id = ? AND profesor_id = ?");
$stmt->execute([$cursoId, $profesorId]);
if (!$stmt->fetch()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}
foreach ($alumnos as $alumnoId) {
$stmt = $pdo->prepare("
INSERT INTO alumnos_cursos (alumno_id, curso_id, estado)
VALUES (?, ?, 'cursando')
ON DUPLICATE KEY UPDATE estado = 'cursando'
");
$stmt->execute([$alumnoId, $cursoId]);
}
echo json_encode([
'success' => true,
'message' => 'Alumnos asignados correctamente'
]);
break;
case 'DELETE':
$alumnoId = $_GET['alumno_id'] ?? null;
$cursoId = $_GET['curso_id'] ?? null;
if (!$alumnoId || !$cursoId) {
if (empty($cursoId) || empty($alumnos)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'IDs requeridos']);
echo json_encode(['success' => false, 'error' => 'Datos incompletos']);
exit;
}
// Verificar que el curso pertenece al profesor
$stmt = $pdo->prepare("SELECT id FROM cursos WHERE id = ? AND profesor_id = ?");
$stmt->execute([$cursoId, $profesorId]);
if (!$stmt->fetch()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}
// Eliminar asignación
foreach ($alumnos as $alumnoId) {
$stmt = $pdo->prepare("
INSERT INTO alumnos_cursos (alumno_id, curso_id, estado)
VALUES (?, ?, 'cursando')
ON DUPLICATE KEY UPDATE estado = 'cursando'
");
$stmt->execute([$alumnoId, $cursoId]);
}
echo json_encode([
'success' => true,
'message' => 'Alumnos asignados correctamente'
]);
break;
case 'DELETE':
$alumnoId = $_GET['alumno_id'] ?? null;
$cursoId = $_GET['curso_id'] ?? null;
if (!$alumnoId || !$cursoId) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'IDs requeridos']);
exit;
}
$stmt = $pdo->prepare("SELECT id FROM cursos WHERE id = ? AND profesor_id = ?");
$stmt->execute([$cursoId, $profesorId]);
if (!$stmt->fetch()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}
$stmt = $pdo->prepare("DELETE FROM alumnos_cursos WHERE alumno_id = ? AND curso_id = ?");
$stmt->execute([$alumnoId, $cursoId]);
echo json_encode([
'success' => true,
'message' => 'Alumno desvinculado del curso correctamente'
]);
break;
default:
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
@ -112,4 +139,3 @@ try {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Error en la base de datos: ' . $e->getMessage()]);
}
?>

View File

@ -27,88 +27,178 @@ try {
]);
break;
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['nombre']) || empty($data['email'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Nombre y email son requeridos']);
exit;
}
if (empty($data['nombre']) || empty($data['email'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Nombre y email son requeridos']);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO alumnos (nombre, email, telefono)
VALUES (?, ?, ?)
");
$stmt->execute([
$data['nombre'],
$data['email'],
$data['telefono'] ?? null
]);
// Verificar email duplicado
$stmt = $pdo->prepare("SELECT id FROM alumnos WHERE email = ?");
$stmt->execute([$data['email']]);
if ($stmt->fetch()) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'El correo ya está registrado']);
exit;
}
echo json_encode([
'success' => true,
'id' => $pdo->lastInsertId(),
'message' => 'Alumno creado exitosamente'
]);
break;
// Verificar teléfono duplicado
if (!empty($data['telefono'])) {
$stmt = $pdo->prepare("SELECT id FROM alumnos WHERE telefono = ?");
$stmt->execute([$data['telefono']]);
if ($stmt->fetch()) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'El teléfono ya está registrado']);
exit;
}
}
case 'PUT':
$data = json_decode(file_get_contents('php://input'), true);
$pdo->beginTransaction();
try {
// Insertar alumno
$stmt = $pdo->prepare("
INSERT INTO alumnos (nombre, email, telefono)
VALUES (?, ?, ?)
");
$stmt->execute([
$data['nombre'],
$data['email'],
$data['telefono'] ?? null
]);
if (empty($data['id']) || empty($data['nombre']) || empty($data['email'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Datos incompletos']);
exit;
}
$alumnoId = $pdo->lastInsertId();
$stmt = $pdo->prepare("
UPDATE alumnos SET
nombre = ?,
email = ?,
telefono = ?
WHERE id = ?
");
$stmt->execute([
$data['nombre'],
$data['email'],
$data['telefono'] ?? null,
$data['id']
]);
// Si viene curso_id, asignar también a curso
if (!empty($data['curso_id'])) {
$stmt = $pdo->prepare("
INSERT INTO alumnos_cursos (alumno_id, curso_id, estado)
VALUES (?, ?, 'cursando')
");
$stmt->execute([$alumnoId, $data['curso_id']]);
}
echo json_encode([
'success' => true,
'message' => 'Alumno actualizado exitosamente'
]);
break;
$pdo->commit();
echo json_encode([
'success' => true,
'id' => $alumnoId,
'message' => 'Alumno creado y asignado exitosamente'
]);
} catch (PDOException $e) {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Error al registrar: ' . $e->getMessage()]);
}
break;
case 'DELETE':
$id = $_GET['id'] ?? null;
if (!$id) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'ID de alumno no proporcionado']);
exit;
}
case 'PUT':
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['id']) || empty($data['nombre']) || empty($data['email'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Datos incompletos']);
exit;
}
// Verificar email duplicado (excluyendo el mismo alumno)
$stmt = $pdo->prepare("SELECT id FROM alumnos WHERE email = ? AND id != ?");
$stmt->execute([$data['email'], $data['id']]);
if ($stmt->fetch()) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'El correo ya está registrado por otro alumno']);
exit;
}
// Verificar teléfono duplicado (excluyendo el mismo alumno)
if (!empty($data['telefono'])) {
$stmt = $pdo->prepare("SELECT id FROM alumnos WHERE telefono = ? AND id != ?");
$stmt->execute([$data['telefono'], $data['id']]);
if ($stmt->fetch()) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'El teléfono ya está registrado por otro alumno']);
exit;
}
}
$stmt = $pdo->prepare("
UPDATE alumnos SET
nombre = ?,
email = ?,
telefono = ?
WHERE id = ?
");
$stmt->execute([
$data['nombre'],
$data['email'],
$data['telefono'] ?? null,
$data['id']
]);
// Si curso_id viene en la actualización, actualizar también el curso
if (!empty($data['curso_id'])) {
// Verifica si ya tiene una relación previa
$check = $pdo->prepare("SELECT id FROM alumnos_cursos WHERE alumno_id = ?");
$check->execute([$data['id']]);
if ($check->fetch()) {
// Si ya tiene relación, actualizamos el curso asignado
$updateCurso = $pdo->prepare("
UPDATE alumnos_cursos SET curso_id = ?, estado = 'cursando'
WHERE alumno_id = ?
");
$updateCurso->execute([$data['curso_id'], $data['id']]);
} else {
// Si no tiene relación previa, la insertamos
$insertCurso = $pdo->prepare("
INSERT INTO alumnos_cursos (alumno_id, curso_id, estado)
VALUES (?, ?, 'cursando')
");
$insertCurso->execute([$data['id'], $data['curso_id']]);
}
}
echo json_encode([
'success' => true,
'message' => 'Alumno actualizado exitosamente'
]);
break;
// Verificar si el alumno está asignado a algún curso primero
$stmt = $pdo->prepare("SELECT COUNT(*) FROM alumnos_cursos WHERE alumno_id = ?");
$stmt->execute([$id]);
$tieneCursos = $stmt->fetchColumn();
if ($tieneCursos > 0) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'No se puede eliminar, el alumno está asignado a cursos']);
exit;
}
$stmt = $pdo->prepare("DELETE FROM alumnos WHERE id = ?");
$stmt->execute([$id]);
echo json_encode([
'success' => true,
'message' => 'Alumno eliminado exitosamente'
]);
break;
case 'DELETE':
$id = $_GET['id'] ?? null;
if (!$id) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'ID de alumno no proporcionado']);
exit;
}
try {
$pdo->beginTransaction();
// Eliminar vinculaciones alumno_curso
$stmt = $pdo->prepare("DELETE FROM alumnos_cursos WHERE alumno_id = ?");
$stmt->execute([$id]);
// Eliminar alumno
$stmt = $pdo->prepare("DELETE FROM alumnos WHERE id = ?");
$stmt->execute([$id]);
$pdo->commit();
echo json_encode([
'success' => true,
'message' => 'Alumno eliminado exitosamente'
]);
} catch (PDOException $e) {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Error al eliminar alumno: ' . $e->getMessage()]);
}
break;
default:
http_response_code(405);

View File

@ -14,73 +14,107 @@ $profesorId = $_SESSION['profesor']['id'];
switch ($method) {
case 'GET':
try {
$query = "SELECT * FROM cursos WHERE profesor_id = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$profesorId]);
echo json_encode($stmt->fetchAll());
$tipo = $_GET['tipo'] ?? null;
if ($tipo) {
$query = "SELECT * FROM cursos WHERE profesor_id = ? AND tipo = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$profesorId, $tipo]);
} else {
$query = "SELECT * FROM cursos WHERE profesor_id = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$profesorId]);
}
echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Error al cargar cursos']);
}
break;
case 'POST':
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['nombre']) || empty($data['tipo'])) {
http_response_code(400);
echo json_encode(['error' => 'Nombre y tipo son requeridos']);
exit;
}
try {
// Validar que no exista otro curso con el mismo nombre y tipo para ese profesor
$stmt = $pdo->prepare("SELECT id FROM cursos WHERE nombre = ? AND tipo = ? AND profesor_id = ?");
$stmt->execute([$data['nombre'], $data['tipo'], $profesorId]);
if ($stmt->fetch()) {
echo json_encode([
'success' => false,
'error' => "Ya existe un curso llamado '{$data['nombre']}' de tipo '{$data['tipo']}'."
]);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO cursos (nombre, descripcion, tipo, competencias, estado, profesor_id)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO cursos (nombre, descripcion, tipo, competencias, docente, horas_trabajadas, estado, profesor_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['nombre'],
$data['descripcion'] ?? null,
$data['tipo'],
$data['competencias'] ?? null,
$data['docente'] ?? null,
$data['horas_trabajadas'] ?? null,
$data['estado'],
$profesorId
]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Error al crear curso']);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'PUT':
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['id']) || empty($data['nombre']) || empty($data['tipo'])) {
http_response_code(400);
echo json_encode(['error' => 'Datos incompletos']);
exit;
if (empty($data['id']) || empty($data['nombre']) || empty($data['tipo'])) {
http_response_code(400);
echo json_encode(['error' => 'Datos incompletos']);
exit;
}
try {
// Verificar que el curso pertenece al profesor
$stmt = $pdo->prepare("SELECT id FROM cursos WHERE id = ? AND profesor_id = ?");
$stmt->execute([$data['id'], $profesorId]);
if (!$stmt->fetch()) {
http_response_code(403);
echo json_encode(['error' => 'No autorizado']);
exit;
}
// Validar que no haya otro curso con el mismo nombre y tipo (excluyendo el actual)
$stmt = $pdo->prepare("SELECT id FROM cursos WHERE nombre = ? AND tipo = ? AND profesor_id = ? AND id != ?");
$stmt->execute([$data['nombre'], $data['tipo'], $profesorId, $data['id']]);
if ($stmt->fetch()) {
echo json_encode([
'success' => false,
'error' => "Ya existe otro curso llamado '{$data['nombre']}' de tipo '{$data['tipo']}'."
]);
exit;
}
$stmt = $pdo->prepare("
UPDATE cursos SET
nombre = ?,
descripcion = ?,
tipo = ?,
competencias = ?,
estado = ?
nombre = ?,
descripcion = ?,
tipo = ?,
competencias = ?,
docente = ?,
horas_trabajadas = ?,
estado = ?
WHERE id = ?
");
$stmt->execute([
@ -88,17 +122,19 @@ switch ($method) {
$data['descripcion'] ?? null,
$data['tipo'],
$data['competencias'] ?? null,
$data['docente'] ?? null,
$data['horas_trabajadas'] ?? null,
$data['estado'],
$data['id']
]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Error al actualizar curso']);
}
break;
case 'DELETE':
$id = $_GET['id'] ?? null;
if (!$id) {
@ -106,30 +142,30 @@ switch ($method) {
echo json_encode(['error' => 'ID de curso no proporcionado']);
exit;
}
try {
// Verificar que el curso pertenece al profesor
$stmt = $pdo->prepare("SELECT id FROM cursos WHERE id = ? AND profesor_id = ?");
$stmt->execute([$id, $profesorId]);
if (!$stmt->fetch()) {
http_response_code(403);
echo json_encode(['error' => 'No autorizado']);
exit;
}
$stmt = $pdo->prepare("DELETE FROM cursos WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Error al eliminar curso']);
}
break;
default:
http_response_code(405);
echo json_encode(['error' => 'Método no permitido']);
}
?>
?>

40
api/dashboard_data.php Normal file
View File

@ -0,0 +1,40 @@
<?php
session_start();
require '../conexion.php';
$profesor_id = $_SESSION['profesor']['id'] ?? null;
if (!$profesor_id) {
http_response_code(401);
echo json_encode(["error" => "No autenticado"]);
exit;
}
function getAll($pdo, $sql, $params = []) {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
$cursos = getAll($pdo, "SELECT * FROM cursos WHERE profesor_id = ?", [$profesor_id]);
$alumnos = getAll($pdo, "
SELECT DISTINCT a.* FROM alumnos a
JOIN alumnos_cursos ac ON a.id = ac.alumno_id
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?
", [$profesor_id]);
$diplomas = getAll($pdo, "
SELECT d.* FROM diplomas d
JOIN alumnos_cursos ac ON d.alumno_curso_id = ac.id
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?
", [$profesor_id]);
echo json_encode([
"cursos" => $cursos,
"alumnos" => $alumnos,
"diplomas" => $diplomas,
"lastUpdated" => date("Y-m-d H:i:s")
]);

View File

@ -1,44 +0,0 @@
<?php
header('Content-Type: application/json');
require '../includes/config.php';
if (!is_logged_in()) {
http_response_code(401);
echo json_encode(['error' => 'No autenticado']);
exit;
}
try {
$profesorId = $_GET['profesor_id'] ?? null;
$query = "
SELECT d.*, a.nombre AS alumno_nombre, c.nombre AS curso_nombre
FROM diplomas d
JOIN alumnos_cursos ac ON d.alumno_curso_id = ac.id
JOIN alumnos a ON ac.alumno_id = a.id
JOIN cursos c ON ac.curso_id = c.id
";
$params = [];
if ($profesorId) {
$query .= " WHERE c.profesor_id = ?";
$params[] = $profesorId;
}
$query .= " ORDER BY d.fecha_emision DESC";
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$diplomas = $stmt->fetchAll();
echo json_encode([
'success' => true,
'data' => $diplomas,
'count' => count($diplomas)
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
?>

96
api/diplomas.php Normal file
View File

@ -0,0 +1,96 @@
<?php
header('Content-Type: application/json');
require '../includes/config.php';
if (!is_logged_in()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'No autenticado']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
$profesorId = $_SESSION['profesor']['id'] ?? null;
switch ($method) {
case 'POST':
$input = json_decode(file_get_contents('php://input'), true);
$alumnoCursoId = $input['alumno_curso_id'] ?? null;
if (!$alumnoCursoId) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Falta ID de alumno_curso']);
exit;
}
$stmt = $pdo->prepare("
SELECT ac.id
FROM alumnos_cursos ac
JOIN cursos c ON ac.curso_id = c.id
WHERE ac.id = ? AND c.profesor_id = ?
");
$stmt->execute([$alumnoCursoId, $profesorId]);
if (!$stmt->fetch()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'No autorizado']);
exit;
}
$stmt = $pdo->prepare("SELECT codigo_unico FROM diplomas WHERE alumno_curso_id = ?");
$stmt->execute([$alumnoCursoId]);
$existing = $stmt->fetchColumn();
if ($existing) {
http_response_code(409);
echo json_encode(['success' => false, 'error' => 'Este alumno ya tiene un diploma', 'codigo_unico' => $existing]);
exit;
}
$codigo = strtoupper(substr(uniqid(), -6));
$fecha = date('Y-m-d');
// Insertar nuevo diploma
$stmt = $pdo->prepare("
INSERT INTO diplomas (alumno_curso_id, codigo_unico, fecha_emision)
VALUES (?, ?, ?)
");
$stmt->execute([$alumnoCursoId, $codigo, $fecha]);
echo json_encode([
'success' => true,
'codigo_unico' => $codigo
]);
break;
case 'GET':
if (!$profesorId) {
echo json_encode(['success' => false, 'error' => 'Profesor no identificado']);
exit;
}
$stmt = $pdo->prepare("
SELECT d.codigo_unico, d.fecha_emision,
ac.id AS alumno_curso_id,
a.nombre AS alumno_nombre, a.email AS alumno_email,
c.nombre AS curso_nombre, c.tipo AS curso_tipo
FROM diplomas d
JOIN alumnos_cursos ac ON d.alumno_curso_id = ac.id
JOIN alumnos a ON ac.alumno_id = a.id
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?
ORDER BY d.fecha_emision DESC
");
$stmt->execute([$profesorId]);
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'data' => $result
]);
break;
default:
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
}

135
api/enviar_diploma.php Normal file
View File

@ -0,0 +1,135 @@
<?php
require '../includes/config.php';
require '../fpdf/fpdf.php';
require '../fpdf/src/autoload.php';
require '../includes/mailer_config.php';
require '../vendor/autoload.php';
use setasign\Fpdi\Fpdi;
use PHPMailer\PHPMailer\Exception;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Writer\PngWriter;
header('Content-Type: application/json');
$codigo = $_GET['codigo'] ?? null;
if (!$codigo) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Código no proporcionado']);
exit;
}
$stmt = $pdo->prepare("
SELECT
a.nombre AS alumno_nombre,
a.email AS alumno_email,
a.telefono AS alumno_telefono,
c.nombre AS curso_nombre,
c.tipo,
c.horas_trabajadas,
c.competencias,
d.fecha_emision
FROM diplomas d
JOIN alumnos_cursos ac ON d.alumno_curso_id = ac.id
JOIN alumnos a ON ac.alumno_id = a.id
JOIN cursos c ON ac.curso_id = c.id
WHERE d.codigo_unico = ?
");
$stmt->execute([$codigo]);
$diploma = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$diploma) {
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Diploma no encontrado']);
exit;
}
function t($txt) {
return iconv('UTF-8', 'windows-1252//TRANSLIT', $txt);
}
$alumno = $diploma['alumno_nombre'];
$email = $diploma['alumno_email'];
$telefono = $diploma['alumno_telefono'];
$telefonoFormateado = '52' . preg_replace('/\D/', '', $telefono);
$curso = $diploma['curso_nombre'];
$tipo = $diploma['tipo'];
$horas = $diploma['horas_trabajadas'];
$competencias = $diploma['competencias'] ?? '';
$fecha = new DateTime($diploma['fecha_emision']);
$dia = str_pad($fecha->format('d'), 2, '0', STR_PAD_LEFT);
$mes = $fecha->format('m');
$anio = $fecha->format('Y');
$host = $_SERVER['HTTP_HOST'];
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
$baseDir = explode('/api', $_SERVER['SCRIPT_NAME'])[0];
$validationUrl = "$protocol://$host$baseDir/certificado.php?codigo=$codigo";
$qrPath = __DIR__ . "/../temp/qr_$codigo.png";
Builder::create()
->writer(new PngWriter())
->data($validationUrl)
->size(150)
->margin(5)
->build()
->saveToFile($qrPath);
$pdf = new Fpdi();
$pdf->AddPage();
$pdf->setSourceFile(__DIR__ . '/../fpdf/formatodiploma.pdf');
$template = $pdf->importPage(1);
$pdf->useTemplate($template);
$pdf->SetTextColor(33, 37, 41);
$pdf->SetFont('Helvetica', 'B', 12);
$pdf->SetXY(0, 245);
$pdf->Cell(210, 10, t("Dr. Juan Manuel Gutiérrez Méndez"), 0, 1, 'C');
$pdf->SetFont('Helvetica', '', 11);
$pdf->Cell(210, 6, t("Director de Proyectos"), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'I', 10);
$pdf->SetXY(0, 268);
$pdf->Cell(210, 6, t("Se expide en la ciudad de Xalapa, Ver., a los {$dia} días de {$mes} de {$anio}"), 0, 1, 'C');
$pdf->Image($qrPath, 10, 240, 25, 25);
unlink($qrPath);
$tempPath = "/tmp/diploma_$codigo.pdf";
$pdf->Output($tempPath, 'F');
$mail = getMailer();
$mail->addAddress($email, $alumno);
$mail->Subject = "Tu diploma del curso {$curso}";
$mail->Body = "Hola {$alumno},\n\nAdjunto encontrarás tu diploma por haber concluido satisfactoriamente el curso tipo " . ucfirst($tipo) . ": {$curso}.\n\nGracias por tu participación.\n\nPuedes validar el diploma escaneando el código QR o visitando:\n$validationUrl";
$mail->addAttachment($tempPath, "Diploma-$codigo.pdf");
try {
$mail->send();
unlink($tempPath);
$instanceId = 'instance126094';
$token = 'd7n8da6p4iixalci';
$mensajeWA = "Hola {$alumno}, hemos enviado el diploma de LANIA a tu correo personal. ¡Felicidades por concluir el curso!";
$waUrl = "https://api.ultramsg.com/$instanceId/messages/chat";
$waData = [
'token' => $token,
'to' => $telefonoFormateado,
'body' => $mensajeWA
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $waUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($waData)
]);
$waResponse = curl_exec($ch);
curl_close($ch);
echo json_encode(['success' => true, 'message' => 'Diploma enviado por correo y WhatsApp']);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $mail->ErrorInfo]);
}

109
api/importar_csv.php Normal file
View File

@ -0,0 +1,109 @@
<?php
require '../includes/config.php';
header('Content-Type: application/json');
if (!is_logged_in()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'No autenticado']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$alumnos = $input['alumnos'] ?? [];
if (empty($alumnos)) {
echo json_encode(['success' => false, 'error' => 'No se recibieron alumnos']);
exit;
}
function limpiar($s) {
return trim($s, "\"' ");
}
$errores = [];
$importados = 0;
foreach ($alumnos as $index => $alumno) {
$fila = $index + 2;
$nombre = limpiar($alumno['nombre'] ?? '');
$email = limpiar($alumno['email'] ?? '');
$telefono = limpiar($alumno['telefono'] ?? '');
$tipoCurso = strtolower(limpiar($alumno['tipoCurso'] ?? ''));
$nombreCurso = limpiar($alumno['nombreCurso'] ?? '');
if (!$nombre || !$email || !$telefono || !$tipoCurso || !$nombreCurso) {
$errores[] = "Fila $fila: faltan datos obligatorios.";
continue;
}
// Verificar curso+tipo
$stmtCurso = $pdo->prepare("
SELECT id
FROM cursos
WHERE nombre = ? AND tipo = ?
");
$stmtCurso->execute([$nombreCurso, $tipoCurso]);
$curso = $stmtCurso->fetch(PDO::FETCH_ASSOC);
if (!$curso) {
$errores[] = "Fila $fila: el curso '$nombreCurso' con tipo '$tipoCurso' no existe.";
continue;
}
// Verificar duplicados por separado
$emailDuplicado = false;
$telefonoDuplicado = false;
$stmtEmail = $pdo->prepare("SELECT id FROM alumnos WHERE email = ?");
$stmtEmail->execute([$email]);
if ($stmtEmail->fetch()) {
$emailDuplicado = true;
}
$stmtTel = $pdo->prepare("SELECT id FROM alumnos WHERE telefono = ?");
$stmtTel->execute([$telefono]);
if ($stmtTel->fetch()) {
$telefonoDuplicado = true;
}
if ($emailDuplicado || $telefonoDuplicado) {
if ($emailDuplicado && $telefonoDuplicado) {
$errores[] = "Fila $fila: el email '$email' y el teléfono '$telefono' ya están registrados.";
} elseif ($emailDuplicado) {
$errores[] = "Fila $fila: el email '$email' ya está registrado.";
} elseif ($telefonoDuplicado) {
$errores[] = "Fila $fila: el teléfono '$telefono' ya está registrado.";
}
continue;
}
try {
$pdo->beginTransaction();
$stmtAlumno = $pdo->prepare("INSERT INTO alumnos (nombre, email, telefono) VALUES (?, ?, ?)");
$stmtAlumno->execute([$nombre, $email, $telefono]);
$alumnoId = $pdo->lastInsertId();
$stmtAsignar = $pdo->prepare("INSERT INTO alumnos_cursos (alumno_id, curso_id, estado) VALUES (?, ?, 'cursando')");
$stmtAsignar->execute([$alumnoId, $curso['id']]);
$pdo->commit();
$importados++;
} catch (PDOException $e) {
$pdo->rollBack();
$errores[] = "Fila $fila: error al guardar en base de datos.";
}
}
if (!empty($errores)) {
echo json_encode([
'success' => false,
'conflicts' => $errores
]);
} else {
echo json_encode([
'success' => true,
'message' => "$importados alumnos importados exitosamente."
]);
}

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -7,11 +7,10 @@
body {
font-family: "Inter", sans-serif;
height: 100vh;
overflow: hidden;
min-height: 100vh;
overflow-y: auto;
}
/* Estilos para el contenedor del login */
.login-container {
display: flex;
justify-content: center;
@ -327,7 +326,6 @@ select:focus {
display: none;
}
/* Estilos para el contenido específico */
.course-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
@ -362,7 +360,6 @@ select:focus {
margin: 1rem 0;
}
/* Estilos para el buscador y tabla */
.search-container {
display: flex;
gap: 10px;
@ -421,9 +418,7 @@ select:focus {
.pagination-btn {
padding: 0.5rem 1rem;
}
/* Mantén todos tus estilos anteriores y añade estos */
/* Estilos para el dashboard */
.admin .sidebar-menu li[data-section="courses"],
.admin .sidebar-menu li[data-section="students"],
.user .sidebar-menu li[data-section="my-courses"],
@ -441,7 +436,6 @@ select:focus {
display: flex;
}
/* Mejoras de responsive */
@media (max-width: 768px) {
.main-container {
flex-direction: column;
@ -457,7 +451,7 @@ select:focus {
padding: 1.5rem;
}
}
/* Estilos adicionales */
.loader {
text-align: center;
padding: 2rem;
@ -509,9 +503,6 @@ select:focus {
border-radius: 6px;
}
/* Añadir al final del archivo */
/* Estilos para el header */
header {
display: flex;
justify-content: space-between;
@ -532,7 +523,6 @@ header h1 {
gap: 1rem;
}
/* Estilos para el enlace de cerrar sesión */
.logout-link {
color: #333;
text-decoration: none;
@ -546,7 +536,6 @@ header h1 {
background-color: #fee2e2;
}
/* Estilos para la columna de acciones */
.courses-table td:last-child {
text-align: center;
}
@ -565,7 +554,7 @@ header h1 {
background-color: #a0aec0;
cursor: not-allowed;
}
/* Badges para tipos de curso */
.badge {
padding: 4px 8px;
border-radius: 12px;
@ -603,7 +592,7 @@ header h1 {
background-color: #9c27b0;
color: white;
}
/* Estilos para celdas de descripción */
.description-cell {
max-width: 300px;
white-space: nowrap;
@ -612,15 +601,11 @@ header h1 {
}
.description-cell:hover {
white-space: normal;
overflow: visible;
position: relative;
z-index: 1;
background: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background: inherit;
box-shadow: none;
cursor: default;
}
/* Estilos para mensaje de no datos */
.no-data {
text-align: center;
padding: 20px;
@ -628,14 +613,12 @@ header h1 {
font-style: italic;
}
/* Mejoras para textarea */
textarea {
min-height: 100px;
resize: vertical;
font-family: "Inter", sans-serif;
}
/* Estilos para tarjetas de error */
.error-card {
border-left-color: #e53e3e !important;
}
@ -643,9 +626,6 @@ textarea {
.error-card h2 {
color: #e53e3e;
}
/* ------------------------- */
/* ESTILOS GESTIÓN ALUMNOS */
/* ------------------------- */
.students-management {
display: flex;
@ -653,7 +633,6 @@ textarea {
gap: 1.5rem;
}
/* Formulario */
#studentForm {
margin-top: 1rem;
}
@ -759,7 +738,6 @@ textarea {
background-color: #f8fafc;
}
/* Botones de acción */
.action-buttons {
display: flex;
gap: 0.5rem;
@ -779,7 +757,6 @@ textarea {
background-color: #d97706;
}
/* Mensaje sin datos */
.no-data {
text-align: center;
padding: 2rem;
@ -800,7 +777,6 @@ textarea {
margin-bottom: 1.5rem;
}
/* Responsive */
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
@ -824,7 +800,7 @@ textarea {
width: 100%;
}
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
@ -874,7 +850,6 @@ textarea {
background-color: #f9fafb;
}
/* Toast styles */
.toast {
position: fixed;
bottom: 20px;
@ -965,3 +940,97 @@ textarea {
font-size: 0.85rem;
font-weight: 500;
}
.description-cell {
max-width: 250px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.truncated-text {
display: inline-block;
max-width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: middle;
}
.modal-description {
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.4;
}
.read-more {
color: #2563eb;
font-size: 0.85rem;
text-decoration: underline;
margin-left: 6px;
cursor: pointer;
}
#courses-content h2 + form,
#courses-content h2 + #courseForm {
margin-top: 1.5rem;
}
#courses-content textarea[name="descripcion"] {
font-size: 0.95rem;
}
textarea[name="descripcion"] {
resize: none;
overflow-y: hidden;
}
.tooltip-csv {
visibility: hidden;
opacity: 0;
position: absolute;
top: 140%;
left: 50%;
transform: translateX(-50%);
background-color: #2563eb;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
white-space: nowrap;
font-size: 0.75rem;
transition: opacity 0.2s ease;
pointer-events: none;
z-index: 10;
}
.tooltip-csv::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -6px;
border-width: 6px;
border-style: solid;
border-color: transparent transparent #2563eb transparent;
}
svg:hover + .tooltip-csv,
.tooltip-csv:hover {
visibility: visible;
opacity: 1;
}
.table-container td,
.table-container th {
vertical-align: middle;
}
.table-container td:nth-child(1),
.table-container td:nth-child(2) {
max-width: 280px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -0,0 +1,2 @@
"nombre,email,telefono,tipoCurso,nombreCurso"
"Rafael,rafael1@mail.com,10987654321,pildora,Curso Avanzado"
1 nombre,email,telefono,tipoCurso,nombreCurso
2 Rafael,rafael1@mail.com,10987654321,pildora,Curso Avanzado

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,161 @@
<?php
// Activar todos los errores para depuración
ob_start();
require 'includes/config.php';
require 'fpdf/fpdf.php';
require 'fpdf/src/autoload.php';
use setasign\Fpdi\Fpdi;
function t($txt) {
return iconv('UTF-8', 'windows-1252//TRANSLIT', $txt);
}
// Incluir FPDF
require 'fpdf/fpdf.php';
$codigo = $_GET['codigo'] ?? null;
if (!$codigo) {
ob_end_clean();
die("Código no proporcionado");
}
// Crear PDF
$pdf = new FPDF('P', 'mm', 'Letter');
$pdf->AddPage();
$stmt = $pdo->prepare("
SELECT
a.nombre AS alumno_nombre,
c.nombre AS curso_nombre,
c.tipo,
c.horas_trabajadas,
c.competencias,
d.fecha_emision
FROM diplomas d
JOIN alumnos_cursos ac ON d.alumno_curso_id = ac.id
JOIN alumnos a ON ac.alumno_id = a.id
JOIN cursos c ON ac.curso_id = c.id
WHERE d.codigo_unico = ?
");
$stmt->execute([$codigo]);
$diploma = $stmt->fetch(PDO::FETCH_ASSOC);
// Configurar fuentes
$pdf->SetFont('Arial', 'B', 24);
$pdf->SetTextColor(0, 0, 0);
if (!$diploma) {
ob_end_clean();
die("Diploma no encontrado");
}
$background_path = 'assets/img/DIPLOMA.png';
$pdf->Image($background_path, 0, 0, 216, 280);
$alumno = $diploma['alumno_nombre'];
$curso = $diploma['curso_nombre'];
$tipo = $diploma['tipo'];
$horas = $diploma['horas_trabajadas'];
$competencias = $diploma['competencias'] ?? '';
$fecha = new DateTime($diploma['fecha_emision']);
$dia = str_pad($fecha->format('d'), 2, '0', STR_PAD_LEFT);
$mes = $fecha->format('m');
$anio = $fecha->format('Y');
// Solo texto básico para prueba (sin imagen)
$pdf->Cell(0, 10, 'DIPLOMA DE PRUEBA', 0, 1, 'C');
$pdf = new Fpdi();
$pdf->AddPage();
$pdf->setSourceFile(__DIR__ . '/fpdf/formatodiploma.pdf');
$template = $pdf->importPage(1);
$pdf->useTemplate($template);
$pdf->SetTextColor(33, 37, 41);
// Generar PDF
$pdf->Output('I', 'diploma_prueba.pdf');
exit;
// Tratamiento
if ($tipo === 'tratamiento') {
$pdf->SetFont('Helvetica', '', 16);
$pdf->SetXY(0, 80);
$pdf->Cell(210, 10, t("Otorga el presente reconocimiento"), 0, 1, 'C');
$pdf->SetFont('Helvetica', '', 14);
$pdf->SetXY(0, 92);
$pdf->Cell(210, 10, t("a:"), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'I', 22);
$pdf->SetXY(0, 102);
$pdf->Cell(210, 10, t($alumno), 0, 1, 'C');
$y = 120;
$pdf->SetFont('Helvetica', '', 12);
$textoBase = "Por haber acreditado en el curso “{$curso}";
if ($horas) $textoBase .= " ({$horas} horas de trabajo)";
$pdf->SetXY(20, $y);
$pdf->MultiCell(170, 7, t($textoBase . ",\nla evaluación de las competencias:"), 0, 'L');
$y = $pdf->GetY() + 2;
$pdf->SetFont('Helvetica', '', 11);
$lineas = explode("\n", $competencias);
foreach ($lineas as $linea) {
$pdf->SetXY(25, $y);
$pdf->MultiCell(160, 6, t("" . trim($linea)), 0, 'L');
$y = $pdf->GetY();
}
// Inyección
} elseif ($tipo === 'inyeccion') {
$pdf->SetFont('Helvetica', '', 16);
$pdf->SetXY(0, 82);
$pdf->Cell(210, 10, t("Otorga la presente"), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'B', 18);
$pdf->SetXY(0, 95);
$pdf->Cell(210, 10, t("CONSTANCIA"), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'I', 14);
$pdf->SetXY(0, 106);
$pdf->Cell(210, 10, t("a:"), 0, 1, 'C');
$pdf->SetFont('Helvetica', '', 22);
$pdf->SetXY(0, 116);
$pdf->Cell(210, 10, t($alumno), 0, 1, 'C');
$pdf->SetFont('Helvetica', '', 13);
$pdf->SetXY(20, 135);
$pdf->MultiCell(170, 8, t("Por su participación en la Inyección Educativa"), 0, 'C');
$pdf->SetFont('Helvetica', 'B', 14);
$pdf->MultiCell(170, 8, t("{$curso}”,"), 0, 'C');
if ($horas) {
$pdf->SetFont('Helvetica', '', 12);
$pdf->MultiCell(170, 8, t("con duración de {$horas} horas."), 0, 'C');
}
// Píldoras
} elseif ($tipo === 'pildora') {
$pdf->SetFont('Helvetica', '', 16);
$pdf->SetXY(0, 82);
$pdf->Cell(210, 10, t("Otorga la presente"), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'B', 18);
$pdf->SetXY(0, 95);
$pdf->Cell(210, 10, t("CONSTANCIA"), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'I', 14);
$pdf->SetXY(0, 106);
$pdf->Cell(210, 10, t("a:"), 0, 1, 'C');
$pdf->SetFont('Helvetica', '', 22);
$pdf->SetXY(0, 116);
$pdf->Cell(210, 10, t($alumno), 0, 1, 'C');
$pdf->SetFont('Helvetica', '', 13);
$pdf->SetXY(20, 135);
$pdf->MultiCell(170, 8, t("Por su asistencia a la píldora educativa"), 0, 'C');
$pdf->SetFont('Helvetica', 'B', 14);
$pdf->MultiCell(170, 8, t("{$curso}"), 0, 'C');
if ($horas) {
$pdf->SetFont('Helvetica', '', 12);
$pdf->MultiCell(170, 8, t("con duración de {$horas} horas."), 0, 'C');
}
}
$pdf->SetFont('Helvetica', 'B', 12);
$pdf->SetXY(0, 245);
$pdf->Cell(210, 10, t("Dr. Juan Manuel Gutiérrez Méndez"), 0, 1, 'C');
$pdf->SetFont('Helvetica', '', 11);
$pdf->Cell(210, 6, t("Director de Proyectos"), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'I', 10);
$pdf->SetXY(0, 268);
$pdf->Cell(210, 6, t("Se expide en la ciudad de Xalapa, Ver., a los {$dia} días de {$mes} de {$anio}"), 0, 1, 'C');
ob_end_clean();
$pdf->Output();

6
composer.json Normal file
View File

@ -0,0 +1,6 @@
{
"require": {
"endroid/qr-code": "^4.3",
"phpmailer/phpmailer": "^6.10"
}
}

276
composer.lock generated Normal file
View File

@ -0,0 +1,276 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "79c894e68e581cbc2c5082bde193fbe1",
"packages": [
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phly/keep-a-changelog": "^2.1",
"phpunit/phpunit": "^7 | ^8 | ^9",
"spatie/phpunit-snapshot-assertions": "^4.2.9",
"squizlabs/php_codesniffer": "^3.4"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
},
"time": "2022-12-07T17:46:57+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.6",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.6"
},
"time": "2024-08-09T14:30:48+00:00"
},
{
"name": "endroid/qr-code",
"version": "4.3.5",
"source": {
"type": "git",
"url": "https://github.com/endroid/qr-code.git",
"reference": "5630e192948b466d418608ecce697465d20260af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/endroid/qr-code/zipball/5630e192948b466d418608ecce697465d20260af",
"reference": "5630e192948b466d418608ecce697465d20260af",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^2.0",
"php": "^7.3||^8.0"
},
"require-dev": {
"endroid/quality": "dev-master",
"ext-gd": "*",
"khanamiryan/qrcode-detector-decoder": "^1.0.4",
"setasign/fpdf": "^1.8.2"
},
"suggest": {
"ext-gd": "Enables you to write PNG images",
"khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
"roave/security-advisories": "Makes sure package versions with known security issues are not installed",
"setasign/fpdf": "Enables you to use the PDF writer"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
}
},
"autoload": {
"psr-4": {
"Endroid\\QrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeroen van den Enden",
"email": "info@endroid.nl"
}
],
"description": "Endroid QR Code",
"homepage": "https://github.com/endroid/qr-code",
"keywords": [
"code",
"endroid",
"php",
"qr",
"qrcode"
],
"support": {
"issues": "https://github.com/endroid/qr-code/issues",
"source": "https://github.com/endroid/qr-code/tree/4.3.5"
},
"funding": [
{
"url": "https://github.com/endroid",
"type": "github"
}
],
"time": "2021-09-17T07:50:43+00:00"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.10.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
},
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"time": "2025-04-24T15:19:31+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@ -2,36 +2,33 @@
include 'includes/config.php';
redirect_if_not_logged_in();
$profesor = $_SESSION['profesor'];
$profesor_id = $profesor['id'];
// Obtener estadísticas directamente desde PHP
$stmt = $pdo->prepare("
SELECT COUNT(*) as total FROM cursos
WHERE profesor_id = ? AND estado = 'activo'
");
$stmt->execute([$profesor['id']]);
$cursos_activos = $stmt->fetch()['total'];
function getCount($pdo, $sql, $params = []) {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetch()['total'] ?? 0;
}
$stmt = $pdo->prepare("
SELECT COUNT(DISTINCT a.id) as total
FROM alumnos a
JOIN alumnos_cursos ac ON a.id = ac.alumno_id
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?
");
$stmt->execute([$profesor['id']]);
$alumnos_registrados = $stmt->fetch()['total'];
$stmt = $pdo->prepare("
SELECT COUNT(*) as total
FROM diplomas d
JOIN alumnos_cursos ac ON d.alumno_curso_id = ac.id
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?
");
$stmt->execute([$profesor['id']]);
$diplomas_emitidos = $stmt->fetch()['total'];
// Estadísticas
$total_cursos = getCount($pdo, "SELECT COUNT(*) as total FROM cursos WHERE profesor_id = ?", [$profesor_id]);
$cursos_activos = getCount($pdo, "SELECT COUNT(*) as total FROM cursos WHERE profesor_id = ? AND estado = 'activo'", [$profesor_id]);
$cursos_inyeccion = getCount($pdo, "SELECT COUNT(*) as total FROM cursos WHERE profesor_id = ? AND tipo = 'inyeccion'", [$profesor_id]);
$cursos_pildora = getCount($pdo, "SELECT COUNT(*) as total FROM cursos WHERE profesor_id = ? AND tipo = 'pildora'", [$profesor_id]);
$cursos_tratamiento = getCount($pdo, "SELECT COUNT(*) as total FROM cursos WHERE profesor_id = ? AND tipo = 'tratamiento'", [$profesor_id]);
$alumnos_registrados = getCount($pdo, "
SELECT COUNT(DISTINCT a.id) as total
FROM alumnos a
JOIN alumnos_cursos ac ON a.id = ac.alumno_id
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?", [$profesor_id]);
$diplomas_emitidos = getCount($pdo, "
SELECT COUNT(*) as total
FROM diplomas d
JOIN alumnos_cursos ac ON d.alumno_curso_id = ac.id
JOIN cursos c ON ac.curso_id = c.id
WHERE c.profesor_id = ?", [$profesor_id]);
?>
<!DOCTYPE html>
<html lang="es">
@ -45,20 +42,19 @@ $diplomas_emitidos = $stmt->fetch()['total'];
<div id="app-content">
<header>
<h1>DiploMaster</h1>
<div id="profesor-info">
<div id="current-profesor" data-id="<?= $profesor['id'] ?>"></div>
<span><?= htmlspecialchars($profesor['nombre']) ?></span>
</div>
<div id="profesor-info">
<div id="current-profesor" data-id="<?= $profesor['id'] ?>"></div>
<span><?= htmlspecialchars($profesor['nombre']) ?></span>
</div>
</header>
<div class="main-container">
<div class="sidebar" id="sidebar">
<ul class="sidebar-menu">
<li class="active" data-section="dashboard"><span>🏠 Inicio</span></li>
<li data-section="courses"><span>📚 Mis Cursos</span></li>
<li data-section="courses"><span>📚 Gestión de Cursos</span></li>
<li data-section="students"><span>👨‍🎓 Gestión de Alumnos</span></li>
<li data-section="diplomas"><span>🎓 Diplomas Emitidos</span></li>
<li data-section="diplomas"><span>🎓 Gestión de Diplomas</span></li>
<li><a href="api/logout.php" class="logout-link">Cerrar sesión</a></li>
</ul>
</div>
@ -70,23 +66,24 @@ $diplomas_emitidos = $stmt->fetch()['total'];
<p>Este es tu panel de gestión de DiploMaster</p>
<div class="stats">
<p><strong>Resumen:</strong></p>
<p> <span id="active-courses-count"><?= $cursos_activos ?></span> cursos activos</p>
<p> <span id="students-count"><?= $alumnos_registrados ?></span> alumnos registrados</p>
<p> <span id="diplomas-count"><?= $diplomas_emitidos ?></span> diplomas emitidos</p>
<p> Total de cursos: <strong><?= $total_cursos ?></strong></p>
<p> Cursos activos: <strong id="active-courses-count"><?= $cursos_activos ?></strong></p>
<p> Tipo Inyección: <strong><?= $cursos_inyeccion ?></strong></p>
<p> Tipo Píldora: <strong><?= $cursos_pildora ?></strong></p>
<p> Tipo Tratamiento: <strong><?= $cursos_tratamiento ?></strong></p>
<p> Alumnos registrados: <strong id="students-count"><?= $alumnos_registrados ?></strong></p>
<p> Diplomas emitidos: <strong id="diplomas-count"><?= $diplomas_emitidos ?></strong></p>
</div>
</div>
</div>
<div id="courses-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<div id="students-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<div id="diplomas-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
</div>
</div>
@ -94,4 +91,4 @@ $diplomas_emitidos = $stmt->fetch()['total'];
<script src="assets/js/main.js"></script>
</body>
</html>
</html>

BIN
fpdf/.DS_Store vendored Normal file

Binary file not shown.

BIN
fpdf/formatodiploma.pdf Normal file

Binary file not shown.

3
fpdf/fpdi.php Normal file
View File

@ -0,0 +1,3 @@
<?php
require_once __DIR__ . '/fpdf.php';
require_once __DIR__ . '/src/autoload.php';

21
fpdf/src/FpdfTpl.php Normal file
View File

@ -0,0 +1,21 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
/**
* Class FpdfTpl
*
* This class adds a templating feature to FPDF.
*/
class FpdfTpl extends \FPDF
{
use FpdfTplTrait;
}

473
fpdf/src/FpdfTplTrait.php Normal file
View File

@ -0,0 +1,473 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
/**
* Trait FpdfTplTrait
*
* This trait adds a templating feature to FPDF and tFPDF.
*/
trait FpdfTplTrait
{
/**
* Data of all created templates.
*
* @var array
*/
protected $templates = [];
/**
* The template id for the currently created template.
*
* @var null|int
*/
protected $currentTemplateId;
/**
* A counter for template ids.
*
* @var int
*/
protected $templateId = 0;
/**
* Set the page format of the current page.
*
* @param array $size An array with two values defining the size.
* @param string $orientation "L" for landscape, "P" for portrait.
* @throws \BadMethodCallException
*/
public function setPageFormat($size, $orientation)
{
if ($this->currentTemplateId !== null) {
throw new \BadMethodCallException('The page format cannot be changed when writing to a template.');
}
if (!\in_array($orientation, ['P', 'L'], true)) {
throw new \InvalidArgumentException(\sprintf(
'Invalid page orientation "%s"! Only "P" and "L" are allowed!',
$orientation
));
}
$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;
$this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt);
}
}
/**
* Draws a template onto the page or another template.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $tpl The template id
* @param array|float|int $x The abscissa of upper-left corner. Alternatively you could use an assoc array
* with the keys "x", "y", "width", "height", "adjustPageSize".
* @param float|int $y The ordinate of upper-left corner.
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @param bool $adjustPageSize
* @return array The size
* @see FpdfTplTrait::getTemplateSize()
*/
public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
{
if (!isset($this->templates[$tpl])) {
throw new \InvalidArgumentException('Template does not exist!');
}
if (\is_array($x)) {
unset($x['tpl']);
\extract($x, EXTR_IF_EXISTS);
/** @noinspection NotOptimalIfConditionsInspection */
/** @phpstan-ignore function.alreadyNarrowedType */
if (\is_array($x)) {
$x = 0;
}
}
$template = $this->templates[$tpl];
$originalSize = $this->getTemplateSize($tpl);
$newSize = $this->getTemplateSize($tpl, $width, $height);
if ($adjustPageSize) {
$this->setPageFormat($newSize, $newSize['orientation']);
}
$this->_out(
// reset standard values, translate and scale
\sprintf(
'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q',
($newSize['width'] / $originalSize['width']),
($newSize['height'] / $originalSize['height']),
$x * $this->k,
($this->h - $y - $newSize['height']) * $this->k,
$template['id']
)
);
return $newSize;
}
/**
* Get the size of a template.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $tpl The template id
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
*/
public function getTemplateSize($tpl, $width = null, $height = null)
{
if (!isset($this->templates[$tpl])) {
return false;
}
if ($width === null && $height === null) {
$width = $this->templates[$tpl]['width'];
$height = $this->templates[$tpl]['height'];
} elseif ($width === null) {
$width = $height * $this->templates[$tpl]['width'] / $this->templates[$tpl]['height'];
}
if ($height === null) {
$height = $width * $this->templates[$tpl]['height'] / $this->templates[$tpl]['width'];
}
if ($height <= 0. || $width <= 0.) {
throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.');
}
return [
'width' => $width,
'height' => $height,
0 => $width,
1 => $height,
'orientation' => $width > $height ? 'L' : 'P'
];
}
/**
* Begins a new template.
*
* @param float|int|null $width The width of the template. If null, the current page width is used.
* @param float|int|null $height The height of the template. If null, the current page height is used.
* @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used).
* @return int A template identifier.
*/
public function beginTemplate($width = null, $height = null, $groupXObject = false)
{
if ($width === null) {
$width = $this->w;
}
if ($height === null) {
$height = $this->h;
}
$templateId = $this->getNextTemplateId();
// initiate buffer with current state of FPDF
$buffer = "2 J\n"
. \sprintf('%.2F w', $this->LineWidth * $this->k) . "\n";
if ($this->FontFamily) {
$buffer .= \sprintf("BT /F%d %.2F Tf ET\n", $this->CurrentFont['i'], $this->FontSizePt);
}
if ($this->DrawColor !== '0 G') {
$buffer .= $this->DrawColor . "\n";
}
if ($this->FillColor !== '0 g') {
$buffer .= $this->FillColor . "\n";
}
if ($groupXObject && \version_compare('1.4', $this->PDFVersion, '>')) {
$this->PDFVersion = '1.4';
}
$this->templates[$templateId] = [
'objectNumber' => null,
'id' => 'TPL' . $templateId,
'buffer' => $buffer,
'width' => $width,
'height' => $height,
'groupXObject' => $groupXObject,
'state' => [
'x' => $this->x,
'y' => $this->y,
'AutoPageBreak' => $this->AutoPageBreak,
'bMargin' => $this->bMargin,
'tMargin' => $this->tMargin,
'lMargin' => $this->lMargin,
'rMargin' => $this->rMargin,
'h' => $this->h,
'hPt' => $this->hPt,
'w' => $this->w,
'wPt' => $this->wPt,
'FontFamily' => $this->FontFamily,
'FontStyle' => $this->FontStyle,
'FontSizePt' => $this->FontSizePt,
'FontSize' => $this->FontSize,
'underline' => $this->underline,
'TextColor' => $this->TextColor,
'DrawColor' => $this->DrawColor,
'FillColor' => $this->FillColor,
'ColorFlag' => $this->ColorFlag
]
];
$this->SetAutoPageBreak(false);
$this->currentTemplateId = $templateId;
$this->h = $height;
$this->hPt = $height * $this->k;
$this->w = $width;
$this->wPt = $width * $this->k;
$this->SetXY($this->lMargin, $this->tMargin);
$this->SetRightMargin($this->w - $width + $this->rMargin);
return $templateId;
}
/**
* Ends a template.
*
* @return bool|int|null A template identifier.
*/
public function endTemplate()
{
if ($this->currentTemplateId === null) {
return false;
}
$templateId = $this->currentTemplateId;
$template = $this->templates[$templateId];
$state = $template['state'];
$this->SetXY($state['x'], $state['y']);
$this->tMargin = $state['tMargin'];
$this->lMargin = $state['lMargin'];
$this->rMargin = $state['rMargin'];
$this->h = $state['h'];
$this->hPt = $state['hPt'];
$this->w = $state['w'];
$this->wPt = $state['wPt'];
$this->SetAutoPageBreak($state['AutoPageBreak'], $state['bMargin']);
$this->FontFamily = $state['FontFamily'];
$this->FontStyle = $state['FontStyle'];
$this->FontSizePt = $state['FontSizePt'];
$this->FontSize = $state['FontSize'];
$this->TextColor = $state['TextColor'];
$this->DrawColor = $state['DrawColor'];
$this->FillColor = $state['FillColor'];
$this->ColorFlag = $state['ColorFlag'];
$this->underline = $state['underline'];
$fontKey = $this->FontFamily . $this->FontStyle;
if ($fontKey) {
$this->CurrentFont =& $this->fonts[$fontKey];
} else {
unset($this->CurrentFont);
}
$this->currentTemplateId = null;
return $templateId;
}
/**
* Get the next template id.
*
* @return int
*/
protected function getNextTemplateId()
{
return $this->templateId++;
}
/* overwritten FPDF methods: */
/**
* @inheritdoc
*/
public function AddPage($orientation = '', $size = '', $rotation = 0)
{
if ($this->currentTemplateId !== null) {
throw new \BadMethodCallException('Pages cannot be added when writing to a template.');
}
parent::AddPage($orientation, $size, $rotation);
}
/**
* @inheritdoc
*/
public function Link($x, $y, $w, $h, $link)
{
if ($this->currentTemplateId !== null) {
throw new \BadMethodCallException('Links cannot be set when writing to a template.');
}
parent::Link($x, $y, $w, $h, $link);
}
/**
* @inheritdoc
*/
public function SetLink($link, $y = 0, $page = -1)
{
if ($this->currentTemplateId !== null) {
throw new \BadMethodCallException('Links cannot be set when writing to a template.');
}
return parent::SetLink($link, $y, $page);
}
/**
* @inheritdoc
*/
public function SetDrawColor($r, $g = null, $b = null)
{
parent::SetDrawColor($r, $g, $b);
if ($this->page === 0 && $this->currentTemplateId !== null) {
$this->_out($this->DrawColor);
}
}
/**
* @inheritdoc
*/
public function SetFillColor($r, $g = null, $b = null)
{
parent::SetFillColor($r, $g, $b);
if ($this->page === 0 && $this->currentTemplateId !== null) {
$this->_out($this->FillColor);
}
}
/**
* @inheritdoc
*/
public function SetLineWidth($width)
{
parent::SetLineWidth($width);
if ($this->page === 0 && $this->currentTemplateId !== null) {
$this->_out(\sprintf('%.2F w', $width * $this->k));
}
}
/**
* @inheritdoc
*/
public function SetFont($family, $style = '', $size = 0)
{
parent::SetFont($family, $style, $size);
if ($this->page === 0 && $this->currentTemplateId !== null) {
$this->_out(\sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
}
}
/**
* @inheritdoc
*/
public function SetFontSize($size)
{
parent::SetFontSize($size);
if ($this->page === 0 && $this->currentTemplateId !== null) {
$this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt));
}
}
protected function _putimages()
{
parent::_putimages();
foreach ($this->templates as $key => $template) {
$this->_newobj();
$this->templates[$key]['objectNumber'] = $this->n;
$this->_put('<</Type /XObject /Subtype /Form /FormType 1');
$this->_put(\sprintf(
'/BBox[0 0 %.2F %.2F]',
$template['width'] * $this->k,
$template['height'] * $this->k
));
$this->_put('/Resources 2 0 R'); // default resources dictionary of FPDF
if ($this->compress) {
$buffer = \gzcompress($template['buffer']);
$this->_put('/Filter/FlateDecode');
} else {
$buffer = $template['buffer'];
}
$this->_put('/Length ' . \strlen($buffer));
if ($template['groupXObject']) {
$this->_put('/Group <</Type/Group/S/Transparency>>');
}
$this->_put('>>');
$this->_putstream($buffer);
$this->_put('endobj');
}
}
/**
* @inheritdoc
*/
protected function _putxobjectdict()
{
foreach ($this->templates as $key => $template) {
$this->_put('/' . $template['id'] . ' ' . $template['objectNumber'] . ' 0 R');
}
parent::_putxobjectdict();
}
/**
* @inheritdoc
*/
public function _out($s)
{
if ($this->currentTemplateId !== null) {
$this->templates[$this->currentTemplateId]['buffer'] .= $s . "\n";
} else {
parent::_out($s);
}
}
}

193
fpdf/src/FpdfTrait.php Normal file
View File

@ -0,0 +1,193 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfNull;
use setasign\Fpdi\PdfParser\Type\PdfType;
/**
* This trait is used for the implementation of FPDI in FPDF and tFPDF.
*/
trait FpdfTrait
{
protected function _enddoc()
{
parent::_enddoc();
$this->cleanUp();
}
/**
* Draws an imported page or a template onto the page or another template.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $tpl The template id
* @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
* with the keys "x", "y", "width", "height", "adjustPageSize".
* @param float|int $y The ordinate of upper-left corner.
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @param bool $adjustPageSize
* @return array The size
* @see Fpdi::getTemplateSize()
*/
public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
{
if (isset($this->importedPages[$tpl])) {
$size = $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize);
if ($this->currentTemplateId !== null) {
$this->templates[$this->currentTemplateId]['resources']['templates']['importedPages'][$tpl] = $tpl;
}
return $size;
}
return parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize);
}
/**
* Get the size of an imported page or template.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $tpl The template id
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
*/
public function getTemplateSize($tpl, $width = null, $height = null)
{
$size = parent::getTemplateSize($tpl, $width, $height);
if ($size === false) {
return $this->getImportedPageSize($tpl, $width, $height);
}
return $size;
}
/**
* @throws CrossReferenceException
* @throws PdfParserException
*/
protected function _putimages()
{
$this->currentReaderId = null;
parent::_putimages();
foreach ($this->importedPages as $key => $pageData) {
$this->_newobj();
$this->importedPages[$key]['objectNumber'] = $this->n;
$this->currentReaderId = $pageData['readerId'];
$this->writePdfType($pageData['stream']);
$this->_put('endobj');
}
foreach (\array_keys($this->readers) as $readerId) {
$parser = $this->getPdfReader($readerId)->getParser();
$this->currentReaderId = $readerId;
while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) {
try {
$object = $parser->getIndirectObject($objectNumber);
} catch (CrossReferenceException $e) {
if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) {
$object = PdfIndirectObject::create($objectNumber, 0, new PdfNull());
} else {
throw $e;
}
}
$this->writePdfType($object);
}
}
$this->currentReaderId = null;
}
/**
* @inheritdoc
*/
protected function _putxobjectdict()
{
foreach ($this->importedPages as $pageData) {
$this->_put('/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R');
}
parent::_putxobjectdict();
}
/**
* @param int $n
* @return void
* @throws PdfParser\Type\PdfTypeException
*/
protected function _putlinks($n)
{
foreach ($this->PageLinks[$n] as $pl) {
$this->_newobj();
$rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]);
$this->_put('<</Type /Annot /Subtype /Link /Rect [' . $rect . ']', false);
if (is_string($pl[4])) {
if (isset($pl['importedLink'])) {
$this->_put('/A <</S /URI /URI (' . $this->_escape($pl[4]) . ')>>');
$values = $pl['importedLink']['pdfObject']->value;
foreach ($values as $name => $entry) {
$this->_put('/' . $name . ' ', false);
$this->writePdfType($entry);
}
if (isset($pl['quadPoints'])) {
$s = '/QuadPoints[';
foreach ($pl['quadPoints'] as $value) {
$s .= sprintf('%.2F ', $value);
}
$s .= ']';
$this->_put($s);
}
} else {
$this->_put('/A <</S /URI /URI ' . $this->_textstring($pl[4]) . '>>');
$this->_put('/Border [0 0 0]', false);
}
$this->_put('>>');
} else {
$this->_put('/Border [0 0 0] ', false);
$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]>>',
$this->PageInfo[$l[0]]['n'],
$h - $l[1] * $this->k
));
}
$this->_put('endobj');
}
}
protected function _put($s, $newLine = true)
{
if ($newLine) {
$this->buffer .= $s . "\n";
} else {
$this->buffer .= $s;
}
}
}

34
fpdf/src/Fpdi.php Normal file
View File

@ -0,0 +1,34 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfNull;
/**
* Class Fpdi
*
* This class let you import pages of existing PDF documents into a reusable structure for FPDF.
*/
class Fpdi extends FpdfTpl
{
use FpdiTrait;
use FpdfTrait;
/**
* FPDI version
*
* @string
*/
const VERSION = '2.6.3';
}

View File

@ -0,0 +1,18 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
/**
* Base exception class for the FPDI package.
*/
class FpdiException extends \Exception
{
}

656
fpdf/src/FpdiTrait.php Normal file
View File

@ -0,0 +1,656 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\Filter\FilterException;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\StreamReader;
use setasign\Fpdi\PdfParser\Type\PdfArray;
use setasign\Fpdi\PdfParser\Type\PdfBoolean;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfHexString;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
use setasign\Fpdi\PdfParser\Type\PdfName;
use setasign\Fpdi\PdfParser\Type\PdfNull;
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
use setasign\Fpdi\PdfParser\Type\PdfStream;
use setasign\Fpdi\PdfParser\Type\PdfString;
use setasign\Fpdi\PdfParser\Type\PdfToken;
use setasign\Fpdi\PdfParser\Type\PdfType;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
use setasign\Fpdi\PdfReader\DataStructure\Rectangle;
use setasign\Fpdi\PdfReader\PageBoundaries;
use setasign\Fpdi\PdfReader\PdfReader;
use setasign\Fpdi\PdfReader\PdfReaderException;
use /* This namespace/class is used by the commercial FPDI PDF-Parser add-on. */
/** @noinspection PhpUndefinedClassInspection */
/** @noinspection PhpUndefinedNamespaceInspection */
setasign\FpdiPdfParser\PdfParser\PdfParser as FpdiPdfParser;
/**
* The FpdiTrait
*
* This trait offers the core functionalities of FPDI. By passing them to a trait we can reuse it with e.g. TCPDF in a
* very easy way.
*/
trait FpdiTrait
{
/**
* The pdf reader instances.
*
* @var PdfReader[]
*/
protected $readers = [];
/**
* Instances created internally.
*
* @var array
*/
protected $createdReaders = [];
/**
* The current reader id.
*
* @var string|null
*/
protected $currentReaderId;
/**
* Data of all imported pages.
*
* @var array
*/
protected $importedPages = [];
/**
* A map from object numbers of imported objects to new assigned object numbers by FPDF.
*
* @var array
*/
protected $objectMap = [];
/**
* An array with information about objects, which needs to be copied to the resulting document.
*
* @var array
*/
protected $objectsToCopy = [];
/**
* Release resources and file handles.
*
* This method is called internally when the document is created successfully. By default it only cleans up
* stream reader instances which were created internally.
*
* @param bool $allReaders
*/
public function cleanUp($allReaders = false)
{
$readers = $allReaders ? array_keys($this->readers) : $this->createdReaders;
foreach ($readers as $id) {
$this->readers[$id]->getParser()->getStreamReader()->cleanUp();
unset($this->readers[$id]);
}
$this->createdReaders = [];
}
/**
* Set the minimal PDF version.
*
* @param string $pdfVersion
*/
protected function setMinPdfVersion($pdfVersion)
{
if (\version_compare($pdfVersion, $this->PDFVersion, '>')) {
$this->PDFVersion = $pdfVersion;
}
}
/** @noinspection PhpUndefinedClassInspection */
/**
* Get a new pdf parser instance.
*
* @param StreamReader $streamReader
* @param array $parserParams Individual parameters passed to the parser instance.
* @return PdfParser|FpdiPdfParser
*/
protected function getPdfParserInstance(StreamReader $streamReader, array $parserParams = [])
{
// note: if you get an exception here - turn off errors/warnings on not found classes for your autoloader.
// psr-4 (https://www.php-fig.org/psr/psr-4/) says: Autoloader implementations MUST NOT throw
// exceptions, MUST NOT raise errors of any level, and SHOULD NOT return a value.
/** @noinspection PhpUndefinedClassInspection */
if (\class_exists(FpdiPdfParser::class)) {
/** @noinspection PhpUndefinedClassInspection */
return new FpdiPdfParser($streamReader, $parserParams);
}
return new PdfParser($streamReader);
}
/**
* Get an unique reader id by the $file parameter.
*
* @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader
* instance or a StreamReader instance.
* @param array $parserParams Individual parameters passed to the parser instance.
* @return string
*/
protected function getPdfReaderId($file, array $parserParams = [])
{
if (\is_resource($file)) {
$id = (string) $file;
} elseif (\is_string($file)) {
$id = \realpath($file);
if ($id === false) {
$id = $file;
}
} elseif (\is_object($file)) {
$id = \spl_object_hash($file);
} else {
throw new \InvalidArgumentException(
\sprintf('Invalid type in $file parameter (%s)', \gettype($file))
);
}
/** @noinspection OffsetOperationsInspection */
if (isset($this->readers[$id])) {
return $id;
}
if (\is_resource($file)) {
$streamReader = new StreamReader($file);
} elseif (\is_string($file)) {
$streamReader = StreamReader::createByFile($file);
$this->createdReaders[] = $id;
} else {
$streamReader = $file;
}
$reader = new PdfReader($this->getPdfParserInstance($streamReader, $parserParams));
/** @noinspection OffsetOperationsInspection */
$this->readers[$id] = $reader;
return $id;
}
/**
* Get a pdf reader instance by its id.
*
* @param string $id
* @return PdfReader
*/
protected function getPdfReader($id)
{
if (isset($this->readers[$id])) {
return $this->readers[$id];
}
throw new \InvalidArgumentException(
\sprintf('No pdf reader with the given id (%s) exists.', $id)
);
}
/**
* Set the source PDF file.
*
* @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance.
* @return int The page count of the PDF document.
* @throws PdfParserException
*/
public function setSourceFile($file)
{
return $this->setSourceFileWithParserParams($file);
}
/**
* Set the source PDF file with parameters which are passed to the parser instance.
*
* This method allows us to pass e.g. authentication information to the parser instance.
*
* @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance.
* @param array $parserParams Individual parameters passed to the parser instance.
* @return int The page count of the PDF document.
* @throws CrossReferenceException
* @throws PdfParserException
* @throws PdfTypeException
*/
public function setSourceFileWithParserParams($file, array $parserParams = [])
{
$this->currentReaderId = $this->getPdfReaderId($file, $parserParams);
$this->objectsToCopy[$this->currentReaderId] = [];
$reader = $this->getPdfReader($this->currentReaderId);
$this->setMinPdfVersion($reader->getPdfVersion());
return $reader->getPageCount();
}
/**
* Imports a page.
*
* @param int $pageNumber The page number.
* @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX.
* @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used).
* @param bool $importExternalLinks Define whether external links are imported or not.
* @return string A unique string identifying the imported page.
* @throws CrossReferenceException
* @throws FilterException
* @throws PdfParserException
* @throws PdfTypeException
* @throws PdfReaderException
* @see PageBoundaries
*/
public function importPage(
$pageNumber,
$box = PageBoundaries::CROP_BOX,
$groupXObject = true,
$importExternalLinks = false
) {
if ($this->currentReaderId === null) {
throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.');
}
$pageId = $this->currentReaderId;
$pageNumber = (int)$pageNumber;
$pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0') . '|' . ($importExternalLinks ? '1' : '0');
// for backwards compatibility with FPDI 1
$box = \ltrim($box, '/');
if (!PageBoundaries::isValidName($box)) {
throw new \InvalidArgumentException(
\sprintf('Box name is invalid: "%s"', $box)
);
}
$pageId .= '|' . $box;
if (isset($this->importedPages[$pageId])) {
return $pageId;
}
$reader = $this->getPdfReader($this->currentReaderId);
$page = $reader->getPage($pageNumber);
$bbox = $page->getBoundary($box);
if ($bbox === false) {
throw new PdfReaderException(
\sprintf("Page doesn't have a boundary box (%s).", $box),
PdfReaderException::MISSING_DATA
);
}
$dict = new PdfDictionary();
$dict->value['Type'] = PdfName::create('XObject');
$dict->value['Subtype'] = PdfName::create('Form');
$dict->value['FormType'] = PdfNumeric::create(1);
$dict->value['BBox'] = $bbox->toPdfArray();
if ($groupXObject) {
$this->setMinPdfVersion('1.4');
$dict->value['Group'] = PdfDictionary::create([
'Type' => PdfName::create('Group'),
'S' => PdfName::create('Transparency')
]);
}
$resources = $page->getAttribute('Resources');
if ($resources !== null) {
$dict->value['Resources'] = $resources;
}
list($width, $height) = $page->getWidthAndHeight($box);
$a = 1;
$b = 0;
$c = 0;
$d = 1;
$e = -$bbox->getLlx();
$f = -$bbox->getLly();
$rotation = $page->getRotation();
if ($rotation !== 0) {
$rotation *= -1;
$angle = $rotation * M_PI / 180;
$a = \cos($angle);
$b = \sin($angle);
$c = -$b;
$d = $a;
switch ($rotation) {
case -90:
$e = -$bbox->getLly();
$f = $bbox->getUrx();
break;
case -180:
$e = $bbox->getUrx();
$f = $bbox->getUry();
break;
case -270:
$e = $bbox->getUry();
$f = -$bbox->getLlx();
break;
}
}
// we need to rotate/translate
if ($a != 1 || $b != 0 || $c != 0 || $d != 1 || $e != 0 || $f != 0) {
$dict->value['Matrix'] = PdfArray::create([
PdfNumeric::create($a), PdfNumeric::create($b), PdfNumeric::create($c),
PdfNumeric::create($d), PdfNumeric::create($e), PdfNumeric::create($f)
]);
}
// try to use the existing content stream
$pageDict = $page->getPageDictionary();
try {
$contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true);
$contents = PdfType::resolve($contentsObject, $reader->getParser());
// just copy the stream reference if it is only a single stream
if (
($contentsIsStream = ($contents instanceof PdfStream))
|| ($contents instanceof PdfArray && \count($contents->value) === 1)
) {
if ($contentsIsStream) {
/**
* @var PdfIndirectObject $contentsObject
*/
$stream = $contents;
} else {
$stream = PdfType::resolve($contents->value[0], $reader->getParser());
}
$filter = PdfDictionary::get($stream->value, 'Filter');
if (!$filter instanceof PdfNull) {
$dict->value['Filter'] = $filter;
}
$length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser());
$dict->value['Length'] = $length;
$stream->value = $dict;
// otherwise extract it from the array and re-compress the whole stream
} else {
$streamContent = $this->compress
? \gzcompress($page->getContentStream())
: $page->getContentStream();
$dict->value['Length'] = PdfNumeric::create(\strlen($streamContent));
if ($this->compress) {
$dict->value['Filter'] = PdfName::create('FlateDecode');
}
$stream = PdfStream::create($dict, $streamContent);
}
// Catch faulty pages and use an empty content stream
} catch (FpdiException $e) {
$dict->value['Length'] = PdfNumeric::create(0);
$stream = PdfStream::create($dict, '');
}
$externalLinks = [];
if ($importExternalLinks) {
$externalLinks = $page->getExternalLinks($box);
}
$this->importedPages[$pageId] = [
'objectNumber' => null,
'readerId' => $this->currentReaderId,
'id' => 'TPL' . $this->getNextTemplateId(),
'width' => $width / $this->k,
'height' => $height / $this->k,
'stream' => $stream,
'externalLinks' => $externalLinks
];
return $pageId;
}
/**
* Draws an imported page onto the page.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $pageId The page id
* @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
* with the keys "x", "y", "width", "height", "adjustPageSize".
* @param float|int $y The ordinate of upper-left corner.
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @param bool $adjustPageSize
* @return array The size.
* @see Fpdi::getTemplateSize()
*/
public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
{
if (\is_array($x)) {
/** @noinspection OffsetOperationsInspection */
unset($x['pageId']);
\extract($x, EXTR_IF_EXISTS);
/** @noinspection NotOptimalIfConditionsInspection */
/** @phpstan-ignore function.alreadyNarrowedType */
if (\is_array($x)) {
$x = 0;
}
}
if (!isset($this->importedPages[$pageId])) {
throw new \InvalidArgumentException('Imported page does not exist!');
}
$importedPage = $this->importedPages[$pageId];
$originalSize = $this->getTemplateSize($pageId);
$newSize = $this->getTemplateSize($pageId, $width, $height);
if ($adjustPageSize) {
$this->setPageFormat($newSize, $newSize['orientation']);
}
$scaleX = ($newSize['width'] / $originalSize['width']);
$scaleY = ($newSize['height'] / $originalSize['height']);
$xPt = $x * $this->k;
$yPt = $y * $this->k;
$newHeightPt = $newSize['height'] * $this->k;
$this->_out(
// reset standard values, translate and scale
\sprintf(
'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q',
$scaleX,
$scaleY,
$xPt,
$this->hPt - $yPt - $newHeightPt,
$importedPage['id']
)
);
if (count($importedPage['externalLinks']) > 0) {
foreach ($importedPage['externalLinks'] as $externalLink) {
// mPDF uses also 'externalLinks' but doesn't come with a rect-value
if (!isset($externalLink['rect'])) {
continue;
}
/** @var Rectangle $rect */
$rect = $externalLink['rect'];
$this->Link(
$x + $rect->getLlx() / $this->k * $scaleX,
$y + $newSize['height'] - ($rect->getLly() + $rect->getHeight()) / $this->k * $scaleY,
$rect->getWidth() / $this->k * $scaleX,
$rect->getHeight() / $this->k * $scaleY,
$externalLink['uri']
);
$this->adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage);
}
}
return $newSize;
}
/**
* This method will add additional data to the last created link/annotation.
*
* It is separated because TCPDF uses its own logic to handle link annotations.
* This method is overwritten in the TCPDF implementation.
*
* @param array $externalLink
* @param float|int $xPt
* @param float|int $scaleX
* @param float|int $yPt
* @param float|int $newHeightPt
* @param float|int $scaleY
* @param array $importedPage
* @return void
*/
protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage)
{
// let's create a relation of the newly created link to the data of the external link
$lastLink = count($this->PageLinks[$this->page]);
$this->PageLinks[$this->page][$lastLink - 1]['importedLink'] = $externalLink;
if (count($externalLink['quadPoints']) > 0) {
$quadPoints = [];
for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) {
$quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX;
$quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY;
}
$this->PageLinks[$this->page][$lastLink - 1]['quadPoints'] = $quadPoints;
}
}
/**
* Get the size of an imported page.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $tpl The template id
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
*/
public function getImportedPageSize($tpl, $width = null, $height = null)
{
if (isset($this->importedPages[$tpl])) {
$importedPage = $this->importedPages[$tpl];
if ($width === null && $height === null) {
$width = $importedPage['width'];
$height = $importedPage['height'];
} elseif ($width === null) {
$width = $height * $importedPage['width'] / $importedPage['height'];
}
if ($height === null) {
$height = $width * $importedPage['height'] / $importedPage['width'];
}
if ($height <= 0. || $width <= 0.) {
throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.');
}
return [
'width' => $width,
'height' => $height,
0 => $width,
1 => $height,
'orientation' => $width > $height ? 'L' : 'P'
];
}
return false;
}
/**
* Writes a PdfType object to the resulting buffer.
*
* @param PdfType $value
* @throws PdfTypeException
*/
protected function writePdfType(PdfType $value)
{
if ($value instanceof PdfNumeric) {
if (\is_int($value->value)) {
$this->_put($value->value . ' ', false);
} else {
$this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false);
}
} elseif ($value instanceof PdfName) {
$this->_put('/' . $value->value . ' ', false);
} elseif ($value instanceof PdfString) {
$this->_put('(' . $value->value . ')', false);
} elseif ($value instanceof PdfHexString) {
$this->_put('<' . $value->value . '>', false);
} elseif ($value instanceof PdfBoolean) {
$this->_put($value->value ? 'true ' : 'false ', false);
} elseif ($value instanceof PdfArray) {
$this->_put('[', false);
foreach ($value->value as $entry) {
$this->writePdfType($entry);
}
$this->_put(']');
} elseif ($value instanceof PdfDictionary) {
$this->_put('<<', false);
foreach ($value->value as $name => $entry) {
$this->_put('/' . $name . ' ', false);
$this->writePdfType($entry);
}
$this->_put('>>');
} elseif ($value instanceof PdfToken) {
$this->_put($value->value);
} elseif ($value instanceof PdfNull) {
$this->_put('null ', false);
} elseif ($value instanceof PdfStream) {
$this->writePdfType($value->value);
$this->_put('stream');
$this->_put($value->getStream());
$this->_put('endstream');
} elseif ($value instanceof PdfIndirectObjectReference) {
if (!isset($this->objectMap[$this->currentReaderId])) {
$this->objectMap[$this->currentReaderId] = [];
}
if (!isset($this->objectMap[$this->currentReaderId][$value->value])) {
$this->objectMap[$this->currentReaderId][$value->value] = ++$this->n;
$this->objectsToCopy[$this->currentReaderId][] = $value->value;
}
$this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false);
} elseif ($value instanceof PdfIndirectObject) {
$n = $this->objectMap[$this->currentReaderId][$value->objectNumber];
$this->_newobj($n);
$this->writePdfType($value->value);
// add newline before "endobj" for all objects in view to PDF/A conformance
if (
!(
($value->value instanceof PdfArray) ||
($value->value instanceof PdfDictionary) ||
($value->value instanceof PdfToken) ||
($value->value instanceof PdfStream)
)
) {
$this->_put("\n", false);
}
$this->_put('endobj');
}
}
}

View File

@ -0,0 +1,97 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
use setasign\Fpdi\Math\Matrix;
use setasign\Fpdi\Math\Vector;
/**
* A simple graphic state class which holds the current transformation matrix.
*/
class GraphicsState
{
/**
* @var Matrix
*/
protected $ctm;
/**
* @param Matrix|null $ctm
*/
public function __construct(?Matrix $ctm = null)
{
if ($ctm === null) {
$ctm = new Matrix();
}
$this->ctm = $ctm;
}
/**
* @param Matrix $matrix
* @return $this
*/
public function add(Matrix $matrix)
{
$this->ctm = $matrix->multiply($this->ctm);
return $this;
}
/**
* @param int|float $x
* @param int|float $y
* @param int|float $angle
* @return $this
*/
public function rotate($x, $y, $angle)
{
if (abs($angle) < 1e-5) {
return $this;
}
$angle = deg2rad($angle);
$c = cos($angle);
$s = sin($angle);
$this->add(new Matrix($c, $s, -$s, $c, $x, $y));
return $this->translate(-$x, -$y);
}
/**
* @param int|float $shiftX
* @param int|float $shiftY
* @return $this
*/
public function translate($shiftX, $shiftY)
{
return $this->add(new Matrix(1, 0, 0, 1, $shiftX, $shiftY));
}
/**
* @param int|float $scaleX
* @param int|float $scaleY
* @return $this
*/
public function scale($scaleX, $scaleY)
{
return $this->add(new Matrix($scaleX, 0, 0, $scaleY, 0, 0));
}
/**
* @param Vector $vector
* @return Vector
*/
public function toUserSpace(Vector $vector)
{
return $vector->multiplyWithMatrix($this->ctm);
}
}

116
fpdf/src/Math/Matrix.php Normal file
View File

@ -0,0 +1,116 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\Math;
/**
* A simple 2D-Matrix class
*/
class Matrix
{
/**
* @var float
*/
protected $a;
/**
* @var float
*/
protected $b;
/**
* @var float
*/
protected $c;
/**
* @var float
*/
protected $d;
/**
* @var float
*/
protected $e;
/**
* @var float
*/
protected $f;
/**
* @param int|float $a
* @param int|float $b
* @param int|float $c
* @param int|float $d
* @param int|float $e
* @param int|float $f
*/
public function __construct($a = 1, $b = 0, $c = 0, $d = 1, $e = 0, $f = 0)
{
$this->a = (float)$a;
$this->b = (float)$b;
$this->c = (float)$c;
$this->d = (float)$d;
$this->e = (float)$e;
$this->f = (float)$f;
}
/**
* @return float[]
*/
public function getValues()
{
return [$this->a, $this->b, $this->c, $this->d, $this->e, $this->f];
}
/**
* @param Matrix $by
* @return Matrix
*/
public function multiply(self $by)
{
$a =
$this->a * $by->a
+ $this->b * $by->c
//+ 0 * $by->e
;
$b =
$this->a * $by->b
+ $this->b * $by->d
//+ 0 * $by->f
;
$c =
$this->c * $by->a
+ $this->d * $by->c
//+ 0 * $by->e
;
$d =
$this->c * $by->b
+ $this->d * $by->d
//+ 0 * $by->f
;
$e =
$this->e * $by->a
+ $this->f * $by->c
+ /*1 * */$by->e;
$f =
$this->e * $by->b
+ $this->f * $by->d
+ /*1 * */$by->f;
return new self($a, $b, $c, $d, $e, $f);
}
}

66
fpdf/src/Math/Vector.php Normal file
View File

@ -0,0 +1,66 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\Math;
/**
* A simple 2D-Vector class
*/
class Vector
{
/**
* @var float
*/
protected $x;
/**
* @var float
*/
protected $y;
/**
* @param int|float $x
* @param int|float $y
*/
public function __construct($x = .0, $y = .0)
{
$this->x = (float)$x;
$this->y = (float)$y;
}
/**
* @return float
*/
public function getX()
{
return $this->x;
}
/**
* @return float
*/
public function getY()
{
return $this->y;
}
/**
* @param Matrix $matrix
* @return Vector
*/
public function multiplyWithMatrix(Matrix $matrix)
{
list($a, $b, $c, $d, $e, $f) = $matrix->getValues();
$x = $a * $this->x + $c * $this->y + $e;
$y = $b * $this->x + $d * $this->y + $f;
return new self($x, $y);
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\CrossReference;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfToken;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
/**
* Abstract class for cross-reference reader classes.
*/
abstract class AbstractReader
{
/**
* @var PdfParser
*/
protected $parser;
/**
* @var PdfDictionary
*/
protected $trailer;
/**
* AbstractReader constructor.
*
* @param PdfParser $parser
* @throws CrossReferenceException
* @throws PdfTypeException
*/
public function __construct(PdfParser $parser)
{
$this->parser = $parser;
$this->readTrailer();
}
/**
* Get the trailer dictionary.
*
* @return PdfDictionary
*/
public function getTrailer()
{
return $this->trailer;
}
/**
* Read the trailer dictionary.
*
* @throws CrossReferenceException
* @throws PdfTypeException
*/
protected function readTrailer()
{
try {
$trailerKeyword = $this->parser->readValue(null, PdfToken::class);
if ($trailerKeyword->value !== 'trailer') {
throw new CrossReferenceException(
\sprintf(
'Unexpected end of cross reference. "trailer"-keyword expected, got: %s.',
$trailerKeyword->value
),
CrossReferenceException::UNEXPECTED_END
);
}
} catch (PdfTypeException $e) {
throw new CrossReferenceException(
'Unexpected end of cross reference. "trailer"-keyword expected, got an invalid object type.',
CrossReferenceException::UNEXPECTED_END,
$e
);
}
try {
$trailer = $this->parser->readValue(null, PdfDictionary::class);
} catch (PdfTypeException $e) {
throw new CrossReferenceException(
'Unexpected end of cross reference. Trailer not found.',
CrossReferenceException::UNEXPECTED_END,
$e
);
}
$this->trailer = $trailer;
}
}

View File

@ -0,0 +1,326 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\CrossReference;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
use setasign\Fpdi\PdfParser\Type\PdfStream;
use setasign\Fpdi\PdfParser\Type\PdfToken;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
/**
* Class CrossReference
*
* This class processes the standard cross reference of a PDF document.
*/
class CrossReference
{
/**
* The byte length in which the "startxref" keyword should be searched.
*
* @var int
*/
public static $trailerSearchLength = 5500;
/**
* @var int
*/
protected $fileHeaderOffset = 0;
/**
* @var PdfParser
*/
protected $parser;
/**
* @var ReaderInterface[]
*/
protected $readers = [];
/**
* CrossReference constructor.
*
* @param PdfParser $parser
* @throws CrossReferenceException
* @throws PdfTypeException
*/
public function __construct(PdfParser $parser, $fileHeaderOffset = 0)
{
$this->parser = $parser;
$this->fileHeaderOffset = $fileHeaderOffset;
$offset = $this->findStartXref();
$reader = null;
/** @noinspection TypeUnsafeComparisonInspection */
while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0
try {
$reader = $this->readXref($offset + $this->fileHeaderOffset);
} catch (CrossReferenceException $e) {
// sometimes the file header offset is part of the byte offsets, so let's retry by resetting it to zero.
if ($e->getCode() === CrossReferenceException::INVALID_DATA && $this->fileHeaderOffset !== 0) {
$this->fileHeaderOffset = 0;
$reader = $this->readXref($offset);
} else {
throw $e;
}
}
$trailer = $reader->getTrailer();
$this->checkForEncryption($trailer);
$this->readers[] = $reader;
if (isset($trailer->value['Prev'])) {
$offset = $trailer->value['Prev']->value;
} else {
$offset = false;
}
}
// fix faulty sub-section header
if ($reader instanceof FixedReader) {
/**
* @var FixedReader $reader
*/
$reader->fixFaultySubSectionShift();
}
if ($reader === null) {
throw new CrossReferenceException('No cross-reference found.', CrossReferenceException::NO_XREF_FOUND);
}
}
/**
* Get the size of the cross reference.
*
* @return integer
*/
public function getSize()
{
return $this->getTrailer()->value['Size']->value;
}
/**
* Get the trailer dictionary.
*
* @return PdfDictionary
*/
public function getTrailer()
{
return $this->readers[0]->getTrailer();
}
/**
* Get the cross reference readser instances.
*
* @return ReaderInterface[]
*/
public function getReaders()
{
return $this->readers;
}
/**
* Get the offset by an object number.
*
* @param int $objectNumber
* @return integer|bool
*/
public function getOffsetFor($objectNumber)
{
foreach ($this->getReaders() as $reader) {
$offset = $reader->getOffsetFor($objectNumber);
if ($offset !== false) {
return $offset;
}
}
return false;
}
/**
* Get an indirect object by its object number.
*
* @param int $objectNumber
* @return PdfIndirectObject
* @throws CrossReferenceException
*/
public function getIndirectObject($objectNumber)
{
$offset = $this->getOffsetFor($objectNumber);
if ($offset === false) {
throw new CrossReferenceException(
\sprintf('Object (id:%s) not found.', $objectNumber),
CrossReferenceException::OBJECT_NOT_FOUND
);
}
$parser = $this->parser;
$parser->getTokenizer()->clearStack();
$parser->getStreamReader()->reset($offset + $this->fileHeaderOffset);
try {
/** @var PdfIndirectObject $object */
$object = $parser->readValue(null, PdfIndirectObject::class);
} catch (PdfTypeException $e) {
throw new CrossReferenceException(
\sprintf('Object (id:%s) not found at location (%s).', $objectNumber, $offset),
CrossReferenceException::OBJECT_NOT_FOUND,
$e
);
}
if ($object->objectNumber !== $objectNumber) {
throw new CrossReferenceException(
\sprintf('Wrong object found, got %s while %s was expected.', $object->objectNumber, $objectNumber),
CrossReferenceException::OBJECT_NOT_FOUND
);
}
return $object;
}
/**
* Read the cross-reference table at a given offset.
*
* Internally the method will try to evaluate the best reader for this cross-reference.
*
* @param int $offset
* @return ReaderInterface
* @throws CrossReferenceException
* @throws PdfTypeException
*/
protected function readXref($offset)
{
$this->parser->getStreamReader()->reset($offset);
$this->parser->getTokenizer()->clearStack();
$initValue = $this->parser->readValue();
return $this->initReaderInstance($initValue);
}
/**
* Get a cross-reference reader instance.
*
* @param PdfToken|PdfIndirectObject $initValue
* @return ReaderInterface|bool
* @throws CrossReferenceException
* @throws PdfTypeException
*/
protected function initReaderInstance($initValue)
{
$position = $this->parser->getStreamReader()->getPosition()
+ $this->parser->getStreamReader()->getOffset() + $this->fileHeaderOffset;
if ($initValue instanceof PdfToken && $initValue->value === 'xref') {
try {
return new FixedReader($this->parser);
} catch (CrossReferenceException $e) {
$this->parser->getStreamReader()->reset($position);
$this->parser->getTokenizer()->clearStack();
return new LineReader($this->parser);
}
}
if ($initValue instanceof PdfIndirectObject) {
try {
$stream = PdfStream::ensure($initValue->value);
} catch (PdfTypeException $e) {
throw new CrossReferenceException(
'Invalid object type at xref reference offset.',
CrossReferenceException::INVALID_DATA,
$e
);
}
$type = PdfDictionary::get($stream->value, 'Type');
if ($type->value !== 'XRef') {
throw new CrossReferenceException(
'The xref position points to an incorrect object type.',
CrossReferenceException::INVALID_DATA
);
}
$this->checkForEncryption($stream->value);
throw new CrossReferenceException(
'This PDF document probably uses a compression technique which is not supported by the ' .
'free parser shipped with FPDI. (See https://www.setasign.com/fpdi-pdf-parser for more details)',
CrossReferenceException::COMPRESSED_XREF
);
}
throw new CrossReferenceException(
'The xref position points to an incorrect object type.',
CrossReferenceException::INVALID_DATA
);
}
/**
* Check for encryption.
*
* @param PdfDictionary $dictionary
* @throws CrossReferenceException
*/
protected function checkForEncryption(PdfDictionary $dictionary)
{
if (isset($dictionary->value['Encrypt'])) {
throw new CrossReferenceException(
'This PDF document is encrypted and cannot be processed with FPDI.',
CrossReferenceException::ENCRYPTED
);
}
}
/**
* Find the start position for the first cross-reference.
*
* @return int The byte-offset position of the first cross-reference.
* @throws CrossReferenceException
*/
protected function findStartXref()
{
$reader = $this->parser->getStreamReader();
$reader->reset(-self::$trailerSearchLength, self::$trailerSearchLength);
$buffer = $reader->getBuffer(false);
$pos = \strrpos($buffer, 'startxref');
$addOffset = 9;
if ($pos === false) {
// Some corrupted documents uses startref, instead of startxref
$pos = \strrpos($buffer, 'startref');
if ($pos === false) {
throw new CrossReferenceException(
'Unable to find pointer to xref table',
CrossReferenceException::NO_STARTXREF_FOUND
);
}
$addOffset = 8;
}
$reader->setOffset($pos + $addOffset);
try {
$value = $this->parser->readValue(null, PdfNumeric::class);
} catch (PdfTypeException $e) {
throw new CrossReferenceException(
'Invalid data after startxref keyword.',
CrossReferenceException::INVALID_DATA,
$e
);
}
return $value->value;
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\CrossReference;
use setasign\Fpdi\PdfParser\PdfParserException;
/**
* Exception used by the CrossReference and Reader classes.
*/
class CrossReferenceException extends PdfParserException
{
/**
* @var int
*/
const INVALID_DATA = 0x0101;
/**
* @var int
*/
const XREF_MISSING = 0x0102;
/**
* @var int
*/
const ENTRIES_TOO_LARGE = 0x0103;
/**
* @var int
*/
const ENTRIES_TOO_SHORT = 0x0104;
/**
* @var int
*/
const NO_ENTRIES = 0x0105;
/**
* @var int
*/
const NO_TRAILER_FOUND = 0x0106;
/**
* @var int
*/
const NO_STARTXREF_FOUND = 0x0107;
/**
* @var int
*/
const NO_XREF_FOUND = 0x0108;
/**
* @var int
*/
const UNEXPECTED_END = 0x0109;
/**
* @var int
*/
const OBJECT_NOT_FOUND = 0x010A;
/**
* @var int
*/
const COMPRESSED_XREF = 0x010B;
/**
* @var int
*/
const ENCRYPTED = 0x010C;
}

View File

@ -0,0 +1,200 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\CrossReference;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\StreamReader;
/**
* Class FixedReader
*
* This reader allows a very less overhead parsing of single entries of the cross-reference, because the main entries
* are only read when needed and not in a single run.
*/
class FixedReader extends AbstractReader implements ReaderInterface
{
/**
* @var StreamReader
*/
protected $reader;
/**
* Data of subsections.
*
* @var array
*/
protected $subSections;
/**
* FixedReader constructor.
*
* @param PdfParser $parser
* @throws CrossReferenceException
*/
public function __construct(PdfParser $parser)
{
$this->reader = $parser->getStreamReader();
$this->read();
parent::__construct($parser);
}
/**
* Get all subsection data.
*
* @return array
*/
public function getSubSections()
{
return $this->subSections;
}
/**
* @inheritdoc
* @return int|false
*/
public function getOffsetFor($objectNumber)
{
foreach ($this->subSections as $offset => list($startObject, $objectCount)) {
/**
* @var int $startObject
* @var int $objectCount
*/
if ($objectNumber >= $startObject && $objectNumber < ($startObject + $objectCount)) {
$position = $offset + 20 * ($objectNumber - $startObject);
$this->reader->ensure($position, 20);
$line = $this->reader->readBytes(20);
if ($line[17] === 'f') {
return false;
}
return (int) \substr($line, 0, 10);
}
}
return false;
}
/**
* Read the cross-reference.
*
* This reader will only read the subsections in this method. The offsets were resolved individually by this
* information.
*
* @throws CrossReferenceException
*/
protected function read()
{
$subSections = [];
$startObject = $entryCount = $lastLineStart = null;
$validityChecked = false;
while (($line = $this->reader->readLine(20)) !== false) {
if (\strpos($line, 'trailer') !== false) {
$this->reader->reset($lastLineStart);
break;
}
// jump over if line content doesn't match the expected string
if (\sscanf($line, '%d %d', $startObject, $entryCount) !== 2) {
continue;
}
$oldPosition = $this->reader->getPosition();
$position = $oldPosition + $this->reader->getOffset();
if (!$validityChecked && $entryCount > 0) {
$nextLine = $this->reader->readBytes(21);
/* Check the next line for maximum of 20 bytes and not longer
* By catching 21 bytes and trimming the length should be still 21.
*/
if (\strlen(\trim($nextLine)) !== 21) {
throw new CrossReferenceException(
'Cross-reference entries are larger than 20 bytes.',
CrossReferenceException::ENTRIES_TOO_LARGE
);
}
/* Check for less than 20 bytes: cut the line to 20 bytes and trim; have to result in exactly 18 bytes.
* If it would have less bytes the substring would get the first bytes of the next line which would
* evaluate to a 20 bytes long string after trimming.
*/
if (\strlen(\trim(\substr($nextLine, 0, 20))) !== 18) {
throw new CrossReferenceException(
'Cross-reference entries are less than 20 bytes.',
CrossReferenceException::ENTRIES_TOO_SHORT
);
}
$validityChecked = true;
}
$subSections[$position] = [$startObject, $entryCount];
$lastLineStart = $position + $entryCount * 20;
$this->reader->reset($lastLineStart);
}
// reset after the last correct parsed line
$this->reader->reset($lastLineStart);
if (\count($subSections) === 0) {
throw new CrossReferenceException(
'No entries found in cross-reference.',
CrossReferenceException::NO_ENTRIES
);
}
$this->subSections = $subSections;
}
/**
* Fixes an invalid object number shift.
*
* This method can be used to repair documents with an invalid subsection header:
*
* <code>
* xref
* 1 7
* 0000000000 65535 f
* 0000000009 00000 n
* 0000412075 00000 n
* 0000412172 00000 n
* 0000412359 00000 n
* 0000412417 00000 n
* 0000412468 00000 n
* </code>
*
* It shall only be called on the first table.
*
* @return bool
*/
public function fixFaultySubSectionShift()
{
$subSections = $this->getSubSections();
if (\count($subSections) > 1) {
return false;
}
$subSection = \current($subSections);
if ($subSection[0] != 1) {
return false;
}
if ($this->getOffsetFor(1) === false) {
foreach ($subSections as $offset => list($startObject, $objectCount)) {
$this->subSections[$offset] = [$startObject - 1, $objectCount];
}
return true;
}
return false;
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\CrossReference;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\StreamReader;
/**
* Class LineReader
*
* This reader class read all cross-reference entries in a single run.
* It supports reading cross-references with e.g. invalid data (e.g. entries with a length < or > 20 bytes).
*/
class LineReader extends AbstractReader implements ReaderInterface
{
/**
* The object offsets.
*
* @var array
*/
protected $offsets;
/**
* LineReader constructor.
*
* @param PdfParser $parser
* @throws CrossReferenceException
*/
public function __construct(PdfParser $parser)
{
$this->read($this->extract($parser->getStreamReader()));
parent::__construct($parser);
}
/**
* @inheritdoc
* @return int|false
*/
public function getOffsetFor($objectNumber)
{
if (isset($this->offsets[$objectNumber])) {
return $this->offsets[$objectNumber][0];
}
return false;
}
/**
* Get all found offsets.
*
* @return array
*/
public function getOffsets()
{
return $this->offsets;
}
/**
* Extracts the cross reference data from the stream reader.
*
* @param StreamReader $reader
* @return string
* @throws CrossReferenceException
*/
protected function extract(StreamReader $reader)
{
$bytesPerCycle = 100;
$reader->reset(null, $bytesPerCycle);
$cycles = 0;
do {
// 6 = length of "trailer" - 1
$pos = \max(($bytesPerCycle * $cycles) - 6, 0);
$trailerPos = \strpos($reader->getBuffer(false), 'trailer', $pos);
$cycles++;
} while ($trailerPos === false && $reader->increaseLength($bytesPerCycle) !== false);
if ($trailerPos === false) {
throw new CrossReferenceException(
'Unexpected end of cross reference. "trailer"-keyword not found.',
CrossReferenceException::NO_TRAILER_FOUND
);
}
$xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos);
$reader->reset($reader->getPosition() + $trailerPos);
return $xrefContent;
}
/**
* Read the cross-reference entries.
*
* @param string $xrefContent
* @throws CrossReferenceException
*/
protected function read($xrefContent)
{
// get eol markers in the first 100 bytes
\preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m);
if (\count($m[0]) === 0) {
throw new CrossReferenceException(
'No data found in cross-reference.',
CrossReferenceException::INVALID_DATA
);
}
// count(array_count_values()) is faster then count(array_unique())
// @see https://github.com/symfony/symfony/pull/23731
// can be reverted for php7.2
$differentLineEndings = \count(\array_count_values($m[0]));
if ($differentLineEndings > 1) {
$lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY);
} else {
$lines = \explode($m[0][0], $xrefContent);
}
unset($differentLineEndings, $m);
if (!\is_array($lines)) {
$this->offsets = [];
return;
}
$start = 0;
$offsets = [];
// trim all lines and remove empty lines
$lines = \array_filter(\array_map('\trim', $lines));
foreach ($lines as $line) {
$pieces = \explode(' ', $line);
switch (\count($pieces)) {
case 2:
$start = (int) $pieces[0];
break;
case 3:
switch ($pieces[2]) {
case 'n':
$offsets[$start] = [(int) $pieces[0], (int) $pieces[1]];
$start++;
break 2;
case 'f':
$start++;
break 2;
}
// fall through if pieces doesn't match
default:
throw new CrossReferenceException(
\sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)),
CrossReferenceException::INVALID_DATA
);
}
}
$this->offsets = $offsets;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\CrossReference;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
/**
* ReaderInterface for cross-reference readers.
*/
interface ReaderInterface
{
/**
* Get an offset by an object number.
*
* @param int $objectNumber
* @return int|bool False if the offset was not found.
*/
public function getOffsetFor($objectNumber);
/**
* Get the trailer related to this cross reference.
*
* @return PdfDictionary
*/
public function getTrailer();
}

View File

@ -0,0 +1,102 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Class for handling ASCII base-85 encoded data
*/
class Ascii85 implements FilterInterface
{
/**
* Decode ASCII85 encoded string.
*
* @param string $data The input string
* @return string
* @throws Ascii85Exception
*/
public function decode($data)
{
$out = '';
$state = 0;
$chn = null;
$data = \preg_replace('/\s/', '', $data);
$l = \strlen($data);
/** @noinspection ForeachInvariantsInspection */
for ($k = 0; $k < $l; ++$k) {
$ch = \ord($data[$k]) & 0xff;
//Start <~
if ($k === 0 && $ch === 60 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 126) {
$k++;
continue;
}
//End ~>
if ($ch === 126 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 62) {
break;
}
if ($ch === 122 /* z */ && $state === 0) {
$out .= \chr(0) . \chr(0) . \chr(0) . \chr(0);
continue;
}
if ($ch < 33 /* ! */ || $ch > 117 /* u */) {
throw new Ascii85Exception(
'Illegal character found while ASCII85 decode.',
Ascii85Exception::ILLEGAL_CHAR_FOUND
);
}
$chn[$state] = $ch - 33;/* ! */
$state++;
if ($state === 5) {
$state = 0;
$r = 0;
for ($j = 0; $j < 5; ++$j) {
/** @noinspection UnnecessaryCastingInspection */
$r = (int)($r * 85 + $chn[$j]);
}
$out .= \chr($r >> 24)
. \chr($r >> 16)
. \chr($r >> 8)
. \chr($r);
}
}
if ($state === 1) {
throw new Ascii85Exception(
'Illegal length while ASCII85 decode.',
Ascii85Exception::ILLEGAL_LENGTH
);
}
if ($state === 2) {
$r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1] + 1) * 85 * 85 * 85;
$out .= \chr($r >> 24);
} elseif ($state === 3) {
$r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + ($chn[2] + 1) * 85 * 85;
$out .= \chr($r >> 24);
$out .= \chr($r >> 16);
} elseif ($state === 4) {
$r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + $chn[2] * 85 * 85 + ($chn[3] + 1) * 85;
$out .= \chr($r >> 24);
$out .= \chr($r >> 16);
$out .= \chr($r >> 8);
}
return $out;
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Exception for Ascii85 filter class
*/
class Ascii85Exception extends FilterException
{
/**
* @var integer
*/
const ILLEGAL_CHAR_FOUND = 0x0301;
/**
* @var integer
*/
const ILLEGAL_LENGTH = 0x0302;
}

View File

@ -0,0 +1,47 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Class for handling ASCII hexadecimal encoded data
*/
class AsciiHex implements FilterInterface
{
/**
* Converts an ASCII hexadecimal encoded string into its binary representation.
*
* @param string $data The input string
* @return string
*/
public function decode($data)
{
$data = \preg_replace('/[^0-9A-Fa-f]/', '', \rtrim($data, '>'));
if ((\strlen($data) % 2) === 1) {
$data .= '0';
}
return \pack('H*', $data);
}
/**
* Converts a string into ASCII hexadecimal representation.
*
* @param string $data The input string
* @param boolean $leaveEOD
* @return string
*/
public function encode($data, $leaveEOD = false)
{
$t = \unpack('H*', $data);
return \current($t)
. ($leaveEOD ? '' : '>');
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
use setasign\Fpdi\PdfParser\PdfParserException;
/**
* Exception for filters
*/
class FilterException extends PdfParserException
{
const UNSUPPORTED_FILTER = 0x0201;
const NOT_IMPLEMENTED = 0x0202;
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Interface for filters
*/
interface FilterInterface
{
/**
* Decode a string.
*
* @param string $data The input string
* @return string
*/
public function decode($data);
}

View File

@ -0,0 +1,77 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Class for handling zlib/deflate encoded data
*/
class Flate implements FilterInterface
{
/**
* Checks whether the zlib extension is loaded.
*
* Used for testing purpose.
*
* @return boolean
* @internal
*/
protected function extensionLoaded()
{
return \extension_loaded('zlib');
}
/**
* Decodes a flate compressed string.
*
* @param string|false $data The input string
* @return string
* @throws FlateException
*/
public function decode($data)
{
if ($this->extensionLoaded()) {
$oData = $data;
$data = (($data !== '') ? @\gzuncompress($data) : '');
if ($data === false) {
// let's try if the checksum is CRC32
$fh = fopen('php://temp', 'w+b');
fwrite($fh, "\x1f\x8b\x08\x00\x00\x00\x00\x00" . $oData);
// "window" == 31 -> 16 + (8 to 15): Uses the low 4 bits of the value as the window size logarithm.
// The input must include a gzip header and trailer (via 16).
stream_filter_append($fh, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 31]);
fseek($fh, 0);
$data = @stream_get_contents($fh);
fclose($fh);
if ($data) {
return $data;
}
// Try this fallback (remove the zlib stream header)
$data = @(gzinflate(substr($oData, 2)));
if ($data === false) {
throw new FlateException(
'Error while decompressing stream.',
FlateException::DECOMPRESS_ERROR
);
}
}
} else {
throw new FlateException(
'To handle FlateDecode filter, enable zlib support in PHP.',
FlateException::NO_ZLIB
);
}
return $data;
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Exception for flate filter class
*/
class FlateException extends FilterException
{
/**
* @var integer
*/
const NO_ZLIB = 0x0401;
/**
* @var integer
*/
const DECOMPRESS_ERROR = 0x0402;
}

View File

@ -0,0 +1,178 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Class for handling LZW encoded data
*/
class Lzw implements FilterInterface
{
/**
* @var null|string
*/
protected $data;
/**
* @var array
*/
protected $sTable = [];
/**
* @var int
*/
protected $dataLength = 0;
/**
* @var int
*/
protected $tIdx;
/**
* @var int
*/
protected $bitsToGet = 9;
/**
* @var int
*/
protected $bytePointer;
/**
* @var int
*/
protected $nextData = 0;
/**
* @var int
*/
protected $nextBits = 0;
/**
* @var array
*/
protected $andTable = [511, 1023, 2047, 4095];
/**
* Method to decode LZW compressed data.
*
* @param string $data The compressed data
* @return string The uncompressed data
* @throws LzwException
*/
public function decode($data)
{
if ($data[0] === "\x00" && $data[1] === "\x01") {
throw new LzwException(
'LZW flavour not supported.',
LzwException::LZW_FLAVOUR_NOT_SUPPORTED
);
}
$this->initsTable();
$this->data = $data;
$this->dataLength = \strlen($data);
// Initialize pointers
$this->bytePointer = 0;
$this->nextData = 0;
$this->nextBits = 0;
$prevCode = 0;
$uncompData = '';
while (($code = $this->getNextCode()) !== 257) {
if ($code === 256) {
$this->initsTable();
} elseif ($prevCode === 256) {
$uncompData .= $this->sTable[$code];
} elseif ($code < $this->tIdx) {
$string = $this->sTable[$code];
$uncompData .= $string;
$this->addStringToTable($this->sTable[$prevCode], $string[0]);
} else {
$string = $this->sTable[$prevCode];
$string .= $string[0];
$uncompData .= $string;
$this->addStringToTable($string);
}
$prevCode = $code;
}
return $uncompData;
}
/**
* Initialize the string table.
*/
protected function initsTable()
{
$this->sTable = [];
for ($i = 0; $i < 256; $i++) {
$this->sTable[$i] = \chr($i);
}
$this->tIdx = 258;
$this->bitsToGet = 9;
}
/**
* Add a new string to the string table.
*
* @param string $oldString
* @param string $newString
*/
protected function addStringToTable($oldString, $newString = '')
{
$string = $oldString . $newString;
// Add this new String to the table
$this->sTable[$this->tIdx++] = $string;
if ($this->tIdx === 511) {
$this->bitsToGet = 10;
} elseif ($this->tIdx === 1023) {
$this->bitsToGet = 11;
} elseif ($this->tIdx === 2047) {
$this->bitsToGet = 12;
}
}
/**
* Returns the next 9, 10, 11 or 12 bits.
*
* @return int
*/
protected function getNextCode()
{
if ($this->bytePointer === $this->dataLength) {
return 257;
}
$this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff);
$this->nextBits += 8;
if ($this->nextBits < $this->bitsToGet) {
$this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff);
$this->nextBits += 8;
}
$code = ($this->nextData >> ($this->nextBits - $this->bitsToGet)) & $this->andTable[$this->bitsToGet - 9];
$this->nextBits -= $this->bitsToGet;
return $code;
}
}

View File

@ -0,0 +1,22 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Filter;
/**
* Exception for LZW filter class
*/
class LzwException extends FilterException
{
/**
* @var integer
*/
const LZW_FLAVOUR_NOT_SUPPORTED = 0x0501;
}

View File

@ -0,0 +1,436 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser;
use setasign\Fpdi\PdfParser\CrossReference\CrossReference;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\Type\PdfArray;
use setasign\Fpdi\PdfParser\Type\PdfBoolean;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfHexString;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
use setasign\Fpdi\PdfParser\Type\PdfName;
use setasign\Fpdi\PdfParser\Type\PdfNull;
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
use setasign\Fpdi\PdfParser\Type\PdfStream;
use setasign\Fpdi\PdfParser\Type\PdfString;
use setasign\Fpdi\PdfParser\Type\PdfToken;
use setasign\Fpdi\PdfParser\Type\PdfType;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
/**
* A PDF parser class
*/
class PdfParser
{
/**
* @var StreamReader
*/
protected $streamReader;
/**
* @var Tokenizer
*/
protected $tokenizer;
/**
* The file header.
*
* @var string
*/
protected $fileHeader;
/**
* The offset to the file header.
*
* @var int
*/
protected $fileHeaderOffset;
/**
* @var CrossReference|null
*/
protected $xref;
/**
* All read objects.
*
* @var array
*/
protected $objects = [];
/**
* PdfParser constructor.
*
* @param StreamReader $streamReader
*/
public function __construct(StreamReader $streamReader)
{
$this->streamReader = $streamReader;
$this->tokenizer = new Tokenizer($streamReader);
}
/**
* Removes cycled references.
*
* @internal
*/
public function cleanUp()
{
$this->xref = null;
}
/**
* Get the stream reader instance.
*
* @return StreamReader
*/
public function getStreamReader()
{
return $this->streamReader;
}
/**
* Get the tokenizer instance.
*
* @return Tokenizer
*/
public function getTokenizer()
{
return $this->tokenizer;
}
/**
* Resolves the file header.
*
* @throws PdfParserException
* @return int
*/
protected function resolveFileHeader()
{
if ($this->fileHeader) {
return $this->fileHeaderOffset;
}
$this->streamReader->reset(0);
$maxIterations = 1000;
while (true) {
$buffer = $this->streamReader->getBuffer(false);
$offset = \strpos($buffer, '%PDF-');
if ($offset === false) {
if (!$this->streamReader->increaseLength(100) || (--$maxIterations === 0)) {
throw new PdfParserException(
'Unable to find PDF file header.',
PdfParserException::FILE_HEADER_NOT_FOUND
);
}
continue;
}
break;
}
$this->fileHeaderOffset = $offset;
$this->streamReader->setOffset($offset);
$this->fileHeader = \trim($this->streamReader->readLine());
return $this->fileHeaderOffset;
}
/**
* Get the cross-reference instance.
*
* @return CrossReference
* @throws CrossReferenceException
* @throws PdfParserException
*/
public function getCrossReference()
{
if ($this->xref === null) {
$this->xref = new CrossReference($this, $this->resolveFileHeader());
}
return $this->xref;
}
/**
* Get the PDF version.
*
* @return int[] An array of major and minor version.
* @throws PdfParserException
*/
public function getPdfVersion()
{
$this->resolveFileHeader();
if (\preg_match('/%PDF-(\d)\.(\d)/', $this->fileHeader, $result) === 0) {
throw new PdfParserException(
'Unable to extract PDF version from file header.',
PdfParserException::PDF_VERSION_NOT_FOUND
);
}
list(, $major, $minor) = $result;
$catalog = $this->getCatalog();
if (isset($catalog->value['Version'])) {
$versionParts = \explode(
'.',
PdfName::unescape(PdfType::resolve($catalog->value['Version'], $this)->value)
);
if (count($versionParts) === 2) {
list($major, $minor) = $versionParts;
}
}
return [(int) $major, (int) $minor];
}
/**
* Get the catalog dictionary.
*
* @return PdfDictionary
* @throws Type\PdfTypeException
* @throws CrossReferenceException
* @throws PdfParserException
*/
public function getCatalog()
{
$trailer = $this->getCrossReference()->getTrailer();
$catalog = PdfType::resolve(PdfDictionary::get($trailer, 'Root'), $this);
return PdfDictionary::ensure($catalog);
}
/**
* Get an indirect object by its object number.
*
* @param int $objectNumber
* @param bool $cache
* @return PdfIndirectObject
* @throws CrossReferenceException
* @throws PdfParserException
*/
public function getIndirectObject($objectNumber, $cache = false)
{
$objectNumber = (int) $objectNumber;
if (isset($this->objects[$objectNumber])) {
return $this->objects[$objectNumber];
}
$object = $this->getCrossReference()->getIndirectObject($objectNumber);
if ($cache) {
$this->objects[$objectNumber] = $object;
}
return $object;
}
/**
* Read a PDF value.
*
* @param null|bool|string $token
* @param null|string $expectedType
* @return false|PdfArray|PdfBoolean|PdfDictionary|PdfHexString|PdfIndirectObject|PdfIndirectObjectReference|PdfName|PdfNull|PdfNumeric|PdfStream|PdfString|PdfToken
* @throws Type\PdfTypeException
*/
public function readValue($token = null, $expectedType = null)
{
if ($token === null) {
$token = $this->tokenizer->getNextToken();
}
if ($token === false) {
if ($expectedType !== null) {
throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE);
}
return false;
}
switch ($token) {
case '(':
$this->ensureExpectedType($token, $expectedType);
return $this->parsePdfString();
case '<':
if ($this->streamReader->getByte() === '<') {
$this->ensureExpectedType('<<', $expectedType);
$this->streamReader->addOffset(1);
return $this->parsePdfDictionary();
}
$this->ensureExpectedType($token, $expectedType);
return $this->parsePdfHexString();
case '/':
$this->ensureExpectedType($token, $expectedType);
return $this->parsePdfName();
case '[':
$this->ensureExpectedType($token, $expectedType);
return $this->parsePdfArray();
default:
if (\is_numeric($token)) {
$token2 = $this->tokenizer->getNextToken();
if ($token2 !== false) {
if (\is_numeric($token2)) {
$token3 = $this->tokenizer->getNextToken();
if ($token3 === 'obj') {
if ($expectedType !== null && $expectedType !== PdfIndirectObject::class) {
throw new Type\PdfTypeException(
'Got unexpected token type.',
Type\PdfTypeException::INVALID_DATA_TYPE
);
}
return $this->parsePdfIndirectObject((int) $token, (int) $token2);
} elseif ($token3 === 'R') {
if (
$expectedType !== null &&
$expectedType !== PdfIndirectObjectReference::class
) {
throw new Type\PdfTypeException(
'Got unexpected token type.',
Type\PdfTypeException::INVALID_DATA_TYPE
);
}
return PdfIndirectObjectReference::create((int) $token, (int) $token2);
} elseif ($token3 !== false) {
$this->tokenizer->pushStack($token3);
}
}
$this->tokenizer->pushStack($token2);
}
if ($expectedType !== null && $expectedType !== PdfNumeric::class) {
throw new Type\PdfTypeException(
'Got unexpected token type.',
Type\PdfTypeException::INVALID_DATA_TYPE
);
}
return PdfNumeric::create($token + 0);
}
if ($token === 'true' || $token === 'false') {
$this->ensureExpectedType($token, $expectedType);
return PdfBoolean::create($token === 'true');
}
if ($token === 'null') {
$this->ensureExpectedType($token, $expectedType);
return new PdfNull();
}
if ($expectedType !== null && $expectedType !== PdfToken::class) {
throw new Type\PdfTypeException(
'Got unexpected token type.',
Type\PdfTypeException::INVALID_DATA_TYPE
);
}
$v = new PdfToken();
$v->value = $token;
return $v;
}
}
/**
* @return PdfString
*/
protected function parsePdfString()
{
return PdfString::parse($this->streamReader);
}
/**
* @return false|PdfHexString
*/
protected function parsePdfHexString()
{
return PdfHexString::parse($this->streamReader);
}
/**
* @return bool|PdfDictionary
* @throws PdfTypeException
*/
protected function parsePdfDictionary()
{
return PdfDictionary::parse($this->tokenizer, $this->streamReader, $this);
}
/**
* @return PdfName
*/
protected function parsePdfName()
{
return PdfName::parse($this->tokenizer, $this->streamReader);
}
/**
* @return false|PdfArray
* @throws PdfTypeException
*/
protected function parsePdfArray()
{
return PdfArray::parse($this->tokenizer, $this);
}
/**
* @param int $objectNumber
* @param int $generationNumber
* @return false|PdfIndirectObject
* @throws Type\PdfTypeException
*/
protected function parsePdfIndirectObject($objectNumber, $generationNumber)
{
return PdfIndirectObject::parse(
$objectNumber,
$generationNumber,
$this,
$this->tokenizer,
$this->streamReader
);
}
/**
* Ensures that the token will evaluate to an expected object type (or not).
*
* @param string $token
* @param string|null $expectedType
* @return bool
* @throws Type\PdfTypeException
*/
protected function ensureExpectedType($token, $expectedType)
{
static $mapping = [
'(' => PdfString::class,
'<' => PdfHexString::class,
'<<' => PdfDictionary::class,
'/' => PdfName::class,
'[' => PdfArray::class,
'true' => PdfBoolean::class,
'false' => PdfBoolean::class,
'null' => PdfNull::class
];
if ($expectedType === null || $mapping[$token] === $expectedType) {
return true;
}
throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE);
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser;
use setasign\Fpdi\FpdiException;
/**
* Exception for the pdf parser class
*/
class PdfParserException extends FpdiException
{
/**
* @var int
*/
const NOT_IMPLEMENTED = 0x0001;
/**
* @var int
*/
const IMPLEMENTED_IN_FPDI_PDF_PARSER = 0x0002;
/**
* @var int
*/
const INVALID_DATA_TYPE = 0x0003;
/**
* @var int
*/
const FILE_HEADER_NOT_FOUND = 0x0004;
/**
* @var int
*/
const PDF_VERSION_NOT_FOUND = 0x0005;
/**
* @var int
*/
const INVALID_DATA_SIZE = 0x0006;
}

View File

@ -0,0 +1,481 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser;
/**
* A stream reader class
*/
class StreamReader
{
/**
* Creates a stream reader instance by a string value.
*
* @param string $content
* @param int $maxMemory
* @return StreamReader
*/
public static function createByString($content, $maxMemory = 2097152)
{
$h = \fopen('php://temp/maxmemory:' . ((int) $maxMemory), 'r+b');
\fwrite($h, $content);
\rewind($h);
return new self($h, true);
}
/**
* Creates a stream reader instance by a filename.
*
* @param string $filename
* @return StreamReader
*/
public static function createByFile($filename)
{
$h = \fopen($filename, 'rb');
return new self($h, true);
}
/**
* Defines whether the stream should be closed when the stream reader instance is deconstructed or not.
*
* @var bool
*/
protected $closeStream;
/**
* The stream resource.
*
* @var resource
*/
protected $stream;
/**
* The byte-offset position in the stream.
*
* @var int
*/
protected $position;
/**
* The byte-offset position in the buffer.
*
* @var int
*/
protected $offset;
/**
* The buffer length.
*
* @var int
*/
protected $bufferLength;
/**
* The total length of the stream.
*
* @var int
*/
protected $totalLength;
/**
* The buffer.
*
* @var string
*/
protected $buffer;
/**
* StreamReader constructor.
*
* @param resource $stream
* @param bool $closeStream Defines whether to close the stream resource if the instance is destructed or not.
*/
public function __construct($stream, $closeStream = false)
{
if (!\is_resource($stream)) {
throw new \InvalidArgumentException(
'No stream given.'
);
}
$metaData = \stream_get_meta_data($stream);
if (!$metaData['seekable']) {
throw new \InvalidArgumentException(
'Given stream is not seekable!'
);
}
if (fseek($stream, 0) === -1) {
throw new \InvalidArgumentException(
'Given stream is not seekable!'
);
}
$this->stream = $stream;
$this->closeStream = $closeStream;
$this->reset();
}
/**
* The destructor.
*/
public function __destruct()
{
$this->cleanUp();
}
/**
* Closes the file handle.
*/
public function cleanUp()
{
if ($this->closeStream && is_resource($this->stream)) {
\fclose($this->stream);
}
}
/**
* Returns the byte length of the buffer.
*
* @param bool $atOffset
* @return int
*/
public function getBufferLength($atOffset = false)
{
if ($atOffset === false) {
return $this->bufferLength;
}
return $this->bufferLength - $this->offset;
}
/**
* Get the current position in the stream.
*
* @return int
*/
public function getPosition()
{
return $this->position;
}
/**
* Returns the current buffer.
*
* @param bool $atOffset
* @return string
*/
public function getBuffer($atOffset = true)
{
if ($atOffset === false) {
return $this->buffer;
}
$string = \substr($this->buffer, $this->offset);
return (string) $string;
}
/**
* Gets a byte at a specific position in the buffer.
*
* If the position is invalid the method will return false.
*
* If the $position parameter is set to null the value of $this->offset will be used.
*
* @param int|null $position
* @return string|bool
*/
public function getByte($position = null)
{
$position = (int) ($position !== null ? $position : $this->offset);
if (
$position >= $this->bufferLength
&& (!$this->increaseLength() || $position >= $this->bufferLength)
) {
return false;
}
return $this->buffer[$position];
}
/**
* Returns a byte at a specific position, and set the offset to the next byte position.
*
* If the position is invalid the method will return false.
*
* If the $position parameter is set to null the value of $this->offset will be used.
*
* @param int|null $position
* @return string|bool
*/
public function readByte($position = null)
{
if ($position !== null) {
$position = (int) $position;
// check if needed bytes are available in the current buffer
if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
$this->reset($position);
$offset = $this->offset;
} else {
$offset = $position - $this->position;
}
} else {
$offset = $this->offset;
}
if (
$offset >= $this->bufferLength
&& ((!$this->increaseLength()) || $offset >= $this->bufferLength)
) {
return false;
}
$this->offset = $offset + 1;
return $this->buffer[$offset];
}
/**
* Read bytes from the current or a specific offset position and set the internal pointer to the next byte.
*
* If the position is invalid the method will return false.
*
* If the $position parameter is set to null the value of $this->offset will be used.
*
* @param int $length
* @param int|null $position
* @return string|false
*/
public function readBytes($length, $position = null)
{
$length = (int) $length;
if ($position !== null) {
// check if needed bytes are available in the current buffer
if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
$this->reset($position, $length);
$offset = $this->offset;
} else {
$offset = $position - $this->position;
}
} else {
$offset = $this->offset;
}
if (
($offset + $length) > $this->bufferLength
&& ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength)
) {
return false;
}
$bytes = \substr($this->buffer, $offset, $length);
$this->offset = $offset + $length;
return $bytes;
}
/**
* Read a line from the current position.
*
* @param int $length
* @return string|bool
*/
public function readLine($length = 1024)
{
if ($this->ensureContent() === false) {
return false;
}
$line = '';
while ($this->ensureContent()) {
$char = $this->readByte();
if ($char === "\n") {
break;
}
if ($char === "\r") {
if ($this->getByte() === "\n") {
$this->addOffset(1);
}
break;
}
$line .= $char;
if (\strlen($line) >= $length) {
break;
}
}
return $line;
}
/**
* Set the offset position in the current buffer.
*
* @param int $offset
*/
public function setOffset($offset)
{
if ($offset > $this->bufferLength || $offset < 0) {
throw new \OutOfRangeException(
\sprintf('Offset (%s) out of range (length: %s)', $offset, $this->bufferLength)
);
}
$this->offset = (int) $offset;
}
/**
* Returns the current offset in the current buffer.
*
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* Add an offset to the current offset.
*
* @param int $offset
*/
public function addOffset($offset)
{
$this->setOffset($this->offset + $offset);
}
/**
* Make sure that there is at least one character beyond the current offset in the buffer.
*
* @return bool
*/
public function ensureContent()
{
while ($this->offset >= $this->bufferLength) {
if (!$this->increaseLength()) {
return false;
}
}
return true;
}
/**
* Returns the stream.
*
* @return resource
*/
public function getStream()
{
return $this->stream;
}
/**
* Gets the total available length.
*
* @return int
*/
public function getTotalLength()
{
if ($this->totalLength === null) {
$stat = \fstat($this->stream);
$this->totalLength = $stat['size'];
}
return $this->totalLength;
}
/**
* Resets the buffer to a position and re-read the buffer with the given length.
*
* If the $pos parameter is negative the start buffer position will be the $pos'th position from
* the end of the file.
*
* If the $pos parameter is negative and the absolute value is bigger then the totalLength of
* the file $pos will set to zero.
*
* @param int|null $pos Start position of the new buffer
* @param int $length Length of the new buffer. Mustn't be negative
*/
public function reset($pos = 0, $length = 200)
{
if ($pos === null) {
$pos = $this->position + $this->offset;
} elseif ($pos < 0) {
$pos = \max(0, $this->getTotalLength() + $pos);
}
\fseek($this->stream, $pos);
$this->position = $pos;
$this->offset = 0;
if ($length > 0) {
$this->buffer = (string) \fread($this->stream, $length);
} else {
$this->buffer = '';
}
$this->bufferLength = \strlen($this->buffer);
// If a stream wrapper is in use it is possible that
// length values > 8096 will be ignored, so use the
// increaseLength()-method to correct that behavior
if ($this->bufferLength < $length && $this->increaseLength($length - $this->bufferLength)) {
// increaseLength parameter is $minLength, so cut to have only the required bytes in the buffer
$this->buffer = (string) \substr($this->buffer, 0, $length);
$this->bufferLength = \strlen($this->buffer);
}
}
/**
* Ensures bytes in the buffer with a specific length and location in the file.
*
* @param int $pos
* @param int $length
* @see reset()
*/
public function ensure($pos, $length)
{
if (
$pos >= $this->position
&& $pos < ($this->position + $this->bufferLength)
&& ($this->position + $this->bufferLength) >= ($pos + $length)
) {
$this->offset = $pos - $this->position;
} else {
$this->reset($pos, $length);
}
}
/**
* Forcefully read more data into the buffer.
*
* @param int $minLength
* @return bool Returns false if the stream reaches the end
*/
public function increaseLength($minLength = 100)
{
$length = \max($minLength, 100);
if (\feof($this->stream) || $this->getTotalLength() === $this->position + $this->bufferLength) {
return false;
}
$newLength = $this->bufferLength + $length;
do {
$this->buffer .= \fread($this->stream, $newLength - $this->bufferLength);
$this->bufferLength = \strlen($this->buffer);
} while (($this->bufferLength !== $newLength) && !\feof($this->stream));
return true;
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser;
/**
* A tokenizer class.
*/
class Tokenizer
{
/**
* @var StreamReader
*/
protected $streamReader;
/**
* A token stack.
*
* @var string[]
*/
protected $stack = [];
/**
* Tokenizer constructor.
*
* @param StreamReader $streamReader
*/
public function __construct(StreamReader $streamReader)
{
$this->streamReader = $streamReader;
}
/**
* Get the stream reader instance.
*
* @return StreamReader
*/
public function getStreamReader()
{
return $this->streamReader;
}
/**
* Clear the token stack.
*/
public function clearStack()
{
$this->stack = [];
}
/**
* Push a token onto the stack.
*
* @param string $token
*/
public function pushStack($token)
{
$this->stack[] = $token;
}
/**
* Get next token.
*
* @return false|string
*/
public function getNextToken()
{
$token = \array_pop($this->stack);
if ($token !== null) {
return $token;
}
if (($byte = $this->streamReader->readByte()) === false) {
return false;
}
if (\in_array($byte, ["\x20", "\x0A", "\x0D", "\x0C", "\x09", "\x00"], true)) {
if ($this->leapWhiteSpaces() === false) {
return false;
}
$byte = $this->streamReader->readByte();
}
switch ($byte) {
case '/':
case '[':
case ']':
case '(':
case ')':
case '{':
case '}':
case '<':
case '>':
return $byte;
case '%':
$this->streamReader->readLine();
return $this->getNextToken();
}
/* This way is faster than checking single bytes.
*/
$bufferOffset = $this->streamReader->getOffset();
do {
$lastBuffer = $this->streamReader->getBuffer(false);
$pos = \strcspn(
$lastBuffer,
"\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%",
$bufferOffset
);
} while (
// Break the loop if a delimiter or white space char is matched
// in the current buffer or increase the buffers length
$bufferOffset + $pos === \strlen($lastBuffer)
&& $this->streamReader->increaseLength()
);
$result = \substr($lastBuffer, $bufferOffset - 1, $pos + 1);
$this->streamReader->setOffset($bufferOffset + $pos);
return $result;
}
/**
* Leap white spaces.
*
* @return boolean
*/
public function leapWhiteSpaces()
{
do {
if (!$this->streamReader->ensureContent()) {
return false;
}
$buffer = $this->streamReader->getBuffer(false);
$matches = \strspn($buffer, "\x20\x0A\x0C\x0D\x09\x00", $this->streamReader->getOffset());
if ($matches > 0) {
$this->streamReader->addOffset($matches);
}
} while ($this->streamReader->getOffset() >= $this->streamReader->getBufferLength());
return true;
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\Tokenizer;
/**
* Class representing a PDF array object
*
* @property array $value The value of the PDF type.
*/
class PdfArray extends PdfType
{
/**
* Parses an array of the passed tokenizer and parser.
*
* @param Tokenizer $tokenizer
* @param PdfParser $parser
* @return false|self
* @throws PdfTypeException
*/
public static function parse(Tokenizer $tokenizer, PdfParser $parser)
{
$result = [];
// Recurse into this function until we reach the end of the array.
while (($token = $tokenizer->getNextToken()) !== ']') {
if ($token === false || ($value = $parser->readValue($token)) === false) {
return false;
}
$result[] = $value;
}
$v = new self();
$v->value = $result;
return $v;
}
/**
* Helper method to create an instance.
*
* @param PdfType[] $values
* @return self
*/
public static function create(array $values = [])
{
$v = new self();
$v->value = $values;
return $v;
}
/**
* Ensures that the passed array is a PdfArray instance with a (optional) specific size.
*
* @param mixed $array
* @param null|int $size
* @return self
* @throws PdfTypeException
*/
public static function ensure($array, $size = null)
{
$result = PdfType::ensureType(self::class, $array, 'Array value expected.');
if ($size !== null && \count($array->value) !== $size) {
throw new PdfTypeException(
\sprintf('Array with %s entries expected.', $size),
PdfTypeException::INVALID_DATA_SIZE
);
}
return $result;
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
/**
* Class representing a boolean PDF object
*/
class PdfBoolean extends PdfType
{
/**
* Helper method to create an instance.
*
* @param bool $value
* @return self
*/
public static function create($value)
{
$v = new self();
$v->value = (bool) $value;
return $v;
}
/**
* Ensures that the passed value is a PdfBoolean instance.
*
* @param mixed $value
* @return self
* @throws PdfTypeException
*/
public static function ensure($value)
{
return PdfType::ensureType(self::class, $value, 'Boolean value expected.');
}
}

View File

@ -0,0 +1,131 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\StreamReader;
use setasign\Fpdi\PdfParser\Tokenizer;
/**
* Class representing a PDF dictionary object
*/
class PdfDictionary extends PdfType
{
/**
* Parses a dictionary of the passed tokenizer, stream-reader and parser.
*
* @param Tokenizer $tokenizer
* @param StreamReader $streamReader
* @param PdfParser $parser
* @return bool|self
* @throws PdfTypeException
*/
public static function parse(Tokenizer $tokenizer, StreamReader $streamReader, PdfParser $parser)
{
$entries = [];
while (true) {
$token = $tokenizer->getNextToken();
if ($token === '>' && $streamReader->getByte() === '>') {
$streamReader->addOffset(1);
break;
}
$key = $parser->readValue($token);
if ($key === false) {
return false;
}
// ensure the first value to be a Name object
if (!($key instanceof PdfName)) {
$lastToken = null;
// ignore all other entries and search for the closing brackets
while (($token = $tokenizer->getNextToken()) !== '>' || $lastToken !== '>') {
if ($token === false) {
return false;
}
$lastToken = $token;
}
break;
}
$value = $parser->readValue();
if ($value === false) {
return false;
}
if ($value instanceof PdfNull) {
continue;
}
// catch missing value
if ($value instanceof PdfToken && $value->value === '>' && $streamReader->getByte() === '>') {
$streamReader->addOffset(1);
break;
}
$entries[$key->value] = $value;
}
$v = new self();
$v->value = $entries;
return $v;
}
/**
* Helper method to create an instance.
*
* @param PdfType[] $entries The keys are the name entries of the dictionary.
* @return self
*/
public static function create(array $entries = [])
{
$v = new self();
$v->value = $entries;
return $v;
}
/**
* Get a value by its key from a dictionary or a default value.
*
* @param mixed $dictionary
* @param string $key
* @param PdfType|null $default
* @return PdfNull|PdfType
* @throws PdfTypeException
*/
public static function get($dictionary, $key, ?PdfType $default = null)
{
$dictionary = self::ensure($dictionary);
if (isset($dictionary->value[$key])) {
return $dictionary->value[$key];
}
return $default ?? new PdfNull();
}
/**
* Ensures that the passed value is a PdfDictionary instance.
*
* @param mixed $dictionary
* @return self
* @throws PdfTypeException
*/
public static function ensure($dictionary)
{
return PdfType::ensureType(self::class, $dictionary, 'Dictionary value expected.');
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\StreamReader;
/**
* Class representing a hexadecimal encoded PDF string object
*/
class PdfHexString extends PdfType
{
/**
* Parses a hexadecimal string object from the stream reader.
*
* @param StreamReader $streamReader
* @return false|self
*/
public static function parse(StreamReader $streamReader)
{
$bufferOffset = $streamReader->getOffset();
while (true) {
$buffer = $streamReader->getBuffer(false);
$pos = \strpos($buffer, '>', $bufferOffset);
if ($pos === false) {
if (!$streamReader->increaseLength()) {
return false;
}
continue;
}
break;
}
$result = \substr($buffer, $bufferOffset, $pos - $bufferOffset);
$streamReader->setOffset($pos + 1);
$v = new self();
$v->value = $result;
return $v;
}
/**
* Helper method to create an instance.
*
* @param string $string The hex encoded string.
* @return self
*/
public static function create($string)
{
$v = new self();
$v->value = $string;
return $v;
}
/**
* Ensures that the passed value is a PdfHexString instance.
*
* @param mixed $hexString
* @return self
* @throws PdfTypeException
*/
public static function ensure($hexString)
{
return PdfType::ensureType(self::class, $hexString, 'Hex string value expected.');
}
}

View File

@ -0,0 +1,103 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\StreamReader;
use setasign\Fpdi\PdfParser\Tokenizer;
/**
* Class representing an indirect object
*/
class PdfIndirectObject extends PdfType
{
/**
* Parses an indirect object from a tokenizer, parser and stream-reader.
*
* @param int $objectNumber
* @param int $objectGenerationNumber
* @param PdfParser $parser
* @param Tokenizer $tokenizer
* @param StreamReader $reader
* @return self|false
* @throws PdfTypeException
*/
public static function parse(
$objectNumber,
$objectGenerationNumber,
PdfParser $parser,
Tokenizer $tokenizer,
StreamReader $reader
) {
$value = $parser->readValue();
if ($value === false) {
return false;
}
$nextToken = $tokenizer->getNextToken();
if ($nextToken === 'stream') {
$value = PdfStream::parse($value, $reader, $parser);
} elseif ($nextToken !== false) {
$tokenizer->pushStack($nextToken);
}
$v = new self();
$v->objectNumber = (int) $objectNumber;
$v->generationNumber = (int) $objectGenerationNumber;
$v->value = $value;
return $v;
}
/**
* Helper method to create an instance.
*
* @param int $objectNumber
* @param int $generationNumber
* @param PdfType $value
* @return self
*/
public static function create($objectNumber, $generationNumber, PdfType $value)
{
$v = new self();
$v->objectNumber = (int) $objectNumber;
$v->generationNumber = (int) $generationNumber;
$v->value = $value;
return $v;
}
/**
* Ensures that the passed value is a PdfIndirectObject instance.
*
* @param mixed $indirectObject
* @return self
* @throws PdfTypeException
*/
public static function ensure($indirectObject)
{
return PdfType::ensureType(self::class, $indirectObject, 'Indirect object expected.');
}
/**
* The object number.
*
* @var int
*/
public $objectNumber;
/**
* The generation number.
*
* @var int
*/
public $generationNumber;
}

View File

@ -0,0 +1,52 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
/**
* Class representing an indirect object reference
*/
class PdfIndirectObjectReference extends PdfType
{
/**
* Helper method to create an instance.
*
* @param int $objectNumber
* @param int $generationNumber
* @return self
*/
public static function create($objectNumber, $generationNumber)
{
$v = new self();
$v->value = (int) $objectNumber;
$v->generationNumber = (int) $generationNumber;
return $v;
}
/**
* Ensures that the passed value is a PdfIndirectObject instance.
*
* @param mixed $value
* @return self
* @throws PdfTypeException
*/
public static function ensure($value)
{
return PdfType::ensureType(self::class, $value, 'Indirect reference value expected.');
}
/**
* The generation number.
*
* @var int
*/
public $generationNumber;
}

View File

@ -0,0 +1,82 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\StreamReader;
use setasign\Fpdi\PdfParser\Tokenizer;
/**
* Class representing a PDF name object
*/
class PdfName extends PdfType
{
/**
* Parses a name object from the passed tokenizer and stream-reader.
*
* @param Tokenizer $tokenizer
* @param StreamReader $streamReader
* @return self
*/
public static function parse(Tokenizer $tokenizer, StreamReader $streamReader)
{
$v = new self();
if (\strspn($streamReader->getByte(), "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%") === 0) {
$v->value = (string) $tokenizer->getNextToken();
return $v;
}
$v->value = '';
return $v;
}
/**
* Unescapes a name string.
*
* @param string $value
* @return string
*/
public static function unescape($value)
{
if (strpos($value, '#') === false) {
return $value;
}
return preg_replace_callback('/#([a-fA-F\d]{2})/', function ($matches) {
return chr(hexdec($matches[1]));
}, $value);
}
/**
* Helper method to create an instance.
*
* @param string $string
* @return self
*/
public static function create($string)
{
$v = new self();
$v->value = $string;
return $v;
}
/**
* Ensures that the passed value is a PdfName instance.
*
* @param mixed $name
* @return self
* @throws PdfTypeException
*/
public static function ensure($name)
{
return PdfType::ensureType(self::class, $name, 'Name value expected.');
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
/**
* Class representing a PDF null object
*/
class PdfNull extends PdfType
{
// empty body
}

View File

@ -0,0 +1,43 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
/**
* Class representing a numeric PDF object
*/
class PdfNumeric extends PdfType
{
/**
* Helper method to create an instance.
*
* @param int|float $value
* @return PdfNumeric
*/
public static function create($value)
{
$v = new self();
$v->value = $value + 0;
return $v;
}
/**
* Ensures that the passed value is a PdfNumeric instance.
*
* @param mixed $value
* @return self
* @throws PdfTypeException
*/
public static function ensure($value)
{
return PdfType::ensureType(self::class, $value, 'Numeric value expected.');
}
}

View File

@ -0,0 +1,352 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\Filter\Ascii85;
use setasign\Fpdi\PdfParser\Filter\AsciiHex;
use setasign\Fpdi\PdfParser\Filter\FilterException;
use setasign\Fpdi\PdfParser\Filter\Flate;
use setasign\Fpdi\PdfParser\Filter\Lzw;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\StreamReader;
use setasign\FpdiPdfParser\PdfParser\Filter\Predictor;
/**
* Class representing a PDF stream object
*/
class PdfStream extends PdfType
{
/**
* Parses a stream from a stream reader.
*
* @param PdfDictionary $dictionary
* @param StreamReader $reader
* @param PdfParser|null $parser Optional to keep backwards compatibility
* @return self
* @throws PdfTypeException
*/
public static function parse(PdfDictionary $dictionary, StreamReader $reader, ?PdfParser $parser = null)
{
$v = new self();
$v->value = $dictionary;
$v->reader = $reader;
$v->parser = $parser;
$offset = $reader->getOffset();
// Find the first "newline"
while (($firstByte = $reader->getByte($offset)) !== false) {
$offset++;
if ($firstByte === "\n" || $firstByte === "\r") {
break;
}
}
if ($firstByte === false) {
throw new PdfTypeException(
'Unable to parse stream data. No newline after the stream keyword found.',
PdfTypeException::NO_NEWLINE_AFTER_STREAM_KEYWORD
);
}
$sndByte = $reader->getByte($offset);
if ($sndByte === "\n" && $firstByte !== "\n") {
$offset++;
}
$reader->setOffset($offset);
// let's only save the byte-offset and read the stream only when needed
$v->stream = $reader->getPosition() + $reader->getOffset();
return $v;
}
/**
* Helper method to create an instance.
*
* @param PdfDictionary $dictionary
* @param string $stream
* @return self
*/
public static function create(PdfDictionary $dictionary, $stream)
{
$v = new self();
$v->value = $dictionary;
$v->stream = (string) $stream;
return $v;
}
/**
* Ensures that the passed value is a PdfStream instance.
*
* @param mixed $stream
* @return self
* @throws PdfTypeException
*/
public static function ensure($stream)
{
return PdfType::ensureType(self::class, $stream, 'Stream value expected.');
}
/**
* The stream or its byte-offset position.
*
* @var int|string
*/
protected $stream;
/**
* The stream reader instance.
*
* @var StreamReader|null
*/
protected $reader;
/**
* The PDF parser instance.
*
* @var PdfParser
*/
protected $parser;
/**
* Get the stream data.
*
* @param bool $cache Whether cache the stream data or not.
* @return bool|string
* @throws PdfTypeException
* @throws CrossReferenceException
* @throws PdfParserException
*/
public function getStream($cache = false)
{
if (\is_int($this->stream)) {
$length = PdfDictionary::get($this->value, 'Length');
if ($this->parser !== null) {
$length = PdfType::resolve($length, $this->parser);
}
if (!($length instanceof PdfNumeric) || $length->value === 0) {
$this->reader->reset($this->stream, 100000);
$buffer = $this->extractStream();
} else {
$this->reader->reset($this->stream, $length->value);
$buffer = $this->reader->getBuffer(false);
if ($this->parser !== null) {
$this->reader->reset($this->stream + strlen($buffer));
$this->parser->getTokenizer()->clearStack();
$token = $this->parser->readValue();
if ($token === false || !($token instanceof PdfToken) || $token->value !== 'endstream') {
$this->reader->reset($this->stream, 100000);
$buffer = $this->extractStream();
$this->reader->reset($this->stream + strlen($buffer));
}
}
}
if ($cache === false) {
return $buffer;
}
$this->stream = $buffer;
$this->reader = null;
}
return $this->stream;
}
/**
* Extract the stream "manually".
*
* @return string
* @throws PdfTypeException
*/
protected function extractStream()
{
while (true) {
$buffer = $this->reader->getBuffer(false);
$length = \strpos($buffer, 'endstream');
if ($length === false) {
if (!$this->reader->increaseLength(100000)) {
throw new PdfTypeException('Cannot extract stream.');
}
continue;
}
break;
}
$buffer = \substr($buffer, 0, $length);
$lastByte = \substr($buffer, -1);
/* Check for EOL marker =
* CARRIAGE RETURN (\r) and a LINE FEED (\n) or just a LINE FEED (\n},
* and not by a CARRIAGE RETURN (\r) alone
*/
if ($lastByte === "\n") {
$buffer = \substr($buffer, 0, -1);
$lastByte = \substr($buffer, -1);
if ($lastByte === "\r") {
$buffer = \substr($buffer, 0, -1);
}
}
// There are streams in the wild, which have only white signs in them but need to be parsed manually due
// to a problem encountered before (e.g. Length === 0). We should set them to empty streams to avoid problems
// in further processing (e.g. applying of filters).
if (trim($buffer) === '') {
$buffer = '';
}
return $buffer;
}
/**
* Get all filters defined for this stream.
*
* @return PdfType[]
* @throws PdfTypeException
*/
public function getFilters()
{
$filters = PdfDictionary::get($this->value, 'Filter');
if ($filters instanceof PdfNull) {
return [];
}
if ($filters instanceof PdfArray) {
$filters = $filters->value;
} else {
$filters = [$filters];
}
return $filters;
}
/**
* Get the unfiltered stream data.
*
* @return string
* @throws FilterException
* @throws PdfParserException
*/
public function getUnfilteredStream()
{
$stream = $this->getStream();
$filters = $this->getFilters();
if ($filters === []) {
return $stream;
}
$decodeParams = PdfDictionary::get($this->value, 'DecodeParms');
if ($decodeParams instanceof PdfArray) {
$decodeParams = $decodeParams->value;
} else {
$decodeParams = [$decodeParams];
}
foreach ($filters as $key => $filter) {
if (!($filter instanceof PdfName)) {
continue;
}
$decodeParam = null;
if (isset($decodeParams[$key])) {
$decodeParam = ($decodeParams[$key] instanceof PdfDictionary ? $decodeParams[$key] : null);
}
switch ($filter->value) {
case 'FlateDecode':
case 'Fl':
case 'LZWDecode':
case 'LZW':
if (\strpos($filter->value, 'LZW') === 0) {
$filterObject = new Lzw();
} else {
$filterObject = new Flate();
}
$stream = $filterObject->decode($stream);
if ($decodeParam instanceof PdfDictionary) {
$predictor = PdfDictionary::get($decodeParam, 'Predictor', PdfNumeric::create(1));
if ($predictor->value !== 1) {
if (!\class_exists(Predictor::class)) {
throw new PdfParserException(
'This PDF document makes use of features which are only implemented in the ' .
'commercial "FPDI PDF-Parser" add-on (see https://www.setasign.com/fpdi-pdf-' .
'parser).',
PdfParserException::IMPLEMENTED_IN_FPDI_PDF_PARSER
);
}
$colors = PdfDictionary::get($decodeParam, 'Colors', PdfNumeric::create(1));
$bitsPerComponent = PdfDictionary::get(
$decodeParam,
'BitsPerComponent',
PdfNumeric::create(8)
);
$columns = PdfDictionary::get($decodeParam, 'Columns', PdfNumeric::create(1));
$filterObject = new Predictor(
$predictor->value,
$colors->value,
$bitsPerComponent->value,
$columns->value
);
$stream = $filterObject->decode($stream);
}
}
break;
case 'ASCII85Decode':
case 'A85':
$filterObject = new Ascii85();
$stream = $filterObject->decode($stream);
break;
case 'ASCIIHexDecode':
case 'AHx':
$filterObject = new AsciiHex();
$stream = $filterObject->decode($stream);
break;
case 'Crypt':
if (!$decodeParam instanceof PdfDictionary) {
break;
}
// Filter is "Identity"
$name = PdfDictionary::get($decodeParam, 'Name');
if (!$name instanceof PdfName || $name->value !== 'Identity') {
break;
}
throw new FilterException(
'Support for Crypt filters other than "Identity" is not implemented.',
FilterException::UNSUPPORTED_FILTER
);
default:
throw new FilterException(
\sprintf('Unsupported filter "%s".', $filter->value),
FilterException::UNSUPPORTED_FILTER
);
}
}
return $stream;
}
}

View File

@ -0,0 +1,202 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\StreamReader;
/**
* Class representing a PDF string object
*/
class PdfString extends PdfType
{
/**
* Parses a string object from the stream reader.
*
* @param StreamReader $streamReader
* @return self
*/
public static function parse(StreamReader $streamReader)
{
$pos = $startPos = $streamReader->getOffset();
$openBrackets = 1;
do {
$buffer = $streamReader->getBuffer(false);
for ($length = \strlen($buffer); $openBrackets !== 0 && $pos < $length; $pos++) {
switch ($buffer[$pos]) {
case '(':
$openBrackets++;
break;
case ')':
$openBrackets--;
break;
case '\\':
$pos++;
}
}
} while ($openBrackets !== 0 && $streamReader->increaseLength());
$result = \substr($buffer, $startPos, $openBrackets + $pos - $startPos - 1);
$streamReader->setOffset($pos);
$v = new self();
$v->value = $result;
return $v;
}
/**
* Helper method to create an instance.
*
* @param string $value The string needs to be escaped accordingly.
* @return self
*/
public static function create($value)
{
$v = new self();
$v->value = $value;
return $v;
}
/**
* Ensures that the passed value is a PdfString instance.
*
* @param mixed $string
* @return self
* @throws PdfTypeException
*/
public static function ensure($string)
{
return PdfType::ensureType(self::class, $string, 'String value expected.');
}
/**
* Escapes sequences in a string according to the PDF specification.
*
* @param string $s
* @return string
*/
public static function escape($s)
{
// Still a bit faster, than direct replacing
if (
\strpos($s, '\\') !== false ||
\strpos($s, ')') !== false ||
\strpos($s, '(') !== false ||
\strpos($s, "\x0D") !== false ||
\strpos($s, "\x0A") !== false ||
\strpos($s, "\x09") !== false ||
\strpos($s, "\x08") !== false ||
\strpos($s, "\x0C") !== false
) {
// is faster than strtr(...)
return \str_replace(
['\\', ')', '(', "\x0D", "\x0A", "\x09", "\x08", "\x0C"],
['\\\\', '\\)', '\\(', '\r', '\n', '\t', '\b', '\f'],
$s
);
}
return $s;
}
/**
* Unescapes escaped sequences in a PDF string according to the PDF specification.
*
* @param string $s
* @return string
*/
public static function unescape($s)
{
$out = '';
/** @noinspection ForeachInvariantsInspection */
for ($count = 0, $n = \strlen($s); $count < $n; $count++) {
if ($s[$count] !== '\\') {
$out .= $s[$count];
} else {
// A backslash at the end of the string - ignore it
if ($count === ($n - 1)) {
break;
}
switch ($s[++$count]) {
case ')':
case '(':
case '\\':
$out .= $s[$count];
break;
case 'f':
$out .= "\x0C";
break;
case 'b':
$out .= "\x08";
break;
case 't':
$out .= "\x09";
break;
case 'r':
$out .= "\x0D";
break;
case 'n':
$out .= "\x0A";
break;
case "\r":
if ($count !== $n - 1 && $s[$count + 1] === "\n") {
$count++;
}
break;
case "\n":
break;
default:
$actualChar = \ord($s[$count]);
// ascii 48 = number 0
// ascii 57 = number 9
if ($actualChar >= 48 && $actualChar <= 57) {
$oct = '' . $s[$count];
/** @noinspection NotOptimalIfConditionsInspection */
if (
$count + 1 < $n
&& \ord($s[$count + 1]) >= 48
&& \ord($s[$count + 1]) <= 57
) {
$count++;
$oct .= $s[$count];
/** @noinspection NotOptimalIfConditionsInspection */
if (
$count + 1 < $n
&& \ord($s[$count + 1]) >= 48
&& \ord($s[$count + 1]) <= 57
) {
$oct .= $s[++$count];
}
}
$out .= \chr(\octdec($oct));
} else {
// If the character is not one of those defined, the backslash is ignored
$out .= $s[$count];
}
}
}
}
return $out;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
/**
* Class representing PDF token object
*/
class PdfToken extends PdfType
{
/**
* Helper method to create an instance.
*
* @param string $token
* @return self
*/
public static function create($token)
{
$v = new self();
$v->value = $token;
return $v;
}
/**
* Ensures that the passed value is a PdfToken instance.
*
* @param mixed $token
* @return self
* @throws PdfTypeException
*/
public static function ensure($token)
{
return PdfType::ensureType(self::class, $token, 'Token value expected.');
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\PdfParserException;
/**
* A class defining a PDF data type
*/
class PdfType
{
/**
* Resolves a PdfType value to its value.
*
* This method is used to evaluate indirect and direct object references until a final value is reached.
*
* @param PdfType $value
* @param PdfParser $parser
* @param bool $stopAtIndirectObject
* @return PdfType
* @throws CrossReferenceException
* @throws PdfParserException
*/
public static function resolve(PdfType $value, PdfParser $parser, $stopAtIndirectObject = false)
{
if ($value instanceof PdfIndirectObject) {
if ($stopAtIndirectObject === true) {
return $value;
}
return self::resolve($value->value, $parser, $stopAtIndirectObject);
}
if ($value instanceof PdfIndirectObjectReference) {
return self::resolve($parser->getIndirectObject($value->value), $parser, $stopAtIndirectObject);
}
return $value;
}
/**
* Ensure that a value is an instance of a specific PDF type.
*
* @param string $type
* @param PdfType $value
* @param string $errorMessage
* @return mixed
* @throws PdfTypeException
*/
protected static function ensureType($type, $value, $errorMessage)
{
if (!($value instanceof $type)) {
throw new PdfTypeException(
$errorMessage,
PdfTypeException::INVALID_DATA_TYPE
);
}
return $value;
}
/**
* Flatten indirect object references to direct objects.
*
* @param PdfType $value
* @param PdfParser $parser
* @return PdfType
* @throws CrossReferenceException
* @throws PdfParserException
*/
public static function flatten(PdfType $value, PdfParser $parser)
{
if ($value instanceof PdfIndirectObjectReference) {
return self::flatten(self::resolve($value, $parser), $parser);
}
if ($value instanceof PdfDictionary || $value instanceof PdfArray) {
foreach ($value->value as $key => $_value) {
$value->value[$key] = self::flatten($_value, $parser);
}
}
if ($value instanceof PdfStream) {
throw new PdfTypeException('There is a stream object found which cannot be flattened to a direct object.');
}
return $value;
}
/**
* The value of the PDF type.
*
* @var mixed
*/
public $value;
}

View File

@ -0,0 +1,24 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfParser\Type;
use setasign\Fpdi\PdfParser\PdfParserException;
/**
* Exception class for pdf type classes
*/
class PdfTypeException extends PdfParserException
{
/**
* @var int
*/
const NO_NEWLINE_AFTER_STREAM_KEYWORD = 0x0601;
}

View File

@ -0,0 +1,179 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfReader\DataStructure;
use setasign\Fpdi\Math\Vector;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\Type\PdfArray;
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
use setasign\Fpdi\PdfParser\Type\PdfType;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
/**
* Class representing a rectangle
*/
class Rectangle
{
/**
* @var int|float
*/
protected $llx;
/**
* @var int|float
*/
protected $lly;
/**
* @var int|float
*/
protected $urx;
/**
* @var int|float
*/
protected $ury;
/**
* Create a rectangle instance by a PdfArray.
*
* @param PdfArray|mixed $array
* @param PdfParser $parser
* @return Rectangle
* @throws PdfTypeException
* @throws CrossReferenceException
* @throws PdfParserException
*/
public static function byPdfArray($array, PdfParser $parser)
{
$array = PdfArray::ensure(PdfType::resolve($array, $parser), 4)->value;
$ax = PdfNumeric::ensure(PdfType::resolve($array[0], $parser))->value;
$ay = PdfNumeric::ensure(PdfType::resolve($array[1], $parser))->value;
$bx = PdfNumeric::ensure(PdfType::resolve($array[2], $parser))->value;
$by = PdfNumeric::ensure(PdfType::resolve($array[3], $parser))->value;
return new self($ax, $ay, $bx, $by);
}
public static function byVectors(Vector $ll, Vector $ur)
{
return new self($ll->getX(), $ll->getY(), $ur->getX(), $ur->getY());
}
/**
* Rectangle constructor.
*
* @param float|int $ax
* @param float|int $ay
* @param float|int $bx
* @param float|int $by
*/
public function __construct($ax, $ay, $bx, $by)
{
$this->llx = \min($ax, $bx);
$this->lly = \min($ay, $by);
$this->urx = \max($ax, $bx);
$this->ury = \max($ay, $by);
}
/**
* Get the width of the rectangle.
*
* @return float|int
*/
public function getWidth()
{
return $this->urx - $this->llx;
}
/**
* Get the height of the rectangle.
*
* @return float|int
*/
public function getHeight()
{
return $this->ury - $this->lly;
}
/**
* Get the lower left abscissa.
*
* @return float|int
*/
public function getLlx()
{
return $this->llx;
}
/**
* Get the lower left ordinate.
*
* @return float|int
*/
public function getLly()
{
return $this->lly;
}
/**
* Get the upper right abscissa.
*
* @return float|int
*/
public function getUrx()
{
return $this->urx;
}
/**
* Get the upper right ordinate.
*
* @return float|int
*/
public function getUry()
{
return $this->ury;
}
/**
* Get the rectangle as an array.
*
* @return array
*/
public function toArray()
{
return [
$this->llx,
$this->lly,
$this->urx,
$this->ury
];
}
/**
* Get the rectangle as a PdfArray.
*
* @return PdfArray
*/
public function toPdfArray()
{
$array = new PdfArray();
$array->value[] = PdfNumeric::create($this->llx);
$array->value[] = PdfNumeric::create($this->lly);
$array->value[] = PdfNumeric::create($this->urx);
$array->value[] = PdfNumeric::create($this->ury);
return $array;
}
}

422
fpdf/src/PdfReader/Page.php Normal file
View File

@ -0,0 +1,422 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfReader;
use setasign\Fpdi\FpdiException;
use setasign\Fpdi\GraphicsState;
use setasign\Fpdi\Math\Vector;
use setasign\Fpdi\PdfParser\Filter\FilterException;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\Type\PdfArray;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfHexString;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfName;
use setasign\Fpdi\PdfParser\Type\PdfNull;
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
use setasign\Fpdi\PdfParser\Type\PdfStream;
use setasign\Fpdi\PdfParser\Type\PdfString;
use setasign\Fpdi\PdfParser\Type\PdfType;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
use setasign\Fpdi\PdfReader\DataStructure\Rectangle;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
/**
* Class representing a page of a PDF document
*/
class Page
{
/**
* @var PdfIndirectObject
*/
protected $pageObject;
/**
* @var PdfDictionary
*/
protected $pageDictionary;
/**
* @var PdfParser
*/
protected $parser;
/**
* Inherited attributes
*
* @var null|array
*/
protected $inheritedAttributes;
/**
* Page constructor.
*
* @param PdfIndirectObject $page
* @param PdfParser $parser
*/
public function __construct(PdfIndirectObject $page, PdfParser $parser)
{
$this->pageObject = $page;
$this->parser = $parser;
}
/**
* Get the indirect object of this page.
*
* @return PdfIndirectObject
*/
public function getPageObject()
{
return $this->pageObject;
}
/**
* Get the dictionary of this page.
*
* @return PdfDictionary
* @throws PdfParserException
* @throws PdfTypeException
* @throws CrossReferenceException
*/
public function getPageDictionary()
{
if ($this->pageDictionary === null) {
$this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser));
}
return $this->pageDictionary;
}
/**
* Get a page attribute.
*
* @param string $name
* @param bool $inherited
* @return PdfType|null
* @throws PdfParserException
* @throws PdfTypeException
* @throws CrossReferenceException
*/
public function getAttribute($name, $inherited = true)
{
$dict = $this->getPageDictionary();
if (isset($dict->value[$name])) {
return $dict->value[$name];
}
$inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate'];
if ($inherited && \in_array($name, $inheritedKeys, true)) {
if ($this->inheritedAttributes === null) {
$this->inheritedAttributes = [];
$inheritedKeys = \array_filter($inheritedKeys, function ($key) use ($dict) {
return !isset($dict->value[$key]);
});
if (\count($inheritedKeys) > 0) {
$parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser);
while ($parentDict instanceof PdfDictionary) {
foreach ($inheritedKeys as $index => $key) {
if (isset($parentDict->value[$key])) {
$this->inheritedAttributes[$key] = $parentDict->value[$key];
unset($inheritedKeys[$index]);
}
}
/** @noinspection NotOptimalIfConditionsInspection */
if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) {
$parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser);
} else {
break;
}
}
}
}
if (isset($this->inheritedAttributes[$name])) {
return $this->inheritedAttributes[$name];
}
}
return null;
}
/**
* Get the rotation value.
*
* @return int
* @throws PdfParserException
* @throws PdfTypeException
* @throws CrossReferenceException
*/
public function getRotation()
{
$rotation = $this->getAttribute('Rotate');
if ($rotation === null) {
return 0;
}
$rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360;
if ($rotation < 0) {
$rotation += 360;
}
return $rotation;
}
/**
* Get a boundary of this page.
*
* @param string $box
* @param bool $fallback
* @return bool|Rectangle
* @throws PdfParserException
* @throws PdfTypeException
* @throws CrossReferenceException
* @see PageBoundaries
*/
public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true)
{
$value = $this->getAttribute($box);
if ($value !== null) {
return Rectangle::byPdfArray($value, $this->parser);
}
if ($fallback === false) {
return false;
}
switch ($box) {
case PageBoundaries::BLEED_BOX:
case PageBoundaries::TRIM_BOX:
case PageBoundaries::ART_BOX:
return $this->getBoundary(PageBoundaries::CROP_BOX, true);
case PageBoundaries::CROP_BOX:
return $this->getBoundary(PageBoundaries::MEDIA_BOX, true);
}
return false;
}
/**
* Get the width and height of this page.
*
* @param string $box
* @param bool $fallback
* @return array|bool
* @throws PdfParserException
* @throws PdfTypeException
* @throws CrossReferenceException
*/
public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true)
{
$boundary = $this->getBoundary($box, $fallback);
if ($boundary === false) {
return false;
}
$rotation = $this->getRotation();
$interchange = ($rotation / 90) % 2;
return [
$interchange ? $boundary->getHeight() : $boundary->getWidth(),
$interchange ? $boundary->getWidth() : $boundary->getHeight()
];
}
/**
* Get the raw content stream.
*
* @return string
* @throws PdfReaderException
* @throws PdfTypeException
* @throws FilterException
* @throws PdfParserException
*/
public function getContentStream()
{
$dict = $this->getPageDictionary();
$contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser);
if ($contents instanceof PdfNull) {
return '';
}
if ($contents instanceof PdfArray) {
$result = [];
foreach ($contents->value as $content) {
$content = PdfType::resolve($content, $this->parser);
if (!($content instanceof PdfStream)) {
continue;
}
$result[] = $content->getUnfilteredStream();
}
return \implode("\n", $result);
}
if ($contents instanceof PdfStream) {
return $contents->getUnfilteredStream();
}
throw new PdfReaderException(
'Array or stream expected.',
PdfReaderException::UNEXPECTED_DATA_TYPE
);
}
/**
* Get information of all external links on this page.
*
* All coordinates are normalized in view to rotation and translation of the boundary-box, so that their
* origin is lower-left.
*
* The URI is the binary value of the PDF string object. It can be in PdfDocEncoding or in UTF-16BE encoding.
*
* @return array
*/
public function getExternalLinks($box = PageBoundaries::CROP_BOX)
{
try {
$dict = $this->getPageDictionary();
$annotations = PdfType::resolve(PdfDictionary::get($dict, 'Annots'), $this->parser);
} catch (FpdiException $e) {
return [];
}
if (!$annotations instanceof PdfArray) {
return [];
}
$links = [];
foreach ($annotations->value as $entry) {
try {
$annotation = PdfType::resolve($entry, $this->parser);
$value = PdfType::resolve(PdfDictionary::get($annotation, 'Subtype'), $this->parser);
if (!$value instanceof PdfName || $value->value !== 'Link') {
continue;
}
$dest = PdfType::resolve(PdfDictionary::get($annotation, 'Dest'), $this->parser);
if (!$dest instanceof PdfNull) {
continue;
}
$action = PdfType::resolve(PdfDictionary::get($annotation, 'A'), $this->parser);
if (!$action instanceof PdfDictionary) {
continue;
}
$actionType = PdfType::resolve(PdfDictionary::get($action, 'S'), $this->parser);
if (!$actionType instanceof PdfName || $actionType->value !== 'URI') {
continue;
}
$uri = PdfType::resolve(PdfDictionary::get($action, 'URI'), $this->parser);
if ($uri instanceof PdfString) {
$uriValue = PdfString::unescape($uri->value);
} elseif ($uri instanceof PdfHexString) {
$uriValue = \hex2bin($uri->value);
} else {
continue;
}
$rect = PdfType::resolve(PdfDictionary::get($annotation, 'Rect'), $this->parser);
if (!$rect instanceof PdfArray || count($rect->value) !== 4) {
continue;
}
$rect = Rectangle::byPdfArray($rect, $this->parser);
if ($rect->getWidth() === 0 || $rect->getHeight() === 0) {
continue;
}
$bbox = $this->getBoundary($box);
$rotation = $this->getRotation();
$gs = new GraphicsState();
$gs->translate(-$bbox->getLlx(), -$bbox->getLly());
$gs->rotate($bbox->getLlx(), $bbox->getLly(), -$rotation);
switch ($rotation) {
case 90:
$gs->translate(-$bbox->getWidth(), 0);
break;
case 180:
$gs->translate(-$bbox->getWidth(), -$bbox->getHeight());
break;
case 270:
$gs->translate(0, -$bbox->getHeight());
break;
}
$normalizedRect = Rectangle::byVectors(
$gs->toUserSpace(new Vector($rect->getLlx(), $rect->getLly())),
$gs->toUserSpace(new Vector($rect->getUrx(), $rect->getUry()))
);
$quadPoints = PdfType::resolve(PdfDictionary::get($annotation, 'QuadPoints'), $this->parser);
$normalizedQuadPoints = [];
if ($quadPoints instanceof PdfArray) {
$quadPointsCount = count($quadPoints->value);
if ($quadPointsCount % 8 === 0) {
for ($i = 0; ($i + 1) < $quadPointsCount; $i += 2) {
$x = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i], $this->parser));
$y = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i + 1], $this->parser));
$v = $gs->toUserSpace(new Vector($x->value, $y->value));
$normalizedQuadPoints[] = $v->getX();
$normalizedQuadPoints[] = $v->getY();
}
}
}
// we remove unsupported/unneeded values here
unset(
$annotation->value['P'],
$annotation->value['NM'],
$annotation->value['AP'],
$annotation->value['AS'],
$annotation->value['Type'],
$annotation->value['Subtype'],
$annotation->value['Rect'],
$annotation->value['A'],
$annotation->value['QuadPoints'],
$annotation->value['Rotate'],
$annotation->value['M'],
$annotation->value['StructParent'],
$annotation->value['OC']
);
// ...and flatten the PDF object to eliminate any indirect references.
// Indirect references are a problem when writing the output in FPDF
// because FPDF uses pre-calculated object numbers while FPDI creates
// them at runtime.
$annotation = PdfType::flatten($annotation, $this->parser);
$links[] = [
'rect' => $normalizedRect,
'quadPoints' => $normalizedQuadPoints,
'uri' => $uriValue,
'pdfObject' => $annotation
];
} catch (FpdiException $e) {
continue;
}
}
return $links;
}
}

View File

@ -0,0 +1,94 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfReader;
/**
* An abstract class for page boundary constants and some helper methods
*/
abstract class PageBoundaries
{
/**
* MediaBox
*
* The media box defines the boundaries of the physical medium on which the page is to be printed.
*
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
* @var string
*/
const MEDIA_BOX = 'MediaBox';
/**
* CropBox
*
* The crop box defines the region to which the contents of the page shall be clipped (cropped) when displayed or
* printed.
*
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
* @var string
*/
const CROP_BOX = 'CropBox';
/**
* BleedBox
*
* The bleed box defines the region to which the contents of the page shall be clipped when output in a
* production environment.
*
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
* @var string
*/
const BLEED_BOX = 'BleedBox';
/**
* TrimBox
*
* The trim box defines the intended dimensions of the finished page after trimming.
*
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
* @var string
*/
const TRIM_BOX = 'TrimBox';
/**
* ArtBox
*
* The art box defines the extent of the pages meaningful content (including potential white space) as intended
* by the pages creator.
*
* @see PDF 32000-1:2008 - 14.11.2 Page Boundaries
* @var string
*/
const ART_BOX = 'ArtBox';
/**
* All page boundaries
*
* @var array
*/
public static $all = array(
self::MEDIA_BOX,
self::CROP_BOX,
self::BLEED_BOX,
self::TRIM_BOX,
self::ART_BOX
);
/**
* Checks if a name is a valid page boundary name.
*
* @param string $name The boundary name
* @return boolean A boolean value whether the name is valid or not.
*/
public static function isValidName($name)
{
return \in_array($name, self::$all, true);
}
}

View File

@ -0,0 +1,243 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfReader;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\PdfParser;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\Type\PdfArray;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference;
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
use setasign\Fpdi\PdfParser\Type\PdfType;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
/**
* A PDF reader class
*/
class PdfReader
{
/**
* @var PdfParser
*/
protected $parser;
/**
* @var int
*/
protected $pageCount;
/**
* Indirect objects of resolved pages.
*
* @var PdfIndirectObjectReference[]|PdfIndirectObject[]
*/
protected $pages = [];
/**
* PdfReader constructor.
*
* @param PdfParser $parser
*/
public function __construct(PdfParser $parser)
{
$this->parser = $parser;
}
/**
* PdfReader destructor.
*/
public function __destruct()
{
if ($this->parser !== null) {
$this->parser->cleanUp();
}
}
/**
* Get the pdf parser instance.
*
* @return PdfParser
*/
public function getParser()
{
return $this->parser;
}
/**
* Get the PDF version.
*
* @return string
* @throws PdfParserException
*/
public function getPdfVersion()
{
return \implode('.', $this->parser->getPdfVersion());
}
/**
* Get the page count.
*
* @return int
* @throws PdfTypeException
* @throws CrossReferenceException
* @throws PdfParserException
*/
public function getPageCount()
{
if ($this->pageCount === null) {
$catalog = $this->parser->getCatalog();
$pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
$count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
$this->pageCount = PdfNumeric::ensure($count)->value;
}
return $this->pageCount;
}
/**
* Get a page instance.
*
* @param int|numeric-string $pageNumber
* @return Page
* @throws PdfTypeException
* @throws CrossReferenceException
* @throws PdfParserException
* @throws \InvalidArgumentException
*/
public function getPage($pageNumber)
{
if (!\is_numeric($pageNumber)) {
throw new \InvalidArgumentException(
'Page number needs to be a number.'
);
}
if ($pageNumber < 1 || $pageNumber > $this->getPageCount()) {
throw new \InvalidArgumentException(
\sprintf(
'Page number "%s" out of available page range (1 - %s)',
$pageNumber,
$this->getPageCount()
)
);
}
$this->readPages();
$page = $this->pages[$pageNumber - 1];
if ($page instanceof PdfIndirectObjectReference) {
$readPages = function ($kids) use (&$readPages) {
$kids = PdfArray::ensure($kids);
/** @noinspection LoopWhichDoesNotLoopInspection */
foreach ($kids->value as $reference) {
$reference = PdfIndirectObjectReference::ensure($reference);
$object = $this->parser->getIndirectObject($reference->value);
$type = PdfDictionary::get($object->value, 'Type');
if ($type->value === 'Pages') {
return $readPages(PdfDictionary::get($object->value, 'Kids'));
}
return $object;
}
throw new PdfReaderException(
'Kids array cannot be empty.',
PdfReaderException::KIDS_EMPTY
);
};
$page = $this->parser->getIndirectObject($page->value);
$dict = PdfType::resolve($page, $this->parser);
$type = PdfDictionary::get($dict, 'Type');
if ($type->value === 'Pages') {
$kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser);
try {
$page = $this->pages[$pageNumber - 1] = $readPages($kids);
} catch (PdfReaderException $e) {
if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) {
throw $e;
}
// let's reset the pages array and read all page objects
$this->pages = [];
$this->readPages(true);
// @phpstan-ignore-next-line
$page = $this->pages[$pageNumber - 1];
}
} else {
$this->pages[$pageNumber - 1] = $page;
}
}
return new Page($page, $this->parser);
}
/**
* Walk the page tree and resolve all indirect objects of all pages.
*
* @param bool $readAll
* @throws CrossReferenceException
* @throws PdfParserException
* @throws PdfTypeException
*/
protected function readPages($readAll = false)
{
if (\count($this->pages) > 0) {
return;
}
$expectedPageCount = $this->getPageCount();
$readPages = function ($kids, $count) use (&$readPages, $readAll, $expectedPageCount) {
$kids = PdfArray::ensure($kids);
$isLeaf = ($count->value === \count($kids->value));
foreach ($kids->value as $reference) {
$reference = PdfIndirectObjectReference::ensure($reference);
if (!$readAll && $isLeaf) {
$this->pages[] = $reference;
continue;
}
$object = $this->parser->getIndirectObject($reference->value);
$type = PdfDictionary::get($object->value, 'Type');
if ($type->value === 'Pages') {
$readPages(
PdfType::resolve(PdfDictionary::get($object->value, 'Kids'), $this->parser),
PdfType::resolve(PdfDictionary::get($object->value, 'Count'), $this->parser)
);
} else {
$this->pages[] = $object;
}
// stop if all pages are read - faulty documents exists with additional entries with invalid data.
if (count($this->pages) === $expectedPageCount) {
break;
}
}
};
$catalog = $this->parser->getCatalog();
$pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser);
$count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser);
$kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $this->parser);
$readPages($kids, $count);
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\PdfReader;
use setasign\Fpdi\FpdiException;
/**
* Exception for the pdf reader class
*/
class PdfReaderException extends FpdiException
{
/**
* @var int
*/
const KIDS_EMPTY = 0x0101;
/**
* @var int
*/
const UNEXPECTED_DATA_TYPE = 0x0102;
/**
* @var int
*/
const MISSING_DATA = 0x0103;
}

391
fpdf/src/Tcpdf/Fpdi.php Normal file
View File

@ -0,0 +1,391 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\Tcpdf;
use setasign\Fpdi\FpdiException;
use setasign\Fpdi\FpdiTrait;
use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException;
use setasign\Fpdi\PdfParser\Filter\AsciiHex;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfParser\Type\PdfArray;
use setasign\Fpdi\PdfParser\Type\PdfDictionary;
use setasign\Fpdi\PdfParser\Type\PdfHexString;
use setasign\Fpdi\PdfParser\Type\PdfIndirectObject;
use setasign\Fpdi\PdfParser\Type\PdfName;
use setasign\Fpdi\PdfParser\Type\PdfNull;
use setasign\Fpdi\PdfParser\Type\PdfNumeric;
use setasign\Fpdi\PdfParser\Type\PdfStream;
use setasign\Fpdi\PdfParser\Type\PdfString;
use setasign\Fpdi\PdfParser\Type\PdfType;
use setasign\Fpdi\PdfParser\Type\PdfTypeException;
/**
* Class Fpdi
*
* This class let you import pages of existing PDF documents into a reusable structure for TCPDF.
*
* @method _encrypt_data(int $n, string $s) string
*/
class Fpdi extends \TCPDF
{
use FpdiTrait {
writePdfType as fpdiWritePdfType;
useImportedPage as fpdiUseImportedPage;
}
/**
* FPDI version
*
* @string
*/
const VERSION = '2.6.3';
/**
* A counter for template ids.
*
* @var int
*/
protected $templateId = 0;
/**
* The currently used object number.
*
* @var int|null
*/
protected $currentObjectNumber;
protected function _enddoc()
{
parent::_enddoc();
$this->cleanUp();
}
/**
* Get the next template id.
*
* @return int
*/
protected function getNextTemplateId()
{
return $this->templateId++;
}
/**
* Draws an imported page onto the page or another template.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $tpl The template id
* @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
* with the keys "x", "y", "width", "height", "adjustPageSize".
* @param float|int $y The ordinate of upper-left corner.
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @param bool $adjustPageSize
* @return array The size
* @see FpdiTrait::getTemplateSize()
*/
public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
{
return $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize);
}
/**
* Draws an imported page onto the page.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $pageId The page id
* @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array
* with the keys "x", "y", "width", "height", "adjustPageSize".
* @param float|int $y The ordinate of upper-left corner.
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @param bool $adjustPageSize
* @return array The size.
* @see Fpdi::getTemplateSize()
*/
public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false)
{
$size = $this->fpdiUseImportedPage($pageId, $x, $y, $width, $height, $adjustPageSize);
if ($this->inxobj) {
$importedPage = $this->importedPages[$pageId];
$this->xobjects[$this->xobjid]['importedPages'][$importedPage['id']] = $pageId;
}
return $size;
}
/**
* Get the size of an imported page.
*
* Give only one of the size parameters (width, height) to calculate the other one automatically in view to the
* aspect ratio.
*
* @param mixed $tpl The template id
* @param float|int|null $width The width.
* @param float|int|null $height The height.
* @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P)
*/
public function getTemplateSize($tpl, $width = null, $height = null)
{
return $this->getImportedPageSize($tpl, $width, $height);
}
/**
* @inheritdoc
* @return string
*/
protected function _getxobjectdict()
{
$out = parent::_getxobjectdict();
foreach ($this->importedPages as $pageData) {
$out .= '/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R ';
}
return $out;
}
/**
* @inheritdoc
* @throws CrossReferenceException
* @throws PdfParserException
*/
protected function _putxobjects()
{
foreach ($this->importedPages as $key => $pageData) {
$this->currentObjectNumber = $this->_newobj();
$this->importedPages[$key]['objectNumber'] = $this->currentObjectNumber;
$this->currentReaderId = $pageData['readerId'];
$this->writePdfType($pageData['stream']);
$this->_put('endobj');
}
foreach (\array_keys($this->readers) as $readerId) {
$parser = $this->getPdfReader($readerId)->getParser();
$this->currentReaderId = $readerId;
while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) {
try {
$object = $parser->getIndirectObject($objectNumber);
} catch (CrossReferenceException $e) {
if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) {
$object = PdfIndirectObject::create($objectNumber, 0, new PdfNull());
} else {
throw $e;
}
}
$this->writePdfType($object);
}
}
// let's prepare resources for imported pages in templates
foreach ($this->xobjects as $xObjectId => $data) {
if (!isset($data['importedPages'])) {
continue;
}
foreach ($data['importedPages'] as $id => $pageKey) {
$page = $this->importedPages[$pageKey];
$this->xobjects[$xObjectId]['xobjects'][$id] = ['n' => $page['objectNumber']];
}
}
parent::_putxobjects();
$this->currentObjectNumber = null;
}
/**
* Append content to the buffer of TCPDF.
*
* @param string $s
* @param bool $newLine
*/
protected function _put($s, $newLine = true)
{
if ($newLine) {
$this->setBuffer($s . "\n");
} else {
$this->setBuffer($s);
}
}
/**
* Begin a new object and return the object number.
*
* @param int|string $objid Object ID (leave empty to get a new ID).
* @return int object number
*/
protected function _newobj($objid = '')
{
$this->_out($this->_getobj($objid));
return $this->n;
}
/**
* Writes a PdfType object to the resulting buffer.
*
* @param PdfType $value
* @throws PdfTypeException
*/
protected function writePdfType(PdfType $value)
{
if (!$this->encrypted) {
$this->fpdiWritePdfType($value);
return;
}
if ($value instanceof PdfString) {
$string = PdfString::unescape($value->value);
$string = $this->_encrypt_data($this->currentObjectNumber, $string);
$value->value = PdfString::escape($string);
} elseif ($value instanceof PdfHexString) {
$filter = new AsciiHex();
$string = $filter->decode($value->value);
$string = $this->_encrypt_data($this->currentObjectNumber, $string);
$value->value = $filter->encode($string, true);
} elseif ($value instanceof PdfStream) {
$stream = $value->getStream();
$stream = $this->_encrypt_data($this->currentObjectNumber, $stream);
$dictionary = $value->value;
$dictionary->value['Length'] = PdfNumeric::create(\strlen($stream));
$value = PdfStream::create($dictionary, $stream);
} elseif ($value instanceof PdfIndirectObject) {
/**
* @var PdfIndirectObject $value
*/
$this->currentObjectNumber = $this->objectMap[$this->currentReaderId][$value->objectNumber];
}
$this->fpdiWritePdfType($value);
}
/**
* This method will add additional data to the last created link/annotation.
*
* It will copy styling properties (supported by TCPDF) of the imported link.
*
* @param array $externalLink
* @param float|int $xPt
* @param float|int $scaleX
* @param float|int $yPt
* @param float|int $newHeightPt
* @param float|int $scaleY
* @param array $importedPage
* @return void
*/
protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage)
{
$parser = $this->getPdfReader($importedPage['readerId'])->getParser();
if ($this->inxobj) {
// store parameters for later use on template
$lastAnnotationKey = count($this->xobjects[$this->xobjid]['annotations']) - 1;
$lastAnnotationOpt = &$this->xobjects[$this->xobjid]['annotations'][$lastAnnotationKey]['opt'];
} else {
$lastAnnotationKey = count($this->PageAnnots[$this->page]) - 1;
$lastAnnotationOpt = &$this->PageAnnots[$this->page][$lastAnnotationKey]['opt'];
}
// ensure we have a default value - otherwise TCPDF will set it to 4 throughout
$lastAnnotationOpt['f'] = 0;
// values in this dictonary are all direct objects and we don't need to resolve them here again.
$values = $externalLink['pdfObject']->value;
foreach ($values as $key => $value) {
try {
switch ($key) {
case 'BS':
$value = PdfDictionary::ensure($value);
$bs = [];
if (isset($value->value['W'])) {
$bs['w'] = PdfNumeric::ensure($value->value['W'])->value;
}
if (isset($value->value['S'])) {
$bs['s'] = PdfName::ensure($value->value['S'])->value;
}
if (isset($value->value['D'])) {
$d = [];
foreach (PdfArray::ensure($value->value['D'])->value as $item) {
$d[] = PdfNumeric::ensure($item)->value;
}
$bs['d'] = $d;
}
$lastAnnotationOpt['bs'] = $bs;
break;
case 'Border':
$borderArray = PdfArray::ensure($value)->value;
if (count($borderArray) < 3) {
continue 2;
}
$border = [
PdfNumeric::ensure($borderArray[0])->value,
PdfNumeric::ensure($borderArray[1])->value,
PdfNumeric::ensure($borderArray[2])->value,
];
if (isset($borderArray[3])) {
$dashArray = [];
foreach (PdfArray::ensure($borderArray[3])->value as $item) {
$dashArray[] = PdfNumeric::ensure($item)->value;
}
$border[] = $dashArray;
}
$lastAnnotationOpt['border'] = $border;
break;
case 'C':
$c = [];
$colors = PdfArray::ensure(PdfType::resolve($value, $parser))->value;
$m = count($colors) === 4 ? 100 : 255;
foreach ($colors as $item) {
$c[] = PdfNumeric::ensure($item)->value * $m;
}
$lastAnnotationOpt['c'] = $c;
break;
case 'F':
$lastAnnotationOpt['f'] = $value->value;
break;
case 'BE':
// is broken in current TCPDF version: "bc" key is checked but "bs" is used.
break;
}
// let's silence invalid/not supported values
} catch (FpdiException $e) {
continue;
}
}
// QuadPoints are not supported by TCPDF
// if (count($externalLink['quadPoints']) > 0) {
// $quadPoints = [];
// for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) {
// $quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX;
// $quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY;
// }
//
// ????? = $quadPoints;
// }
}
}

23
fpdf/src/TcpdfFpdi.php Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi;
/**
* Class TcpdfFpdi
*
* This class let you import pages of existing PDF documents into a reusable structure for TCPDF.
*
* @deprecated Class was moved to \setasign\Fpdi\Tcpdf\Fpdi
*/
class TcpdfFpdi extends \setasign\Fpdi\Tcpdf\Fpdi
{
// this class is moved to \setasign\Fpdi\Tcpdf\Fpdi
}

View File

@ -0,0 +1,23 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\Tfpdf;
use setasign\Fpdi\FpdfTplTrait;
/**
* Class FpdfTpl
*
* We need to change some access levels and implement the setPageFormat() method to bring back compatibility to tFPDF.
*/
class FpdfTpl extends \tFPDF
{
use FpdfTplTrait;
}

32
fpdf/src/Tfpdf/Fpdi.php Normal file
View File

@ -0,0 +1,32 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
namespace setasign\Fpdi\Tfpdf;
use setasign\Fpdi\FpdfTrait;
use setasign\Fpdi\FpdiTrait;
/**
* Class Fpdi
*
* This class let you import pages of existing PDF documents into a reusable structure for tFPDF.
*/
class Fpdi extends FpdfTpl
{
use FpdiTrait;
use FpdfTrait;
/**
* FPDI version
*
* @string
*/
const VERSION = '2.6.3';
}

21
fpdf/src/autoload.php Normal file
View File

@ -0,0 +1,21 @@
<?php
/**
* This file is part of FPDI
*
* @package setasign\Fpdi
* @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com)
* @license http://opensource.org/licenses/mit-license The MIT License
*/
spl_autoload_register(static function ($class) {
if (strpos($class, 'setasign\Fpdi\\') === 0) {
$filename = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 14)) . '.php';
$fullpath = __DIR__ . DIRECTORY_SEPARATOR . $filename;
if (is_file($fullpath)) {
/** @noinspection PhpIncludeInspection */
require_once $fullpath;
}
}
});

View File

@ -4,7 +4,7 @@ session_start();
$host = 'localhost';
$db = 'diplomaster';
$user = 'root';
$pass = '';
$pass = 'root';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";

View File

@ -0,0 +1,27 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require __DIR__ . '/../vendor/autoload.php';
function getMailer() {
$mail = new PHPMailer(true);
try {
// Configuración del servidor de correos
$mail->isSMTP();
$mail->Host = 'smtp-relay.brevo.com';
$mail->SMTPAuth = true;
$mail->Username = '8fcde0001@smtp-brevo.com';
$mail->Password = 'WJckTKnPgENtSzqd';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('diplomaster.sender@gmail.com', 'LANIA: Diplomas');
return $mail;
} catch (Exception $e) {
error_log("Mailer Error: {$e->getMessage()}");
return null;
}
}

22
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit5726834464b54df5c26cde494ec31af2::getLoader();

22
vendor/bacon/bacon-qr-code/LICENSE vendored Normal file
View File

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

39
vendor/bacon/bacon-qr-code/README.md vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<testsuites>
<testsuite name="BaconQrCode Tests">
<directory>./test</directory>
</testsuite>
</testsuites>
</phpunit>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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