Compare commits

...

11 Commits

Author SHA1 Message Date
Administrator 11c93d3e82 Merge branch 'develop_fix_bugs' into 'develop'
update financial controller and fix curriculum subjects

See merge request root/alrahma_sunday_school!3
2026-03-02 05:06:01 +00:00
root 7c5028a76d update financial controller and fix curriculum subjects 2026-03-02 00:04:38 -05:00
Administrator aa1260afd6 Merge branch 'develop_fix_bugs' into 'develop'
Develop fix bugs

See merge request root/alrahma_sunday_school!2
2026-03-01 22:25:37 +00:00
root 35b9cba882 AVP-85 Active link when clicking of Parent's email address 2026-03-01 17:24:20 -05:00
root a18198d547 AVP-86 Below 60 score 2026-03-01 16:56:45 -05:00
root 97c3b03c39 Merge branch 'develop' of https://gitlab.rentaldrivego.ma/root/alrahma_sunday_school into develop 2026-03-01 16:35:13 -05:00
root 7770b60658 Add full CodeIgniter 4 + Composer + npm + Docker .gitignore 2026-03-01 16:29:16 -05:00
Administrator 4f5925e10a Merge branch 'feature_updatecode' into 'develop'
Feature updatecode

See merge request root/alrahma_sunday_school!1
2026-02-27 06:27:32 +00:00
root 3cc5546733 AVP-78 Feature to check if parents looked at report card 2026-02-27 01:21:39 -05:00
root fe7b99581d update code 2026-02-26 23:50:24 -05:00
root a6880ea14f update code 2026-02-26 23:27:50 -05:00
6419 changed files with 2766 additions and 998803 deletions
+134 -113
View File
@@ -14,62 +14,6 @@
# ENVIRONMENT # ENVIRONMENT
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# CI_ENVIRONMENT = production
#--------------------------------------------------------------------
# APP
#--------------------------------------------------------------------
# app.baseURL = ''
# If you have trouble with `.`, you could also use `_`.
# app_baseURL = ''
# app.forceGlobalSecureRequests = false
# app.CSPEnabled = false
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
# database.default.hostname = localhost
# database.default.database = ci4
# database.default.username = root
# database.default.password = root
# database.default.DBDriver = MySQLi
# database.default.DBPrefix =
# database.default.port = 3306
# If you use MySQLi as tests, first update the values of Config\Database::$tests.
# database.tests.hostname = localhost
# database.tests.database = ci4_test
# database.tests.username = root
# database.tests.password = root
# database.tests.DBDriver = MySQLi
# database.tests.DBPrefix =
# database.tests.charset = utf8mb4
# database.tests.DBCollat = utf8mb4_general_ci
# database.tests.port = 3306
#--------------------------------------------------------------------
# ENCRYPTION
#--------------------------------------------------------------------
# encryption.key =
#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
# session.savePath = null
#--------------------------------------------------------------------
# LOGGER
#--------------------------------------------------------------------
# logger.threshold = 4
#--------------------------------------------------------------------
# ENVIRONMENT
#--------------------------------------------------------------------
CI_ENVIRONMENT = development CI_ENVIRONMENT = development
# Which profile to use if none is supplied to sendEmail() # Which profile to use if none is supplied to sendEmail()
@@ -77,53 +21,20 @@ CI_ENVIRONMENT = development
MAIL_PROFILE_DEFAULT=default MAIL_PROFILE_DEFAULT=default
# ===== DEFAULT (system) ===== # ===== DEFAULT (system) =====
MAIL_DEFAULT_HOST=smtp.hostinger.com # ---- Legacy fallbacks (kept for compatibility) ----
MAIL_DEFAULT_USER=no-replay@alrahmaisgl.org SMTP_HOST = smtp.gmail.com
MAIL_DEFAULT_PASS="W8;xZ/5g" SMTP_USER = alrahma.sunday.school@gmail.com
MAIL_DEFAULT_PORT=587 SMTP_PASS = "psnp emdq dykw ypul"
MAIL_DEFAULT_ENCRYPTION=ssl SMTP_PORT = 587
MAIL_DEFAULT_FROM_EMAIL=no-replay@alrahmaisgl.org SMTP_ENCRYPTION = tls # was ssl; use tls for port 587
MAIL_DEFAULT_FROM_NAME="Al Rahma Sunday School"
# Optional Reply-To (uncomment if you want a replyable inbox)
MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_DEFAULT_REPLY_TO_NAME="No-Reply"
# Optional bounce address (envelope sender)
# MAIL_DEFAULT_RETURN_PATH=bounces@alrahmaisgl.org
# Optional DKIM
# MAIL_DEFAULT_DKIM_DOMAIN=alrahmaisgl.org
# MAIL_DEFAULT_DKIM_SELECTOR=default
# MAIL_DEFAULT_DKIM_PRIVATE=/path/to/private.key
# ===== COMMUNICATION ===== # Reply-To for all mail profiles
MAIL_COMMUNICATION_HOST=smtp.hostinger.com MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_COMMUNICATION_USER=no-replay@alrahmaisgl.org MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School"
MAIL_COMMUNICATION_PASS="W8;xZ/5g"
MAIL_COMMUNICATION_PORT=587
MAIL_COMMUNICATION_ENCRYPTION=ssl
MAIL_COMMUNICATION_FROM_EMAIL=no-replay@alrahmaisgl.org
MAIL_COMMUNICATION_FROM_NAME="School Communications"
MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications" MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
# MAIL_COMMUNICATION_RETURN_PATH=bounces@alrahmaisgl.org
# ===== PAYMENT =====
MAIL_PAYMENT_HOST=smtp.hostinger.com
MAIL_PAYMENT_USER=no-replay@alrahmaisgl.org
MAIL_PAYMENT_PASS="W8;xZ/5g"
MAIL_PAYMENT_PORT=587
MAIL_PAYMENT_ENCRYPTION=ssl
MAIL_PAYMENT_FROM_EMAIL=no-replay@alrahmaisgl.org
MAIL_PAYMENT_FROM_NAME="School Payments"
MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_PAYMENT_REPLY_TO_NAME="School Payments" MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
# MAIL_PAYMENT_RETURN_PATH=bounces@alrahmaisgl.org
# ---- Legacy fallbacks (kept for compatibility) ----
SMTP_HOST = smtp.hostinger.com
SMTP_USER = no-replay@alrahmaosgl.org
SMTP_PASS = "W8;xZ/5g"
SMTP_PORT = 587
SMTP_ENCRYPTION = ssl # was SSL; set to ssl to match 587
# Optional sender directory you already use elsewhere # Optional sender directory you already use elsewhere
MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\ MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\
@@ -131,11 +42,20 @@ MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al R
\"notifications\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Notifications\"},\ \"notifications\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Notifications\"},\
\"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}" \"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}"
# Principal notification recipient for TimeOff requests (comma-separated allowed)
PRINCIPAL_EMAIL="melabidi@alrahmaisgl.org"
MAIL_PARENT_REPORT_TO="alrahma.isgl@gmail.com"
MAIL_FROM_ADDRESS = "alrahma.sunday.school@gmail.com"
session.expiration = 43200
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# APP # APP
#-------------------------------------------------------------------- #--------------------------------------------------------------------
app.baseURL = 'http://localhost:8080/'
app.forceGlobalSecureRequests = false # If you have trouble with `.`, you could also use `_`.
app_baseURL = 'http://localhost:8080/'
# app.forceGlobalSecureRequests = true
# app.CSPEnabled = false
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# DATABASE # DATABASE
@@ -143,21 +63,122 @@ app.forceGlobalSecureRequests = false
database.default.hostname = 127.0.0.1 database.default.hostname = 127.0.0.1
database.default.database = school database.default.database = school
database.default.username = root database.default.username = root
database.default.password = database.default.password =
database.default.DBDriver = MySQLi database.default.DBDriver = MySQLi
database.default.DBPrefix = database.default.DBPrefix =
database.default.port = 3306 database.default.port = 3306
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# CODEIGNITER EMAIL (only used if you use CI Email class) # CONTENT SECURITY POLICY
# These don't affect PHPMailer but keep them aligned anyway. #--------------------------------------------------------------------
mail.protocol = smtp
mail.SMTPHost = ${SMTP_HOST} # contentsecuritypolicy.reportOnly = false
mail.SMTPUser = ${SMTP_USER} # contentsecuritypolicy.defaultSrc = 'none'
mail.SMTPPass = ${SMTP_PASS} # contentsecuritypolicy.scriptSrc = 'self'
mail.SMTPPort = ${SMTP_PORT} # contentsecuritypolicy.styleSrc = 'self'
mail.SMTPCrypto = ssl # contentsecuritypolicy.imageSrc = 'self'
mail.fromEmail = ${SMTP_USER} # contentsecuritypolicy.baseURI = null
mail.fromName = "Al Rahma Sunday School" # contentsecuritypolicy.childSrc = null
mail.mailType = html # contentsecuritypolicy.connectSrc = 'self'
mail.charset = UTF-8 # contentsecuritypolicy.fontSrc = null
# contentsecuritypolicy.formAction = null
# contentsecuritypolicy.frameAncestors = null
# contentsecuritypolicy.frameSrc = null
# contentsecuritypolicy.mediaSrc = null
# contentsecuritypolicy.objectSrc = null
# contentsecuritypolicy.pluginTypes = null
# contentsecuritypolicy.reportURI = null
# contentsecuritypolicy.sandbox = false
# contentsecuritypolicy.upgradeInsecureRequests = false
# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
# contentsecuritypolicy.autoNonce = true
#--------------------------------------------------------------------
# COOKIE
#--------------------------------------------------------------------
# cookie.prefix = ''
# cookie.expires = 0
# cookie.path = '/'
# cookie.domain = ''
# cookie.secure = false
# cookie.httponly = false
# cookie.samesite = 'Lax'
# cookie.raw = false
#--------------------------------------------------------------------
# ENCRYPTION
#--------------------------------------------------------------------
# encryption.key =
# encryption.driver = OpenSSL
# encryption.blockSize = 16
# encryption.digest = SHA512
#--------------------------------------------------------------------
# HONEYPOT
#--------------------------------------------------------------------
# honeypot.hidden = 'true'
# honeypot.label = 'Fill This Field'
# honeypot.name = 'honeypot'
# honeypot.template = '<label>{label}</label><input type="text" name="{name}" value=""/>'
# honeypot.container = '<div style="display:none">{template}</div>'
#--------------------------------------------------------------------
# SECURITY
#--------------------------------------------------------------------
# security.csrfProtection = 'cookie'
# security.tokenRandomize = false
# security.tokenName = 'csrf_token_name'
# security.headerName = 'X-CSRF-TOKEN'
# security.cookieName = 'csrf_cookie_name'
# security.expires = 7200
# security.regenerate = true
# security.redirect = false
# security.samesite = 'Lax'
#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
# session.cookieName = 'ci_session'
# session.expiration = 7200
# session.savePath = null
# session.matchIP = false
# session.timeToUpdate = 300
# session.regenerateDestroy = false
#--------------------------------------------------------------------
# LOGGER
#--------------------------------------------------------------------
logger.threshold = 4
MAIL_DEBUG = true
MAIL_TIMEOUT = 15
#--------------------------------------------------------------------
# CURLRequest
#--------------------------------------------------------------------
# curlrequest.shareOptions = false
mail.protocol = smtp
mail.SMTPHost = ${SMTP_HOST}
mail.SMTPUser = ${SMTP_USER}
mail.SMTPPass = ${SMTP_PASS}
mail.SMTPPort = ${SMTP_PORT}
mail.SMTPCrypto = tls
mail.fromEmail = ${SMTP_USER}
mail.fromName = "Al Rahma Sunday School"
mail.mailType = text
mail.charset = UTF-8
+39
View File
@@ -0,0 +1,39 @@
.env
.env.*
!.env.example
/vendor/
/node_modules/
/writable/cache/*
/writable/debugbar/*
/writable/logs/*
/writable/session/*
/writable/uploads/*
/writable/tmp/*
/writable/*.log
!/writable/.gitkeep
!/writable/cache/.gitkeep
!/writable/debugbar/.gitkeep
!/writable/logs/.gitkeep
!/writable/session/.gitkeep
!/writable/uploads/.gitkeep
!/writable/tmp/.gitkeep
/public/build/
/public/hot/
docker-compose.override.yml
docker-compose.local.yml
*.log
.DS_Store
Thumbs.db
.idea/
.vscode/
/coverage/
/build/
/phpunit.xml.cache
/.phpunit.result.cache
+7
View File
@@ -108,6 +108,7 @@ $routes->get('administrator/student-score-card', 'View\StudentController::scoreC
// API for report card meta (students, class sections, school years) // API for report card meta (students, class sections, school years)
$routes->get('api/printables/report-card/meta', 'View\ReportCardsController::reportCardMeta', ['filter' => 'auth']); $routes->get('api/printables/report-card/meta', 'View\ReportCardsController::reportCardMeta', ['filter' => 'auth']);
$routes->get('api/printables/report-card/completeness', 'View\ReportCardsController::reportCardCompleteness', ['filter' => 'auth']); $routes->get('api/printables/report-card/completeness', 'View\ReportCardsController::reportCardCompleteness', ['filter' => 'auth']);
$routes->get('api/printables/report-card/ack', 'View\ReportCardsController::reportCardAcknowledgement', ['filter' => 'auth']);
//Badges //Badges
$routes->get('printables_reports/badge_form', 'View\BadgesController::badgeForm'); $routes->get('printables_reports/badge_form', 'View\BadgesController::badgeForm');
@@ -330,6 +331,9 @@ $routes->post('/administrator/scores/update/{id}', 'View\ScoreController::update
// Route to delete a score record // Route to delete a score record
$routes->get('/administrator/scores/delete/{id}', 'View\ScoreController::destroy'); $routes->get('/administrator/scores/delete/{id}', 'View\ScoreController::destroy');
$routes->get('/parent/scores', 'View\ScoreController::viewStudentScore'); $routes->get('/parent/scores', 'View\ScoreController::viewStudentScore');
$routes->get('parent/report-cards', 'ParentReportCardController::index', ['filter' => 'auth:parent']);
$routes->get('parent/report-cards/view/(:num)', 'ParentReportCardController::view/$1', ['filter' => 'auth:parent']);
$routes->post('parent/report-cards/sign/(:num)', 'ParentReportCardController::sign/$1', ['filter' => 'auth:parent']);
@@ -495,6 +499,7 @@ $routes->get('grading/project/(:num)', 'View\ProjectController::showProjectMngt/
$routes->post('grading/updateProject', 'View\ProjectController::updateProject'); $routes->post('grading/updateProject', 'View\ProjectController::updateProject');
$routes->get('grading/below-60', 'View\GradingController::belowSixty', ['filter' => 'auth:read']); $routes->get('grading/below-60', 'View\GradingController::belowSixty', ['filter' => 'auth:read']);
$routes->get('grading/below-60/email/edit', 'View\GradingController::editBelowSixtyEmail', ['filter' => 'auth:read']);
$routes->post('grading/below-60/email', 'View\GradingController::sendBelowSixtyEmail', ['filter' => 'auth:read']); $routes->post('grading/below-60/email', 'View\GradingController::sendBelowSixtyEmail', ['filter' => 'auth:read']);
$routes->post('grading/below-60/status', 'View\GradingController::updateBelowSixtyStatus', ['filter' => 'auth:read']); $routes->post('grading/below-60/status', 'View\GradingController::updateBelowSixtyStatus', ['filter' => 'auth:read']);
$routes->get('grading/below-60/schedule', 'View\GradingController::scheduleBelowSixty', ['filter' => 'auth:read']); $routes->get('grading/below-60/schedule', 'View\GradingController::scheduleBelowSixty', ['filter' => 'auth:read']);
@@ -998,6 +1003,8 @@ $routes->group('family', static function ($routes) {
$routes->get('index', 'View\FamilyAdminController::index'); $routes->get('index', 'View\FamilyAdminController::index');
$routes->get('search', 'View\FamilyAdminController::search'); $routes->get('search', 'View\FamilyAdminController::search');
$routes->get('card', 'View\FamilyAdminController::card'); $routes->get('card', 'View\FamilyAdminController::card');
$routes->get('compose-email', 'View\FamilyAdminController::composeEmail');
$routes->post('compose-email/send', 'View\FamilyAdminController::sendComposeEmail');
}); });
// Convenience alias // Convenience alias
$routes->get('family', 'View\FamilyAdminController::index'); $routes->get('family', 'View\FamilyAdminController::index');
+370 -7
View File
@@ -5,7 +5,11 @@ namespace App\Controllers;
use App\Models\ClassProgressReportModel; use App\Models\ClassProgressReportModel;
use App\Models\ClassProgressAttachmentModel; use App\Models\ClassProgressAttachmentModel;
use App\Models\ClassSectionModel; use App\Models\ClassSectionModel;
use App\Models\CalendarModel;
use App\Models\ConfigurationModel;
use App\Models\StudentClassModel; use App\Models\StudentClassModel;
use App\Models\SubjectCurriculumModel;
use App\Services\SemesterRangeService;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
@@ -15,6 +19,10 @@ class AdminProgressController extends BaseController
protected ClassProgressAttachmentModel $attachmentModel; protected ClassProgressAttachmentModel $attachmentModel;
protected ClassSectionModel $classSectionModel; protected ClassSectionModel $classSectionModel;
protected StudentClassModel $studentClassModel; protected StudentClassModel $studentClassModel;
protected CalendarModel $calendarModel;
protected ConfigurationModel $configModel;
protected SemesterRangeService $semesterRangeService;
protected SubjectCurriculumModel $curriculumModel;
public function __construct() public function __construct()
{ {
@@ -23,6 +31,10 @@ class AdminProgressController extends BaseController
$this->attachmentModel = new ClassProgressAttachmentModel(); $this->attachmentModel = new ClassProgressAttachmentModel();
$this->classSectionModel = new ClassSectionModel(); $this->classSectionModel = new ClassSectionModel();
$this->studentClassModel = new StudentClassModel(); $this->studentClassModel = new StudentClassModel();
$this->calendarModel = new CalendarModel();
$this->configModel = new ConfigurationModel();
$this->semesterRangeService = new SemesterRangeService($this->configModel);
$this->curriculumModel = new SubjectCurriculumModel();
} }
public function index() public function index()
@@ -53,22 +65,30 @@ class AdminProgressController extends BaseController
} }
$rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray(); $rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray();
$reportGroups = []; $reportGroupsBySection = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
$key = ($row['week_start'] ?? '') . '_' . ($row['class_section_id'] ?? ''); $sectionId = (int) ($row['class_section_id'] ?? 0);
if ($key === '_') { $weekKey = (string) ($row['week_start'] ?? '');
if ($sectionId === 0 || $weekKey === '') {
continue; continue;
} }
if (! isset($reportGroups[$key])) { if (! isset($reportGroupsBySection[$sectionId])) {
$reportGroups[$key] = [ $reportGroupsBySection[$sectionId] = [];
}
if (! isset($reportGroupsBySection[$sectionId][$weekKey])) {
$reportGroupsBySection[$sectionId][$weekKey] = [
'week_start' => $row['week_start'], 'week_start' => $row['week_start'],
'week_end' => $row['week_end'], 'week_end' => $row['week_end'],
'class_section_name' => $row['class_section_name'] ?? '', 'class_section_name' => $row['class_section_name'] ?? '',
'reports' => [], 'reports' => [],
]; ];
} }
$reportGroups[$key]['reports'][$row['subject']] = $row; $reportGroupsBySection[$sectionId][$weekKey]['reports'][$row['subject']] = $row;
}
foreach ($reportGroupsBySection as $sectionId => $groups) {
krsort($groups);
$reportGroupsBySection[$sectionId] = $groups;
} }
$classSections = $this->classSectionModel->getClassSections(); $classSections = $this->classSectionModel->getClassSections();
@@ -78,12 +98,29 @@ class AdminProgressController extends BaseController
return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0; return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0;
})); }));
$filterStart = $this->normalizeDate($filters['from'] ?? '');
$filterEnd = $this->normalizeDate($filters['to'] ?? '');
[$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet] = $this->buildSemesterDates();
[$expectedDays, $activeDatesSet] = $this->resolveExpectedDays(
$dateList,
$noSchoolDays,
$totalPassedDays,
$passedDatesSet,
$filterStart,
$filterEnd
);
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
$sectionSubjectCounts = $this->buildSectionSubjectCounts($rows);
return view('admin/class_progress_list', [ return view('admin/class_progress_list', [
'reportGroups' => $reportGroups, 'reportGroupsBySection' => $reportGroupsBySection,
'filters' => $filters, 'filters' => $filters,
'classSections' => $filteredSections, 'classSections' => $filteredSections,
'statusOptions' => ClassProgressController::STATUS_OPTIONS, 'statusOptions' => ClassProgressController::STATUS_OPTIONS,
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS, 'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
'sectionStats' => $sectionStats,
'sectionSubjectCounts' => $sectionSubjectCounts,
'expectedDays' => $expectedDays,
]); ]);
} }
@@ -211,4 +248,330 @@ class AdminProgressController extends BaseController
$flags = json_decode($json, true); $flags = json_decode($json, true);
return is_array($flags) ? $flags : []; return is_array($flags) ? $flags : [];
} }
protected function normalizeDate(string $value): string
{
$value = trim($value);
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
return '';
}
[$y, $m, $d] = array_map('intval', explode('-', $value));
return checkdate($m, $d, $y) ? $value : '';
}
protected function buildSemesterDates(): array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string) ($this->configModel->getConfig('school_year') ?? '');
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYearForRange);
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
if ($semesterNorm !== '' && $schoolYearForRange !== '') {
$semRange = $this->semesterRangeService->getSemesterRange($schoolYearForRange, $semesterNorm);
if ($semRange) {
[$rangeStart, $rangeEnd] = $semRange;
}
}
$dateList = [];
try {
$start = new \DateTimeImmutable($rangeStart);
$end = new \DateTimeImmutable($rangeEnd);
$cursor = $start;
$w = (int) $cursor->format('w');
if ($w !== 0) {
$cursor = $cursor->modify('next sunday');
}
while ($cursor <= $end) {
$dateList[] = $cursor->format('Y-m-d');
$cursor = $cursor->modify('+7 days');
}
} catch (\Throwable $e) {
$dateList = [];
}
$noSchoolDays = [];
$events = [];
try {
$events = $this->calendarModel->getEvents();
} catch (\Throwable $e) {
$events = [];
}
foreach ($events as $event) {
$d = substr((string) ($event['date'] ?? ''), 0, 10);
if ($d === '' || empty($event['no_school'])) {
continue;
}
if ($d < $rangeStart || $d > $rangeEnd) {
continue;
}
$eventYear = trim((string) ($event['school_year'] ?? ''));
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
continue;
}
$noSchoolDays[$d] = true;
}
$anchorSundayYmd = '';
try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tzObj = new \DateTimeZone($tzName ?: 'UTC');
} catch (\Throwable $e) {
try {
$tzObj = new \DateTimeZone(user_timezone() ?: 'UTC');
} catch (\Throwable $e2) {
$tzObj = new \DateTimeZone('UTC');
}
}
try {
$nowDate = new \DateTime('now', $tzObj);
} catch (\Throwable $e) {
$nowDate = new \DateTime('now');
}
$weekday = (int) $nowDate->format('w');
$anchorSundayYmd = $weekday === 0
? $nowDate->format('Y-m-d')
: $nowDate->modify('next sunday')->format('Y-m-d');
$passedDatesSet = [];
if (! empty($dateList) && $anchorSundayYmd !== '') {
foreach ($dateList as $d) {
if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) {
$passedDatesSet[$d] = true;
}
}
}
$totalPassedDays = count($passedDatesSet);
return [$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet];
}
protected function resolveExpectedDays(
array $dateList,
array $noSchoolDays,
int $totalPassedDays,
array $passedDatesSet,
string $filterStart,
string $filterEnd
): array {
$activeDatesSet = [];
if ($filterStart === '' && $filterEnd === '') {
$expectedDays = $totalPassedDays;
if ($expectedDays > 0) {
$activeDatesSet = $passedDatesSet;
}
return [$expectedDays, $activeDatesSet];
}
$expectedDays = 0;
foreach ($dateList as $d) {
if ($d === '') {
continue;
}
if ($filterStart !== '' && $d < $filterStart) {
continue;
}
if ($filterEnd !== '' && $d > $filterEnd) {
continue;
}
if (! empty($noSchoolDays[$d])) {
continue;
}
$activeDatesSet[$d] = true;
$expectedDays++;
}
return [$expectedDays, $activeDatesSet];
}
protected function buildSectionSubmissionStats(array $rows, array $activeDatesSet, int $expectedDays): array
{
$submittedBySection = [];
foreach ($rows as $row) {
$sectionId = (int) ($row['class_section_id'] ?? 0);
$weekStart = (string) ($row['week_start'] ?? '');
if ($sectionId === 0 || $weekStart === '') {
continue;
}
$submittedBySection[$sectionId][$weekStart] = true;
}
$stats = [];
foreach ($submittedBySection as $sectionId => $weeks) {
$submitted = count($weeks);
$stats[$sectionId] = $this->buildSectionStat($submitted, $expectedDays);
}
return $stats;
}
protected function buildSectionSubjectCounts(array $rows): array
{
$allowedSubjects = [];
foreach (ClassProgressController::SUBJECT_SECTIONS as $section) {
$allowedSubjects[] = $section['db_subject'] ?? $section['label'] ?? '';
}
$allowedSubjects = array_values(array_filter($allowedSubjects));
$counts = [];
$latestWeekBySection = [];
$sectionClassMap = [];
foreach ($rows as $row) {
$sectionId = (int) ($row['class_section_id'] ?? 0);
$subject = (string) ($row['subject'] ?? '');
$weekStart = (string) ($row['week_start'] ?? '');
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
continue;
}
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
continue;
}
if (! isset($sectionClassMap[$sectionId])) {
$sectionClassMap[$sectionId] = $this->classSectionModel->getClassId($sectionId);
}
if (
! isset($latestWeekBySection[$sectionId])
|| $weekStart > $latestWeekBySection[$sectionId]
) {
$latestWeekBySection[$sectionId] = $weekStart;
}
}
$curriculumChapters = $this->buildCurriculumChapterMap(array_values(array_filter($sectionClassMap)));
foreach ($rows as $row) {
$sectionId = (int) ($row['class_section_id'] ?? 0);
$subject = (string) ($row['subject'] ?? '');
$weekStart = (string) ($row['week_start'] ?? '');
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
continue;
}
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
continue;
}
if (empty($latestWeekBySection[$sectionId]) || $weekStart !== $latestWeekBySection[$sectionId]) {
continue;
}
$subjectSlug = $this->resolveSubjectSlug($subject);
$classId = $sectionClassMap[$sectionId] ?? null;
$chapterSet = [];
if ($classId && $subjectSlug && ! empty($curriculumChapters[$classId][$subjectSlug])) {
$chapterSet = $curriculumChapters[$classId][$subjectSlug];
}
$counts[$sectionId] = ($counts[$sectionId] ?? 0) + $this->countChapterSegments(
(string) ($row['unit_title'] ?? ''),
$chapterSet
);
}
return $counts;
}
protected function buildCurriculumChapterMap(array $classIds): array
{
$classIds = array_values(array_filter(array_map('intval', $classIds)));
if (empty($classIds)) {
return [];
}
$rows = $this->curriculumModel
->whereIn('class_id', $classIds)
->findAll();
$map = [];
foreach ($rows as $row) {
$classId = (int) ($row['class_id'] ?? 0);
$subject = (string) ($row['subject'] ?? '');
$chapter = trim((string) ($row['chapter_name'] ?? ''));
if ($classId === 0 || $subject === '' || $chapter === '') {
continue;
}
$map[$classId][$subject][$chapter] = true;
}
return $map;
}
protected function resolveSubjectSlug(string $subject): ?string
{
foreach (ClassProgressController::SUBJECT_SECTIONS as $slug => $section) {
$dbSubject = (string) ($section['db_subject'] ?? '');
$label = (string) ($section['label'] ?? '');
if ($subject === $dbSubject || $subject === $label) {
return $slug;
}
}
return null;
}
protected function countChapterSegments(string $unitTitle, array $chapterSet): int
{
$unitTitle = trim($unitTitle);
if ($unitTitle === '') {
return 1;
}
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
if (! $parts) {
return 1;
}
$count = 0;
$seen = [];
foreach ($parts as $part) {
$chapter = $this->extractChapterFromSegment($part);
$key = $chapter !== '' ? $chapter : $part;
if (! empty($chapterSet) && $chapter !== '' && empty($chapterSet[$chapter])) {
$key = $part;
}
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$count++;
}
return $count > 0 ? $count : 1;
}
protected function extractChapterFromSegment(string $segment): string
{
$segment = trim($segment);
if ($segment === '') {
return '';
}
$pos = strrpos($segment, '/');
if ($pos === false) {
return $segment;
}
return trim(substr($segment, $pos + 1));
}
protected function buildSectionStat(int $submitted, int $expectedDays): array
{
$percent = $expectedDays > 0 ? round(($submitted * 100) / $expectedDays, 1) : 0.0;
if ($expectedDays === 0) {
$badgeClass = 'bg-secondary';
$labelClass = 'text-muted';
} elseif ($percent >= 100) {
$badgeClass = 'bg-success';
$labelClass = 'text-success';
} elseif ($percent >= 60) {
$badgeClass = 'bg-warning text-dark';
$labelClass = 'text-warning';
} elseif ($percent >= 40) {
$badgeClass = 'bg-orange text-dark';
$labelClass = 'text-warning';
} else {
$badgeClass = 'bg-danger';
$labelClass = 'text-danger';
}
return [
'submitted' => $submitted,
'expected' => $expectedDays,
'percent' => $percent,
'badgeClass' => $badgeClass,
'labelClass' => $labelClass,
];
}
} }
+127 -8
View File
@@ -7,6 +7,7 @@ use App\Models\ClassProgressAttachmentModel;
use App\Models\ConfigurationModel; use App\Models\ConfigurationModel;
use App\Models\SubjectCurriculumModel; use App\Models\SubjectCurriculumModel;
use App\Models\TeacherClassModel; use App\Models\TeacherClassModel;
use App\Services\SemesterRangeService;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
class ClassProgressController extends BaseController class ClassProgressController extends BaseController
@@ -56,6 +57,7 @@ class ClassProgressController extends BaseController
$classSectionName = $first['class_section_name'] ?? null; $classSectionName = $first['class_section_name'] ?? null;
$classId = $first['class_id'] ?? null; $classId = $first['class_id'] ?? null;
$sundayOptions = $this->buildSundayOptions(); $sundayOptions = $this->buildSundayOptions();
$defaultWeekStart = $this->pickDefaultWeekStart($sundayOptions);
$subjectCurriculum = []; $subjectCurriculum = [];
if ($classId) { if ($classId) {
foreach (self::SUBJECT_SECTIONS as $slug => $section) { foreach (self::SUBJECT_SECTIONS as $slug => $section) {
@@ -69,7 +71,7 @@ class ClassProgressController extends BaseController
'classSectionName' => $classSectionName, 'classSectionName' => $classSectionName,
'classId' => $classId, 'classId' => $classId,
'sundayOptions' => $sundayOptions, 'sundayOptions' => $sundayOptions,
'defaultWeekStart' => $sundayOptions[0] ?? '', 'defaultWeekStart' => $defaultWeekStart,
]; ];
return view('teacher/class_progress_submit', $data); return view('teacher/class_progress_submit', $data);
} }
@@ -171,10 +173,16 @@ class ClassProgressController extends BaseController
if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) { if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) {
$selectedSectionId = $validSectionIds[0] ?? null; $selectedSectionId = $validSectionIds[0] ?? null;
} }
[$semester, $schoolYear] = $this->resolveCurrentTerm();
$allowedTeacherIds = $this->resolveAssignedTeacherIds($selectedSectionId, $semester, $schoolYear);
if (empty($allowedTeacherIds)) {
$allowedTeacherIds = [$teacherId];
}
$builder = $this->reportModel $builder = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name') ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId); ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->whereIn('teacher_id', $allowedTeacherIds);
if ($selectedSectionId) { if ($selectedSectionId) {
$builder->where('class_progress_reports.class_section_id', $selectedSectionId); $builder->where('class_progress_reports.class_section_id', $selectedSectionId);
} }
@@ -216,20 +224,33 @@ class ClassProgressController extends BaseController
{ {
$teacherId = (int) session()->get('user_id'); $teacherId = (int) session()->get('user_id');
$row = $this->reportModel $row = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name') ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId) ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->find((int) $id); ->where('class_progress_reports.id', (int) $id)
->first();
if (! $row) { if (! $row) {
throw new PageNotFoundException('Progress report not found.'); throw new PageNotFoundException('Progress report not found.');
} }
[$semester, $schoolYear] = $this->resolveCurrentTerm();
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
if (empty($allowedTeacherIds)) {
if ($teacherId !== (int) $row['teacher_id']) {
throw new PageNotFoundException('Progress report not found.');
}
$allowedTeacherIds = [(int) $row['teacher_id']];
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
throw new PageNotFoundException('Progress report not found.');
}
$row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
$weeklyReports = $this->reportModel $weeklyReports = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name') ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId) ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->whereIn('teacher_id', $allowedTeacherIds)
->where('class_progress_reports.class_section_id', $row['class_section_id']) ->where('class_progress_reports.class_section_id', $row['class_section_id'])
->where('week_start', $row['week_start']) ->where('week_start', $row['week_start'])
->orderBy('subject', 'ASC') ->orderBy('subject', 'ASC')
@@ -427,6 +448,39 @@ class ClassProgressController extends BaseController
} }
protected function buildSundayOptions(int $count = 12): array protected function buildSundayOptions(int $count = 12): array
{
$range = $this->resolveProgressDateRange();
if ($range === null) {
return $this->buildUpcomingSundayOptions($count);
}
[$rangeStart, $rangeEnd] = $range;
try {
$start = new \DateTime($rangeStart);
$end = new \DateTime($rangeEnd);
} catch (\Exception $e) {
return $this->buildUpcomingSundayOptions($count);
}
if ($end < $start) {
[$start, $end] = [$end, $start];
}
$weekday = (int) $start->format('w');
if ($weekday !== 0) {
$start->modify('next sunday');
}
$options = [];
while ($start <= $end) {
$options[] = $start->format('Y-m-d');
$start->modify('+7 days');
}
return $options;
}
protected function buildUpcomingSundayOptions(int $count): array
{ {
$start = new \DateTime('today'); $start = new \DateTime('today');
$weekday = (int) $start->format('w'); $weekday = (int) $start->format('w');
@@ -443,6 +497,48 @@ class ClassProgressController extends BaseController
return $options; return $options;
} }
protected function resolveProgressDateRange(): ?array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
if ($schoolYear === '') {
return null;
}
$semesterResolver = new SemesterRangeService($this->configModel);
$semester = $semesterResolver->normalizeSemester((string) ($this->configModel->getConfig('semester') ?? ''));
if ($semester === '') {
$semester = $semesterResolver->getSemesterForDate();
}
if ($semester !== '') {
$range = $semesterResolver->getSemesterRange($schoolYear, $semester);
if ($range !== null) {
return $range;
}
}
return $semesterResolver->getSchoolYearRange($schoolYear);
}
protected function pickDefaultWeekStart(array $options): string
{
if (empty($options)) {
return '';
}
$today = date('Y-m-d');
$default = '';
foreach ($options as $option) {
if ($option <= $today) {
$default = $option;
continue;
}
break;
}
return $default !== '' ? $default : $options[0];
}
protected function buildWeekEndFromStart(string $weekStart): string protected function buildWeekEndFromStart(string $weekStart): string
{ {
try { try {
@@ -468,4 +564,27 @@ class ClassProgressController extends BaseController
mkdir($this->attachmentStoragePath, 0755, true); mkdir($this->attachmentStoragePath, 0755, true);
} }
} }
protected function resolveCurrentTerm(): array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
return [$semester, $schoolYear];
}
protected function resolveAssignedTeacherIds(?int $classSectionId, string $semester, string $schoolYear): array
{
if (! $classSectionId || $schoolYear === '') {
return [];
}
$rows = $this->teacherClassModel->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
$ids = [];
foreach ($rows as $row) {
$id = (int) ($row['teacher_id'] ?? 0);
if ($id > 0) {
$ids[$id] = true;
}
}
return array_keys($ids);
}
} }
@@ -0,0 +1,186 @@
<?php
namespace App\Controllers;
use App\Models\ConfigurationModel;
use App\Models\ReportCardAcknowledgementModel;
use App\Models\StudentModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\Database\BaseConnection;
class ParentReportCardController extends BaseController
{
protected ConfigurationModel $configModel;
protected ReportCardAcknowledgementModel $ackModel;
protected StudentModel $studentModel;
protected BaseConnection $db;
public function __construct()
{
helper(['url', 'form']);
$this->db = \Config\Database::connect();
$this->configModel = new ConfigurationModel();
$this->ackModel = new ReportCardAcknowledgementModel();
$this->studentModel = new StudentModel();
}
public function index()
{
$parentId = $this->resolvePrimaryParentId();
if (! $parentId) {
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
}
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->configModel->getConfig('school_year') ?? ''));
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
$builder = $this->db->table('students s')
->select('s.id, s.firstname, s.lastname, cs.class_section_name')
->join('student_class sc', 'sc.student_id = s.id', 'left')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
->where('s.parent_id', $parentId)
->orderBy('s.firstname', 'ASC')
->orderBy('s.lastname', 'ASC');
if ($schoolYear !== '') {
$builder->where('sc.school_year', $schoolYear);
}
if ($semester !== '') {
$builder->where('sc.semester', $semester);
}
$students = $builder->get()->getResultArray();
$studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students)));
$ackMap = [];
if (! empty($studentIds)) {
$rows = $this->ackModel
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->whereIn('student_id', $studentIds)
->findAll();
foreach ($rows as $row) {
$ackMap[(int) $row['student_id']] = $row;
}
}
return view('parent/report_cards', [
'students' => $students,
'ackMap' => $ackMap,
'schoolYear' => $schoolYear,
'semester' => $semester,
]);
}
public function view($studentId)
{
$parentId = $this->resolvePrimaryParentId();
if (! $parentId) {
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
}
$student = $this->studentModel->find((int) $studentId);
if (! $student || (int) ($student['parent_id'] ?? 0) !== $parentId) {
throw new PageNotFoundException('Student not found.');
}
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->configModel->getConfig('school_year') ?? ''));
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
'viewed_at' => date('Y-m-d H:i:s'),
]);
$url = site_url('report-card/student/' . (int) $studentId);
$query = [];
if ($schoolYear !== '') {
$query['school_year'] = $schoolYear;
}
if ($semester !== '') {
$query['semester'] = $semester;
}
if ($query) {
$url .= '?' . http_build_query($query);
}
return redirect()->to($url);
}
public function sign($studentId)
{
$parentId = $this->resolvePrimaryParentId();
if (! $parentId) {
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
}
$student = $this->studentModel->find((int) $studentId);
if (! $student || (int) ($student['parent_id'] ?? 0) !== $parentId) {
throw new PageNotFoundException('Student not found.');
}
$name = trim((string) $this->request->getPost('signed_name'));
if ($name === '') {
return redirect()->back()->with('error', 'Please type your full name to sign.');
}
$schoolYear = trim((string) ($this->configModel->getConfig('school_year') ?? ''));
$semester = trim((string) ($this->configModel->getConfig('semester') ?? ''));
$now = date('Y-m-d H:i:s');
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
'viewed_at' => $now,
'signed_at' => $now,
'signed_name' => $name,
'signer_ip' => $this->request->getIPAddress(),
]);
return redirect()->to(site_url('parent/report-cards'))->with('success', 'Report card acknowledged.');
}
protected function resolvePrimaryParentId(): ?int
{
$parentId = (int) (session()->get('user_id') ?? 0);
$userType = (string) ($_SESSION['user_type'] ?? '');
if ($userType === 'primary') {
return $parentId ?: null;
}
if ($userType === 'secondary') {
$row = $this->db->table('parents')
->select('parent_id')
->where('secondparent_user_id', $parentId)
->get()
->getRowArray();
return $row ? (int) ($row['parent_id'] ?? 0) : null;
}
if ($userType === 'tertiary') {
$row = $this->db->table('authorized_users')
->select('user_id as parent_id')
->where('authorized_user_id', $parentId)
->get()
->getRowArray();
return $row ? (int) ($row['parent_id'] ?? 0) : null;
}
return $parentId ?: null;
}
protected function touchAcknowledgement(
int $parentId,
int $studentId,
string $schoolYear,
string $semester,
array $values
): void {
$criteria = [
'parent_id' => $parentId,
'student_id' => $studentId,
'school_year' => $schoolYear,
'semester' => $semester,
];
$existing = $this->ackModel->where($criteria)->first();
if ($existing) {
$this->ackModel->update((int) $existing['id'], $values);
return;
}
$this->ackModel->insert(array_merge($criteria, $values));
}
}
@@ -416,8 +416,10 @@ class AdministratorController extends BaseController
$totalStudents = (int) ( $totalStudents = (int) (
$this->db->table('student_class') $this->db->table('student_class')
->select('COUNT(DISTINCT student_class.student_id) AS cnt') ->select('COUNT(DISTINCT student_class.student_id) AS cnt')
->join('students', 'students.id = student_class.student_id', 'inner')
->where('student_class.school_year', $this->schoolYear) ->where('student_class.school_year', $this->schoolYear)
->where('student_class.class_section_id IS NOT NULL', null, false) ->where('student_class.class_section_id IS NOT NULL', null, false)
->where('students.is_active', 1)
->get() ->get()
->getRow('cnt') ->getRow('cnt')
?? 0 ?? 0
+16 -17
View File
@@ -797,26 +797,25 @@ public function showUpdateAttendanceForm()
} }
$noSchoolDays = []; $noSchoolDays = [];
if ($schoolYearForRange !== '') { $events = [];
try {
$events = $this->calendarModel->getEvents();
} catch (\Throwable $e) {
$events = []; $events = [];
try { }
$events = $this->calendarModel->getEventsBySchoolYearAndSemester( foreach ($events as $event) {
$schoolYearForRange, $d = substr((string)($event['date'] ?? ''), 0, 10);
$semesterNorm !== '' ? $semesterNorm : null if ($d === '' || empty($event['no_school'])) {
); continue;
} catch (\Throwable $e) {
$events = [];
} }
foreach ($events as $event) { if ($d < $rangeStart || $d > $rangeEnd) {
$d = substr((string)($event['date'] ?? ''), 0, 10); continue;
if ($d === '' || empty($event['no_school'])) {
continue;
}
if ($d < $rangeStart || $d > $rangeEnd) {
continue;
}
$noSchoolDays[$d] = true;
} }
$eventYear = trim((string)($event['school_year'] ?? ''));
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
continue;
}
$noSchoolDays[$d] = true;
} }
// Total passed attendance days up to this Sunday (inclusive), excluding no-school days. // Total passed attendance days up to this Sunday (inclusive), excluding no-school days.
@@ -60,13 +60,15 @@ class ClassPreparationController extends BaseController
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester); $limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
// 1) Get student count per class-section (distinct students, correct semester) // 1) Get student count per class-section (distinct students, correct semester)
$scQ = $this->studentClassModel $scQ = $this->db->table('student_class sc')
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count') ->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
->where('school_year', $schoolYear); ->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$scQ->where('semester', $semester); $scQ->where('sc.semester', $semester);
} }
$classSections = $scQ->groupBy('class_section_id')->findAll(); $classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
// 2) Inventory availability — prefer good_qty when present, else condition='good' quantity. // 2) Inventory availability — prefer good_qty when present, else condition='good' quantity.
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed); $inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
@@ -298,14 +300,16 @@ class ClassPreparationController extends BaseController
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; $className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
// Distinct student count for this term // Distinct student count for this term
$studentQ = $this->studentClassModel $studentQ = $this->db->table('student_class sc')
->select('COUNT(DISTINCT student_id) AS cnt') ->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
->where('class_section_id', $classSectionId) ->join('students s', 's.id = sc.student_id', 'inner')
->where('school_year', $schoolYear); ->where('s.is_active', 1)
->where('sc.class_section_id', $classSectionId)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$studentQ->where('semester', $semester); $studentQ->where('sc.semester', $semester);
} }
$studentRow = $studentQ->first(); $studentRow = $studentQ->get()->getRowArray();
$studentCount = (int)($studentRow['cnt'] ?? 0); $studentCount = (int)($studentRow['cnt'] ?? 0);
// Live calc + adjustments // Live calc + adjustments
@@ -355,13 +359,15 @@ class ClassPreparationController extends BaseController
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester); $limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
// Student counts // Student counts
$scQ = $this->studentClassModel $scQ = $this->db->table('student_class sc')
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count') ->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
->where('school_year', $schoolYear); ->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$scQ->where('semester', $semester); $scQ->where('sc.semester', $semester);
} }
$classSections = $scQ->groupBy('class_section_id')->findAll(); $classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
// Build inventory availability maps // Build inventory availability maps
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed); $inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
@@ -448,14 +454,16 @@ class ClassPreparationController extends BaseController
$now = utc_now(); $now = utc_now();
$count = 0; $count = 0;
foreach ($ids as $classSectionId) { foreach ($ids as $classSectionId) {
$studentQ = $this->studentClassModel $studentQ = $this->db->table('student_class sc')
->select('COUNT(DISTINCT student_id) AS cnt') ->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
->where('class_section_id', $classSectionId) ->join('students s', 's.id = sc.student_id', 'inner')
->where('school_year', $schoolYear); ->where('s.is_active', 1)
->where('sc.class_section_id', $classSectionId)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$studentQ->where('semester', $semester); $studentQ->where('sc.semester', $semester);
} }
$studentRow = $studentQ->first(); $studentRow = $studentQ->get()->getRowArray();
$studentCount = (int)($studentRow['cnt'] ?? 0); $studentCount = (int)($studentRow['cnt'] ?? 0);
$classLevel = $this->getClassLevelBySection((string)$classSectionId); $classLevel = $this->getClassLevelBySection((string)$classSectionId);
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; $className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
@@ -406,4 +406,82 @@ class FamilyAdminController extends BaseController
return service('response')->setBody(view('family/card', ['f' => $family])); return service('response')->setBody(view('family/card', ['f' => $family]));
} }
public function composeEmail()
{
$to = trim((string)$this->request->getGet('to'));
$name = trim((string)$this->request->getGet('name'));
$returnUrl = trim((string)$this->request->getGet('return_url'));
if ($returnUrl === '') {
$returnUrl = trim((string)$this->request->getServer('HTTP_REFERER'));
}
if ($returnUrl === '') {
$returnUrl = site_url('family');
}
return view('family/compose_email', [
'to' => $to,
'name' => $name,
'return_url' => $returnUrl,
]);
}
public function sendComposeEmail()
{
if (!$this->request->is('post')) {
return redirect()->to(site_url('family'));
}
$to = trim((string)$this->request->getPost('to'));
$subject = trim((string)$this->request->getPost('subject'));
$html = (string)($this->request->getPost('html') ?? '');
$returnUrl = trim((string)$this->request->getPost('return_url'));
if ($returnUrl === '' || !$this->isLocalReturnUrl($returnUrl)) {
$returnUrl = site_url('family');
}
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
return redirect()->back()->withInput()->with('error', 'Please enter a valid email address.');
}
if ($subject === '') {
return redirect()->back()->withInput()->with('error', 'Subject is required.');
}
if (trim($html) === '') {
return redirect()->back()->withInput()->with('error', 'Email body is required.');
}
$wrapped = view('emails/custom_html', [
'subject' => $subject,
'body_html' => $html,
]);
$mailer = new \App\Controllers\View\EmailController();
$ok = $mailer->sendEmail($to, $subject, $wrapped, 'communication');
if ($ok) {
return redirect()->to($returnUrl)->with('status', 'Email sent.');
}
return redirect()->to($returnUrl)->with('error', 'Unable to send email.');
}
private function isLocalReturnUrl(string $url): bool
{
$url = trim($url);
if ($url === '') return false;
$parts = parse_url($url);
if ($parts === false) return false;
if (!empty($parts['scheme']) || !empty($parts['host'])) {
$base = parse_url(site_url('/'));
if (!$base || empty($base['host'])) return false;
$hostMatch = strcasecmp((string)($parts['host'] ?? ''), (string)$base['host']) === 0;
return $hostMatch;
}
// Relative URL (e.g., /family?x=1 or family?x=1)
return true;
}
} }
+5 -2
View File
@@ -1283,10 +1283,11 @@ public function financialReport()
$parentIds = array_values(array_unique(array_map(static fn($r) => (int)($r['parent_id'] ?? 0), $rows))); $parentIds = array_values(array_unique(array_map(static fn($r) => (int)($r['parent_id'] ?? 0), $rows)));
$hasPayments = []; $hasPayments = [];
$paidTotals = []; $paidTotals = [];
$paymentCounts = [];
if (!empty($parentIds)) { if (!empty($parentIds)) {
try { try {
$pRows = $db->table('payments') $pRows = $db->table('payments')
->select('parent_id, SUM(paid_amount) AS total_paid') ->select('parent_id, SUM(paid_amount) AS total_paid, COUNT(*) AS payment_count')
->whereIn('parent_id', $parentIds) ->whereIn('parent_id', $parentIds)
->where('school_year', $schoolYear) ->where('school_year', $schoolYear)
->groupBy('parent_id') ->groupBy('parent_id')
@@ -1296,6 +1297,7 @@ public function financialReport()
if ($pid > 0) { if ($pid > 0) {
$hasPayments[$pid] = true; $hasPayments[$pid] = true;
$paidTotals[$pid] = (float)($pr['total_paid'] ?? 0); $paidTotals[$pid] = (float)($pr['total_paid'] ?? 0);
$paymentCounts[$pid] = (int)($pr['payment_count'] ?? 0);
} }
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -1303,7 +1305,7 @@ public function financialReport()
} }
} }
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $nextInstallmentYmd) { $dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $paymentCounts, $nextInstallmentYmd) {
$pid = (int)($r['parent_id'] ?? 0); $pid = (int)($r['parent_id'] ?? 0);
$name = trim((string)($r['firstname'] ?? '') . ' ' . (string)($r['lastname'] ?? '')); $name = trim((string)($r['firstname'] ?? '') . ' ' . (string)($r['lastname'] ?? ''));
return [ return [
@@ -1317,6 +1319,7 @@ public function financialReport()
'installment_amount' => (float)($r['installment_amount'] ?? 0), 'installment_amount' => (float)($r['installment_amount'] ?? 0),
'type' => isset($hasPayments[$pid]) ? 'installment' : 'no_payment', 'type' => isset($hasPayments[$pid]) ? 'installment' : 'no_payment',
'total_paid' => isset($paidTotals[$pid]) ? (float)$paidTotals[$pid] : 0.0, 'total_paid' => isset($paidTotals[$pid]) ? (float)$paidTotals[$pid] : 0.0,
'payment_count' => isset($paymentCounts[$pid]) ? (int)$paymentCounts[$pid] : 0,
'has_installment'=> isset($hasPayments[$pid]) ? 1 : 0, 'has_installment'=> isset($hasPayments[$pid]) ? 1 : 0,
'next_installment' => $nextInstallmentYmd, 'next_installment' => $nextInstallmentYmd,
]; ];
+157 -4
View File
@@ -28,6 +28,7 @@ use App\Models\PlacementLevelModel;
use App\Models\PlacementBatchModel; use App\Models\PlacementBatchModel;
use App\Models\PlacementScoreModel; use App\Models\PlacementScoreModel;
use App\Models\GradingLockModel; use App\Models\GradingLockModel;
use App\Services\NavbarService;
//use App\Models\ScoreModel; //use App\Models\ScoreModel;
@@ -1079,19 +1080,22 @@ class GradingController extends Controller
$schoolYears = $this->getSchoolYearsForScores($schoolYear); $schoolYears = $this->getSchoolYearsForScores($schoolYear);
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester); $rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
$canViewGrading = $this->userHasMenuUrl('grading');
return view('grading/below_sixty', [ return view('grading/below_sixty', [
'rows' => $rows, 'rows' => $rows,
'semester' => $semester, 'semester' => $semester,
'schoolYear' => $schoolYear, 'schoolYear' => $schoolYear,
'schoolYears' => $schoolYears, 'schoolYears' => $schoolYears,
'canViewGrading' => $canViewGrading,
]); ]);
} }
public function sendBelowSixtyEmail() public function editBelowSixtyEmail()
{ {
$studentId = (int)$this->request->getPost('student_id'); $studentId = (int)$this->request->getGet('student_id');
$semester = trim((string)$this->request->getPost('semester')); $semester = trim((string)$this->request->getGet('semester'));
$schoolYear = trim((string)$this->request->getPost('school_year')); $schoolYear = trim((string)$this->request->getGet('school_year'));
if ($studentId <= 0 || $semester === '' || $schoolYear === '') { if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
return redirect()->back()->with('error', 'Missing student or term.'); return redirect()->back()->with('error', 'Missing student or term.');
@@ -1103,6 +1107,65 @@ class GradingController extends Controller
} }
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? '')); $studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
$subject = $this->buildBelowSixtySubject($studentName, $semester, $schoolYear);
$parentName = $this->fetchBelowSixtyParentName($studentId);
$scores = [
'homework_avg' => $row['homework_avg'] ?? null,
'project_avg' => $row['project_avg'] ?? null,
'participation_score' => $row['participation_score'] ?? null,
'test_avg' => $row['test_avg'] ?? null,
'ptap_score' => $row['ptap_score'] ?? null,
'attendance_score' => $row['attendance_score'] ?? null,
'midterm_exam_score' => $row['midterm_exam_score'] ?? null,
'semester_score' => $row['semester_score'] ?? null,
];
$emailData = [
'title' => $subject,
'parent_name' => $parentName,
'student_name' => $studentName !== '' ? $studentName : 'your student',
'class_section_name' => $row['class_section_name'] ?? '',
'semester' => $semester,
'school_year' => $schoolYear,
'scores' => $scores,
'comment' => $row['comment'] ?? '',
'sent_at' => utc_now(),
];
$html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]);
return view('grading/below_sixty_email_editor', [
'studentId' => $studentId,
'studentName' => $studentName,
'semester' => $semester,
'schoolYear' => $schoolYear,
'subject' => $subject,
'html' => $html,
]);
}
public function sendBelowSixtyEmail()
{
$studentId = (int)$this->request->getPost('student_id');
$semester = trim((string)$this->request->getPost('semester'));
$schoolYear = trim((string)$this->request->getPost('school_year'));
$subjectInput = trim((string)$this->request->getPost('subject'));
$htmlInput = (string)($this->request->getPost('html') ?? '');
if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
return redirect()->back()->with('error', 'Missing student or term.');
}
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
if (empty($row)) {
return redirect()->back()->with('error', 'Student record not found for the selected term.');
}
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
$subject = $subjectInput !== ''
? $subjectInput
: $this->buildBelowSixtySubject($studentName, $semester, $schoolYear);
$scores = [ $scores = [
'homework_avg' => $row['homework_avg'] ?? null, 'homework_avg' => $row['homework_avg'] ?? null,
'project_avg' => $row['project_avg'] ?? null, 'project_avg' => $row['project_avg'] ?? null,
@@ -1122,8 +1185,13 @@ class GradingController extends Controller
'school_year' => $schoolYear, 'school_year' => $schoolYear,
'scores' => $scores, 'scores' => $scores,
'comment' => $row['comment'] ?? '', 'comment' => $row['comment'] ?? '',
'subject' => $subject,
]; ];
if (trim($htmlInput) !== '') {
$payload['html'] = $htmlInput;
}
Events::trigger('below60.email', $payload); Events::trigger('below60.email', $payload);
return redirect()->back()->with('status', 'Email sent to parent(s).'); return redirect()->back()->with('status', 'Email sent to parent(s).');
@@ -1799,6 +1867,91 @@ class GradingController extends Controller
return $row; return $row;
} }
private function userHasMenuUrl(string $needle): bool
{
$needle = strtolower(trim($needle));
if ($needle === '') {
return false;
}
$rawRole = session()->get('role');
$roles = is_array($rawRole) ? $rawRole : [$rawRole ?? 'guest'];
$roles = array_values(array_filter(array_map('strval', $roles)));
if (empty($roles)) {
return false;
}
$service = new NavbarService();
$menu = $service->getMenuForRoles($roles);
if (empty($menu)) {
return false;
}
$normalize = static function (string $url) use ($needle): string {
$url = strtolower(trim($url));
if ($url === '') return '';
$url = preg_replace('#^https?://[^/]+/#i', '', $url);
$url = ltrim($url, '/');
return $url;
};
$target = $normalize($needle);
$stack = $menu;
while (!empty($stack)) {
$node = array_shift($stack);
if (!empty($node['url'])) {
$url = $normalize((string)$node['url']);
if ($url !== '' && $url === $target) {
return true;
}
}
if (!empty($node['children']) && is_array($node['children'])) {
foreach ($node['children'] as $child) {
$stack[] = $child;
}
}
}
return false;
}
private function fetchBelowSixtyParentName(int $studentId): string
{
$parentName = 'Parent/Guardian';
try {
$rows = $this->db->query(
"SELECT u.firstname, u.lastname
FROM family_students fs
JOIN family_guardians fg ON fg.family_id = fs.family_id
JOIN users u ON u.id = fg.user_id
WHERE fs.student_id = ?
ORDER BY fg.is_primary DESC, u.lastname, u.firstname
LIMIT 1",
[$studentId]
)->getResultArray();
if (!empty($rows[0])) {
$candidate = trim((string)($rows[0]['firstname'] ?? '') . ' ' . (string)($rows[0]['lastname'] ?? ''));
if ($candidate !== '') {
$parentName = $candidate;
}
}
} catch (\Throwable $e) {
}
return $parentName;
}
private function buildBelowSixtySubject(string $studentName, string $semester, string $schoolYear): string
{
$subject = 'Student Performance Alert';
if ($studentName !== '') {
$subject .= ' — ' . $studentName;
}
if ($semester !== '' || $schoolYear !== '') {
$subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')';
}
return $subject;
}
private function fetchBelowSixtyMeetingContext(int $studentId, string $schoolYear, string $semester): array private function fetchBelowSixtyMeetingContext(int $studentId, string $schoolYear, string $semester): array
{ {
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
@@ -71,12 +71,15 @@ class ParentAttendanceReportController extends BaseController
->orderBy('s.lastname', 'ASC') ->orderBy('s.lastname', 'ASC')
->get()->getResultArray(); ->get()->getResultArray();
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
return view('parent/report_attendance', [ return view('parent/report_attendance', [
'students' => $students, 'students' => $students,
'today' => local_date(utc_now(), 'Y-m-d'), 'today' => local_date(utc_now(), 'Y-m-d'),
'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items 'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items
'defaultDate' => $defaultDate, // preselected date (upcoming Sunday) 'defaultDate' => $defaultDate, // preselected date (upcoming Sunday)
'myReports' => $previewRows, 'myReports' => $previewRows,
'cutoffThreshold' => $cutoffThreshold,
]); ]);
} }
@@ -141,6 +144,17 @@ class ParentAttendanceReportController extends BaseController
// Enforce Sunday-only and future-or-today selection // Enforce Sunday-only and future-or-today selection
$todayCheck = new \DateTime('today'); $todayCheck = new \DateTime('today');
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = null;
$nowTz = null;
try {
$tz = new \DateTimeZone($tzName ?: 'UTC');
$nowTz = new \DateTime('now', $tz);
} catch (\Throwable $e) {
$tz = null;
$nowTz = null;
}
$validDates = []; $validDates = [];
$cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year')); $cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year'));
@@ -150,9 +164,11 @@ class ParentAttendanceReportController extends BaseController
$lateTodayStr = $todayCheck->format('Y-m-d'); $lateTodayStr = $todayCheck->format('Y-m-d');
if ($type === 'late') { if ($type === 'late') {
try { try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); if (!$tz) {
$tz = new \DateTimeZone($tzName ?: 'UTC'); $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$lateNow = new \DateTime('now', $tz); $tz = new \DateTimeZone($tzName ?: 'UTC');
}
$lateNow = $nowTz ?: new \DateTime('now', $tz);
$cm = $this->configModel; $cm = $this->configModel;
$cutoffStr = (string) ($cm->getConfig('late_report_cutoff') $cutoffStr = (string) ($cm->getConfig('late_report_cutoff')
?: $cm->getConfig('late_cutoff_time') ?: $cm->getConfig('late_cutoff_time')
@@ -183,6 +199,17 @@ class ParentAttendanceReportController extends BaseController
if ($dt < $todayCheck) { if ($dt < $todayCheck) {
return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.'); return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.');
} }
if ($nowTz && $tz) {
try {
$cutoff = new \DateTime($entry . ' ' . $cutoffThreshold . ':00', $tz);
if ($nowTz >= $cutoff) {
$abbr = $cutoff->format('T');
return redirect()->back()->withInput()->with('error', 'Reporting closes at ' . $cutoffThreshold . ' ' . $abbr . ' on the report date.');
}
} catch (\Throwable $e) {
// ignore and continue
}
}
if ($dt > $cap) { if ($dt > $cap) {
return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.'); return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.');
} }
@@ -800,6 +827,36 @@ class ParentAttendanceReportController extends BaseController
$update['reported'] = 1; $update['reported'] = 1;
} }
$this->attendanceDataModel->update((int) $existing['id'], $update); $this->attendanceDataModel->update((int) $existing['id'], $update);
return;
}
if ($existingStatus === '' || $existingStatus === 'present') {
$update = [
'status' => $status,
'reason' => $existing['reason'] ?: $reasonText,
'updated_at' => utc_now(),
];
if ($hasIsReported) {
$update['is_reported'] = 'yes';
}
if ($hasLegacyReported) {
$update['reported'] = 1;
}
$this->attendanceDataModel->update((int) $existing['id'], $update);
$classSectionId = (int) ($existing['class_section_id'] ?? $classSectionId ?? 0);
$schoolId = (string) ($existing['school_id'] ?? '');
if ($classSectionId > 0 && $schoolId !== '') {
$this->adjustAttendanceRecordForStatusChange(
studentId: $studentId,
classSectionId: $classSectionId,
schoolId: $schoolId,
semester: $semester,
schoolYear: $schoolYear,
oldStatus: $existingStatus,
newStatus: $status
);
}
} }
} }
@@ -856,6 +913,61 @@ class ParentAttendanceReportController extends BaseController
$this->attendanceRecordModel->insert($insert); $this->attendanceRecordModel->insert($insert);
} }
private function adjustAttendanceRecordForStatusChange(
int $studentId,
int $classSectionId,
string $schoolId,
string $semester,
string $schoolYear,
string $oldStatus,
string $newStatus
): void {
$oldStatus = strtolower($oldStatus);
$newStatus = strtolower($newStatus);
if ($oldStatus === $newStatus) {
return;
}
$record = $this->attendanceRecordModel->where([
'student_id' => $studentId,
'semester' => $semester,
'school_year' => $schoolYear,
])->first();
if (!$record) {
$this->bumpAttendanceRecordFromParent($studentId, $classSectionId, $schoolId, $newStatus, $semester, $schoolYear);
return;
}
$updates = [
'total_presence' => (int) ($record['total_presence'] ?? 0),
'total_absence' => (int) ($record['total_absence'] ?? 0),
'total_late' => (int) ($record['total_late'] ?? 0),
'total_attendance' => (int) ($record['total_attendance'] ?? 0),
'updated_at' => utc_now(),
'modified_by' => (int) (session()->get('user_id') ?? 0),
];
if ($oldStatus === 'present') {
$updates['total_presence'] = max(0, $updates['total_presence'] - 1);
} elseif ($oldStatus === 'absent') {
$updates['total_absence'] = max(0, $updates['total_absence'] - 1);
} elseif ($oldStatus === 'late') {
$updates['total_late'] = max(0, $updates['total_late'] - 1);
}
if ($newStatus === 'present') {
$updates['total_presence']++;
} elseif ($newStatus === 'absent') {
$updates['total_absence']++;
} elseif ($newStatus === 'late') {
$updates['total_late']++;
}
$this->attendanceRecordModel->update((int) $record['id'], $updates);
}
// GET: staff view to list reports (today & upcoming by default) // GET: staff view to list reports (today & upcoming by default)
public function list() public function list()
{ {
@@ -1503,6 +1615,20 @@ class ParentAttendanceReportController extends BaseController
return $userId; // fallback return $userId; // fallback
} }
private function normalizeCutoffTime(string $raw, string $default = '09:00'): string
{
$raw = trim($raw);
if ($raw === '') {
return $default;
}
if (preg_match('/^(\d{1,2}):(\d{2})$/', $raw, $m)) {
$hh = str_pad((string) ((int) $m[1]), 2, '0', STR_PAD_LEFT);
$mm = $m[2];
return $hh . ':' . $mm;
}
return $default;
}
/** /**
* Send confirmation/notification emails for parent attendance submissions. * Send confirmation/notification emails for parent attendance submissions.
*/ */
@@ -4,18 +4,21 @@ namespace App\Controllers\View;
use App\Models\CalendarModel; use App\Models\CalendarModel;
use App\Models\AttendanceDataModel; use App\Models\AttendanceDataModel;
use App\Models\ReportCardAcknowledgementModel;
use App\Services\SemesterRangeService; use App\Services\SemesterRangeService;
class ReportCardsController extends PrintablesBaseController class ReportCardsController extends PrintablesBaseController
{ {
protected $calendarModel; protected $calendarModel;
protected $semesterRangeService; protected $semesterRangeService;
protected $ackModel;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->calendarModel = new CalendarModel(); $this->calendarModel = new CalendarModel();
$this->semesterRangeService = new SemesterRangeService($this->configModel); $this->semesterRangeService = new SemesterRangeService($this->configModel);
$this->ackModel = new ReportCardAcknowledgementModel();
} }
public function report_card() public function report_card()
@@ -369,6 +372,24 @@ class ReportCardsController extends PrintablesBaseController
$isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v); $isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v);
$ackMap = [];
try {
$ackRows = $this->ackModel
->whereIn('student_id', $studentIds)
->where('school_year', $year)
->where('semester', $sem)
->orderBy('updated_at', 'DESC')
->findAll();
foreach ($ackRows as $row) {
$sid = (int) ($row['student_id'] ?? 0);
if ($sid > 0 && ! isset($ackMap[$sid])) {
$ackMap[$sid] = $row;
}
}
} catch (\Throwable $e) {
$ackMap = [];
}
$results = []; $results = [];
$completeCount = 0; $completeCount = 0;
$warningCount = 0; $warningCount = 0;
@@ -471,12 +492,16 @@ class ReportCardsController extends PrintablesBaseController
$warningCount++; $warningCount++;
} }
$ack = $sid > 0 ? ($ackMap[$sid] ?? null) : null;
$results[] = [ $results[] = [
'id' => $sid, 'id' => $sid,
'name' => $name !== '' ? $name : 'N/A', 'name' => $name !== '' ? $name : 'N/A',
'missing' => $missing, 'missing' => $missing,
'warnings' => $warnings, 'warnings' => $warnings,
'complete' => $isComplete, 'complete' => $isComplete,
'viewed_at' => $ack['viewed_at'] ?? null,
'signed_at' => $ack['signed_at'] ?? null,
'signed_name' => $ack['signed_name'] ?? null,
]; ];
} }
@@ -501,6 +526,37 @@ class ReportCardsController extends PrintablesBaseController
]); ]);
} }
/**
* API: report card parent acknowledgement status
* GET params: student_id, school_year, semester
*/
public function reportCardAcknowledgement()
{
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
if ($studentId <= 0) {
return $this->response->setJSON(['ok' => false, 'error' => 'Missing student_id'])->setStatusCode(400);
}
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->schoolYear ?? ''));
$semester = trim((string) ($this->request->getGet('semester') ?? $this->semester ?? ''));
$row = $this->ackModel
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->orderBy('updated_at', 'DESC')
->first();
return $this->response->setJSON([
'ok' => true,
'student_id' => $studentId,
'school_year' => $schoolYear,
'semester' => $semester,
'viewed_at' => $row['viewed_at'] ?? null,
'signed_at' => $row['signed_at'] ?? null,
'signed_name' => $row['signed_name'] ?? null,
]);
}
protected function fetchStudentsByClass(int $sectionId, ?string $schoolYear) protected function fetchStudentsByClass(int $sectionId, ?string $schoolYear)
{ {
$b = $this->studentClassModel $b = $this->studentClassModel
+1
View File
@@ -1378,6 +1378,7 @@ $students = $this->db->table('students')
$releaseScores = (strcasecmp($semester, 'Fall') === 0) ? $releaseFall $releaseScores = (strcasecmp($semester, 'Fall') === 0) ? $releaseFall
: ((strcasecmp($semester, 'Spring') === 0) ? $releaseSpring : $releaseAny); : ((strcasecmp($semester, 'Spring') === 0) ? $releaseSpring : $releaseAny);
$scores[$selectedYear][$semester][$studentId] = [ $scores[$selectedYear][$semester][$studentId] = [
'student_id' => $studentId,
'school_id' => $student['school_id'], 'school_id' => $student['school_id'],
'student_firstname' => $student['firstname'], 'student_firstname' => $student['firstname'],
'student_lastname' => $student['lastname'], 'student_lastname' => $student['lastname'],
@@ -0,0 +1,139 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddBelowSixtyNavItem extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->tableExists('nav_items')) {
return;
}
$parentColumn = null;
if ($db->fieldExists('menu_parent_id', 'nav_items')) {
$parentColumn = 'menu_parent_id';
} elseif ($db->fieldExists('parent_id', 'nav_items')) {
$parentColumn = 'parent_id';
}
if ($parentColumn === null) {
return;
}
$navBuilder = $db->table('nav_items');
$now = date('Y-m-d H:i:s');
$communication = $navBuilder
->select('id')
->where('label', 'Communication')
->where($parentColumn, null)
->get()
->getRowArray();
if ($communication) {
$communicationId = (int) $communication['id'];
} else {
$navBuilder->insert([
$parentColumn => null,
'label' => 'Communication',
'url' => null,
'sort_order' => 80,
'is_enabled' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
$communicationId = (int) $db->insertID();
}
$belowSixtyUrl = 'grading/below-60';
$existing = $navBuilder->select('id')
->where('url', $belowSixtyUrl)
->get()
->getRowArray();
if (!$existing) {
$navBuilder->insert([
$parentColumn => $communicationId ?: null,
'label' => 'Below 60 Summary',
'url' => $belowSixtyUrl,
'sort_order' => 4,
'is_enabled' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
$belowSixtyId = (int) $db->insertID();
} else {
$belowSixtyId = (int) $existing['id'];
}
if (!$db->tableExists('role_nav_items') || !$db->tableExists('roles')) {
return;
}
if ($belowSixtyId <= 0) {
return;
}
$roleRows = $db->table('roles')
->select('id, name')
->whereIn('name', ['administrator', 'principal', 'vice_principal', 'admin'])
->get()
->getResultArray();
$roleMap = $db->table('role_nav_items');
foreach ($roleRows as $role) {
$roleId = (int) ($role['id'] ?? 0);
if ($roleId <= 0) {
continue;
}
$exists = $roleMap
->where('role_id', $roleId)
->where('nav_item_id', $belowSixtyId)
->get()
->getRowArray();
if ($exists) {
continue;
}
$roleMap->insert([
'role_id' => $roleId,
'nav_item_id' => $belowSixtyId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
public function down()
{
$db = \Config\Database::connect();
if (!$db->tableExists('nav_items')) {
return;
}
$belowSixtyUrl = 'grading/below-60';
$row = $db->table('nav_items')
->select('id')
->where('url', $belowSixtyUrl)
->get()
->getRowArray();
if ($row && $db->tableExists('role_nav_items')) {
$db->table('role_nav_items')
->where('nav_item_id', (int) $row['id'])
->delete();
}
$db->table('nav_items')
->where('url', $belowSixtyUrl)
->delete();
}
}
@@ -13,11 +13,11 @@ class AllowCompetitionWinnerRankTies extends Migration
return; return;
} }
if ($db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) { if ($this->indexExists($db, 'competition_winners', 'competition_id_class_section_id_rank')) {
$db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners'); $db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners');
} }
if (!$db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) { if (! $this->indexExists($db, 'competition_winners', 'competition_id_class_section_id_rank')) {
$db->query( $db->query(
'CREATE INDEX competition_id_class_section_id_rank ON competition_winners (competition_id, class_section_id, rank)' 'CREATE INDEX competition_id_class_section_id_rank ON competition_winners (competition_id, class_section_id, rank)'
); );
@@ -31,7 +31,7 @@ class AllowCompetitionWinnerRankTies extends Migration
return; return;
} }
if ($db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) { if ($this->indexExists($db, 'competition_winners', 'competition_id_class_section_id_rank')) {
$db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners'); $db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners');
} }
@@ -39,4 +39,23 @@ class AllowCompetitionWinnerRankTies extends Migration
'ALTER TABLE competition_winners ADD UNIQUE KEY competition_id_class_section_id_rank (competition_id, class_section_id, rank)' 'ALTER TABLE competition_winners ADD UNIQUE KEY competition_id_class_section_id_rank (competition_id, class_section_id, rank)'
); );
} }
private function indexExists($db, string $table, string $index): bool
{
if (method_exists($db, 'indexExists')) {
return $db->indexExists($table, $index);
}
$dbName = $db->getDatabase();
$builder = $db->table('information_schema.STATISTICS');
$row = $builder
->select('INDEX_NAME')
->where('TABLE_SCHEMA', $dbName)
->where('TABLE_NAME', $table)
->where('INDEX_NAME', $index)
->get()
->getRowArray();
return ! empty($row);
}
} }
@@ -1 +1,37 @@
Print all <?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPageSelectionToPrintRequests extends Migration
{
public function up()
{
if (! $this->db->tableExists('print_requests')) {
return;
}
if (! $this->db->fieldExists('page_selection', 'print_requests')) {
$this->forge->addColumn('print_requests', [
'page_selection' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
'after' => 'num_copies',
],
]);
}
}
public function down()
{
if (! $this->db->tableExists('print_requests')) {
return;
}
if ($this->db->fieldExists('page_selection', 'print_requests')) {
$this->forge->dropColumn('print_requests', 'page_selection');
}
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateReportCardAcknowledgements extends Migration
{
public function up()
{
if ($this->db->tableExists('report_card_acknowledgements')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'parent_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'student_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 20,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 20,
],
'viewed_at' => [
'type' => 'DATETIME',
'null' => true,
],
'signed_at' => [
'type' => 'DATETIME',
'null' => true,
],
'signed_name' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
],
'signer_ip' => [
'type' => 'VARCHAR',
'constraint' => 45,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['parent_id', 'student_id', 'school_year', 'semester'], false, true);
$this->forge->createTable('report_card_acknowledgements');
}
public function down()
{
$this->forge->dropTable('report_card_acknowledgements', true);
}
}
+4 -1
View File
@@ -71,7 +71,10 @@ class BelowSixtyEmailListener
'sent_at' => utc_now(), 'sent_at' => utc_now(),
]; ];
$html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]); $html = trim((string)($payload['html'] ?? ''));
if ($html === '') {
$html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]);
}
$okAny = false; $okAny = false;
foreach ($emails as $to) { foreach ($emails as $to) {
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class ReportCardAcknowledgementModel extends Model
{
protected $table = 'report_card_acknowledgements';
protected $primaryKey = 'id';
protected $allowedFields = [
'parent_id',
'student_id',
'school_year',
'semester',
'viewed_at',
'signed_at',
'signed_name',
'signer_ip',
'created_at',
'updated_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $returnType = 'array';
}
+141 -71
View File
@@ -1,8 +1,21 @@
<?= $this->extend('layout/management_layout') ?> <?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<style> <style>
#adminProgressTable thead th { .admin-progress-table thead th {
position: static !important; position: static !important;
top: auto !important;
z-index: auto !important;
background: #f8f9fa;
white-space: nowrap;
line-height: 1.2;
padding-top: .75rem;
padding-bottom: .75rem;
}
.admin-progress-table td {
vertical-align: top;
}
.bg-orange {
background-color: #fd7e14 !important;
} }
</style> </style>
<div class="container py-4"> <div class="container py-4">
@@ -56,79 +69,136 @@
</form> </form>
<div class="card shadow-sm mt-5"> <div class="card shadow-sm mt-5">
<div class="card-body pt-4 px-0"> <div class="card-body pt-4">
<div class="table-responsive px-4" style="margin-top: 1.5rem;"> <?php
<table id="adminProgressTable" class="table table-hover align-middle mb-0"> $reportGroupsBySection = $reportGroupsBySection ?? [];
<thead class="table-light"> $subjectSections = $subjectSections ?? [];
<tr> $classSections = $classSections ?? [];
<th>Week</th> $sectionStats = $sectionStats ?? [];
<th>Subjects</th> $sectionSubjectCounts = $sectionSubjectCounts ?? [];
<th>Teacher</th> $expectedDays = (int) ($expectedDays ?? 0);
<th class="text-end">Action</th> ?>
</tr> <?php if (empty($classSections)): ?>
</thead> <div class="text-center text-muted py-4">No class sections found.</div>
<tbody> <?php else: ?>
<div class="accordion" id="adminProgressAccordion">
<?php foreach ($classSections as $index => $cs): ?>
<?php <?php
$reportGroups = $reportGroups ?? []; $sectionId = (int) ($cs['class_section_id'] ?? 0);
$subjectSections = $subjectSections ?? []; $sectionName = $cs['class_section_name'] ?? 'Unknown Section';
$sectionGroups = $reportGroupsBySection[$sectionId] ?? [];
$hasReports = ! empty($sectionGroups);
$collapseId = 'progress-section-' . $sectionId;
$headingId = 'progress-heading-' . $sectionId;
$stat = $sectionStats[$sectionId] ?? null;
$subjectCount = (int) ($sectionSubjectCounts[$sectionId] ?? 0);
if (! $stat) {
if ($expectedDays === 0) {
$stat = [
'submitted' => 0,
'expected' => 0,
'percent' => 0,
'badgeClass' => 'bg-secondary',
'labelClass' => 'text-muted',
];
} else {
$stat = [
'submitted' => 0,
'expected' => $expectedDays,
'percent' => 0,
'badgeClass' => 'bg-danger',
'labelClass' => 'text-danger',
];
}
}
$percentLabel = $expectedDays > 0 ? number_format((float) $stat['percent'], 1) . '%' : 'N/A';
$submissionLabel = $expectedDays > 0
? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')')
: 'Submitted: N/A';
$subjectLabel = 'Subjects: ' . $subjectCount;
?> ?>
<?php if (empty($reportGroups)): ?> <div class="accordion-item mb-2">
<tr><td colspan="4" class="text-center text-muted py-4">No reports found.</td></tr> <h2 class="accordion-header" id="<?= esc($headingId) ?>">
<?php else: ?> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
<?php foreach ($reportGroups as $group): ?> <?= esc($sectionName) ?>
<?php <span class="badge <?= esc($stat['badgeClass']) ?> ms-2"><?= esc($submissionLabel) ?></span>
$weekLabel = $group['week_start'] ? date('M d, Y', strtotime($group['week_start'])) : '-'; <span class="badge bg-info text-dark ms-2"><?= esc($subjectLabel) ?></span>
if (!empty($group['week_end'])) { <?php if ($hasReports): ?>
$weekLabel .= ' ' . date('M d, Y', strtotime($group['week_end'])); <span class="badge bg-secondary ms-2"><?= count($sectionGroups) ?> weeks</span>
} <?php endif; ?>
$reports = $group['reports'] ?? []; </button>
$teacherName = ''; </h2>
foreach ($reports as $report) { <div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="<?= esc($headingId) ?>" data-bs-parent="#adminProgressAccordion">
if (!empty($report['teacher_name'])) { <div class="accordion-body">
$teacherName = $report['teacher_name']; <?php if (! $hasReports): ?>
break; <div class="text-muted">No reports found for this section.</div>
} <?php else: ?>
} <div class="table-responsive">
$exampleId = $reports ? reset($reports)['id'] : null; <table class="table table-hover align-middle mb-0 admin-progress-table no-mgmt-sticky" data-no-mgmt-sticky>
?> <thead class="table-light">
<tr> <tr>
<td> <th>Week</th>
<div class="fw-semibold"><?= esc($weekLabel) ?></div> <th>Subjects</th>
<?php if (!empty($group['class_section_name'])): ?> <th>Teacher</th>
<div class="text-muted small">Class: <?= esc($group['class_section_name']) ?></div> <th class="text-end">Action</th>
<?php endif; ?> </tr>
</td> </thead>
<td> <tbody>
<div class="d-flex flex-column gap-2"> <?php foreach ($sectionGroups as $group): ?>
<?php foreach ($subjectSections as $slug => $section): ?> <?php
<?php $weekLabel = $group['week_start'] ? date('M d, Y', strtotime($group['week_start'])) : '-';
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; if (! empty($group['week_end'])) {
$report = $reports[$subjectName] ?? null; $weekLabel .= ' ' . date('M d, Y', strtotime($group['week_end']));
$statusTag = $report ? ($report['status_label'] ?? 'Unknown') : 'No entry'; }
$badgeClass = $report ? 'bg-secondary' : 'bg-light text-muted'; $reports = $group['reports'] ?? [];
?> $teacherName = '';
<div class="border rounded-3 p-2"> foreach ($reports as $report) {
<div class="d-flex justify-content-between align-items-center"> if (! empty($report['teacher_name'])) {
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong> $teacherName = $report['teacher_name'];
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span> break;
</div> }
<div class="small text-muted"><?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?></div> }
</div> $exampleId = $reports ? reset($reports)['id'] : null;
<?php endforeach; ?> ?>
<tr>
<td><div class="fw-semibold"><?= esc($weekLabel) ?></div></td>
<td>
<div class="d-flex flex-column gap-2">
<?php foreach ($subjectSections as $slug => $section): ?>
<?php
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
$report = $reports[$subjectName] ?? null;
$statusTag = $report ? ($report['status_label'] ?? 'Unknown') : 'No entry';
$badgeClass = $report ? 'bg-secondary' : 'bg-light text-muted';
?>
<div class="border rounded-3 p-2">
<div class="d-flex justify-content-between align-items-center">
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
</div>
<div class="small text-muted"><?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?></div>
</div>
<?php endforeach; ?>
</div>
</td>
<td><?= esc($teacherName ?: '-') ?></td>
<td class="text-end">
<?php if ($exampleId): ?>
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('admin/progress/view/' . $exampleId) ?>">View</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div> </div>
</td> <?php endif; ?>
<td><?= esc($teacherName ?: '-') ?></td> </div>
<td class="text-end"> </div>
<?php if ($exampleId): ?> </div>
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('admin/progress/view/' . $exampleId) ?>">View</a> <?php endforeach; ?>
<?php endif; ?> </div>
</td> <?php endif; ?>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -517,37 +517,28 @@
<section class="mb-5"> <section class="mb-5">
<h2 class="mb-4 text-center">Statistics</h2> <h2 class="mb-4 text-center">Statistics</h2>
<div class="stats-row"> <div class="stats-row">
<!-- Students -->
<div class="stat-circle circle-primary"> <div class="stat-circle circle-primary">
<div class="stat-icon"><i class="bi bi-people-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-graduate"></i></div>
<div class="stat-value" data-stat="students">—</div> <div class="stat-value" data-stat="students">—</div>
<div class="stat-title">Students</div> <div class="stat-title">Students</div>
</div> </div>
<!-- Teachers -->
<div class="stat-circle circle-info"> <div class="stat-circle circle-info">
<div class="stat-icon"><i class="bi bi-person-video2"></i></div> <div class="stat-icon"><i class="fas fa-chalkboard-teacher"></i></div>
<div class="stat-value" data-stat="teachers">—</div> <div class="stat-value" data-stat="teachers">—</div>
<div class="stat-title">Teachers</div> <div class="stat-title">Teachers</div>
</div> </div>
<!-- Teacher Assistants -->
<div class="stat-circle circle-success"> <div class="stat-circle circle-success">
<div class="stat-icon"><i class="bi bi-person-badge-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-cog"></i></div>
<div class="stat-value" data-stat="teacherAssistants">—</div> <div class="stat-value" data-stat="teacherAssistants">—</div>
<div class="stat-title">Teachers' Assistants</div> <div class="stat-title">TAs</div>
</div> </div>
<!-- Admins -->
<div class="stat-circle circle-warning"> <div class="stat-circle circle-warning">
<div class="stat-icon"><i class="bi bi-shield-lock-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-shield"></i></div>
<div class="stat-value" data-stat="admins">—</div> <div class="stat-value" data-stat="admins">—</div>
<div class="stat-title">Admins</div> <div class="stat-title">Admins</div>
</div> </div>
<!-- Parents -->
<div class="stat-circle circle-light"> <div class="stat-circle circle-light">
<div class="stat-icon"><i class="bi bi-people"></i></div> <div class="stat-icon"><i class="fas fa-users"></i></div>
<div class="stat-value" data-stat="parents">—</div> <div class="stat-value" data-stat="parents">—</div>
<div class="stat-title">Parents</div> <div class="stat-title">Parents</div>
</div> </div>
@@ -71,9 +71,20 @@
max-height: none; max-height: none;
overflow: visible; overflow: visible;
} }
.attn-pie-wrap {
width: 320px;
height: 320px;
margin: 0 auto;
}
@media (max-width: 1200px) { @media (max-width: 1200px) {
.attn-analysis-half { grid-column: span 12; } .attn-analysis-half { grid-column: span 12; }
} }
@media (max-width: 576px) {
.attn-pie-wrap {
width: 180px;
height: 180px;
}
}
</style> </style>
<?= $this->endSection() ?> <?= $this->endSection() ?>
@@ -291,7 +302,9 @@
<div class="attn-analysis-grid"> <div class="attn-analysis-grid">
<div class="attn-analysis-card attn-analysis-half"> <div class="attn-analysis-card attn-analysis-half">
<h6>Overall Distribution</h6> <h6>Overall Distribution</h6>
<canvas id="attnPieChart" height="50" aria-label="Overall attendance pie chart"></canvas> <div class="attn-pie-wrap">
<canvas id="attnPieChart" aria-label="Overall attendance pie chart"></canvas>
</div>
<table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky> <table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky>
<thead> <thead>
<tr> <tr>
@@ -415,6 +428,8 @@
}] }]
}, },
options: { options: {
responsive: true,
maintainAspectRatio: false,
plugins: { plugins: {
tooltip: { tooltip: {
callbacks: { callbacks: {
+4
View File
@@ -0,0 +1,4 @@
<?= $this->extend('layout/email_layout') ?>
<?= $this->section('content') ?>
<?= $body_html ?? '' ?>
<?= $this->endSection() ?>
+12 -1
View File
@@ -22,6 +22,10 @@ if ($title === '' || preg_match('/^\s*Family\s+of\s+User\s*\d+\s*$/i', $title))
} }
$title = $lastName !== '' ? ('Family of ' . $lastName) : 'Family'; $title = $lastName !== '' ? ('Family of ' . $lastName) : 'Family';
} }
$returnUrl = trim((string)service('request')->getServer('HTTP_REFERER'));
if ($returnUrl === '') {
$returnUrl = site_url('family');
}
?> ?>
<div class="family-card-root" data-family-title="<?= esc($title) ?>"> <div class="family-card-root" data-family-title="<?= esc($title) ?>">
@@ -187,7 +191,14 @@ if ($title === '' || preg_match('/^\s*Family\s+of\s+User\s*\d+\s*$/i', $title))
<td class="small" data-label="Contact"> <td class="small" data-label="Contact">
<div> <div>
<i class="bi bi-envelope me-1 text-muted"></i> <i class="bi bi-envelope me-1 text-muted"></i>
<?= !empty($g['email']) ? ('<a href="mailto:'.esc($g['email']).'">'.esc($g['email']).'</a>') : '<span class="text-muted">—</span>' ?> <?php
$gEmail = trim((string)($g['email'] ?? ''));
$gName = trim((string)($g['firstname'] ?? '') . ' ' . (string)($g['lastname'] ?? ''));
$composeUrl = $gEmail !== ''
? site_url('family/compose-email?to=' . rawurlencode($gEmail) . '&name=' . rawurlencode($gName) . '&return_url=' . rawurlencode($returnUrl))
: '';
?>
<?= $gEmail !== '' ? ('<a href="' . esc($composeUrl) . '" target="_blank" rel="noopener">' . esc($gEmail) . '</a>') : '<span class="text-muted">—</span>' ?>
</div> </div>
<div class="mt-1"> <div class="mt-1">
<i class="bi bi-telephone me-1 text-muted"></i> <i class="bi bi-telephone me-1 text-muted"></i>
+87
View File
@@ -0,0 +1,87 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div>
<h2 class="h4 mb-1">Compose Email</h2>
<?php if (!empty($name)): ?>
<div class="text-muted"><?= esc($name) ?></div>
<?php endif; ?>
</div>
<a class="btn btn-outline-secondary btn-sm" href="<?= esc((string)($return_url ?? site_url('family'))) ?>">Back</a>
</div>
<form class="card shadow-sm" method="post" action="<?= site_url('family/compose-email/send') ?>">
<?= csrf_field() ?>
<input type="hidden" name="html" id="compose_html" value="">
<input type="hidden" name="return_url" value="<?= esc((string)($return_url ?? '')) ?>">
<div class="card-body">
<div class="mb-3">
<label class="form-label" for="composeTo">To</label>
<input type="email" class="form-control" id="composeTo" name="to" value="<?= esc((string)($to ?? '')) ?>" placeholder="parent@example.com" required>
</div>
<div class="mb-3">
<label class="form-label" for="composeSubject">Subject</label>
<input type="text" class="form-control" id="composeSubject" name="subject" placeholder="Subject" required>
</div>
<div class="mb-3">
<label class="form-label" for="composeEditor">Body (Rich Text)</label>
<textarea class="form-control" id="composeEditor" rows="14"></textarea>
<div class="form-text">Use the toolbar to format your message before sending.</div>
<noscript>
<div class="alert alert-warning mt-2">
JavaScript is disabled. The message will be sent from the hidden <code>html</code> field.
</div>
</noscript>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="submit" class="btn btn-primary">Send Email</button>
</div>
</div>
</form>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
<script>
(function () {
const form = document.querySelector('form[action$="family/compose-email/send"]');
const hiddenHtml = document.getElementById('compose_html');
tinymce.init({
selector: '#composeEditor',
base_url: '<?= base_url('assets/tinymce') ?>',
suffix: '.min',
license_key: 'gpl',
height: 420,
menubar: true,
branding: false,
promotion: false,
plugins: 'advlist autolink lists link image charmap preview anchor ' +
'searchreplace visualblocks code fullscreen insertdatetime media table ' +
'help wordcount emoticons codesample',
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | ' +
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | ' +
'link image media table | emoticons codesample | removeformat | preview code',
convert_urls: false,
paste_data_images: true,
image_caption: true,
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }',
setup(editor) {
editor.on('keyup change undo redo SetContent', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
if (form) {
form.addEventListener('submit', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
}
}
});
})();
</script>
<?= $this->endSection() ?>
+14 -13
View File
@@ -11,9 +11,13 @@
<div class="text-muted"> <div class="text-muted">
<?= esc(ucfirst($semester ?? '')) ?> • <?= esc($schoolYear ?? '') ?> <?= esc(ucfirst($semester ?? '')) ?> • <?= esc($schoolYear ?? '') ?>
</div> </div>
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>"> <?php if (!empty($canViewGrading)): ?>
Back to Grading <a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
</a> Back to Grading
</a>
<?php else: ?>
<span class="text-muted small">You do not have access to the Grading page.</span>
<?php endif; ?>
</div> </div>
<?php <?php
@@ -94,17 +98,14 @@
</form> </form>
</td> </td>
<td class="text-center"> <td class="text-center">
<form method="post" action="<?= site_url('grading/below-60/email') ?>" class="d-inline"> <?php if ($isClosed): ?>
<?= csrf_field() ?> <button type="button" class="btn btn-sm btn-secondary" disabled>Send Email</button>
<input type="hidden" name="student_id" value="<?= esc((string)($row['student_id'] ?? '')) ?>"> <?php else: ?>
<input type="hidden" name="semester" value="<?= esc((string)($semester ?? '')) ?>"> <a class="btn btn-sm btn-outline-primary"
<input type="hidden" name="school_year" value="<?= esc((string)($schoolYear ?? '')) ?>"> href="<?= site_url('grading/below-60/email/edit?student_id=' . (int)($row['student_id'] ?? 0) . '&semester=' . rawurlencode((string)($semester ?? '')) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
<button type="submit"
class="btn btn-sm <?= $isClosed ? 'btn-secondary' : 'btn-outline-primary' ?>"
<?= $isClosed ? 'disabled' : '' ?>>
Send Email Send Email
</button> </a>
</form> <?php endif; ?>
</td> </td>
<td class="text-center"> <td class="text-center">
<?php if ($isClosed): ?> <?php if ($isClosed): ?>
@@ -0,0 +1,96 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="wrapper">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div>
<h2 class="h4 mb-1">Edit Email</h2>
<div class="text-muted">
<?= esc($studentName !== '' ? $studentName : 'Student') ?> • <?= esc($semester ?? '') ?> • <?= esc($schoolYear ?? '') ?>
</div>
</div>
<a class="btn btn-outline-secondary btn-sm"
href="<?= site_url('grading/below-60?semester=' . rawurlencode((string)($semester ?? '')) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
Back to Below 60
</a>
</div>
<form method="post" action="<?= site_url('grading/below-60/email') ?>" class="card shadow-sm">
<?= csrf_field() ?>
<input type="hidden" name="student_id" value="<?= esc((string)($studentId ?? '')) ?>">
<input type="hidden" name="semester" value="<?= esc((string)($semester ?? '')) ?>">
<input type="hidden" name="school_year" value="<?= esc((string)($schoolYear ?? '')) ?>">
<input type="hidden" name="html" id="below60_html" value="<?= htmlspecialchars((string)($html ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
<div class="card-body">
<div class="mb-3">
<label class="form-label" for="below60Subject">Subject</label>
<input type="text" class="form-control" id="below60Subject" name="subject" value="<?= esc((string)($subject ?? '')) ?>" required>
</div>
<div class="mb-3">
<label class="form-label" for="below60Editor">Body (Rich Text)</label>
<textarea class="form-control" id="below60Editor" rows="14"><?= (string)($html ?? '') ?></textarea>
<div class="form-text">Use the toolbar to format the message before sending.</div>
<noscript>
<div class="alert alert-warning mt-2">
JavaScript is disabled. The message will be sent from the hidden <code>html</code> field.
</div>
</noscript>
</div>
<div class="d-flex justify-content-end gap-2">
<a class="btn btn-outline-secondary"
href="<?= site_url('grading/below-60?semester=' . rawurlencode((string)($semester ?? '')) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
Cancel
</a>
<button type="submit" class="btn btn-primary">Send Email</button>
</div>
</div>
</form>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
<script>
(function () {
const form = document.querySelector('form[action$="grading/below-60/email"]');
const hiddenHtml = document.getElementById('below60_html');
if (!window.tinymce) return;
tinymce.init({
selector: '#below60Editor',
base_url: '<?= base_url('assets/tinymce') ?>',
suffix: '.min',
license_key: 'gpl',
height: 420,
menubar: true,
branding: false,
promotion: false,
plugins: 'advlist autolink lists link image charmap preview anchor ' +
'searchreplace visualblocks code fullscreen insertdatetime media table ' +
'help wordcount emoticons codesample',
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | ' +
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | ' +
'link image media table | emoticons codesample | removeformat | preview code',
convert_urls: false,
paste_data_images: true,
image_caption: true,
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }',
setup(editor) {
editor.on('keyup change undo redo SetContent', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
if (form) {
form.addEventListener('submit', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
}
}
});
})();
</script>
<?= $this->endSection() ?>
+173 -14
View File
@@ -322,6 +322,72 @@
min-height: 400px; min-height: 400px;
} }
.home-stats {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
padding: 2.5rem 1.5rem;
}
.home-stats .stats-row {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: center;
}
.home-stats .stat-circle {
width: 190px;
height: 190px;
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #ffffff;
font-weight: 700;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.12);
}
.home-stats .stat-value {
font-size: 1.8rem;
line-height: 1;
}
.home-stats .stat-icon {
font-size: 2rem;
margin-bottom: .2rem;
opacity: .9;
}
.home-stats .stat-title {
font-size: 1.1rem;
margin-top: .25rem;
font-weight: 500;
text-align: center;
line-height: 1.2;
}
.home-stats .circle-primary {
background: radial-gradient(circle at 30% 30%, #0d6efd, #0044aa);
}
.home-stats .circle-success {
background: radial-gradient(circle at 30% 30%, #198754, #0c4f2c);
}
.home-stats .circle-warning {
background: radial-gradient(circle at 30% 30%, #daa544ff, #a66f00);
}
.home-stats .circle-info {
background: radial-gradient(circle at 30% 30%, #0dcaf0, #047e9c);
}
.home-stats .circle-light {
background: radial-gradient(circle at 30% 30%, #63af4cff, #00eb7dff);
}
@media (max-width: 992px) { @media (max-width: 992px) {
.image-column { .image-column {
min-height: 300px; min-height: 300px;
@@ -332,6 +398,13 @@
border-radius: 0 0 10px 10px !important; border-radius: 0 0 10px 10px !important;
} }
} }
@media (max-width: 768px) {
.home-stats .stat-circle {
width: 160px;
height: 160px;
}
}
</style> </style>
</head> </head>
@@ -448,8 +521,43 @@
<h4 class="fw-bold my-0"><u>Join Al Rahma family today! Click here for a quick guide on how to create an account.</u></h4> <h4 class="fw-bold my-0"><u>Join Al Rahma family today! Click here for a quick guide on how to create an account.</u></h4>
</a> </a>
</section> </section>
<!-- Statistics Start -->
<div class="container-xxl py-2 content-section">
<div class="container">
<div class="home-stats" data-dashboard-endpoint="<?= site_url('api/administrator/dashboard') ?>">
<h1 class="d-flex justify-content-center p-md-1">Active Participants</h1>
<br>
<div class="stats-row">
<div class="stat-circle circle-primary">
<div class="stat-icon"><i class="fa-solid fa-user-graduate"></i></div>
<div class="stat-value" data-stat="students">—</div>
<div class="stat-title">Students</div>
</div>
<div class="stat-circle circle-info">
<div class="stat-icon"><i class="fa-solid fa-chalkboard-user"></i></div>
<div class="stat-value" data-stat="teachers">—</div>
<div class="stat-title">Teachers</div>
</div>
<div class="stat-circle circle-success">
<div class="stat-icon"><i class="fa-solid fa-user-gear"></i></div>
<div class="stat-value" data-stat="teacherAssistants">—</div>
<div class="stat-title">TAs</div>
</div>
<div class="stat-circle circle-warning">
<div class="stat-icon"><i class="fa-solid fa-user-shield"></i></div>
<div class="stat-value" data-stat="admins">—</div>
<div class="stat-title">Admins</div>
</div>
<div class="stat-circle circle-light">
<div class="stat-icon"><i class="fa-solid fa-people-group"></i></div>
<div class="stat-value" data-stat="parents">—</div>
<div class="stat-title">Parents</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics End -->
<div class="container-xxl py-2 content-section"> <div class="container-xxl py-2 content-section">
<div class="container"> <div class="container">
<div class="row bg-light squared align-items-center"> <div class="row bg-light squared align-items-center">
@@ -461,18 +569,18 @@
<p class="mb-4">Under the umbrella of ISGL, Al Rahma Sunday School has been serving the community for over thirty years. Throughout the decades, it has provided students with a strong foundation in Islamic Studies and Quran, fostering both knowledge and character. With its dedicated teachers and well-rounded academic program, the school continues to guide generations of Muslim youth in their faith, values and practice of Islam.</p> <p class="mb-4">Under the umbrella of ISGL, Al Rahma Sunday School has been serving the community for over thirty years. Throughout the decades, it has provided students with a strong foundation in Islamic Studies and Quran, fostering both knowledge and character. With its dedicated teachers and well-rounded academic program, the school continues to guide generations of Muslim youth in their faith, values and practice of Islam.</p>
<p>For more information, click below to visit ISGL official website.</p> <p>For more information, click below to visit ISGL official website.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0"> <table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr> <tr>
<td align="center" bgcolor="#28a745" style="border-radius:6px;"> <td align="center" bgcolor="#28a745" style="border-radius:6px;">
<a href="https://isgl.org" <a href="https://isgl.org"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;"> style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;">
ISGL Official Website ISGL Official Website
</a> </a>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
<!-- Image Column - Fixed with correct path --> <!-- Image Column - Fixed with correct path -->
@@ -677,6 +785,57 @@
} }
}); });
</script> </script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.querySelector('.home-stats[data-dashboard-endpoint]');
if (!container) return;
const endpoint = container.dataset.dashboardEndpoint;
const statElements = container.querySelectorAll('[data-stat]');
const numberFormatter = new Intl.NumberFormat();
let isLoading = false;
const loadStats = function() {
if (isLoading) return;
isLoading = true;
fetch(endpoint, {
headers: {
'Accept': 'application/json'
},
credentials: 'same-origin',
})
.then(function(response) {
if (!response.ok) {
throw new Error('Request failed');
}
return response.json();
})
.then(function(payload) {
const counts = payload && typeof payload === 'object' && payload.counts ?
payload.counts :
{};
statElements.forEach(function(element) {
const key = element.dataset.stat;
const value = counts[key];
element.textContent = Number.isFinite(value) ?
numberFormatter.format(value) :
'—';
});
})
.catch(function() {})
.finally(function() {
isLoading = false;
});
};
loadStats();
setInterval(loadStats, 60000);
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
loadStats();
}
});
});
</script>
</body> </body>
</html> </html>
@@ -121,6 +121,15 @@
</div> </div>
</div> </div>
<!-- Report Cards -->
<div class="col-auto">
<a href="<?= base_url('parent/report-cards') ?>" class="d-inline-block text-decoration-none">
<div class="circle-box bg-dark text-white position-relative" role="button" aria-label="Open report cards">
<h4 class="fw-bold mb-0">Report Cards</h4>
</div>
</a>
</div>
<!-- Enrollment --> <!-- Enrollment -->
<div class="col-auto"> <div class="col-auto">
<div class="circle-box bg-info text-white"> <div class="circle-box bg-info text-white">
+93 -5
View File
@@ -122,15 +122,40 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<?php
$cutoffDate = '';
$cutoffTime = '';
$cutoffThreshold = (string) ($cutoffThreshold ?? '09:00');
$cutoffTzAbbr = '';
try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = new DateTimeZone($tzName ?: 'UTC');
$nowTz = new DateTime('now', $tz);
$cutoffDate = $nowTz->format('Y-m-d');
$cutoffTime = $nowTz->format('H:i');
$cutoffTzAbbr = $nowTz->format('T');
} catch (\Throwable $e) {
$cutoffDate = '';
$cutoffTime = '';
$cutoffTzAbbr = '';
}
?>
<form id="reportForm" <form id="reportForm"
method="post" method="post"
action="<?= site_url('parent/report-attendance') ?>" action="<?= site_url('parent/report-attendance') ?>"
data-csrf-name="<?= esc(csrf_token()) ?>" data-csrf-name="<?= esc(csrf_token()) ?>"
data-csrf-value="<?= esc(csrf_hash()) ?>" data-csrf-value="<?= esc(csrf_hash()) ?>"
data-check-url="<?= site_url('api/parent/report-attendance/check') ?>"> data-check-url="<?= site_url('api/parent/report-attendance/check') ?>"
data-cutoff-date="<?= esc($cutoffDate) ?>"
data-cutoff-time="<?= esc($cutoffTime) ?>"
data-cutoff-threshold="<?= esc($cutoffThreshold) ?>"
data-cutoff-blocked="0">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>"> <input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
<div id="clientCheckAlert" class="alert d-none" role="alert"></div> <div id="clientCheckAlert" class="alert d-none" role="alert"></div>
<div id="cutoffAlert" class="alert alert-warning d-none" role="alert">
Reporting closes at <?= esc($cutoffThreshold) ?><?= $cutoffTzAbbr !== '' ? ' ' . esc($cutoffTzAbbr) : '' ?> on the report date.
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Select Child(ren)</label> <label class="form-label fw-semibold">Select Child(ren)</label>
@@ -352,7 +377,10 @@
const dates = selectedDates(); const dates = selectedDates();
if (!checkUrl || !dateSel || !typeSel) return; if (!checkUrl || !dateSel || !typeSel) return;
if (!ids.length || !dates.length) { if (!ids.length || !dates.length) {
if (submitBtn) submitBtn.disabled = false; if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
return; return;
} }
@@ -395,7 +423,10 @@
} }
if (!data || !data.ok) { if (!data || !data.ok) {
if (submitBtn) submitBtn.disabled = false; if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
return; return;
} }
@@ -454,10 +485,16 @@
setAlert('', ''); setAlert('', '');
} }
if (submitBtn) submitBtn.disabled = (selectedIds().length === 0); if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked || (selectedIds().length === 0);
}
} catch (e) { } catch (e) {
setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.'); setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.');
if (submitBtn) submitBtn.disabled = false; if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
} }
} }
@@ -472,4 +509,55 @@
runCheck(); runCheck();
}); });
</script> </script>
<script>
// Cutoff enforcement helper (9:00 AM on the selected Sunday).
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('reportForm');
if (!form) return;
const dateSel = form.querySelector('select[name="dates[]"]');
const submitBtn = form.querySelector('button[type="submit"]');
const cutoffAlert = document.getElementById('cutoffAlert');
const cutoffDate = form.getAttribute('data-cutoff-date') || '';
const cutoffTime = form.getAttribute('data-cutoff-time') || '';
const cutoffThreshold = form.getAttribute('data-cutoff-threshold') || '09:00';
function parseTimeToMinutes(val) {
const parts = (val || '').split(':');
if (parts.length < 2) return null;
const hh = parseInt(parts[0], 10);
const mm = parseInt(parts[1], 10);
if (Number.isNaN(hh) || Number.isNaN(mm)) return null;
return (hh * 60) + mm;
}
function updateCutoffState() {
if (!dateSel || !cutoffDate || !cutoffTime) return;
const dates = Array.from(dateSel.selectedOptions || [])
.map(opt => opt.value)
.filter(v => v);
const nowMinutes = parseTimeToMinutes(cutoffTime);
const cutoffMinutes = parseTimeToMinutes(cutoffThreshold);
const isTodaySelected = dates.includes(cutoffDate);
const blocked = isTodaySelected && nowMinutes !== null && cutoffMinutes !== null && nowMinutes >= cutoffMinutes;
form.setAttribute('data-cutoff-blocked', blocked ? '1' : '0');
if (cutoffAlert) {
cutoffAlert.classList.toggle('d-none', !blocked);
}
if (submitBtn && blocked) {
submitBtn.disabled = true;
}
}
form.addEventListener('change', function(e) {
const tgt = e.target;
if (!tgt) return;
if (tgt.matches('select[name="dates[]"], input[name="student_ids[]"], #reportType')) {
updateCutoffState();
}
});
updateCutoffState();
});
</script>
<?= $this->endSection() ?> <?= $this->endSection() ?>
+72
View File
@@ -0,0 +1,72 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?>
<div class="container my-5">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div>
<h3 class="text-success mb-1" style="font-family: Arial, sans-serif;">Report Cards</h3>
<div class="text-muted small">
<?= esc($schoolYear ?: 'N/A') ?> <?= $semester ? '• ' . esc($semester) . ' Semester' : '' ?>
</div>
</div>
</div>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (empty($students)): ?>
<div class="alert alert-info">No students available for report cards.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Student</th>
<th>Class Section</th>
<th>Viewed</th>
<th>Signature</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<?php
$sid = (int) ($student['id'] ?? 0);
$ack = $ackMap[$sid] ?? null;
$viewedAt = $ack['viewed_at'] ?? '';
$signedAt = $ack['signed_at'] ?? '';
$signedName = $ack['signed_name'] ?? '';
?>
<tr>
<td><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td>
<td><?= esc($student['class_section_name'] ?? 'N/A') ?></td>
<td><?= $viewedAt ? esc(local_datetime($viewedAt, 'm-d-Y H:i')) : 'Not viewed' ?></td>
<td>
<?php if ($signedAt): ?>
<?= esc($signedName ?: 'Signed') ?><br>
<small class="text-muted"><?= esc(local_datetime($signedAt, 'm-d-Y H:i')) ?></small>
<?php else: ?>
<span class="text-muted">Not signed</span>
<?php endif; ?>
</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('parent/report-cards/view/' . $sid) ?>">View Report</a>
<?php if (! $signedAt): ?>
<form class="d-inline-flex align-items-center gap-2 ms-2" method="post" action="<?= base_url('parent/report-cards/sign/' . $sid) ?>">
<?= csrf_field() ?>
<input type="text" name="signed_name" class="form-control form-control-sm" placeholder="Full name" required>
<button class="btn btn-sm btn-success" type="submit">Sign</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?= $this->endSection() ?>
+30 -1
View File
@@ -1,7 +1,9 @@
<?= $this->extend('layout/main_layout') ?> <?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<div class="container my-5"> <div class="container my-5">
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Scores</h2> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<h3 class="text-success mb-0" style="font-family: Arial, sans-serif;">Scores</h3>
</div>
</div> </div>
<!-- Filter Form --> <!-- Filter Form -->
@@ -73,6 +75,7 @@ $showExamScoresBySemester = $showExamScoresBySemester ?? [];
<?php if ($showExamScoresForSemester): ?><th>Exam</th><?php endif; ?> <?php if ($showExamScoresForSemester): ?><th>Exam</th><?php endif; ?>
<?php endif; ?> <?php endif; ?>
<?php if ($showExamScoresForSemester): ?><th>Semester Score</th><?php endif; ?> <?php if ($showExamScoresForSemester): ?><th>Semester Score</th><?php endif; ?>
<?php if ($showExamScoresForSemester): ?><th>Report Card</th><?php endif; ?>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -96,6 +99,32 @@ $showExamScoresBySemester = $showExamScoresBySemester ?? [];
<?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['midterm_exam']['score'] ?? $student['scores']['final_exam']['score'] ?? '-') ?></td><?php endif; ?> <?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['midterm_exam']['score'] ?? $student['scores']['final_exam']['score'] ?? '-') ?></td><?php endif; ?>
<?php endif; ?> <?php endif; ?>
<?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['semester']['score'] ?? '-') ?></td><?php endif; ?> <?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['semester']['score'] ?? '-') ?></td><?php endif; ?>
<?php if ($showExamScoresForSemester): ?>
<td>
<?php
$reportStudentId = (int) ($student['student_id'] ?? 0);
if ($reportStudentId > 0) {
$reportYear = (string) ($selectedYear ?? '');
$reportSemester = (string) ($semester ?? '');
$reportDate = date('Y-m-d');
$reportUrl = base_url('parent/report-cards/view/' . $reportStudentId);
$query = http_build_query([
'school_year' => $reportYear,
'semester' => $reportSemester,
'report_date' => $reportDate,
]);
if ($query) {
$reportUrl .= '?' . $query;
}
}
?>
<?php if ($reportStudentId > 0): ?>
<a class="btn btn-sm btn-outline-primary" href="<?= esc($reportUrl) ?>" target="_blank" rel="noopener">View</a>
<?php else: ?>
<span class="text-muted">Unavailable</span>
<?php endif; ?>
</td>
<?php endif; ?>
</tr> </tr>
<?php if (!empty($student['comment'])): ?> <?php if (!empty($student['comment'])): ?>
<tr> <tr>
+6
View File
@@ -334,6 +334,12 @@ switch ($role) {
'label' => 'Scores', 'label' => 'Scores',
'title' => 'Review progressive academic score for the year (homeworks, projects and exams).', 'title' => 'Review progressive academic score for the year (homeworks, projects and exams).',
], ],
[
'href' => base_url('/parent/report-cards'),
'icon' => 'bi-file-earmark-text',
'label' => 'Report Cards',
'title' => 'View and acknowledge student report cards.',
],
[ [
'href' => base_url('/parent/progress'), 'href' => base_url('/parent/progress'),
'icon' => 'bi-journal-check', 'icon' => 'bi-journal-check',
+3 -5
View File
@@ -1,7 +1,7 @@
<?php <?php
/** @var string $school_year */ /** @var string $school_year */
/** @var array<string> $schoolYears */ /** @var array<string> $schoolYears */
/** @var array<int,array{parent_id:int,parent_name:string,email:string,total_invoice:float,total_balance:float,total_discount:float,total_paid:float,remaining_installments:int,installment_amount:float,type:string,has_installment?:int,next_installment?:string}> $rows */ /** @var array<int,array{parent_id:int,parent_name:string,email:string,total_invoice:float,total_balance:float,total_discount:float,total_paid:float,payment_count:int,remaining_installments:int,installment_amount:float,type:string,has_installment?:int,next_installment?:string}> $rows */
?> ?>
<?= $this->extend('layout/management_layout') ?> <?= $this->extend('layout/management_layout') ?>
@@ -15,8 +15,6 @@
.table thead th { background: var(--mgmt-thead-bg, #f1f3f5); } .table thead th { background: var(--mgmt-thead-bg, #f1f3f5); }
.actions { white-space: nowrap; } .actions { white-space: nowrap; }
.actions .btn { --bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; } .actions .btn { --bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; }
.email-cell { max-width: 280px; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 576px){ .email-cell { max-width: 180px; } }
/* Disable sticky header for this table to avoid overlap */ /* Disable sticky header for this table to avoid overlap */
table.no-mgmt-sticky thead th { position: static !important; } table.no-mgmt-sticky thead th { position: static !important; }
</style> </style>
@@ -54,7 +52,7 @@
<thead> <thead>
<tr> <tr>
<th>Parent</th> <th>Parent</th>
<th>Email</th> <th class="text-center">Nbr of Installements</th>
<th>Type</th> <th>Type</th>
<th class="text-end">Invoice Amount</th> <th class="text-end">Invoice Amount</th>
<th class="text-end">Applied Discount</th> <th class="text-end">Applied Discount</th>
@@ -86,7 +84,7 @@
</a> </a>
<small class="text-muted">#<?= (int)$r['parent_id'] ?></small> <small class="text-muted">#<?= (int)$r['parent_id'] ?></small>
</td> </td>
<td class="email-cell"><a href="mailto:<?= esc($r['email']) ?>"><?= esc($r['email']) ?></a></td> <td class="text-center"><?= (int)($r['payment_count'] ?? 0) ?></td>
<td> <td>
<?php if (($r['type'] ?? '') === 'no_payment'): ?> <?php if (($r['type'] ?? '') === 'no_payment'): ?>
<span class="badge bg-danger badge-type">no payment</span> <span class="badge bg-danger badge-type">no payment</span>
+44 -1
View File
@@ -48,6 +48,7 @@
<a id="downloadStudentBtn" href="#" class="btn btn-success external-link" target="_blank">Download</a> <a id="downloadStudentBtn" href="#" class="btn btn-success external-link" target="_blank">Download</a>
</div> </div>
</div> </div>
<div class="text-muted small text-center" id="ackStatus"></div>
</form> </form>
<div class="card shadow-sm mb-3"> <div class="card shadow-sm mb-3">
@@ -67,10 +68,12 @@
<table class="table table-sm table-bordered align-middle mb-0"> <table class="table table-sm table-bordered align-middle mb-0">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th style="width: 35%;">Student</th> <th style="width: 28%;">Student</th>
<th style="width: 15%;">Status</th> <th style="width: 15%;">Status</th>
<th>Missing</th> <th>Missing</th>
<th>Warnings</th> <th>Warnings</th>
<th style="width: 16%;">Viewed</th>
<th style="width: 16%;">Signed</th>
</tr> </tr>
</thead> </thead>
<tbody id="completenessBody"></tbody> <tbody id="completenessBody"></tbody>
@@ -91,6 +94,7 @@
document.addEventListener('DOMContentLoaded', function(){ document.addEventListener('DOMContentLoaded', function(){
const API = <?= json_encode(site_url('api/printables/report-card/meta')) ?>; const API = <?= json_encode(site_url('api/printables/report-card/meta')) ?>;
const COMPLETENESS_API = <?= json_encode(site_url('api/printables/report-card/completeness')) ?>; const COMPLETENESS_API = <?= json_encode(site_url('api/printables/report-card/completeness')) ?>;
const ACK_API = <?= json_encode(site_url('api/printables/report-card/ack')) ?>;
// add cache-busting cb param // add cache-busting cb param
const withCB = (u) => u + (u.includes('?') ? '&' : '?') + 'cb=' + Date.now(); const withCB = (u) => u + (u.includes('?') ? '&' : '?') + 'cb=' + Date.now();
@@ -119,6 +123,7 @@ document.addEventListener('DOMContentLoaded', function(){
const $completenessSummary = document.getElementById('completenessSummary'); const $completenessSummary = document.getElementById('completenessSummary');
const $completenessWrap = document.getElementById('completenessTableWrap'); const $completenessWrap = document.getElementById('completenessTableWrap');
const $completenessBody = document.getElementById('completenessBody'); const $completenessBody = document.getElementById('completenessBody');
const $ackStatus = document.getElementById('ackStatus');
function opt(el, val, label) { function opt(el, val, label) {
const o = document.createElement('option'); o.value = String(val); o.textContent = label; el.appendChild(o); const o = document.createElement('option'); o.value = String(val); o.textContent = label; el.appendChild(o);
@@ -157,10 +162,40 @@ document.addEventListener('DOMContentLoaded', function(){
// Classes // Classes
$class.innerHTML = ''; $class.innerHTML = '';
(data.classSections || []).forEach(c => opt($class, c.class_section_id || c.id, c.class_section_name)); (data.classSections || []).forEach(c => opt($class, c.class_section_id || c.id, c.class_section_name));
updateAckStatus();
}
async function updateAckStatus() {
if (! $ackStatus) return;
const sid = $student.value;
const y = $year.value || '';
const s = $sem.value || '';
if (!sid) {
$ackStatus.textContent = '';
return;
}
try {
const url = `${ACK_API}?student_id=${encodeURIComponent(sid)}&school_year=${encodeURIComponent(y)}&semester=${encodeURIComponent(s)}`;
const res = await fetch(url, { credentials: 'same-origin' });
const data = await res.json();
if (!res.ok || data?.ok === false) {
$ackStatus.textContent = 'Parent acknowledgement: unavailable';
return;
}
const viewed = data.viewed_at ? `Viewed: ${data.viewed_at}` : 'Viewed: not yet';
const signed = data.signed_at
? `Signed: ${data.signed_at} (${data.signed_name || 'name missing'})`
: 'Signed: not yet';
$ackStatus.textContent = `Parent acknowledgement — ${viewed} · ${signed}`;
} catch (err) {
$ackStatus.textContent = 'Parent acknowledgement: unavailable';
}
} }
$year.addEventListener('change', function(){ loadMeta(this.value, $sem.value).catch(console.error); }); $year.addEventListener('change', function(){ loadMeta(this.value, $sem.value).catch(console.error); });
$sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); }); $sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); });
$student.addEventListener('change', function(){ updateAckStatus(); });
$viewS.addEventListener('click', function(){ $viewS.addEventListener('click', function(){
const sid = $student.value; const y = $year.value; const s=$sem.value; const sid = $student.value; const y = $year.value; const s=$sem.value;
@@ -254,10 +289,18 @@ document.addEventListener('DOMContentLoaded', function(){
tdMissing.textContent = missing.length ? missing.join(', ') : 'None'; tdMissing.textContent = missing.length ? missing.join(', ') : 'None';
const tdWarnings = document.createElement('td'); const tdWarnings = document.createElement('td');
tdWarnings.textContent = warningsList.length ? warningsList.join(', ') : 'None'; tdWarnings.textContent = warningsList.length ? warningsList.join(', ') : 'None';
const tdViewed = document.createElement('td');
tdViewed.textContent = row.viewed_at ? row.viewed_at : 'Not viewed';
const tdSigned = document.createElement('td');
tdSigned.textContent = row.signed_at
? `${row.signed_at}${row.signed_name ? ` (${row.signed_name})` : ''}`
: 'Not signed';
tr.appendChild(tdName); tr.appendChild(tdName);
tr.appendChild(tdStatus); tr.appendChild(tdStatus);
tr.appendChild(tdMissing); tr.appendChild(tdMissing);
tr.appendChild(tdWarnings); tr.appendChild(tdWarnings);
tr.appendChild(tdViewed);
tr.appendChild(tdSigned);
$completenessBody.appendChild(tr); $completenessBody.appendChild(tr);
}); });
$completenessWrap.hidden = false; $completenessWrap.hidden = false;
+6 -1
View File
@@ -3,7 +3,7 @@
<div class="container py-4"> <div class="container py-4">
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<div> <div>
<h3 class="mb-0">My Progress Reports</h3> <h3 class="mb-0">Class Progress Reports</h3>
<div class="text-muted">Review weekly submissions and compare Islamic Studies with Quran/Arabic.</div> <div class="text-muted">Review weekly submissions and compare Islamic Studies with Quran/Arabic.</div>
</div> </div>
<a href="<?= base_url('teacher/progress/submit') ?>" class="btn btn-outline-secondary">Submit New Report</a> <a href="<?= base_url('teacher/progress/submit') ?>" class="btn btn-outline-secondary">Submit New Report</a>
@@ -78,6 +78,11 @@
<div class="small text-muted"> <div class="small text-muted">
<?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?> <?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?>
</div> </div>
<?php if ($report): ?>
<div class="small text-muted">
Submitted by: <?= esc(trim((string) ($report['teacher_name'] ?? '')) ?: 'Unknown') ?>
</div>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
+19 -4
View File
@@ -175,10 +175,13 @@
<?php for ($i = 0; $i < $rowsCount; $i++): ?> <?php for ($i = 0; $i < $rowsCount; $i++): ?>
<div class="row g-2 align-items-end mb-2 unit-chapter-entry"> <div class="row g-2 align-items-end mb-2 unit-chapter-entry">
<div class="col"> <div class="col">
<input type="text" name="unit_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Unit name" value="<?= esc($unitValues[$i] ?? '') ?>"> <input type="text" name="unit_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Unit name" value="<?= esc($unitValues[$i] ?? '') ?>" readonly>
</div> </div>
<div class="col"> <div class="col">
<input type="text" name="chapter_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Chapter" value="<?= esc($chapterValues[$i] ?? '') ?>"> <input type="text" name="chapter_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Chapter" value="<?= esc($chapterValues[$i] ?? '') ?>" readonly>
</div>
<div class="col-auto">
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-unit-chapter aria-label="Remove entry">X</button>
</div> </div>
</div> </div>
<?php endfor; ?> <?php endfor; ?>
@@ -263,10 +266,13 @@
row.className = 'row g-2 align-items-end mb-2 unit-chapter-entry'; row.className = 'row g-2 align-items-end mb-2 unit-chapter-entry';
row.innerHTML = ` row.innerHTML = `
<div class="col"> <div class="col">
<input type="text" name="unit_${slug}[]" class="form-control form-control-sm" placeholder="Unit name" /> <input type="text" name="unit_${slug}[]" class="form-control form-control-sm" placeholder="Unit name" readonly />
</div> </div>
<div class="col"> <div class="col">
<input type="text" name="chapter_${slug}[]" class="form-control form-control-sm" placeholder="Chapter" /> <input type="text" name="chapter_${slug}[]" class="form-control form-control-sm" placeholder="Chapter" readonly />
</div>
<div class="col-auto">
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-unit-chapter aria-label="Remove entry">X</button>
</div> </div>
`; `;
const inputs = row.querySelectorAll('input'); const inputs = row.querySelectorAll('input');
@@ -338,6 +344,15 @@
}); });
}); });
document.addEventListener('click', event => {
const button = event.target.closest('[data-remove-unit-chapter]');
if (!button) return;
const row = button.closest('.unit-chapter-entry');
if (row) {
row.remove();
}
});
document.querySelectorAll('[data-custom-entry]').forEach(button => { document.querySelectorAll('[data-custom-entry]').forEach(button => {
button.addEventListener('click', event => { button.addEventListener('click', event => {
event.stopPropagation(); event.stopPropagation();
@@ -37,6 +37,7 @@
<?php if (! $report): ?> <?php if (! $report): ?>
<div class="text-muted">No entry submitted for this subject this week.</div> <div class="text-muted">No entry submitted for this subject this week.</div>
<?php else: ?> <?php else: ?>
<div class="mb-2"><strong>Submitted by:</strong> <?= esc(trim((string) ($report['teacher_name'] ?? '')) ?: 'Unknown') ?></div>
<div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div> <div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div>
<div class="mb-3"><strong>Activities</strong><div class="mt-1"><?= nl2br(esc($report['covered'] ?? '-')) ?></div></div> <div class="mb-3"><strong>Activities</strong><div class="mt-1"><?= nl2br(esc($report['covered'] ?? '-')) ?></div></div>
<div class="mb-3"> <div class="mb-3">
+253 -258
View File
@@ -1,259 +1,254 @@
Grade,Unit,Unit Title,chapter Grade,Unit,Unit Title,chapter
1,1,Aqaid: Our Belief,Allah: Our Creator 1,1,Aqaid: Our Belief,1. Allah: Our Creator
1,1,Aqaid: Our Belief,Islam 1,1,Aqaid: Our Belief,2. Islam
1,1,Aqaid: Our Belief,Our Faith 1,1,Aqaid: Our Belief,3. Our Faith
1,1,Aqaid: Our Belief,Nabi Muhammad 1,1,Aqaid: Our Belief,4. Nabi Muhammad (s)
1,1,Aqaid: Our Belief,The Quran 1,1,Aqaid: Our Belief,5. The Quran
1,2,Knowing Allah,Allah Loves Us 1,2,Knowing Allah,6. Allah Loves Us
1,2,Knowing Allah,Remembering Allah 1,2,Knowing Allah,7. Remembering Allah
1,2,Knowing Allah,Allah Rewards Us 1,2,Knowing Allah,8. Allah Rewards Us
1,3,Our Ibadat,Five Pillars of Islam 1,3,Our Ibadat,9. Five Pillars of Islam
1,3,Our Ibadat,Shahadah: The First Pillar 1,3,Our Ibadat,10. Shahadah: The First Pillar
1,3,Our Ibadat,Salah: The Second Pillar 1,3,Our Ibadat,11. Salat: The Second Pillar
1,3,Our Ibadat,Zakat: The Third Pillar 1,3,Our Ibadat,12. Zakat: The Third Pillar
1,3,Our Ibadat,Fasting: The Fourth Pillar 1,3,Our Ibadat,13. Fasting: The Fourth Pillar
1,3,Our Ibadat,Hajj: The Fifth Pillar 1,3,Our Ibadat,14. Hajj: The Fifth Pillar
1,4,Messengers of Allah,Adam (A): The First Nabi 1,4,Messengers of Allah,15. Adam (A): The First Nabi
1,4,Messengers of Allah,Nuh (A): Saved From Flood 1,4,Messengers of Allah,16. Nuh (A): Saved From the Great Flood
1,4,Messengers of Allah,Ibrahim (A): Never Listen to Shaitan 1,4,Messengers of Allah,17. Ibrahim (A): Never Listen to Shaitan
1,4,Messengers of Allah,Musa (A): Challenging A Bad Ruler 1,4,Messengers of Allah,18. Musa (A): Challenging a Bad Ruler
1,4,Messengers of Allah,Isa (A): A Great Nabi of Allah 1,4,Messengers of Allah,19. Isa (A): A Great Nabi of Allah
1,5,Other Basics of Islam,Angels: They Always Work for Allah 1,5,Other Basics of Islam,20. Angels: They Always Work for Allah
1,5,Other Basics of Islam,Shaitan: Our Enemy 1,5,Other Basics of Islam,21. Shaitan: Our Enemy
1,5,Other Basics of Islam,Makkah and Madinah 1,5,Other Basics of Islam,22. Makkah and Madinah
1,5,Other Basics of Islam,Eid: Two Festivals 1,5,Other Basics of Islam,23. Eid: Two Festivals
1,6,Akhlaq and Adab in Islam,Good Manners 1,6,Akhlaq and Adab in Islam,24. Good Manners
1,6,Akhlaq and Adab in Islam,Kindness and Sharing 1,6,Akhlaq and Adab in Islam,25. Kindness and Sharing
1,6,Akhlaq and Adab in Islam,Respect 1,6,Akhlaq and Adab in Islam,26. Respect
1,6,Akhlaq and Adab in Islam,Forgiveness 1,6,Akhlaq and Adab in Islam,27. Forgiveness
1,6,Akhlaq and Adab in Islam,Thanking Allah 1,6,Akhlaq and Adab in Islam,28. Thanking Allah
2,1,The CreatorHis Message,Allah: Our Creator 2,1,The Creator and His Message,1. Allah: Our Creator
2,1,The CreatorHis Message,How Does Allah Create? 2,1,The Creator and His Message,2. How Does Allah Create?
2,1,The CreatorHis Message,Allahﷻ: What Does He Do? 2,1,The Creator and His Message,3. What Does Allah Do?
2,1,The CreatorHis Message,What Does Allahﷻ Not Do 2,1,The Creator and His Message,4, Allah: What Does He Not Do
2,1,The CreatorHis Message,The Quran 2,1,The Creator and His Message,5. The Quran
2,1,The CreatorHis Message,Hadith and Sunnah 2,1,The Creator and His Message,6. Hadith and Sunnah
2,2,Our Ibadat,Shahadah: The First Pillar 2,2,Our Ibadat,7. Shahadah: The First Pillar
2,2,Our Ibadat,Salah: The Second Pillar 2,2,Our Ibadat,8. Salat: The Second Pillar
2,2,Our Ibadat,Zakah: The Third Pillar 2,2,Our Ibadat,9. Zakah: The Third Pillar
2,2,Our Ibadat,Sawm: The Fourth Pillar 2,2,Our Ibadat,10. Sawm: The Fourth Pillar
2,2,Our Ibadat,Hajj: The Fifth Pillar 2,2,Our Ibadat,11. Hajj: The Fifth Pillar
2,2,Our Ibadat,Wudu: Keeping Our Bodies Clean 2,2,Our Ibadat,12. Wudu: Cleaning Before Salat
2,3,Messengers of Allah,Ibrahim (A): A Friend of Allah 2,3,The Messengers of Allah,13. Ibrahim (A): A Friend of Allah
2,3,Messengers of Allah,Yaqub (A) and Yusuf (A) 2,3,The Messengers of Allah,14. Yaqub (A) and Yusuf (A)
2,3,Messengers of Allah,Musa (A) and Harun (A) 2,3,The Messengers of Allah,15. Musa (A) and Harun (A)
2,3,Messengers of Allah,Yunus (A) 2,3,The Messengers of Allah,16. Yunus (A)
2,3,Messengers of Allah,Nabi Muhammadﷺ 2,3,The Messengers of Allah,17. Nabi Muhammad
2,4,Learning About Islam,Obey Allah Obey Rasulﷺ 2,4,Learning About Islam,18. Obey Allah, Obey Rasul
2,4,Learning About Islam,Day of Judgment 2,4,Learning About Islam,19. Day of Judgment
2,4,Learning About Islam,Our Masjid 2,4,Learning About Islam,20. Our Masjid
2,4,Learning About Islam,Islamic Phrases 2,4,Learning About Islam,21. Islamic Phrases
2,4,Learning About Islam,Food that We May Eat 2,4,Learning About Islam,22. Food that We May Eat
2,5,Akhlaq and Adab in Islam,Truthfulness 2,5,Akhlaq and Adab in Islam,23. Truthfulness
2,5,Akhlaq and Adab in Islam,Kindness 2,5,Akhlaq and Adab in Islam,24. Kindness
2,5,Akhlaq and Adab in Islam,Respect 2,5,Akhlaq and Adab in Islam,25. Respect
2,5,Akhlaq and Adab in Islam,Responsibility 2,5,Akhlaq and Adab in Islam,26. Responsibility
2,5,Akhlaq and Adab in Islam,Obedience 2,5,Akhlaq and Adab in Islam,27. Obedience
2,5,Akhlaq and Adab in Islam,Cleanliness 2,5,Akhlaq and Adab in Islam,28. Cleanliness
2,5,Akhlaq and Adab in Islam,Honesty 2,5,Akhlaq and Adab in Islam,29. Honesty
3,1,Knowing About Allah,What Does Allahﷻ Do? 3,1,Knowing About Allāh ﷺ,1. Who Is Allāh ﷺ?
3,1,Knowing About Allah,What Allahﷻ Is and Is Not 3,1,Knowing About Allāh ﷺ,2. What Allāh ﷺ Is and Is Not
3,1,Knowing About Allah,Allahﷻ: The Most-Merciful 3,1,Knowing About Allāh ﷺ,3. Allāh ﷺ: The Most-Merciful, Most-Rewarding
3,1,Knowing About Allah,Allahﷻ: The Best Judge 3,1,Knowing About Allāh ﷺ,4. Allāh ﷺ: The Best Judge
3,2,What Islam Says,We Are Muslims: We Have Iman 3,1,Knowing About Allāh ﷺ,5. What Does Allāh ﷺ Want Us to Do?
3,2,What Islam Says,What Does Allahﷻ Want Us to Do? 3,2,Teachings of Islam,6. We Are Muslims: We Have ‘Īmān
3,2,What Islam Says,Hadith 3,2,Teachings of Islam,7. Belief in the Qur’ān
3,2,What Islam Says,Jinn 3,2,Teachings of Islam,8. Belief in the Messengers
3,2,What Islam Says,Muslims in North America 3,2,Teachings of Islam,9. Hadīth and Sunnah
3,2,What Islam Says,The Right Path: The Straight Path 3,2,Teachings of Islam,10. Jinn
3,3,Why Do We Worship,Shahadah: Allahﷻ is One 3,2,Teachings of Islam,11. Muslims in North America
3,3,Why Do We Worship,Types of Salat 3,2,Teachings of Islam,12. The Straight Path: The Right Path
3,3,Why Do We Worship,Why We Make Salat 3,3,Nabi Muhammad ﷺ,13. Kindness of Rasūlullāh ﷺ
3,3,Why Do We Worship,Why Do We pay Zakat? 3,3,Nabi Muhammad ﷺ,14. How Rasūlullāh ﷺ Treated Others
3,3,Why Do We Worship,Why Do We Fast? 3,3,Nabi Muhammad ﷺ,15. Our Relationship With Rasūlullāh ﷺ
3,3,Why Do We Worship,Why Do We Go for Hajj? 3,4,Messengers of Allāh ﷺ,16. Ismā‘īl (A) and Ishāq (A): Nabi of Allāh ﷺ
3,4,Life of Nabi Muhammadﷺ,The Nabiﷺ in Makkah 3,4,Messengers of Allāh ﷺ,17. Shuaib (A): A Nabi of Allāh ﷺ
3,4,Life of Nabi Muhammadﷺ,The Nabiﷺ in Madinah 3,4,Messengers of Allāh ﷺ,18. Dāwūd (A): A Nabi of Allāh ﷺ
3,4,Life of Nabi Muhammadﷺ,How Rasulullahﷺ Treated Others 3,4,Messengers of Allāh ﷺ,19. ‘Īsā (A): A Nabi of Allāh ﷺ
3,5,Messengers of Allah,Ismail (A) and Ishaq (A) 3,5,Learning About Islam,20. The Kabah
3,5,Messengers of Allah,Dawud (A): A Nabi of Allahﷻ 3,5,Learning About Islam,21. Masjid an-Nabawī: The Nabis Masjid
3,5,Messengers of Allah,Isa (A): A Nabi of Allahﷻ 3,5,Learning About Islam,22. Bilāl ibn Rabāh
3,6,Akhlaq and Adab in Islam,Being Kind: A Virtue of the Believers 3,5,Learning About Islam,23. Zaid ibn Hārithah
3,6,Akhlaq and Adab in Islam,Forgiveness: A Good Quality 3,6,Akhlaq and Adab in Islam,24. Ways To Be a Good Person
3,6,Akhlaq and Adab in Islam,Good Deeds: A Duty of the Believers 3,6,Akhlaq and Adab in Islam,25. Kindness: A Virtue of the Believers
3,6,Akhlaq and Adab in Islam,Cleanliness: A Quality of Believers 3,6,Akhlaq and Adab in Islam,26. Forgiveness: A Quality of the Believers
3,6,Akhlaq and Adab in Islam,A Muslim Family 3,6,Akhlaq and Adab in Islam,27. Good Deeds: A Duty of the Believers
3,6,Akhlaq and Adab in Islam,Perseverance: Never Give Up 3,6,Akhlaq and Adab in Islam,28. Perseverance: Never Give Up
3,6,Akhlaq and Adab in Islam,Punctuality: Doing Things on Time 3,6,Akhlaq and Adab in Islam,29. Punctuality: Doing Things on Time
4,1,Knowing the Creator,Rewards of Allah: Everybody Receives Them 4,1,Knowing the Creator,1. Rewards of Allah: Everybody Receives Them
4,1,Knowing the Creator,Discipline of Allah 4,1,Knowing the Creator,1. Discipline of Allah: Because He Loves Us
4,1,Knowing the Creator,Names of Allah 4,1,Knowing the Creator,3. Names of Allah
4,1,Knowing the Creator,Books of Allah 4,1,Knowing the Creator,4. Books of Allah
4,2,How Islam Changed Arabia,Pre-Islamic Arabia 4,2,How Islam Changed Arabia,5. Pre-Islamic Arabia: Age of Ignorance
4,2,How Islam Changed Arabia,The Year of the Elephant 4,2,How Islam Changed Arabia,6. The Year of the Elephant
4,2,How Islam Changed Arabia,Early Life of Muhammadﷺ 4,2,How Islam Changed Arabia,7. Early Life of Muhammad
4,2,How Islam Changed Arabia,Life Before Becoming a Nabi 4,2,How Islam Changed Arabia,8. Life Before Becoming a Nabi
4,2,How Islam Changed Arabia,First Revelation 4,2,How Islam Changed Arabia,9. First Revelation
4,2,How Islam Changed Arabia,Makkah Period 4,2,How Islam Changed Arabia,10. Makkah Period: The Early Years of the Muslims
4,2,How Islam Changed Arabia,Hijrat to Madinah 4,2,How Islam Changed Arabia,11. Hijrat to Madinah: The Migration that Shaped History
4,2,How Islam Changed Arabia,Madinah Period 4,2,How Islam Changed Arabia,12. Madinah Period: Islam Prospers
4,3,The Rightly Guided Khalifah,Abu Bakr: The First Khalifah 4,3,The Rightly Guided Khalifah,13. Abū Bakr (R): The First Khalifah
4,3,The Rightly Guided Khalifah,Umar ibn al-Khattab 4,3,The Rightly Guided Khalifah,14. Umar al-Khaṭṭāb (R): The Second Khalifah
4,3,The Rightly Guided Khalifah,Uthman ibn Affan 4,3,The Rightly Guided Khalifah,15. Uthman Ibn Affān (R): The Third Khalifah
4,3,The Rightly Guided Khalifah,Ali ibn Abu Talib 4,3,The Rightly Guided Khalifah,16. Ali Ibn Abu Ṭālib (R): The Fourth Khalifah
4,4,The Messengers of Allah,Hud (A): Struggle to Guide People 4,4,Messengers of Allah,17. Hūd (A): Struggle to Guide Mankind
4,4,The Messengers of Allah,Salih (A): To Guide the Misguided 4,4,Messengers of Allah,18. Ṣāli (A): Struggle to Guide the Misguided
4,4,The Messengers of Allah,Musa (A): His Life and Actions 4,4,Messengers of Allah,19. Mūsā (A): His Life and Achievements
4,4,The Messengers of Allah,Sulaiman (A): A Humble King 4,4,Messengers of Allah,20. Sulaimān (A): A King and a Servant of Allah ﷺ
4,5,Fiqh of Salat,Preparation for Salat 4,5,Fiqh of Salat,21. Preparation for Salat
4,5,Fiqh of Salat,Requirements of Salat 4,5,Fiqh of Salat,22. The Requirements of Salat
4,5,Fiqh of Salat,Mubtilat us-Salat 4,5,Fiqh of Salat,23. Mubilāt-us-Salāt: Things that Invalidate Salāt
4,5,Fiqh of Salat,How to Pray Behind an Imam 4,5,Fiqh of Salat,24. How to Pray Behind an Imām
4,6,General Islamic Topics,Compilers of Hadith 4,6,General Islamic Topics,25. Compilers of Hadīth
4,6,General Islamic Topics,Shaitans Mode of Operation 4,6,General Islamic Topics,26. Shaitans Mode of Operation
4,6,General Islamic Topics,Day of Judgment 4,6,General Islamic Topics,27. Day of Judgment: The Day of Ultimate Justice
4,6,General Islamic Topics,Eid: Its Significance 4,6,General Islamic Topics,28. Eid: Significance of the Festivities
4,6,General Islamic Topics,Truthfulness: A Quality of Muslim 4,6,General Islamic Topics,29. Truthfulness: An Important Quality for Muslims
4,6,General Islamic Topics,Perseverance: Keep on Trying 4,6,General Islamic Topics,30. Perseverance: Keep on Trying
5,1,"The Creator, His Message","Tawhid, Kafir, Kufr, Shirk, Nifaq" 5,1,The Creator,1. His Message, and His Messengers,Tawhid, Kafir, Kufr, Shirk, Nifaq
5,1,"The Creator, His Message",Why Should We Worship Allah? 5,1,The Creator,2. His Message, and His Messengers,Why Should We Worship Allah?
5,1,"The Creator, His Message",Revelation of the Quran 5,1,The Creator,3. His Message, and His Messengers,The Revelation of the Quran
5,1,"The Creator, His Message",Characteristics of the Messengers 5,1,The Creator,4. His Message, and His Messengers,Characteristics of the Messengers
5,2,"The Battles, Developments",Pledges of Aqabah 5,2,The Battles and Other Developments,5. Pledges of Aqabah: Invitation to Migrate
5,2,"The Battles, Developments",The Battle of Badr 5,2,The Battles and Other Developments,6. The Battle of Badr: Allah Supports the Righteous
5,2,"The Battles, Developments",The Battle of Uhud 5,2,The Battles and Other Developments,7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
5,2,"The Battles, Developments",The Battle of the Trench 5,2,The Battles and Other Developments,8. The Battle of the Trench: A Bloodless Battle
5,2,"The Battles, Developments",The Treaty of Hudaibiyah 5,2,The Battles and Other Developments,9. The Treaty of Hudaibiyah: A Clear Victory
5,2,"The Battles, Developments",Liberation of Makkah 5,2,The Battles and Other Developments,10. Liberation of Makkah: A Bloodless Victory
5,3,The Messengers of Allah,Adam (A): The Creation of Mankind 5,3,Stories of the Messengers of Allah,11. Adam (A): The Creation of Human Beings
5,3,The Messengers of Allah,Ibrahim (A) Debate with Polytheists 5,3,Stories of the Messengers of Allah,12. Ibrahim (A): His Debate with the Polytheists
5,3,The Messengers of Allah,Ibrahim (A): Plan Against Idols 5,3,Stories of the Messengers of Allah,13. Ibrahim (A): His Plan Against the Idols
5,3,The Messengers of Allah,Luqman (A): A Wise Mans Lifelong Teachings 5,3,Stories of the Messengers of Allah,14. Luqmān (A): A Wise Mans Lifelong Advice
5,3,The Messengers of Allah,Yusuf (A): His Childhood 5,3,Stories of the Messengers of Allah,15. Yūsuf (A): His Childhood and Life in Azizs Home
5,3,The Messengers of Allah,Yusuf (A): His Righteousness 5,3,Stories of the Messengers of Allah,16. Yūsuf (A): Standing Up for Righteousness
5,3,The Messengers of Allah,Yusuf (A): Dream Comes True 5,3,Stories of the Messengers of Allah,17. Yūsuf (A): A Childhood Dream Comes True
5,3,The Messengers of Allah,"Ayyub (A): Patience, Perseverance" 5,4,Islam in The World,20. Major Masājid in the World
5,3,The Messengers of Allah,"Zakariyyah (A), Yahya (A)" 5,5,Islamic Values and Teachings,21. Upholding Truth: A Duty of All Believers
5,4,Islam in the World,Major Masajid in the World 5,5,Islamic Values and Teachings,22. Responsibility and Punctuality
5,5,"Islamic Values, Teachings",Upholding Truth: A Duty for All Believers 5,5,Islamic Values and Teachings,23. My Mind, My Body: The Body is a Mirror of the Mind
5,5,"Islamic Values, Teachings",Responsibility and Punctuality 5,5,Islamic Values and Teachings,24. Kindness and Forgiveness
5,5,"Islamic Values, Teachings",My Mind My Body 5,5,Islamic Values and Teachings,25. The Middle Path: Ways to Avoid the Two Extremes
5,5,"Islamic Values, Teachings",Kindness and Forgiveness 5,5,Islamic Values and Teachings,26. Salat: Its Significance
5,5,"Islamic Values, Teachings",The Middle Path: Ways to Avoid Two Extremes 5,5,Islamic Values and Teachings,27. Sawm: Its Significance
5,5,"Islamic Values, Teachings",Salat: Its Significance 5,5,Islamic Values and Teachings,28. Zakat and Sadaqah: Similarities and Differences
5,5,"Islamic Values, Teachings",Sawm: Its Significance 6,1,The Creator,1. His Message, and His Messengers,Tawhid, Kafir, Kufr, Shirk, Nifaq
5,5,"Islamic Values, Teachings",Zakat and Sadaqah: Similarities and Differences 6,1,The Creator,2. His Message, and His Messengers,Why Should We Worship Allah?
6,1,The CreatorHis Message,Attributes of Allahﷻ 6,1,The Creator,3. His Message, and His Messengers,The Revelation of the Quran
6,1,The CreatorHis Message,The Promise of Allahﷻ 6,1,The Creator,4. His Message, and His Messengers,Characteristics of the Messengers
6,2,The Quran and Hadith,Objectives of the Quran? 6,2,The Battles and Other Developments,5. Pledges of Aqabah: Invitation to Migrate
6,2,The Quran and Hadith,Compilation of the Quran 6,2,The Battles and Other Developments,6. The Battle of Badr: Allah Supports the Righteous
6,2,The Quran and Hadith,Previous Scriptures and the Quran 6,2,The Battles and Other Developments,7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
6,2,The Quran and Hadith,Compilation of Hadith 6,2,The Battles and Other Developments,8. The Battle of the Trench: A Bloodless Battle
6,3,Fundamentals of Deen,Importance of Shahadah 6,2,The Battles and Other Developments,9. The Treaty of Hudaibiyah: A Clear Victory
6,3,Fundamentals of Deen,Khushu in Salat 6,2,The Battles and Other Developments,10. Liberation of Makkah: A Bloodless Victory
6,3,Fundamentals of Deen,Taqwa: A Quality of Believers 6,3,Stories of the Messengers of Allah,11. Adam (A): The Creation of Human Beings
6,4,Messengers of Allah,Nuh (A) 6,3,Stories of the Messengers of Allah,12. Ibrahim (A): His Debate with the Polytheists
6,4,Messengers of Allah,"Talut, Jalut, and Dawud (A)" 6,3,Stories of the Messengers of Allah,13. Ibrahim (A): His Plan Against the Idols
6,4,Messengers of Allah,Dawud (A) and Sulaiman (A) 6,3,Stories of the Messengers of Allah,14. Luqmān (A): A Wise Mans Lifelong Advice
6,4,Messengers of Allah,Musa (A) and Firawn 6,3,Stories of the Messengers of Allah,15. Yūsuf (A): His Childhood and Life in Azizs Home
6,4,Messengers of Allah,Musa (A) and Khidir 6,3,Stories of the Messengers of Allah,16. Yūsuf (A): Standing Up for Righteousness
6,4,Messengers of Allah,Isa (A) and Maryam (ra) 6,3,Stories of the Messengers of Allah,17. Yūsuf (A): A Childhood Dream Comes True
6,5,Some Prominent Muslimah,Khadijah (ra) 6,4,Islam in The World,20. Major Masājid in the World
6,5,Some Prominent Muslimah,Aishah (ra) 6,5,Islamic Values and Teachings,21. Upholding Truth: A Duty of All Believers
6,5,Some Prominent Muslimah,Fatimah (ra) 6,5,Islamic Values and Teachings,22. Responsibility and Punctuality
6,5,Some Prominent Muslimah,Some Prominent Muslimahs 6,5,Islamic Values and Teachings,23. My Mind, My Body: The Body is a Mirror of the Mind
6,6,Knowledge Enrichment,Al-Qiyamah: The Awakening 6,5,Islamic Values and Teachings,24. Kindness and Forgiveness
6,6,Knowledge Enrichment,Ruh and Nafs: An Overview 6,5,Islamic Values and Teachings,25. The Middle Path: Ways to Avoid the Two Extremes
6,6,Knowledge Enrichment,Angels and Jinn: An Overview 6,5,Islamic Values and Teachings,26. Salat: Its Significance
6,6,Knowledge Enrichment,Shaitan: The Invisible Enemy 6,5,Islamic Values and Teachings,27. Sawm: Its Significance
6,7,The Current Society,My Friend Is Muslim Now 6,5,Islamic Values and Teachings,28. Zakat and Sadaqah: Similarities and Differences
6,7,The Current Society,Friendship 7,1,The Creator,1. Why Islam? What is Islam?
6,7,The Current Society,Muslims Around the World 7,1,The Creator,2. Belief in Allah
6,7,The Current Society,People of Other Faith 7,1,The Creator,3. The Quran: Its Qualitative Names
6,8,Developing Islamic Values,Greed and Dishonesty 7,1,The Creator,4. Istighfār: Seeking Forgiveness and Protection
6,8,Developing Islamic Values,Avoiding Extravagance 7,1,The Creator,5. Allah: Angry or Kind?
7,1,The Creator,Why Islam? what is Islam? 7,2,Stories of the Messengers,6. Ādam (A): The Trial of the First Messenger
7,1,The Creator,Belief in Allahﷻ 7,2,Stories of the Messengers,7. The Life of Ibrāhīm (A): Beginning a Nation
7,1,The Creator,The Quran: Its Qualitative Names 7,2,Stories of the Messengers,8. The Sacrifice of Ibrāhīm (A)
7,1,The Creator,Istighfar: Seeking Forgiveness of Allahﷻ 7,2,Stories of the Messengers,9. Lūt (A): A Message for Modern Societies
7,1,The Creator,Allahﷻ: Angry or Kind 7,2,Stories of the Messengers,10. Yūsuf (A): The Will to Overcome Temptation
7,2,Stories of the Messengers,Adam (A): Trial of the Messenger 7,3,Stories from the Quran,11. The Companions of the Cave
7,2,Stories of the Messengers,Life of Ibrahim (A) 7,3,Stories from the Quran,12. Dhu al-Qarnain: The Journey of a King
7,2,Stories of the Messengers,Sacrifice of Ibrahim (A) 7,3,Stories from the Quran,13. Effective Debate and Negotiation Styles in the Quran
7,2,Stories of the Messengers,Lut (A): Message for Modern Societies 7,4,Two Companions Who Shaped Islam,14. Abū Sufyān: His Life and Achievements
7,2,Stories of the Messengers,Yusuf (A)—The Will to Overcome Temptation 7,4,Two Companions Who Shaped Islam,15. Khālid Ibn al-Walīd: The “Sword of Allah”
7,3,Stories from the Quran,The Companions of the Cave 7,5,Knowledge Enrichment,16. Character of the Messengers
7,3,Stories from the Quran,Dhul Qurnain: Journey of a King 7,5,Knowledge Enrichment,17. Rasūlullāhs Marriages
7,3,Stories from the Quran,Effective Debate and Negotiation Styles in the Quran 7,5,Knowledge Enrichment,18. Lailatul Qadr: The Night of Majesty
7,4,Two Companions,Abu Sufyan 7,5,Knowledge Enrichment,19. Fasting During Ramadan: The Month of Benefits
7,4,Two Companions,Khalid Ibn Walid (R) 7,5,Knowledge Enrichment,20. My Family is Muslim Now
7,5,Knowledge Enrichment,The character of the Messengers 7,5,Knowledge Enrichment,21. Science in the Quran
7,5,Knowledge Enrichment,Rasulullahﷺ Marriages 7,5,Knowledge Enrichment,22. Lessons From Past Civilizations
7,5,Knowledge Enrichment,Lailatul Qadr 7,6,Akhlaq and Adab in Islam,23. Amr Bil Marūf: Enjoin Good Deeds
7,5,Knowledge Enrichment,Fasting During Ramadan 7,6,Akhlaq and Adab in Islam,24. Guard Your Tongue: Think Before You Speak
7,5,Knowledge Enrichment,My Family is Muslim Now 7,6,Akhlaq and Adab in Islam,25. Islamic Greeting: Wishing Peace
7,5,Knowledge Enrichment,Science in the Quran 7,6,Akhlaq and Adab in Islam,26. How to Achieve Success
7,5,Knowledge Enrichment,Lessons from Past Civilizations 7,6,Akhlaq and Adab in Islam,27. Permitted and Prohibited
7,6,Teachings of the Quran,Amr Bil Maruf 7,6,Akhlaq and Adab in Islam,28. Types of Behavior Allah Loves
7,6,Teachings of the Quran,Guard Your Tongue 8,1,Knowing the Creator,1. Divine Names
7,6,Teachings of the Quran,Islamic Greetings 8,1,Knowing the Creator,2. Sunan of Allah
7,6,Teachings of the Quran,How to Achieve Success 8,1,Knowing the Creator,3. Objectives of the Quran
7,6,Teachings of the Quran,Permitted and Prohibited 8,1,Knowing the Creator,4. Lessons from Sūrah al-Hujurāt
7,6,Teachings of the Quran,Types of Behavior Allahﷻ Loves 8,1,Knowing the Creator,5. True Piety: A Synthesis of Belief, Practice, and Conduct
8,1,Knowing the Creator,Divine Names 8,1,Knowing the Creator,6. Āyatul Kursi: The Throne Verse
8,1,Knowing the Creator,Sunan of Allahﷻ 8,2,Knowing the Messenger ﷺ,7. The Person Muhammad ﷺ
8,1,Knowing the Creator,Objectives of the Quran 8,2,Knowing the Messenger ﷺ,8. Farewell Pilgrimage
8,1,Knowing the Creator,Surah Hujurat: Its Teachings 8,2,Knowing the Messenger ﷺ,9. Finality of Prophethood
8,1,Knowing the Creator,True Piety: Analysis of Verse 2:177 8,2,Knowing the Messenger ﷺ,10. Hadith: Collection and Classification
8,1,Knowing the Creator,Ayatul Kursi 8,3,Challenges in Madinah,11. Hypocrites
8,2,Knowing the Messengerﷺ,The Person Muhammadﷺ 8,3,Challenges in Madinah,12. Banu Qaynuqa: Threat Within Madinah
8,2,Knowing the Messengerﷺ,Farewell Pilgrimage 8,3,Challenges in Madinah,13. Banu Nadir: Treachery Within Madinah
8,2,Knowing the Messengerﷺ,Finality of Prophethood 8,3,Challenges in Madinah,14. Banu Qurayzah
8,2,Knowing the Messengerﷺ,"Hadith: Collection, Classification" 8,3,Challenges in Madinah,15. Mission to Tabūk: A Test of Steadfastness
8,3,Challenges in Madinah,Hypocrites 8,4,Islamic Ethical Framework,16. Friends and Friendship: Who is a Good Friend?
8,3,Challenges in Madinah,Banu Qaynuqa 8,4,Islamic Ethical Framework,17. Friendship With Non-Muslims
8,3,Challenges in Madinah,Banu Nadir 8,4,Islamic Ethical Framework,18. Dating: How Islam Views the Practice
8,3,Challenges in Madinah,Banu Qurayzah 8,4,Islamic Ethical Framework,19. Hold Firmly the Rope of Allah
8,3,Challenges in Madinah,Mission to Tabuk 8,4,Islamic Ethical Framework,20. Elements of a Bad Life
8,4,Islamic Ethical Framework,Friends and Friendship 8,5,Islamic Values and Teachings,21. Duties Towards Parents
8,4,Islamic Ethical Framework,Friendship With Non-Muslims 8,5,Islamic Values and Teachings,22. Hope, Hopefulness, Hopelessness
8,4,Islamic Ethical Framework,Dating in Islam 8,5,Islamic Values and Teachings,23. Trials in Life: Everyone Will Experience Them
8,4,Islamic Ethical Framework,Hold Firmly the Rope of Allah 8,5,Islamic Values and Teachings,24. Permitted and Prohibited Food
8,4,Islamic Ethical Framework,Elements of Bad Life 8,5,Islamic Values and Teachings,25. Performance of Hajj
8,5,"Islamic Values, Teachings",Duties Toward Parents 8,5,Islamic Values and Teachings,26. Parables in the Quran
8,5,"Islamic Values, Teachings","Hope, Hopefulness, Hopelessness" 8,6,Islam After the Messenger ﷺ,27. Early History of Shiah Muslims
8,5,"Islamic Values, Teachings",Trials in Life 8,6,Islam After the Messenger ﷺ,28. Umayyad Dynasty
8,5,"Islamic Values, Teachings",Permitted and Prohibited Food 8,6,Islam After the Messenger ﷺ,29. Abbasid Dynasty
8,5,"Islamic Values, Teachings",Performance of Hajj 9,1,A Reflection on the Divine,1. Signs of Allahﷻ in Nature
8,5,"Islamic Values, Teachings",Parables in the Quran 9,1,A Reflection on the Divine,2. Pondering the Quran
8,6,Islam After the Rasul (S),Origin and History of Shiah 9,1,A Reflection on the Divine,3. Preservation and Compilation of the Quran
8,6,Islam After the Rasul (S),Ummayad Dynasty 9,1,A Reflection on the Divine,4. Ibadat—Easy Ways to Do It
8,6,Islam After the Rasul (S),Abbasid Dynasty 9,1,A Reflection on the Divine,5. Surah Baqarah—Statement of Faith and Commitment
9,1,A Reflection on the Divine,Signs of Allahﷻ in Nature 9,2,Islam and Muslim,6. Why Human Beings Are Superior
9,1,A Reflection on the Divine,Pondering the Quran 9,2,Islam and Muslim,7. Life Cycle of Truth
9,1,A Reflection on the Divine,Preservation and Compilation of the Quran 9,2,Islam and Muslim,8. Is Islam a Violent Religion?
9,1,A Reflection on the Divine,Ibadat—Easy Ways to Do It 9,2,Islam and Muslim,9. Present Life: Vanity, Deception, Play
9,1,A Reflection on the Divine,Surah Baqarah—Statement of Faith and Commitment 9,2,Islam and Muslim,10. Shariah
9,2,Islam and Muslim,Why Human Beings Are Superior 9,2,Islam and Muslim,11. Justice in Islam
9,2,Islam and Muslim,Life Cycle of Truth 9,3,Ethical Standard in Islam,12. Choices We Make
9,2,Islam and Muslim,Is Islam a Violent Religion? 9,3,Ethical Standard in Islam,13. Peer Pressure
9,2,Islam and Muslim,"Present Life: Vanity, Deception, Play" 9,3,Ethical Standard in Islam,14. Islamic Perspective on Dating
9,2,Islam and Muslim,Shariah 9,3,Ethical Standard in Islam,15. Indecency
9,2,Islam and Muslim,Justice in Islam 9,3,Ethical Standard in Islam,16. Alcohol and Gambling
9,3,Ethical Standard in Islam,Choices We Make 9,3,Ethical Standard in Islam,17. Permitted and Prohibited Food
9,3,Ethical Standard in Islam,Peer Pressure 9,3,Ethical Standard in Islam,18. Food of the People of the Book
9,3,Ethical Standard in Islam,Islamic Perspective on Dating 9,3,Ethical Standard in Islam,19. Let Ramadan Bring The Best in Us
9,3,Ethical Standard in Islam,Indecency 9,4,Essays on Rasulullahﷺ,20. Khadijah (ra)
9,3,Ethical Standard in Islam,Alcohol and Gambling 9,4,Essays on Rasulullahﷺ,21. Rasulullahﷺ Multiple Marriages
9,3,Ethical Standard in Islam,Permitted and Prohibited Food 9,4,Essays on Rasulullahﷺ,22. Marriage to Zainab (ra)
9,3,Ethical Standard in Islam,Food of the People of the Book 9,4,Essays on Rasulullahﷺ,23. Rasulullahﷺ: A Great Army General
9,3,Ethical Standard in Islam,Let Ramadan Bring The Best in Us 9,4,Essays on Rasulullahﷺ,24. Prophecy of Muhammadﷺ in the Bible
9,4,Essays on Rasulullahﷺ,Khadijah (ra) 9,4,Essays on Rasulullahﷺ,25. Allegations Against Rasulullahﷺ
9,4,Essays on Rasulullahﷺ,Rasulullahﷺ Multiple Marriages 9,5,Faith-Based Wealth Building,26. Faith Based Wealth Building
9,4,Essays on Rasulullahﷺ,Marriage to Zainab (ra) 9,5,Faith-Based Wealth Building,27. Earn, Save, Spend, Invest
9,4,Essays on Rasulullahﷺ,Rasulullahﷺ: A Great Army General 9,5,Faith-Based Wealth Building,28. Let Investment Work for You
9,4,Essays on Rasulullahﷺ,Prophecy of Muhammadﷺ in the Bible
9,4,Essays on Rasulullahﷺ,Allegations Against Rasulullahﷺ
9,5,Faith-Based Wealth Building,Faith Based Wealth Building
9,5,Faith-Based Wealth Building,"Earn, Save, Spend, Invest"
9,5,Faith-Based Wealth Building,Let Investment Work for You
1 Grade Unit Unit Title chapter
2 1 1 Aqaid: Our Belief Allahﷻ: Our Creator 1. Allah: Our Creator
3 1 1 Aqaid: Our Belief Islam 2. Islam
4 1 1 Aqaid: Our Belief Our Faith 3. Our Faith
5 1 1 Aqaid: Our Belief Nabi Muhammadﷺ 4. Nabi Muhammad (s)
6 1 1 Aqaid: Our Belief The Qur’an 5. The Qur’an
7 1 2 Knowing Allahﷻ Knowing Allah Allahﷻ Loves Us 6. Allah Loves Us
8 1 2 Knowing Allahﷻ Knowing Allah Remembering Allahﷻ 7. Remembering Allah
9 1 2 Knowing Allahﷻ Knowing Allah Allahﷻ Rewards Us 8. Allah Rewards Us
10 1 3 Our Ibadat Five Pillars of Islam 9. Five Pillars of Islam
11 1 3 Our Ibadat Shahadah: The First Pillar 10. Shahadah: The First Pillar
12 1 3 Our Ibadat Salah: The Second Pillar 11. Salat: The Second Pillar
13 1 3 Our Ibadat Zakat: The Third Pillar 12. Zakat: The Third Pillar
14 1 3 Our Ibadat Fasting: The Fourth Pillar 13. Fasting: The Fourth Pillar
15 1 3 Our Ibadat Hajj: The Fifth Pillar 14. Hajj: The Fifth Pillar
16 1 4 Messengers of Allah Adam (A): The First Nabi 15. Adam (A): The First Nabi
17 1 4 Messengers of Allah Nuh (A): Saved From Flood 16. Nuh (A): Saved From the Great Flood
18 1 4 Messengers of Allah Ibrahim (A): Never Listen to Shaitan 17. Ibrahim (A): Never Listen to Shaitan
19 1 4 Messengers of Allah Musa (A): Challenging A Bad Ruler 18. Musa (A): Challenging a Bad Ruler
20 1 4 Messengers of Allah Isa (A): A Great Nabi of Allahﷻ 19. Isa (A): A Great Nabi of Allah
21 1 5 Other Basics of Islam Angels: They Always Work for Allahﷻ 20. Angels: They Always Work for Allah
22 1 5 Other Basics of Islam Shaitan: Our Enemy 21. Shaitan: Our Enemy
23 1 5 Other Basics of Islam Makkah and Madinah 22. Makkah and Madinah
24 1 5 Other Basics of Islam Eid: Two Festivals 23. Eid: Two Festivals
25 1 6 Akhlaq and Adab in Islam Good Manners 24. Good Manners
26 1 6 Akhlaq and Adab in Islam Kindness and Sharing 25. Kindness and Sharing
27 1 6 Akhlaq and Adab in Islam Respect 26. Respect
28 1 6 Akhlaq and Adab in Islam Forgiveness 27. Forgiveness
29 1 6 Akhlaq and Adab in Islam Thanking Allahﷻ 28. Thanking Allah
30 2 1 The Creator—His Message The Creator and His Message Allahﷻ: Our Creator 1. Allah: Our Creator
31 2 1 The Creator—His Message The Creator and His Message How Does Allahﷻ Create? 2. How Does Allah Create?
32 2 1 The Creator—His Message The Creator and His Message Allahﷻ: What Does He Do? 3. What Does Allah Do?
33 2 1 The Creator—His Message The Creator and His Message What Does Allahﷻ Not Do 4
34 2 1 The Creator—His Message The Creator and His Message The Qur’an 5. The Qur’an
35 2 1 The Creator—His Message The Creator and His Message Hadith and Sunnah 6. Hadith and Sunnah
36 2 2 Our Ibadat Shahadah: The First Pillar 7. Shahadah: The First Pillar
37 2 2 Our Ibadat Salah: The Second Pillar 8. Salat: The Second Pillar
38 2 2 Our Ibadat Zakah: The Third Pillar 9. Zakah: The Third Pillar
39 2 2 Our Ibadat Sawm: The Fourth Pillar 10. Sawm: The Fourth Pillar
40 2 2 Our Ibadat Hajj: The Fifth Pillar 11. Hajj: The Fifth Pillar
41 2 2 Our Ibadat Wudu: Keeping Our Bodies Clean 12. Wudu: Cleaning Before Salat
42 2 3 Messengers of Allah The Messengers of Allah Ibrahim (A): A Friend of Allah 13. Ibrahim (A): A Friend of Allah
43 2 3 Messengers of Allah The Messengers of Allah Yaqub (A) and Yusuf (A) 14. Yaqub (A) and Yusuf (A)
44 2 3 Messengers of Allah The Messengers of Allah Musa (A) and Harun (A) 15. Musa (A) and Harun (A)
45 2 3 Messengers of Allah The Messengers of Allah Yunus (A) 16. Yunus (A)
46 2 3 Messengers of Allah The Messengers of Allah Nabi Muhammadﷺ 17. Nabi Muhammad ﷺ
47 2 4 Learning About Islam Obey Allahﷻ Obey Rasulﷺ 18. Obey Allah
48 2 4 Learning About Islam Day of Judgment 19. Day of Judgment
49 2 4 Learning About Islam Our Masjid 20. Our Masjid
50 2 4 Learning About Islam Islamic Phrases 21. Islamic Phrases
51 2 4 Learning About Islam Food that We May Eat 22. Food that We May Eat
52 2 5 Akhlaq and Adab in Islam Truthfulness 23. Truthfulness
53 2 5 Akhlaq and Adab in Islam Kindness 24. Kindness
54 2 5 Akhlaq and Adab in Islam Respect 25. Respect
55 2 5 Akhlaq and Adab in Islam Responsibility 26. Responsibility
56 2 5 Akhlaq and Adab in Islam Obedience 27. Obedience
57 2 5 Akhlaq and Adab in Islam Cleanliness 28. Cleanliness
58 2 5 Akhlaq and Adab in Islam Honesty 29. Honesty
59 3 1 Knowing About Allah Knowing About Allāh ﷺ What Does Allahﷻ Do? 1. Who Is Allāh ﷺ?
60 3 1 Knowing About Allah Knowing About Allāh ﷺ What Allahﷻ Is and Is Not 2. What Allāh ﷺ Is and Is Not
61 3 1 Knowing About Allah Knowing About Allāh ﷺ Allahﷻ: The Most-Merciful 3. Allāh ﷺ: The Most-Merciful
62 3 1 Knowing About Allah Knowing About Allāh ﷺ Allahﷻ: The Best Judge 4. Allāh ﷺ: The Best Judge
63 3 2 1 What Islam Says Knowing About Allāh ﷺ We Are Muslims: We Have ‘Iman 5. What Does Allāh ﷺ Want Us to Do?
64 3 2 What Islam Says Teachings of Islam What Does Allahﷻ Want Us to Do? 6. We Are Muslims: We Have ‘Īmān
65 3 2 What Islam Says Teachings of Islam Hadith 7. Belief in the Qur’ān
66 3 2 What Islam Says Teachings of Islam Jinn 8. Belief in the Messengers
67 3 2 What Islam Says Teachings of Islam Muslims in North America 9. Hadīth and Sunnah
68 3 2 What Islam Says Teachings of Islam The Right Path: The Straight Path 10. Jinn
69 3 3 2 Why Do We Worship Teachings of Islam Shahadah: Allahﷻ is One 11. Muslims in North America
70 3 3 2 Why Do We Worship Teachings of Islam Types of Salat 12. The Straight Path: The Right Path
71 3 3 Why Do We Worship Nabi Muhammad ﷺ Why We Make Salat 13. Kindness of Rasūlullāh ﷺ
72 3 3 Why Do We Worship Nabi Muhammad ﷺ Why Do We pay Zakat? 14. How Rasūlullāh ﷺ Treated Others
73 3 3 Why Do We Worship Nabi Muhammad ﷺ Why Do We Fast? 15. Our Relationship With Rasūlullāh ﷺ
74 3 3 4 Why Do We Worship Messengers of Allāh ﷺ Why Do We Go for Hajj? 16. Ismā‘īl (A) and Ishāq (A): Nabi of Allāh ﷺ
75 3 4 Life of Nabi Muhammadﷺ Messengers of Allāh ﷺ The Nabiﷺ in Makkah 17. Shu‘aib (A): A Nabi of Allāh ﷺ
76 3 4 Life of Nabi Muhammadﷺ Messengers of Allāh ﷺ The Nabiﷺ in Madinah 18. Dāwūd (A): A Nabi of Allāh ﷺ
77 3 4 Life of Nabi Muhammadﷺ Messengers of Allāh ﷺ How Rasulullahﷺ Treated Others 19. ‘Īsā (A): A Nabi of Allāh ﷺ
78 3 5 Messengers of Allah Learning About Islam Isma‘il (A) and Ishaq (A) 20. The Ka‘bah
79 3 5 Messengers of Allah Learning About Islam Dawud (A): A Nabi of Allahﷻ 21. Masjid an-Nabawī: The Nabi’s Masjid
80 3 5 Messengers of Allah Learning About Islam ‘Isa (A): A Nabi of Allahﷻ 22. Bilāl ibn Rabāh
81 3 6 5 Akhlaq and Adab in Islam Learning About Islam Being Kind: A Virtue of the Believers 23. Zaid ibn Hārithah
82 3 6 Akhlaq and Adab in Islam Forgiveness: A Good Quality 24. Ways To Be a Good Person
83 3 6 Akhlaq and Adab in Islam Good Deeds: A Duty of the Believers 25. Kindness: A Virtue of the Believers
84 3 6 Akhlaq and Adab in Islam Cleanliness: A Quality of Believers 26. Forgiveness: A Quality of the Believers
85 3 6 Akhlaq and Adab in Islam A Muslim Family 27. Good Deeds: A Duty of the Believers
86 3 6 Akhlaq and Adab in Islam Perseverance: Never Give Up 28. Perseverance: Never Give Up
87 3 6 Akhlaq and Adab in Islam Punctuality: Doing Things on Time 29. Punctuality: Doing Things on Time
88 4 1 Knowing the Creator Rewards of Allahﷻ: Everybody Receives Them 1. Rewards of Allah: Everybody Receives Them
89 4 1 Knowing the Creator Discipline of Allahﷻ 1. Discipline of Allah: Because He Loves Us
90 4 1 Knowing the Creator Names of Allahﷻ 3. Names of Allah
91 4 1 Knowing the Creator Books of Allahﷻ 4. Books of Allah
92 4 2 How Islam Changed Arabia Pre-Islamic Arabia 5. Pre-Islamic Arabia: Age of Ignorance
93 4 2 How Islam Changed Arabia The Year of the Elephant 6. The Year of the Elephant
94 4 2 How Islam Changed Arabia Early Life of Muhammadﷺ 7. Early Life of Muhammad ﷺ
95 4 2 How Islam Changed Arabia Life Before Becoming a Nabi 8. Life Before Becoming a Nabi
96 4 2 How Islam Changed Arabia First Revelation 9. First Revelation
97 4 2 How Islam Changed Arabia Makkah Period 10. Makkah Period: The Early Years of the Muslims
98 4 2 How Islam Changed Arabia Hijrat to Madinah 11. Hijrat to Madinah: The Migration that Shaped History
99 4 2 How Islam Changed Arabia Madinah Period 12. Madinah Period: Islam Prospers
100 4 3 The Rightly Guided Khalifah Abu Bakr: The First Khalifah 13. Abū Bakr (R): The First Khalifah
101 4 3 The Rightly Guided Khalifah ‘Umar ibn al-Khattab 14. ‘Umar al-Khaṭṭāb (R): The Second Khalifah
102 4 3 The Rightly Guided Khalifah ‘Uthman ibn ‘Affan 15. ‘Uthman Ibn ‘Affān (R): The Third Khalifah
103 4 3 The Rightly Guided Khalifah ‘Ali ibn Abu Talib 16. ‘Ali Ibn Abu Ṭālib (R): The Fourth Khalifah
104 4 4 The Messengers of Allah Messengers of Allah Hud (A): Struggle to Guide People 17. Hūd (A): Struggle to Guide Mankind
105 4 4 The Messengers of Allah Messengers of Allah Salih (A): To Guide the Misguided 18. Ṣāliḥ (A): Struggle to Guide the Misguided
106 4 4 The Messengers of Allah Messengers of Allah Musa (A): His Life and Actions 19. Mūsā (A): His Life and Achievements
107 4 4 The Messengers of Allah Messengers of Allah Sulaiman (A): A Humble King 20. Sulaimān (A): A King and a Servant of Allah ﷺ
108 4 5 Fiqh of Salat Preparation for Salat 21. Preparation for Salat
109 4 5 Fiqh of Salat Requirements of Salat 22. The Requirements of Salat
110 4 5 Fiqh of Salat Mubtilat us-Salat 23. Mubṭilāt-us-Salāt: Things that Invalidate Salāt
111 4 5 Fiqh of Salat How to Pray Behind an Imam 24. How to Pray Behind an Imām
112 4 6 General Islamic Topics Compilers of Hadith 25. Compilers of Hadīth
113 4 6 General Islamic Topics Shaitan’s Mode of Operation 26. Shaitan’s Mode of Operation
114 4 6 General Islamic Topics Day of Judgment 27. Day of Judgment: The Day of Ultimate Justice
115 4 6 General Islamic Topics Eid: Its Significance 28. ‘Eid: Significance of the Festivities
116 4 6 General Islamic Topics Truthfulness: A Quality of Muslim 29. Truthfulness: An Important Quality for Muslims
117 4 6 General Islamic Topics Perseverance: Keep on Trying 30. Perseverance: Keep on Trying
118 5 1 The Creator, His Message The Creator Tawhid, Kafir, Kufr, Shirk, Nifaq 1. His Message
119 5 1 The Creator, His Message The Creator Why Should We Worship Allahﷻ? 2. His Message
120 5 1 The Creator, His Message The Creator Revelation of the Qur’an 3. His Message
121 5 1 The Creator, His Message The Creator Characteristics of the Messengers 4. His Message
122 5 2 The Battles, Developments The Battles and Other Developments Pledges of ‘Aqabah 5. Pledges of ‘Aqabah: Invitation to Migrate
123 5 2 The Battles, Developments The Battles and Other Developments The Battle of Badr 6. The Battle of Badr: Allah Supports the Righteous
124 5 2 The Battles, Developments The Battles and Other Developments The Battle of Uhud 7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
125 5 2 The Battles, Developments The Battles and Other Developments The Battle of the Trench 8. The Battle of the Trench: A Bloodless Battle
126 5 2 The Battles, Developments The Battles and Other Developments The Treaty of Hudaibiyah 9. The Treaty of Hudaibiyah: A Clear Victory
127 5 2 The Battles, Developments The Battles and Other Developments Liberation of Makkah 10. Liberation of Makkah: A Bloodless Victory
128 5 3 The Messengers of Allah Stories of the Messengers of Allah Adam (A): The Creation of Mankind 11. Adam (A): The Creation of Human Beings
129 5 3 The Messengers of Allah Stories of the Messengers of Allah Ibrahim (A) Debate with Polytheists 12. Ibrahim (A): His Debate with the Polytheists
130 5 3 The Messengers of Allah Stories of the Messengers of Allah Ibrahim (A): Plan Against Idols 13. Ibrahim (A): His Plan Against the Idols
131 5 3 The Messengers of Allah Stories of the Messengers of Allah Luqman (A): A Wise Man’s Lifelong Teachings 14. Luqmān (A): A Wise Man’s Lifelong Advice
132 5 3 The Messengers of Allah Stories of the Messengers of Allah Yusuf (A): His Childhood 15. Yūsuf (A): His Childhood and Life in Aziz’s Home
133 5 3 The Messengers of Allah Stories of the Messengers of Allah Yusuf (A): His Righteousness 16. Yūsuf (A): Standing Up for Righteousness
134 5 3 The Messengers of Allah Stories of the Messengers of Allah Yusuf (A): Dream Comes True 17. Yūsuf (A): A Childhood Dream Comes True
135 5 3 4 The Messengers of Allah Islam in The World Ayyub (A): Patience, Perseverance 20. Major Masājid in the World
136 5 3 5 The Messengers of Allah Islamic Values and Teachings Zakariyyah (A), Yahya (A) 21. Upholding Truth: A Duty of All Believers
137 5 4 5 Islam in the World Islamic Values and Teachings Major Masajid in the World 22. Responsibility and Punctuality
138 5 5 Islamic Values, Teachings Islamic Values and Teachings Upholding Truth: A Duty for All Believers 23. My Mind
139 5 5 Islamic Values, Teachings Islamic Values and Teachings Responsibility and Punctuality 24. Kindness and Forgiveness
140 5 5 Islamic Values, Teachings Islamic Values and Teachings My Mind My Body 25. The Middle Path: Ways to Avoid the Two Extremes
141 5 5 Islamic Values, Teachings Islamic Values and Teachings Kindness and Forgiveness 26. Salat: Its Significance
142 5 5 Islamic Values, Teachings Islamic Values and Teachings The Middle Path: Ways to Avoid Two Extremes 27. Sawm: Its Significance
143 5 5 Islamic Values, Teachings Islamic Values and Teachings Salat: Its Significance 28. Zakat and Sadaqah: Similarities and Differences
144 5 6 5 1 Islamic Values, Teachings The Creator Sawm: Its Significance 1. His Message
145 5 6 5 1 Islamic Values, Teachings The Creator Zakat and Sadaqah: Similarities and Differences 2. His Message
146 6 1 The Creator—His Message The Creator Attributes of Allahﷻ 3. His Message
147 6 1 The Creator—His Message The Creator The Promise of Allahﷻ 4. His Message
148 6 2 The Qur’an and Hadith The Battles and Other Developments Objectives of the Qur’an? 5. Pledges of ‘Aqabah: Invitation to Migrate
149 6 2 The Qur’an and Hadith The Battles and Other Developments Compilation of the Qur’an 6. The Battle of Badr: Allah Supports the Righteous
150 6 2 The Qur’an and Hadith The Battles and Other Developments Previous Scriptures and the Qur’an 7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
151 6 2 The Qur’an and Hadith The Battles and Other Developments Compilation of Hadith 8. The Battle of the Trench: A Bloodless Battle
152 6 3 2 Fundamentals of Deen The Battles and Other Developments Importance of Shahadah 9. The Treaty of Hudaibiyah: A Clear Victory
153 6 3 2 Fundamentals of Deen The Battles and Other Developments Khushu in Salat 10. Liberation of Makkah: A Bloodless Victory
154 6 3 Fundamentals of Deen Stories of the Messengers of Allah Taqwa: A Quality of Believers 11. Adam (A): The Creation of Human Beings
155 6 4 3 Messengers of Allah Stories of the Messengers of Allah Nuh (A) 12. Ibrahim (A): His Debate with the Polytheists
156 6 4 3 Messengers of Allah Stories of the Messengers of Allah Talut, Jalut, and Dawud (A) 13. Ibrahim (A): His Plan Against the Idols
157 6 4 3 Messengers of Allah Stories of the Messengers of Allah Dawud (A) and Sulaiman (A) 14. Luqmān (A): A Wise Man’s Lifelong Advice
158 6 4 3 Messengers of Allah Stories of the Messengers of Allah Musa (A) and Fir‘awn 15. Yūsuf (A): His Childhood and Life in Aziz’s Home
159 6 4 3 Messengers of Allah Stories of the Messengers of Allah Musa (A) and Khidir 16. Yūsuf (A): Standing Up for Righteousness
160 6 4 3 Messengers of Allah Stories of the Messengers of Allah ‘Isa (A) and Maryam (ra) 17. Yūsuf (A): A Childhood Dream Comes True
161 6 5 4 Some Prominent Muslimah Islam in The World Khadijah (ra) 20. Major Masājid in the World
162 6 5 Some Prominent Muslimah Islamic Values and Teachings ‘Aishah (ra) 21. Upholding Truth: A Duty of All Believers
163 6 5 Some Prominent Muslimah Islamic Values and Teachings Fatimah (ra) 22. Responsibility and Punctuality
164 6 5 Some Prominent Muslimah Islamic Values and Teachings Some Prominent Muslimahs 23. My Mind
165 6 6 5 Knowledge Enrichment Islamic Values and Teachings Al-Qiyamah: The Awakening 24. Kindness and Forgiveness
166 6 6 5 Knowledge Enrichment Islamic Values and Teachings Ruh and Nafs: An Overview 25. The Middle Path: Ways to Avoid the Two Extremes
167 6 6 5 Knowledge Enrichment Islamic Values and Teachings Angels and Jinn: An Overview 26. Salat: Its Significance
168 6 6 5 Knowledge Enrichment Islamic Values and Teachings Shaitan: The Invisible Enemy 27. Sawm: Its Significance
169 6 7 5 The Current Society Islamic Values and Teachings My Friend Is Muslim Now 28. Zakat and Sadaqah: Similarities and Differences
170 6 7 7 1 The Current Society The Creator Friendship 1. Why Islam? What is Islam?
171 6 7 7 1 The Current Society The Creator Muslims Around the World 2. Belief in Allah
172 6 7 7 1 The Current Society The Creator People of Other Faith 3. The Qur’an: Its Qualitative Names
173 6 7 8 1 Developing Islamic Values The Creator Greed and Dishonesty 4. Istighfār: Seeking Forgiveness and Protection
174 6 7 8 1 Developing Islamic Values The Creator Avoiding Extravagance 5. Allah: Angry or Kind?
175 7 1 2 The Creator Stories of the Messengers Why Islam? what is Islam? 6. Ādam (A): The Trial of the First Messenger
176 7 1 2 The Creator Stories of the Messengers Belief in Allahﷻ 7. The Life of Ibrāhīm (A): Beginning a Nation
177 7 1 2 The Creator Stories of the Messengers The Qur’an: Its Qualitative Names 8. The Sacrifice of Ibrāhīm (A)
178 7 1 2 The Creator Stories of the Messengers Istighfar: Seeking Forgiveness of Allahﷻ 9. Lūt (A): A Message for Modern Societies
179 7 1 2 The Creator Stories of the Messengers Allahﷻ: Angry or Kind 10. Yūsuf (A): The Will to Overcome Temptation
180 7 2 3 Stories of the Messengers Stories from the Qur’an Adam (A): Trial of the Messenger 11. The Companions of the Cave
181 7 2 3 Stories of the Messengers Stories from the Qur’an Life of Ibrahim (A) 12. Dhu al-Qarnain: The Journey of a King
182 7 2 3 Stories of the Messengers Stories from the Qur’an Sacrifice of Ibrahim (A) 13. Effective Debate and Negotiation Styles in the Qur’an
183 7 2 4 Stories of the Messengers Two Companions Who Shaped Islam Lut (A): Message for Modern Societies 14. Abū Sufyān: His Life and Achievements
184 7 2 4 Stories of the Messengers Two Companions Who Shaped Islam Yusuf (A)—The Will to Overcome Temptation 15. Khālid Ibn al-Walīd: The “Sword of Allah”
185 7 3 5 Stories from the Qur’an Knowledge Enrichment The Companions of the Cave 16. Character of the Messengers
186 7 3 5 Stories from the Qur’an Knowledge Enrichment Dhul Qurnain: Journey of a King 17. Rasūlullāh’s Marriages
187 7 3 5 Stories from the Qur’an Knowledge Enrichment Effective Debate and Negotiation Styles in the Qur’an 18. Lailatul Qadr: The Night of Majesty
188 7 4 5 Two Companions Knowledge Enrichment Abu Sufyan 19. Fasting During Ramadan: The Month of Benefits
189 7 4 5 Two Companions Knowledge Enrichment Khalid Ibn Walid (R) 20. My Family is Muslim Now
190 7 5 Knowledge Enrichment The character of the Messengers 21. Science in the Qur’an
191 7 5 Knowledge Enrichment Rasulullahﷺ Marriages 22. Lessons From Past Civilizations
192 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Lailatul Qadr 23. Amr Bil Ma’rūf: Enjoin Good Deeds
193 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Fasting During Ramadan 24. Guard Your Tongue: Think Before You Speak
194 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam My Family is Muslim Now 25. Islamic Greeting: Wishing Peace
195 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Science in the Qur’an 26. How to Achieve Success
196 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Lessons from Past Civilizations 27. Permitted and Prohibited
197 7 6 Teachings of the Qur’an Akhlaq and Adab in Islam Amr Bil Ma‘ruf 28. Types of Behavior Allah Loves
198 7 8 6 1 Teachings of the Qur’an Knowing the Creator Guard Your Tongue 1. Divine Names
199 7 8 6 1 Teachings of the Qur’an Knowing the Creator Islamic Greetings 2. Sunan of Allah
200 7 8 6 1 Teachings of the Qur’an Knowing the Creator How to Achieve Success 3. Objectives of the Qur’an
201 7 8 6 1 Teachings of the Qur’an Knowing the Creator Permitted and Prohibited 4. Lessons from Sūrah al-Hujurāt
202 7 8 6 1 Teachings of the Qur’an Knowing the Creator Types of Behavior Allahﷻ Loves 5. True Piety: A Synthesis of Belief
203 8 1 Knowing the Creator Divine Names 6. Āyatul Kursi: The Throne Verse
204 8 1 2 Knowing the Creator Knowing the Messenger ﷺ Sunan of Allahﷻ 7. The Person Muhammad ﷺ
205 8 1 2 Knowing the Creator Knowing the Messenger ﷺ Objectives of the Qur’an 8. Farewell Pilgrimage
206 8 1 2 Knowing the Creator Knowing the Messenger ﷺ Surah Hujurat: Its Teachings 9. Finality of Prophethood
207 8 1 2 Knowing the Creator Knowing the Messenger ﷺ True Piety: Analysis of Verse 2:177 10. Hadith: Collection and Classification
208 8 1 3 Knowing the Creator Challenges in Madinah Ayatul Kursi 11. Hypocrites
209 8 2 3 Knowing the Messengerﷺ Challenges in Madinah The Person Muhammadﷺ 12. Banu Qaynuqa: Threat Within Madinah
210 8 2 3 Knowing the Messengerﷺ Challenges in Madinah Farewell Pilgrimage 13. Banu Nadir: Treachery Within Madinah
211 8 2 3 Knowing the Messengerﷺ Challenges in Madinah Finality of Prophethood 14. Banu Qurayzah
212 8 2 3 Knowing the Messengerﷺ Challenges in Madinah Hadith: Collection, Classification 15. Mission to Tabūk: A Test of Steadfastness
213 8 3 4 Challenges in Madinah Islamic Ethical Framework Hypocrites 16. Friends and Friendship: Who is a Good Friend?
214 8 3 4 Challenges in Madinah Islamic Ethical Framework Banu Qaynuqa 17. Friendship With Non-Muslims
215 8 3 4 Challenges in Madinah Islamic Ethical Framework Banu Nadir 18. Dating: How Islam Views the Practice
216 8 3 4 Challenges in Madinah Islamic Ethical Framework Banu Qurayzah 19. Hold Firmly the Rope of Allah
217 8 3 4 Challenges in Madinah Islamic Ethical Framework Mission to Tabuk 20. Elements of a Bad Life
218 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Friends and Friendship 21. Duties Towards Parents
219 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Friendship With Non-Muslims 22. Hope
220 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Dating in Islam 23. Trials in Life: Everyone Will Experience Them
221 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Hold Firmly the Rope of Allah 24. Permitted and Prohibited Food
222 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Elements of Bad Life 25. Performance of Hajj
223 8 5 Islamic Values, Teachings Islamic Values and Teachings Duties Toward Parents 26. Parables in the Qur’an
224 8 5 6 Islamic Values, Teachings Islam After the Messenger ﷺ Hope, Hopefulness, Hopelessness 27. Early History of Shi‘ah Muslims
225 8 5 6 Islamic Values, Teachings Islam After the Messenger ﷺ Trials in Life 28. Umayyad Dynasty
226 8 5 6 Islamic Values, Teachings Islam After the Messenger ﷺ Permitted and Prohibited Food 29. Abbasid Dynasty
227 8 9 5 1 Islamic Values, Teachings A Reflection on the Divine Performance of Hajj 1. Signs of Allahﷻ in Nature
228 8 9 5 1 Islamic Values, Teachings A Reflection on the Divine Parables in the Qur’an 2. Pondering the Qur’an
229 8 9 6 1 Islam After the Rasul (S) A Reflection on the Divine Origin and History of Shi‘ah 3. Preservation and Compilation of the Qur’an
230 8 9 6 1 Islam After the Rasul (S) A Reflection on the Divine Ummayad Dynasty 4. Ibadat—Easy Ways to Do It
231 8 9 6 1 Islam After the Rasul (S) A Reflection on the Divine Abbasid Dynasty 5. Surah Baqarah—Statement of Faith and Commitment
232 9 1 2 A Reflection on the Divine Islam and Muslim Signs of Allahﷻ in Nature 6. Why Human Beings Are Superior
233 9 1 2 A Reflection on the Divine Islam and Muslim Pondering the Qur’an 7. Life Cycle of Truth
234 9 1 2 A Reflection on the Divine Islam and Muslim Preservation and Compilation of the Qur’an 8. Is Islam a Violent Religion?
235 9 1 2 A Reflection on the Divine Islam and Muslim Ibadat—Easy Ways to Do It 9. Present Life: Vanity
236 9 1 2 A Reflection on the Divine Islam and Muslim Surah Baqarah—Statement of Faith and Commitment 10. Shariah
237 9 2 Islam and Muslim Why Human Beings Are Superior 11. Justice in Islam
238 9 2 3 Islam and Muslim Ethical Standard in Islam Life Cycle of Truth 12. Choices We Make
239 9 2 3 Islam and Muslim Ethical Standard in Islam Is Islam a Violent Religion? 13. Peer Pressure
240 9 2 3 Islam and Muslim Ethical Standard in Islam Present Life: Vanity, Deception, Play 14. Islamic Perspective on Dating
241 9 2 3 Islam and Muslim Ethical Standard in Islam Shariah 15. Indecency
242 9 2 3 Islam and Muslim Ethical Standard in Islam Justice in Islam 16. Alcohol and Gambling
243 9 3 Ethical Standard in Islam Choices We Make 17. Permitted and Prohibited Food
244 9 3 Ethical Standard in Islam Peer Pressure 18. Food of the People of the Book
245 9 3 Ethical Standard in Islam Islamic Perspective on Dating 19. Let Ramadan Bring The Best in Us
246 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Indecency 20. Khadijah (ra)
247 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Alcohol and Gambling 21. Rasulullahﷺ Multiple Marriages
248 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Permitted and Prohibited Food 22. Marriage to Zainab (ra)
249 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Food of the People of the Book 23. Rasulullahﷺ: A Great Army General
250 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Let Ramadan Bring The Best in Us 24. Prophecy of Muhammadﷺ in the Bible
251 9 4 Essays on Rasulullahﷺ Khadijah (ra) 25. Allegations Against Rasulullahﷺ
252 9 4 5 Essays on Rasulullahﷺ Faith-Based Wealth Building Rasulullahﷺ Multiple Marriages 26. Faith Based Wealth Building
253 9 4 5 Essays on Rasulullahﷺ Faith-Based Wealth Building Marriage to Zainab (ra) 27. Earn
254 9 4 5 Essays on Rasulullahﷺ Faith-Based Wealth Building Rasulullahﷺ: A Great Army General 28. Let Investment Work for You
9 4 Essays on Rasulullahﷺ Prophecy of Muhammadﷺ in the Bible
9 4 Essays on Rasulullahﷺ Allegations Against Rasulullahﷺ
9 5 Faith-Based Wealth Building Faith Based Wealth Building
9 5 Faith-Based Wealth Building Earn, Save, Spend, Invest
9 5 Faith-Based Wealth Building Let Investment Work for You
-22
View File
@@ -1,22 +0,0 @@
<?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 ComposerAutoloaderInitdc0354af1b1c06805688413b1e11bb3c::getLoader();
-22
View File
@@ -1,22 +0,0 @@
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.
-57
View File
@@ -1,57 +0,0 @@
# 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
### GDLib Renderer
GD library has so many limitations, that GD support is not added as backend, but as separated renderer.
Use `GDLibRenderer` instead of `ImageRenderer`. These are the limitations:
- Does not support gradient.
- Does not support any curves, so you QR code is always squared.
Example usage:
```php
use BaconQrCode\Renderer\GDLibRenderer;
use BaconQrCode\Writer;
$renderer = new GDLibRenderer(400);
$writer = new Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```
-50
View File
@@ -1,50 +0,0 @@
{
"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": "^8.1",
"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/"
}
},
"autoload-dev": {
"psr-4": {
"BaconQrCodeTest\\": "test/"
}
},
"require-dev": {
"phpunit/phpunit": "^10.5.11 || 11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"squizlabs/php_codesniffer": "^3.9",
"phly/keep-a-changelog": "^2.12"
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"php-http/discovery": true
}
},
"archive": {
"exclude": [
"/test",
"/phpunit.xml.dist"
]
}
}
-364
View File
@@ -1,364 +0,0 @@
<?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 SplFixedArray $bits;
/**
* Creates a new bit array with a given size.
*/
public function __construct(private int $size = 0)
{
$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 min($result, $this->size);
}
/**
* 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 min($result, $this->size);
}
/**
* 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;
}
}
-307
View File
@@ -1,307 +0,0 @@
<?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.
*/
private int $width;
/**
* Height of the bit matrix.
*/
private ?int $height;
/**
* Size in bits of each individual row.
*/
private int $rowSize;
/**
* Bits representation.
*
* @var SplFixedArray<int>
*/
private SplFixedArray $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;
}
}
-41
View File
@@ -1,41 +0,0 @@
<?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;
}
}
@@ -1,177 +0,0 @@
<?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 string[]
*/
private array $otherEncodingNames;
/**
* @var array<int, self>|null
*/
private static ?array $valueToEci;
/**
* @var array<string, self>|null
*/
private static ?array $nameToEci = null;
/**
* @param int[] $values
*/
public function __construct(private readonly array $values, string ...$otherEncodingNames)
{
$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;
}
}
-33
View File
@@ -1,33 +0,0 @@
<?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
{
public function __construct(private readonly int $count, private readonly int $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;
}
}
-66
View File
@@ -1,66 +0,0 @@
<?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
{
/**
* List of EC blocks.
*
* @var EcBlock[]
*/
private array $ecBlocks;
public function __construct(private readonly int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
{
$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;
}
}
@@ -1,57 +0,0 @@
<?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];
protected function __construct(private readonly int $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;
}
}
@@ -1,196 +0,0 @@
<?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.
*/
private ErrorCorrectionLevel $ecLevel;
private int $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
);
}
}
-69
View File
@@ -1,69 +0,0 @@
<?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];
/**
* @param int[] $characterCountBitsForVersions
*/
protected function __construct(
private readonly array $characterCountBitsForVersions,
private readonly int $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;
}
}
@@ -1,454 +0,0 @@
<?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.
*/
private int $symbolSize;
/**
* Block size in symbols.
*/
private int $blockSize;
/**
* First root of RS code generator polynomial, index form.
*/
private int $firstRoot;
/**
* Primitive element to generate polynomial roots, index form.
*/
private int $primitive;
/**
* Prim-th root of 1, index form.
*/
private int $iPrimitive;
/**
* RS code generator polynomial degree (number of roots).
*/
private int $numRoots;
/**
* Padding bytes at front of shortened block.
*/
private int $padding;
/**
* Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $alphaTo;
/**
* Anti-Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $indexOf;
/**
* Generator polynomial.
*
* @var SplFixedArray
*/
private SplFixedArray $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;
}
}
-592
View File
@@ -1,592 +0,0 @@
<?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.
*/
private int $versionNumber;
/**
* Alignment pattern centers.
*
* @var SplFixedArray|array
*/
private SplFixedArray|array $alignmentPatternCenters;
/**
* Error correction blocks.
*
* @var EcBlocks[]
*/
private array $ecBlocks;
/**
* Total number of codewords.
*/
private null|int|float $totalCodewords;
/**
* Cached version instances.
*
* @var array<int, self>|null
*/
private static ?array $versions = null;
/**
* @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(2, 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))
),
];
}
}
-44
View File
@@ -1,44 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
/**
* Block pair.
*/
final class BlockPair
{
/**
* Creates a new block pair.
*
* @param SplFixedArray<int> $dataBytes Data bytes in the block.
* @param SplFixedArray<int> $errorCorrectionBytes Error correction bytes in the block.
*/
public function __construct(
private readonly SplFixedArray $dataBytes,
private readonly SplFixedArray $errorCorrectionBytes
) {
}
/**
* 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;
}
}
-134
View File
@@ -1,134 +0,0 @@
<?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 SplFixedArray $bytes;
public function __construct(private readonly int $width, private readonly int $height)
{
$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;
}
}
-679
View File
@@ -1,679 +0,0 @@
<?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_ENCODING = 'ISO-8859-1';
/** @deprecated use DEFAULT_BYTE_MODE_ENCODING */
public const DEFAULT_BYTE_MODE_ECODING = self::DEFAULT_BYTE_MODE_ENCODING;
/**
* 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 array $codecs = [];
/**
* Encodes "content" with the error correction level "ecLevel".
*/
public static function encode(
string $content,
ErrorCorrectionLevel $ecLevel,
string $encoding = self::DEFAULT_BYTE_MODE_ENCODING,
?Version $forcedVersion = null,
// Barcode scanner might not be able to read the encoded message of the QR code with the prefix ECI of UTF-8
bool $prefixEci = true
) : 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 ($prefixEci && Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ENCODING !== $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 = ord($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
{
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
if (false === $bytes) {
throw new WriterException('Content could not be converted to SHIFT-JIS');
}
if (strlen($bytes) % 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($bytes);
for ($i = 0; $i < $length; $i += 2) {
$byte1 = ord($bytes[$i]) & 0xff;
$byte2 = ord($bytes[$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);
}
}
-271
View File
@@ -1,271 +0,0 @@
<?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
*/
public const N1 = 3;
public const N2 = 3;
public const N3 = 40;
public 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;
}
}
-513
View File
@@ -1,513 +0,0 @@
<?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() .')');
}
}
}
-108
View File
@@ -1,108 +0,0 @@
<?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;
/**
* Mask pattern of the QR code.
*/
private int $maskPattern = -1;
/**
* Matrix of the QR code.
*/
private ByteMatrix $matrix;
public function __construct(
private readonly Mode $mode,
private readonly ErrorCorrectionLevel $errorCorrectionLevel,
private readonly Version $version,
int $maskPattern,
ByteMatrix $matrix
) {
$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;
}
public function getMatrix(): ByteMatrix
{
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;
}
}
@@ -1,10 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}
@@ -1,8 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
@@ -1,8 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}
@@ -1,8 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -1,8 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
{
}
@@ -1,8 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class WriterException extends \RuntimeException implements ExceptionInterface
{
}
-44
View File
@@ -1,44 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Alpha implements ColorInterface
{
/**
* @param int $alpha the alpha value, 0 to 100
*/
public function __construct(private readonly int $alpha, private readonly ColorInterface $baseColor)
{
if ($alpha < 0 || $alpha > 100) {
throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100');
}
}
public function getAlpha() : int
{
return $this->alpha;
}
public function getBaseColor() : ColorInterface
{
return $this->baseColor;
}
public function toRgb() : Rgb
{
return $this->baseColor->toRgb();
}
public function toCmyk() : Cmyk
{
return $this->baseColor->toCmyk();
}
public function toGray() : Gray
{
return $this->baseColor->toGray();
}
}
-82
View File
@@ -1,82 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Cmyk implements ColorInterface
{
/**
* @param int $cyan the cyan amount, 0 to 100
* @param int $magenta the magenta amount, 0 to 100
* @param int $yellow the yellow amount, 0 to 100
* @param int $black the black amount, 0 to 100
*/
public function __construct(
private readonly int $cyan,
private readonly int $magenta,
private readonly int $yellow,
private readonly int $black
) {
if ($cyan < 0 || $cyan > 100) {
throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100');
}
if ($magenta < 0 || $magenta > 100) {
throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100');
}
if ($yellow < 0 || $yellow > 100) {
throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100');
}
if ($black < 0 || $black > 100) {
throw new Exception\InvalidArgumentException('Black must be between 0 and 100');
}
}
public function getCyan() : int
{
return $this->cyan;
}
public function getMagenta() : int
{
return $this->magenta;
}
public function getYellow() : int
{
return $this->yellow;
}
public function getBlack() : int
{
return $this->black;
}
public function toRgb() : Rgb
{
$k = $this->black / 100;
$c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100;
$m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100;
$y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100;
return new Rgb(
(int) (-$c * 255 + 255),
(int) (-$m * 255 + 255),
(int) (-$y * 255 + 255)
);
}
public function toCmyk() : Cmyk
{
return $this;
}
public function toGray() : Gray
{
return $this->toRgb()->toGray();
}
}
@@ -1,22 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
interface ColorInterface
{
/**
* Converts the color to RGB.
*/
public function toRgb() : Rgb;
/**
* Converts the color to CMYK.
*/
public function toCmyk() : Cmyk;
/**
* Converts the color to gray.
*/
public function toGray() : Gray;
}
-39
View File
@@ -1,39 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Gray implements ColorInterface
{
/**
* @param int $gray the gray value between 0 (black) and 100 (white)
*/
public function __construct(private readonly int $gray)
{
if ($gray < 0 || $gray > 100) {
throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
}
}
public function getGray() : int
{
return $this->gray;
}
public function toRgb() : Rgb
{
return new Rgb((int) ($this->gray * 2.55), (int) ($this->gray * 2.55), (int) ($this->gray * 2.55));
}
public function toCmyk() : Cmyk
{
return new Cmyk(0, 0, 0, 100 - $this->gray);
}
public function toGray() : Gray
{
return $this;
}
}
-73
View File
@@ -1,73 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Rgb implements ColorInterface
{
/**
* @param int $red the red amount of the color, 0 to 255
* @param int $green the green amount of the color, 0 to 255
* @param int $blue the blue amount of the color, 0 to 255
*/
public function __construct(private readonly int $red, private readonly int $green, private readonly int $blue)
{
if ($red < 0 || $red > 255) {
throw new Exception\InvalidArgumentException('Red must be between 0 and 255');
}
if ($green < 0 || $green > 255) {
throw new Exception\InvalidArgumentException('Green must be between 0 and 255');
}
if ($blue < 0 || $blue > 255) {
throw new Exception\InvalidArgumentException('Blue must be between 0 and 255');
}
}
public function getRed() : int
{
return $this->red;
}
public function getGreen() : int
{
return $this->green;
}
public function getBlue() : int
{
return $this->blue;
}
public function toRgb() : Rgb
{
return $this;
}
public function toCmyk() : Cmyk
{
$c = 1 - ($this->red / 255);
$m = 1 - ($this->green / 255);
$y = 1 - ($this->blue / 255);
$k = min($c, $m, $y);
if ($k === 0) {
return new Cmyk(0, 0, 0, 0);
}
return new Cmyk(
(int) (100 * ($c - $k) / (1 - $k)),
(int) (100 * ($m - $k) / (1 - $k)),
(int) (100 * ($y - $k) / (1 - $k)),
(int) (100 * $k)
);
}
public function toGray() : Gray
{
return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55));
}
}
@@ -1,26 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Combines the style of two different eyes.
*/
final class CompositeEye implements EyeInterface
{
public function __construct(private readonly EyeInterface $externalEye, private readonly EyeInterface $internalEye)
{
}
public function getExternalPath() : Path
{
return $this->externalEye->getExternalPath();
}
public function getInternalPath() : Path
{
return $this->internalEye->getInternalPath();
}
}
@@ -1,26 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Interface for describing the look of an eye.
*/
interface EyeInterface
{
/**
* Returns the path of the external eye element.
*
* The path origin point (0, 0) must be anchored at the middle of the path.
*/
public function getExternalPath() : Path;
/**
* Returns the path of the internal eye element.
*
* The path origin point (0, 0) must be anchored at the middle of the path.
*/
public function getInternalPath() : Path;
}
@@ -1,48 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Module\ModuleInterface;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders an eye based on a module renderer.
*/
final class ModuleEye implements EyeInterface
{
public function __construct(private readonly ModuleInterface $module)
{
}
public function getExternalPath() : Path
{
$matrix = new ByteMatrix(7, 7);
for ($x = 0; $x < 7; ++$x) {
$matrix->set($x, 0, 1);
$matrix->set($x, 6, 1);
}
for ($y = 1; $y < 6; ++$y) {
$matrix->set(0, $y, 1);
$matrix->set(6, $y, 1);
}
return $this->module->createPath($matrix)->translate(-3.5, -3.5);
}
public function getInternalPath() : Path
{
$matrix = new ByteMatrix(3, 3);
for ($x = 0; $x < 3; ++$x) {
for ($y = 0; $y < 3; ++$y) {
$matrix->set($x, $y, 1);
}
}
return $this->module->createPath($matrix)->translate(-1.5, -1.5);
}
}
@@ -1,56 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the outer eye as solid with a curved corner and inner eye as a circle.
*/
final class PointyEye implements EyeInterface
{
/**
* @var self|null
*/
private static $instance;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, 3.5)
->line(-3.5, 0)
->ellipticArc(3.5, 3.5, 0, false, true, 0, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->close()
->move(2.5, 0)
->ellipticArc(2.5, 2.5, 0, false, true, 0, 2.5)
->ellipticArc(2.5, 2.5, 0, false, true, -2.5, 0)
->ellipticArc(2.5, 2.5, 0, false, true, 0, -2.5)
->ellipticArc(2.5, 2.5, 0, false, true, 2.5, 0)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(1.5, 0)
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
->close()
;
}
}
@@ -1,51 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the inner eye as a circle.
*/
final class SimpleCircleEye implements EyeInterface
{
private static ?SimpleCircleEye $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->line(-3.5, 3.5)
->close()
->move(-2.5, -2.5)
->line(-2.5, 2.5)
->line(2.5, 2.5)
->line(2.5, -2.5)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(1.5, 0)
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
->close()
;
}
}
@@ -1,50 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the eyes in their default square shape.
*/
final class SquareEye implements EyeInterface
{
private static ?SquareEye $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->line(-3.5, 3.5)
->close()
->move(-2.5, -2.5)
->line(-2.5, 2.5)
->line(2.5, 2.5)
->line(2.5, -2.5)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(-1.5, -1.5)
->line(1.5, -1.5)
->line(1.5, 1.5)
->line(-1.5, 1.5)
->close()
;
}
}
@@ -1,238 +0,0 @@
<?php
declare(strict_types=1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Encoder\MatrixUtil;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\Fill;
use GdImage;
final class GDLibRenderer implements RendererInterface
{
private ?GdImage $image;
/**
* @var array<string, int>
*/
private array $colors;
public function __construct(
private int $size,
private int $margin = 4,
private string $imageFormat = 'png',
private int $compressionQuality = 9,
private ?Fill $fill = null
) {
if (! extension_loaded('gd') || ! function_exists('gd_info')) {
throw new RuntimeException('You need to install the GD extension to use this back end');
}
if ($this->fill === null) {
$this->fill = Fill::default();
}
if ($this->fill->hasGradientFill()) {
throw new InvalidArgumentException('GDLibRenderer does not support gradients');
}
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode): string
{
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
MatrixUtil::removePositionDetectionPatterns($matrix);
$this->newImage();
$this->draw($matrix);
return $this->renderImage();
}
private function newImage(): void
{
$img = imagecreatetruecolor($this->size, $this->size);
if ($img === false) {
throw new RuntimeException('Failed to create image of that size');
}
$this->image = $img;
imagealphablending($this->image, false);
imagesavealpha($this->image, true);
$bg = $this->getColor($this->fill->getBackgroundColor());
imagefilledrectangle($this->image, 0, 0, $this->size, $this->size, $bg);
imagealphablending($this->image, true);
}
private function draw(ByteMatrix $matrix): void
{
$matrixSize = $matrix->getWidth();
$pointsOnSide = $matrix->getWidth() + $this->margin * 2;
$pointInPx = $this->size / $pointsOnSide;
$this->drawEye(0, 0, $pointInPx, $this->fill->getTopLeftEyeFill());
$this->drawEye($matrixSize - 7, 0, $pointInPx, $this->fill->getTopRightEyeFill());
$this->drawEye(0, $matrixSize - 7, $pointInPx, $this->fill->getBottomLeftEyeFill());
$rows = $matrix->getArray()->toArray();
$color = $this->getColor($this->fill->getForegroundColor());
for ($y = 0; $y < $matrixSize; $y += 1) {
for ($x = 0; $x < $matrixSize; $x += 1) {
if (! $rows[$y][$x]) {
continue;
}
$points = $this->normalizePoints([
($this->margin + $x) * $pointInPx, ($this->margin + $y) * $pointInPx,
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y) * $pointInPx,
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
($this->margin + $x) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
]);
imagefilledpolygon($this->image, $points, $color);
}
}
}
private function drawEye(int $xOffset, int $yOffset, float $pointInPx, EyeFill $eyeFill): void
{
$internalColor = $this->getColor($eyeFill->inheritsInternalColor()
? $this->fill->getForegroundColor()
: $eyeFill->getInternalColor());
$externalColor = $this->getColor($eyeFill->inheritsExternalColor()
? $this->fill->getForegroundColor()
: $eyeFill->getExternalColor());
for ($y = 0; $y < 7; $y += 1) {
for ($x = 0; $x < 7; $x += 1) {
if ((($y === 1 || $y === 5) && $x > 0 && $x < 6) || (($x === 1 || $x === 5) && $y > 0 && $y < 6)) {
continue;
}
$points = $this->normalizePoints([
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
]);
if ($y > 1 && $y < 5 && $x > 1 && $x < 5) {
imagefilledpolygon($this->image, $points, $internalColor);
} else {
imagefilledpolygon($this->image, $points, $externalColor);
}
}
}
}
/**
* Normalize points will trim right and bottom line by 1 pixel.
* Otherwise pixels of neighbors are overlapping which leads to issue with transparency and small QR codes.
*/
private function normalizePoints(array $points): array
{
$maxX = $maxY = 0;
for ($i = 0; $i < count($points); $i += 2) {
// Do manual round as GD just removes decimal part
$points[$i] = $newX = round($points[$i]);
$points[$i + 1] = $newY = round($points[$i + 1]);
$maxX = max($maxX, $newX);
$maxY = max($maxY, $newY);
}
// Do trimming only if there are 4 points (8 coordinates), assumes this is square.
for ($i = 0; $i < count($points); $i += 2) {
$points[$i] = min($points[$i], $maxX - 1);
$points[$i + 1] = min($points[$i + 1], $maxY - 1);
}
return $points;
}
private function renderImage(): string
{
ob_start();
$quality = $this->compressionQuality;
switch ($this->imageFormat) {
case 'png':
if ($quality > 9 || $quality < 0) {
$quality = 9;
}
imagepng($this->image, null, $quality);
break;
case 'gif':
imagegif($this->image, null);
break;
case 'jpeg':
case 'jpg':
if ($quality > 100 || $quality < 0) {
$quality = 85;
}
imagejpeg($this->image, null, $quality);
break;
default:
ob_end_clean();
throw new InvalidArgumentException(
'Supported image formats are jpeg, png and gif, got: ' . $this->imageFormat
);
}
imagedestroy($this->image);
$this->colors = [];
$this->image = null;
return ob_get_clean();
}
private function getColor(ColorInterface $color): int
{
$alpha = 100;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha();
$color = $color->getBaseColor();
}
$rgb = $color->toRgb();
$colorKey = sprintf('%02X%02X%02X%02X', $rgb->getRed(), $rgb->getGreen(), $rgb->getBlue(), $alpha);
if (! isset($this->colors[$colorKey])) {
$colorId = imagecolorallocatealpha(
$this->image,
$rgb->getRed(),
$rgb->getGreen(),
$rgb->getBlue(),
(int)((100 - $alpha) / 100 * 127) // Alpha for GD is in range 0 (opaque) - 127 (transparent)
);
if ($colorId === false) {
throw new RuntimeException('Failed to create color: #' . $colorKey);
}
$this->colors[$colorKey] = $colorId;
}
return $this->colors[$colorKey];
}
}
@@ -1,373 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
final class EpsImageBackEnd implements ImageBackEndInterface
{
private const PRECISION = 3;
private ?string $eps;
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
. "%%Creator: BaconQrCode\n"
. sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
. "%%BeginProlog\n"
. "save\n"
. "50 dict begin\n"
. "/q { gsave } bind def\n"
. "/Q { grestore } bind def\n"
. "/s { scale } bind def\n"
. "/t { translate } bind def\n"
. "/r { rotate } bind def\n"
. "/n { newpath } bind def\n"
. "/m { moveto } bind def\n"
. "/l { lineto } bind def\n"
. "/c { curveto } bind def\n"
. "/z { closepath } bind def\n"
. "/f { eofill } bind def\n"
. "/rgb { setrgbcolor } bind def\n"
. "/cmyk { setcmykcolor } bind def\n"
. "/gray { setgray } bind def\n"
. "%%EndProlog\n"
. "1 -1 s\n"
. sprintf("0 -%d t\n", $size);
if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
return;
}
$this->eps .= wordwrap(
'0 0 m'
. sprintf(' %s 0 l', (string) $size)
. sprintf(' %s %s l', (string) $size, (string) $size)
. sprintf(' 0 %s l', (string) $size)
. ' z'
. ' ' .$this->getColorSetString($backgroundColor) . " f\n",
75,
"\n "
);
}
public function scale(float $size) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
}
public function translate(float $x, float $y) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
}
public function rotate(int $degrees) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%d r\n", $degrees);
}
public function push() : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "q\n";
}
public function pop() : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "Q\n";
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$fromX = 0;
$fromY = 0;
$this->eps .= wordwrap(
'n '
. $this->drawPathOperations($path, $fromX, $fromY)
. ' ' . $this->getColorSetString($color) . " f\n",
75,
"\n "
);
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$fromX = 0;
$fromY = 0;
$this->eps .= wordwrap(
'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
75,
"\n "
);
$this->createGradientFill($gradient, $x, $y, $width, $height);
}
public function done() : string
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "%%TRAILER\nend restore\n%%EOF";
$blob = $this->eps;
$this->eps = null;
return $blob;
}
private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
{
$pathData = [];
foreach ($ops as $op) {
switch (true) {
case $op instanceof Move:
$fromX = $toX = round($op->getX(), self::PRECISION);
$fromY = $toY = round($op->getY(), self::PRECISION);
$pathData[] = sprintf('%s %s m', $toX, $toY);
break;
case $op instanceof Line:
$fromX = $toX = round($op->getX(), self::PRECISION);
$fromY = $toY = round($op->getY(), self::PRECISION);
$pathData[] = sprintf('%s %s l', $toX, $toY);
break;
case $op instanceof EllipticArc:
$pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
break;
case $op instanceof Curve:
$x1 = round($op->getX1(), self::PRECISION);
$y1 = round($op->getY1(), self::PRECISION);
$x2 = round($op->getX2(), self::PRECISION);
$y2 = round($op->getY2(), self::PRECISION);
$fromX = $x3 = round($op->getX3(), self::PRECISION);
$fromY = $y3 = round($op->getY3(), self::PRECISION);
$pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
break;
case $op instanceof Close:
$pathData[] = 'z';
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
return implode(' ', $pathData);
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
{
$startColor = $gradient->getStartColor();
$endColor = $gradient->getEndColor();
if ($startColor instanceof Alpha) {
$startColor = $startColor->getBaseColor();
}
$startColorType = get_class($startColor);
if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
$startColorType = Cmyk::class;
$startColor = $startColor->toCmyk();
}
if (get_class($endColor) !== $startColorType) {
switch ($startColorType) {
case Cmyk::class:
$endColor = $endColor->toCmyk();
break;
case Rgb::class:
$endColor = $endColor->toRgb();
break;
case Gray::class:
$endColor = $endColor->toGray();
break;
}
}
$this->eps .= "eoclip\n<<\n";
if ($gradient->getType() === GradientType::RADIAL()) {
$this->eps .= " /ShadingType 3\n";
} else {
$this->eps .= " /ShadingType 2\n";
}
$this->eps .= " /Extend [ true true ]\n"
. " /AntiAlias true\n";
switch ($startColorType) {
case Cmyk::class:
$this->eps .= " /ColorSpace /DeviceCMYK\n";
break;
case Rgb::class:
$this->eps .= " /ColorSpace /DeviceRGB\n";
break;
case Gray::class:
$this->eps .= " /ColorSpace /DeviceGray\n";
break;
}
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x + $width, self::PRECISION),
round($y, self::PRECISION)
);
break;
case GradientType::VERTICAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x, self::PRECISION),
round($y + $height, self::PRECISION)
);
break;
case GradientType::DIAGONAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x + $width, self::PRECISION),
round($y + $height, self::PRECISION)
);
break;
case GradientType::INVERSE_DIAGONAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y + $height, self::PRECISION),
round($x + $width, self::PRECISION),
round($y, self::PRECISION)
);
break;
case GradientType::RADIAL():
$centerX = ($x + $width) / 2;
$centerY = ($y + $height) / 2;
$this->eps .= sprintf(
" /Coords [ %s %s 0 %s %s %s ]\n",
round($centerX, self::PRECISION),
round($centerY, self::PRECISION),
round($centerX, self::PRECISION),
round($centerY, self::PRECISION),
round(max($width, $height) / 2, self::PRECISION)
);
break;
}
$this->eps .= " /Function\n"
. " <<\n"
. " /FunctionType 2\n"
. " /Domain [ 0 1 ]\n"
. sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor))
. sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor))
. " /N 1\n"
. " >>\n>>\nshfill\nQ\n";
}
private function getColorSetString(ColorInterface $color) : string
{
if ($color instanceof Rgb) {
return $this->getColorString($color) . ' rgb';
}
if ($color instanceof Cmyk) {
return $this->getColorString($color) . ' cmyk';
}
if ($color instanceof Gray) {
return $this->getColorString($color) . ' gray';
}
return $this->getColorSetString($color->toCmyk());
}
private function getColorString(ColorInterface $color) : string
{
if ($color instanceof Rgb) {
return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
}
if ($color instanceof Cmyk) {
return sprintf(
'%s %s %s %s',
$color->getCyan() / 100,
$color->getMagenta() / 100,
$color->getYellow() / 100,
$color->getBlack() / 100
);
}
if ($color instanceof Gray) {
return sprintf('%s', $color->getGray() / 100);
}
return $this->getColorString($color->toCmyk());
}
}
@@ -1,87 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
/**
* Interface for back ends able to to produce path based images.
*/
interface ImageBackEndInterface
{
/**
* Starts a new image.
*
* If a previous image was already started, previous data get erased.
*/
public function new(int $size, ColorInterface $backgroundColor) : void;
/**
* Transforms all following drawing operation coordinates by scaling them by a given factor.
*
* @throws RuntimeException if no image was started yet.
*/
public function scale(float $size) : void;
/**
* Transforms all following drawing operation coordinates by translating them by a given amount.
*
* @throws RuntimeException if no image was started yet.
*/
public function translate(float $x, float $y) : void;
/**
* Transforms all following drawing operation coordinates by rotating them by a given amount.
*
* @throws RuntimeException if no image was started yet.
*/
public function rotate(int $degrees) : void;
/**
* Pushes the current coordinate transformation onto a stack.
*
* @throws RuntimeException if no image was started yet.
*/
public function push() : void;
/**
* Pops the last coordinate transformation from a stack.
*
* @throws RuntimeException if no image was started yet.
*/
public function pop() : void;
/**
* Draws a path with a given color.
*
* @throws RuntimeException if no image was started yet.
*/
public function drawPathWithColor(Path $path, ColorInterface $color) : void;
/**
* Draws a path with a given gradient which spans the box described by the position and size.
*
* @throws RuntimeException if no image was started yet.
*/
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void;
/**
* Ends the image drawing operation and returns the resulting blob.
*
* This should reset the state of the back end and thus this method should only be callable once per image.
*
* @throws RuntimeException if no image was started yet.
*/
public function done() : string;
}
@@ -1,318 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
use Imagick;
use ImagickDraw;
use ImagickPixel;
final class ImagickImageBackEnd implements ImageBackEndInterface
{
private string $imageFormat;
private int $compressionQuality;
private ?Imagick $image;
private ?ImagickDraw $draw;
private ?int $gradientCount;
/**
* @var TransformationMatrix[]|null
*/
private ?array $matrices;
private ?int $matrixIndex;
public function __construct(string $imageFormat = 'png', int $compressionQuality = 100)
{
if (! class_exists(Imagick::class)) {
throw new RuntimeException('You need to install the imagick extension to use this back end');
}
$this->imageFormat = $imageFormat;
$this->compressionQuality = $compressionQuality;
}
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->image = new Imagick();
$this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
$this->image->setImageFormat($this->imageFormat);
$this->image->setCompressionQuality($this->compressionQuality);
$this->draw = new ImagickDraw();
$this->gradientCount = 0;
$this->matrices = [new TransformationMatrix()];
$this->matrixIndex = 0;
}
public function scale(float $size) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->scale($size, $size);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::scale($size));
}
public function translate(float $x, float $y) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->translate($x, $y);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::translate($x, $y));
}
public function rotate(int $degrees) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->rotate($degrees);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::rotate($degrees));
}
public function push() : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->push();
$this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
}
public function pop() : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->pop();
unset($this->matrices[$this->matrixIndex--]);
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->setFillColor($this->getColorPixel($color));
$this->drawPath($path);
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
$this->drawPath($path);
}
public function done() : string
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->image->drawImage($this->draw);
$blob = $this->image->getImageBlob();
$this->draw->clear();
$this->image->clear();
$this->draw = null;
$this->image = null;
$this->gradientCount = null;
return $blob;
}
private function drawPath(Path $path) : void
{
$this->draw->pathStart();
foreach ($path as $op) {
switch (true) {
case $op instanceof Move:
$this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
break;
case $op instanceof Line:
$this->draw->pathLineToAbsolute($op->getX(), $op->getY());
break;
case $op instanceof EllipticArc:
$this->draw->pathEllipticArcAbsolute(
$op->getXRadius(),
$op->getYRadius(),
$op->getXAxisAngle(),
$op->isLargeArc(),
$op->isSweep(),
$op->getX(),
$op->getY()
);
break;
case $op instanceof Curve:
$this->draw->pathCurveToAbsolute(
$op->getX1(),
$op->getY1(),
$op->getX2(),
$op->getY2(),
$op->getX3(),
$op->getY3()
);
break;
case $op instanceof Close:
$this->draw->pathClose();
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
$this->draw->pathFinish();
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
{
list($width, $height) = $this->matrices[$this->matrixIndex]->apply($width, $height);
$startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
$endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
$gradientImage = new Imagick();
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
$gradientImage->rotateImage('transparent', -90);
break;
case GradientType::VERTICAL():
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
break;
case GradientType::DIAGONAL():
case GradientType::INVERSE_DIAGONAL():
$gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
if (GradientType::DIAGONAL() === $gradient->getType()) {
$gradientImage->rotateImage('transparent', -45);
} else {
$gradientImage->rotateImage('transparent', -135);
}
$rotatedWidth = $gradientImage->getImageWidth();
$rotatedHeight = $gradientImage->getImageHeight();
$gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
$gradientImage->cropImage(
intdiv($rotatedWidth, 2) - 2,
intdiv($rotatedHeight, 2) - 2,
intdiv($rotatedWidth, 4) + 1,
intdiv($rotatedWidth, 4) + 1
);
break;
case GradientType::RADIAL():
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
'radial-gradient:%s-%s',
$startColor,
$endColor
));
break;
}
$id = sprintf('g%d', ++$this->gradientCount);
$this->draw->pushPattern($id, 0, 0, $width, $height);
$this->draw->composite(Imagick::COMPOSITE_COPY, 0, 0, $width, $height, $gradientImage);
$this->draw->popPattern();
return $id;
}
private function getColorPixel(ColorInterface $color) : ImagickPixel
{
$alpha = 100;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha();
$color = $color->getBaseColor();
}
if ($color instanceof Rgb) {
return new ImagickPixel(sprintf(
'rgba(%d, %d, %d, %F)',
$color->getRed(),
$color->getGreen(),
$color->getBlue(),
$alpha / 100
));
}
if ($color instanceof Cmyk) {
return new ImagickPixel(sprintf(
'cmyka(%d, %d, %d, %d, %F)',
$color->getCyan(),
$color->getMagenta(),
$color->getYellow(),
$color->getBlack(),
$alpha / 100
));
}
if ($color instanceof Gray) {
return new ImagickPixel(sprintf(
'graya(%d%%, %F)',
$color->getGray(),
$alpha / 100
));
}
return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
}
}
@@ -1,363 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
use XMLWriter;
final class SvgImageBackEnd implements ImageBackEndInterface
{
private const PRECISION = 3;
private const SCALE_FORMAT = 'scale(%.' . self::PRECISION . 'F)';
private const TRANSLATE_FORMAT = 'translate(%.' . self::PRECISION . 'F,%.' . self::PRECISION . 'F)';
private ?XMLWriter $xmlWriter;
private ?array $stack;
private ?int $currentStack;
private ?int $gradientCount;
public function __construct()
{
if (! class_exists(XMLWriter::class)) {
throw new RuntimeException('You need to install the libxml extension to use this back end');
}
}
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->xmlWriter = new XMLWriter();
$this->xmlWriter->openMemory();
$this->xmlWriter->startDocument('1.0', 'UTF-8');
$this->xmlWriter->startElement('svg');
$this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
$this->xmlWriter->writeAttribute('version', '1.1');
$this->xmlWriter->writeAttribute('width', (string) $size);
$this->xmlWriter->writeAttribute('height', (string) $size);
$this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
$this->gradientCount = 0;
$this->currentStack = 0;
$this->stack[0] = 0;
$alpha = 1;
if ($backgroundColor instanceof Alpha) {
$alpha = $backgroundColor->getAlpha() / 100;
}
if (0 === $alpha) {
return;
}
$this->xmlWriter->startElement('rect');
$this->xmlWriter->writeAttribute('x', '0');
$this->xmlWriter->writeAttribute('y', '0');
$this->xmlWriter->writeAttribute('width', (string) $size);
$this->xmlWriter->writeAttribute('height', (string) $size);
$this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
if ($alpha < 1) {
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
}
$this->xmlWriter->endElement();
}
public function scale(float $size) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute(
'transform',
sprintf(self::SCALE_FORMAT, round($size, self::PRECISION))
);
++$this->stack[$this->currentStack];
}
public function translate(float $x, float $y) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute(
'transform',
sprintf(self::TRANSLATE_FORMAT, round($x, self::PRECISION), round($y, self::PRECISION))
);
++$this->stack[$this->currentStack];
}
public function rotate(int $degrees) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
++$this->stack[$this->currentStack];
}
public function push() : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->stack[] = 1;
++$this->currentStack;
}
public function pop() : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
$this->xmlWriter->endElement();
}
array_pop($this->stack);
--$this->currentStack;
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$alpha = 1;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha() / 100;
}
$this->startPathElement($path);
$this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
if ($alpha < 1) {
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
}
$this->xmlWriter->endElement();
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
$this->startPathElement($path);
$this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
$this->xmlWriter->endElement();
}
public function done() : string
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
foreach ($this->stack as $openElements) {
for ($i = $openElements; $i > 0; --$i) {
$this->xmlWriter->endElement();
}
}
$this->xmlWriter->endDocument();
$blob = $this->xmlWriter->outputMemory(true);
$this->xmlWriter = null;
$this->stack = null;
$this->currentStack = null;
$this->gradientCount = null;
return $blob;
}
private function startPathElement(Path $path) : void
{
$pathData = [];
foreach ($path as $op) {
switch (true) {
case $op instanceof Move:
$pathData[] = sprintf(
'M%s %s',
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof Line:
$pathData[] = sprintf(
'L%s %s',
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof EllipticArc:
$pathData[] = sprintf(
'A%s %s %s %u %u %s %s',
round($op->getXRadius(), self::PRECISION),
round($op->getYRadius(), self::PRECISION),
round($op->getXAxisAngle(), self::PRECISION),
$op->isLargeArc(),
$op->isSweep(),
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof Curve:
$pathData[] = sprintf(
'C%s %s %s %s %s %s',
round($op->getX1(), self::PRECISION),
round($op->getY1(), self::PRECISION),
round($op->getX2(), self::PRECISION),
round($op->getY2(), self::PRECISION),
round($op->getX3(), self::PRECISION),
round($op->getY3(), self::PRECISION)
);
break;
case $op instanceof Close:
$pathData[] = 'Z';
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
$this->xmlWriter->startElement('path');
$this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
$this->xmlWriter->writeAttribute('d', implode('', $pathData));
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
{
$this->xmlWriter->startElement('defs');
$startColor = $gradient->getStartColor();
$endColor = $gradient->getEndColor();
if ($gradient->getType() === GradientType::RADIAL()) {
$this->xmlWriter->startElement('radialGradient');
} else {
$this->xmlWriter->startElement('linearGradient');
}
$this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
break;
case GradientType::VERTICAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
break;
case GradientType::DIAGONAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
break;
case GradientType::INVERSE_DIAGONAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
break;
case GradientType::RADIAL():
$this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
$this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
$this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
break;
}
$toBeHashed = $this->getColorString($startColor) . $this->getColorString($endColor) . $gradient->getType();
if ($startColor instanceof Alpha) {
$toBeHashed .= (string) $startColor->getAlpha();
}
$id = sprintf('g%d-%s', ++$this->gradientCount, hash('xxh64', $toBeHashed));
$this->xmlWriter->writeAttribute('id', $id);
$this->xmlWriter->startElement('stop');
$this->xmlWriter->writeAttribute('offset', '0%');
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
if ($startColor instanceof Alpha) {
$this->xmlWriter->writeAttribute('stop-opacity', (string) $startColor->getAlpha());
}
$this->xmlWriter->endElement();
$this->xmlWriter->startElement('stop');
$this->xmlWriter->writeAttribute('offset', '100%');
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
if ($endColor instanceof Alpha) {
$this->xmlWriter->writeAttribute('stop-opacity', (string) $endColor->getAlpha());
}
$this->xmlWriter->endElement();
$this->xmlWriter->endElement();
$this->xmlWriter->endElement();
return $id;
}
private function getColorString(ColorInterface $color) : string
{
$color = $color->toRgb();
return sprintf(
'#%02x%02x%02x',
$color->getRed(),
$color->getGreen(),
$color->getBlue()
);
}
}
@@ -1,68 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
final class TransformationMatrix
{
/**
* @var float[]
*/
private array $values;
public function __construct()
{
$this->values = [1, 0, 0, 1, 0, 0];
}
public function multiply(self $other) : self
{
$matrix = new self();
$matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1];
$matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1];
$matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3];
$matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3];
$matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5]
+ $this->values[4];
$matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5]
+ $this->values[5];
return $matrix;
}
public static function scale(float $size) : self
{
$matrix = new self();
$matrix->values = [$size, 0, 0, $size, 0, 0];
return $matrix;
}
public static function translate(float $x, float $y) : self
{
$matrix = new self();
$matrix->values = [1, 0, 0, 1, $x, $y];
return $matrix;
}
public static function rotate(int $degrees) : self
{
$matrix = new self();
$rad = deg2rad($degrees);
$matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0];
return $matrix;
}
/**
* Applies this matrix onto a point and returns the resulting viewport point.
*
* @return float[]
*/
public function apply(float $x, float $y) : array
{
return [
$x * $this->values[0] + $y * $this->values[2] + $this->values[4],
$x * $this->values[1] + $y * $this->values[3] + $this->values[5],
];
}
}
@@ -1,150 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\MatrixUtil;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Image\ImageBackEndInterface;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
final class ImageRenderer implements RendererInterface
{
public function __construct(
private readonly RendererStyle $rendererStyle,
private readonly ImageBackEndInterface $imageBackEnd
) {
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode) : string
{
$size = $this->rendererStyle->getSize();
$margin = $this->rendererStyle->getMargin();
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
$totalSize = $matrixSize + ($margin * 2);
$moduleSize = $size / $totalSize;
$fill = $this->rendererStyle->getFill();
$this->imageBackEnd->new($size, $fill->getBackgroundColor());
$this->imageBackEnd->scale((float) $moduleSize);
$this->imageBackEnd->translate((float) $margin, (float) $margin);
$module = $this->rendererStyle->getModule();
$moduleMatrix = clone $matrix;
MatrixUtil::removePositionDetectionPatterns($moduleMatrix);
$modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix));
if ($fill->hasGradientFill()) {
$this->imageBackEnd->drawPathWithGradient(
$modulePath,
$fill->getForegroundGradient(),
0,
0,
$matrixSize,
$matrixSize
);
} else {
$this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor());
}
return $this->imageBackEnd->done();
}
private function drawEyes(int $matrixSize, Path $modulePath) : Path
{
$fill = $this->rendererStyle->getFill();
$eye = $this->rendererStyle->getEye();
$externalPath = $eye->getExternalPath();
$internalPath = $eye->getInternalPath();
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getTopLeftEyeFill(),
3.5,
3.5,
0,
$modulePath
);
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getTopRightEyeFill(),
$matrixSize - 3.5,
3.5,
90,
$modulePath
);
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getBottomLeftEyeFill(),
3.5,
$matrixSize - 3.5,
-90,
$modulePath
);
return $modulePath;
}
private function drawEye(
Path $externalPath,
Path $internalPath,
EyeFill $fill,
float $xTranslation,
float $yTranslation,
int $rotation,
Path $modulePath
) : Path {
if ($fill->inheritsBothColors()) {
return $modulePath
->append(
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
)
->append(
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
}
$this->imageBackEnd->push();
$this->imageBackEnd->translate($xTranslation, $yTranslation);
if (0 !== $rotation) {
$this->imageBackEnd->rotate($rotation);
}
if ($fill->inheritsExternalColor()) {
$modulePath = $modulePath->append(
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
} else {
$this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
}
if ($fill->inheritsInternalColor()) {
$modulePath = $modulePath->append(
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
} else {
$this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
}
$this->imageBackEnd->pop();
return $modulePath;
}
}
@@ -1,56 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders individual modules as dots.
*/
final class DotsModule implements ModuleInterface
{
public const LARGE = 1;
public const MEDIUM = .8;
public const SMALL = .6;
public function __construct(private readonly float $size)
{
if ($size <= 0 || $size > 1) {
throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)');
}
}
public function createPath(ByteMatrix $matrix) : Path
{
$width = $matrix->getWidth();
$height = $matrix->getHeight();
$path = new Path();
$halfSize = $this->size / 2;
$margin = (1 - $this->size) / 2;
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
if (! $matrix->get($x, $y)) {
continue;
}
$pathX = $x + $margin;
$pathY = $y + $margin;
$path = $path
->move($pathX + $this->size, $pathY + $halfSize)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY + $this->size)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX, $pathY + $halfSize)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $this->size, $pathY + $halfSize)
->close()
;
}
}
return $path;
}
}
@@ -1,82 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module\EdgeIterator;
final class Edge
{
/**
* @var array<int[]>
*/
private array $points = [];
/**
* @var array<int[]>|null
*/
private ?array $simplifiedPoints = null;
private int $minX = PHP_INT_MAX;
private int $minY = PHP_INT_MAX;
private int $maxX = -1;
private int $maxY = -1;
public function __construct(private readonly bool $positive)
{
}
public function addPoint(int $x, int $y) : void
{
$this->points[] = [$x, $y];
$this->minX = min($this->minX, $x);
$this->minY = min($this->minY, $y);
$this->maxX = max($this->maxX, $x);
$this->maxY = max($this->maxY, $y);
}
public function isPositive() : bool
{
return $this->positive;
}
/**
* @return array<int[]>
*/
public function getPoints() : array
{
return $this->points;
}
public function getMaxX() : int
{
return $this->maxX;
}
public function getSimplifiedPoints() : array
{
if (null !== $this->simplifiedPoints) {
return $this->simplifiedPoints;
}
$points = [];
$length = count($this->points);
for ($i = 0; $i < $length; ++$i) {
$previousPoint = $this->points[(0 === $i ? $length : $i) - 1];
$nextPoint = $this->points[($length - 1 === $i ? -1 : $i) + 1];
$currentPoint = $this->points[$i];
if (($previousPoint[0] === $currentPoint[0] && $currentPoint[0] === $nextPoint[0])
|| ($previousPoint[1] === $currentPoint[1] && $currentPoint[1] === $nextPoint[1])
) {
continue;
}
$points[] = $currentPoint;
}
return $this->simplifiedPoints = $points;
}
}
@@ -1,160 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module\EdgeIterator;
use BaconQrCode\Encoder\ByteMatrix;
use IteratorAggregate;
use Traversable;
/**
* Edge iterator based on potrace.
*/
final class EdgeIterator implements IteratorAggregate
{
/**
* @var int[]
*/
private array $bytes = [];
private ?int $size;
private int $width;
private int $height;
public function __construct(ByteMatrix $matrix)
{
$this->bytes = iterator_to_array($matrix->getBytes());
$this->size = count($this->bytes);
$this->width = $matrix->getWidth();
$this->height = $matrix->getHeight();
}
/**
* @return Traversable<Edge>
*/
public function getIterator() : Traversable
{
$originalBytes = $this->bytes;
$point = $this->findNext(0, 0);
while (null !== $point) {
$edge = $this->findEdge($point[0], $point[1]);
$this->xorEdge($edge);
yield $edge;
$point = $this->findNext($point[0], $point[1]);
}
$this->bytes = $originalBytes;
}
/**
* @return int[]|null
*/
private function findNext(int $x, int $y) : ?array
{
$i = $this->width * $y + $x;
while ($i < $this->size && 1 !== $this->bytes[$i]) {
++$i;
}
if ($i < $this->size) {
return $this->pointOf($i);
}
return null;
}
private function findEdge(int $x, int $y) : Edge
{
$edge = new Edge($this->isSet($x, $y));
$startX = $x;
$startY = $y;
$dirX = 0;
$dirY = 1;
while (true) {
$edge->addPoint($x, $y);
$x += $dirX;
$y += $dirY;
if ($x === $startX && $y === $startY) {
break;
}
$left = $this->isSet($x + ($dirX + $dirY - 1 ) / 2, $y + ($dirY - $dirX - 1) / 2);
$right = $this->isSet($x + ($dirX - $dirY - 1) / 2, $y + ($dirY + $dirX - 1) / 2);
if ($right && ! $left) {
$tmp = $dirX;
$dirX = -$dirY;
$dirY = $tmp;
} elseif ($right) {
$tmp = $dirX;
$dirX = -$dirY;
$dirY = $tmp;
} elseif (! $left) {
$tmp = $dirX;
$dirX = $dirY;
$dirY = -$tmp;
}
}
return $edge;
}
private function xorEdge(Edge $path) : void
{
$points = $path->getPoints();
$y1 = $points[0][1];
$length = count($points);
$maxX = $path->getMaxX();
for ($i = 1; $i < $length; ++$i) {
$y = $points[$i][1];
if ($y === $y1) {
continue;
}
$x = $points[$i][0];
$minY = min($y1, $y);
for ($j = $x; $j < $maxX; ++$j) {
$this->flip($j, $minY);
}
$y1 = $y;
}
}
private function isSet(int $x, int $y) : bool
{
return (
$x >= 0
&& $x < $this->width
&& $y >= 0
&& $y < $this->height
) && 1 === $this->bytes[$this->width * $y + $x];
}
/**
* @return int[]
*/
private function pointOf(int $i) : array
{
$y = intdiv($i, $this->width);
return [$i - $y * $this->width, $y];
}
private function flip(int $x, int $y) : void
{
$this->bytes[$this->width * $y + $x] = (
$this->isSet($x, $y) ? 0 : 1
);
}
}
@@ -1,18 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Path\Path;
/**
* Interface describing how modules should be rendered.
*
* A module always receives a byte matrix (with values either being 1 or 0). It returns a path, where the origin
* coordinate (0, 0) equals the top left corner of the first matrix value.
*/
interface ModuleInterface
{
public function createPath(ByteMatrix $matrix) : Path;
}
@@ -1,124 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
use BaconQrCode\Renderer\Path\Path;
/**
* Rounds the corners of module groups.
*/
final class RoundnessModule implements ModuleInterface
{
public const STRONG = 1;
public const MEDIUM = .5;
public const SOFT = .25;
public function __construct(private float $intensity)
{
if ($intensity <= 0 || $intensity > 1) {
throw new InvalidArgumentException('Intensity must between 0 (exclusive) and 1 (inclusive)');
}
$this->intensity = $intensity / 2;
}
public function createPath(ByteMatrix $matrix) : Path
{
$path = new Path();
foreach (new EdgeIterator($matrix) as $edge) {
$points = $edge->getSimplifiedPoints();
$length = count($points);
$currentPoint = $points[0];
$nextPoint = $points[1];
$horizontal = ($currentPoint[1] === $nextPoint[1]);
if ($horizontal) {
$right = $nextPoint[0] > $currentPoint[0];
$path = $path->move(
$currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
$currentPoint[1]
);
} else {
$up = $nextPoint[0] < $currentPoint[0];
$path = $path->move(
$currentPoint[0],
$currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
);
}
for ($i = 1; $i <= $length; ++$i) {
if ($i === $length) {
$previousPoint = $points[$length - 1];
$currentPoint = $points[0];
$nextPoint = $points[1];
} else {
$previousPoint = $points[(0 === $i ? $length : $i) - 1];
$currentPoint = $points[$i];
$nextPoint = $points[($length - 1 === $i ? -1 : $i) + 1];
}
$horizontal = ($previousPoint[1] === $currentPoint[1]);
if ($horizontal) {
$right = $previousPoint[0] < $currentPoint[0];
$up = $nextPoint[1] < $currentPoint[1];
$sweep = ($up xor $right);
if ($this->intensity < 0.5
|| ($right && $previousPoint[0] !== $currentPoint[0] - 1)
|| (! $right && $previousPoint[0] - 1 !== $currentPoint[0])
) {
$path = $path->line(
$currentPoint[0] + ($right ? -$this->intensity : $this->intensity),
$currentPoint[1]
);
}
$path = $path->ellipticArc(
$this->intensity,
$this->intensity,
0,
false,
$sweep,
$currentPoint[0],
$currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
);
} else {
$up = $previousPoint[1] > $currentPoint[1];
$right = $nextPoint[0] > $currentPoint[0];
$sweep = ! ($up xor $right);
if ($this->intensity < 0.5
|| ($up && $previousPoint[1] !== $currentPoint[1] + 1)
|| (! $up && $previousPoint[0] + 1 !== $currentPoint[0])
) {
$path = $path->line(
$currentPoint[0],
$currentPoint[1] + ($up ? $this->intensity : -$this->intensity)
);
}
$path = $path->ellipticArc(
$this->intensity,
$this->intensity,
0,
false,
$sweep,
$currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
$currentPoint[1]
);
}
}
$path = $path->close();
}
return $path;
}
}
@@ -1,44 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
use BaconQrCode\Renderer\Path\Path;
/**
* Groups modules together to a single path.
*/
final class SquareModule implements ModuleInterface
{
private static ?SquareModule $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function createPath(ByteMatrix $matrix) : Path
{
$path = new Path();
foreach (new EdgeIterator($matrix) as $edge) {
$points = $edge->getSimplifiedPoints();
$length = count($points);
$path = $path->move($points[0][0], $points[0][1]);
for ($i = 1; $i < $length; ++$i) {
$path = $path->line($points[$i][0], $points[$i][1]);
}
$path = $path->close();
}
return $path;
}
}
-34
View File
@@ -1,34 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Close implements OperationInterface
{
private static ?Close $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return $this;
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
return $this;
}
}
-86
View File
@@ -1,86 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Curve implements OperationInterface
{
public function __construct(
private readonly float $x1,
private readonly float $y1,
private readonly float $x2,
private readonly float $y2,
private readonly float $x3,
private readonly float $y3
) {
}
public function getX1() : float
{
return $this->x1;
}
public function getY1() : float
{
return $this->y1;
}
public function getX2() : float
{
return $this->x2;
}
public function getY2() : float
{
return $this->y2;
}
public function getX3() : float
{
return $this->x3;
}
public function getY3() : float
{
return $this->y3;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self(
$this->x1 + $x,
$this->y1 + $y,
$this->x2 + $x,
$this->y2 + $y,
$this->x3 + $x,
$this->y3 + $y
);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$x1r = $this->x1 * $cos - $this->y1 * $sin;
$y1r = $this->x1 * $sin + $this->y1 * $cos;
$x2r = $this->x2 * $cos - $this->y2 * $sin;
$y2r = $this->x2 * $sin + $this->y2 * $cos;
$x3r = $this->x3 * $cos - $this->y3 * $sin;
$y3r = $this->x3 * $sin + $this->y3 * $cos;
return new self(
$x1r,
$y1r,
$x2r,
$y2r,
$x3r,
$y3r
);
}
}
@@ -1,264 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class EllipticArc implements OperationInterface
{
private const ZERO_TOLERANCE = 1e-05;
private float $xRadius;
private float $yRadius;
private float $xAxisAngle;
public function __construct(
float $xRadius,
float $yRadius,
float $xAxisAngle,
private readonly bool $largeArc,
private readonly bool $sweep,
private readonly float $x,
private readonly float $y
) {
$this->xRadius = abs($xRadius);
$this->yRadius = abs($yRadius);
$this->xAxisAngle = $xAxisAngle % 360;
}
public function getXRadius() : float
{
return $this->xRadius;
}
public function getYRadius() : float
{
return $this->yRadius;
}
public function getXAxisAngle() : float
{
return $this->xAxisAngle;
}
public function isLargeArc() : bool
{
return $this->largeArc;
}
public function isSweep() : bool
{
return $this->sweep;
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self(
$this->xRadius,
$this->yRadius,
$this->xAxisAngle,
$this->largeArc,
$this->sweep,
$this->x + $x,
$this->y + $y
);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$xr = $this->x * $cos - $this->y * $sin;
$yr = $this->x * $sin + $this->y * $cos;
return new self(
$this->xRadius,
$this->yRadius,
$this->xAxisAngle,
$this->largeArc,
$this->sweep,
$xr,
$yr
);
}
/**
* Converts the elliptic arc to multiple curves.
*
* Since not all image back ends support elliptic arcs, this method allows to convert the arc into multiple curves
* resembling the same result.
*
* @see https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
* @return array<Curve|Line>
*/
public function toCurves(float $fromX, float $fromY) : array
{
if (sqrt(($fromX - $this->x) ** 2 + ($fromY - $this->y) ** 2) < self::ZERO_TOLERANCE) {
return [];
}
if ($this->xRadius < self::ZERO_TOLERANCE || $this->yRadius < self::ZERO_TOLERANCE) {
return [new Line($this->x, $this->y)];
}
return $this->createCurves($fromX, $fromY);
}
/**
* @return Curve[]
*/
private function createCurves(float $fromX, float $fromY) : array
{
$xAngle = deg2rad($this->xAxisAngle);
list($centerX, $centerY, $radiusX, $radiusY, $startAngle, $deltaAngle) =
$this->calculateCenterPointParameters($fromX, $fromY, $xAngle);
$s = $startAngle;
$e = $s + $deltaAngle;
$sign = ($e < $s) ? -1 : 1;
$remain = abs($e - $s);
$p1 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s);
$curves = [];
while ($remain > self::ZERO_TOLERANCE) {
$step = min($remain, pi() / 2);
$signStep = $step * $sign;
$p2 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s + $signStep);
$alphaT = tan($signStep / 2);
$alpha = sin($signStep) * (sqrt(4 + 3 * $alphaT ** 2) - 1) / 3;
$d1 = self::derivative($radiusX, $radiusY, $xAngle, $s);
$d2 = self::derivative($radiusX, $radiusY, $xAngle, $s + $signStep);
$curves[] = new Curve(
$p1[0] + $alpha * $d1[0],
$p1[1] + $alpha * $d1[1],
$p2[0] - $alpha * $d2[0],
$p2[1] - $alpha * $d2[1],
$p2[0],
$p2[1]
);
$s += $signStep;
$remain -= $step;
$p1 = $p2;
}
return $curves;
}
/**
* @return float[]
*/
private function calculateCenterPointParameters(float $fromX, float $fromY, float $xAngle): array
{
$rX = $this->xRadius;
$rY = $this->yRadius;
// F.6.5.1
$dx2 = ($fromX - $this->x) / 2;
$dy2 = ($fromY - $this->y) / 2;
$x1p = cos($xAngle) * $dx2 + sin($xAngle) * $dy2;
$y1p = -sin($xAngle) * $dx2 + cos($xAngle) * $dy2;
// F.6.5.2
$rxs = $rX ** 2;
$rys = $rY ** 2;
$x1ps = $x1p ** 2;
$y1ps = $y1p ** 2;
$cr = $x1ps / $rxs + $y1ps / $rys;
if ($cr > 1) {
$s = sqrt($cr);
$rX *= $s;
$rY *= $s;
$rxs = $rX ** 2;
$rys = $rY ** 2;
}
$dq = ($rxs * $y1ps + $rys * $x1ps);
$pq = ($rxs * $rys - $dq) / $dq;
$q = sqrt(max(0, $pq));
if ($this->largeArc === $this->sweep) {
$q = -$q;
}
$cxp = $q * $rX * $y1p / $rY;
$cyp = -$q * $rY * $x1p / $rX;
// F.6.5.3
$cx = cos($xAngle) * $cxp - sin($xAngle) * $cyp + ($fromX + $this->x) / 2;
$cy = sin($xAngle) * $cxp + cos($xAngle) * $cyp + ($fromY + $this->y) / 2;
// F.6.5.5
$theta = self::angle(1, 0, ($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY);
// F.6.5.6
$delta = self::angle(($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY, (-$x1p - $cxp) / $rX, (-$y1p - $cyp) / $rY);
$delta = fmod($delta, pi() * 2);
if (! $this->sweep) {
$delta -= 2 * pi();
}
return [$cx, $cy, $rX, $rY, $theta, $delta];
}
private static function angle(float $ux, float $uy, float $vx, float $vy) : float
{
// F.6.5.4
$dot = $ux * $vx + $uy * $vy;
$length = sqrt($ux ** 2 + $uy ** 2) * sqrt($vx ** 2 + $vy ** 2);
$angle = acos(min(1, max(-1, $dot / $length)));
if (($ux * $vy - $uy * $vx) < 0) {
return -$angle;
}
return $angle;
}
/**
* @return float[]
*/
private static function point(
float $centerX,
float $centerY,
float $radiusX,
float $radiusY,
float $xAngle,
float $angle
) : array {
return [
$centerX + $radiusX * cos($xAngle) * cos($angle) - $radiusY * sin($xAngle) * sin($angle),
$centerY + $radiusX * sin($xAngle) * cos($angle) + $radiusY * cos($xAngle) * sin($angle),
];
}
/**
* @return float[]
*/
private static function derivative(float $radiusX, float $radiusY, float $xAngle, float $angle) : array
{
return [
-$radiusX * cos($xAngle) * sin($angle) - $radiusY * sin($xAngle) * cos($angle),
-$radiusX * sin($xAngle) * sin($angle) + $radiusY * cos($xAngle) * cos($angle),
];
}
}
-42
View File
@@ -1,42 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Line implements OperationInterface
{
public function __construct(private readonly float $x, private readonly float $y)
{
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self($this->x + $x, $this->y + $y);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$xr = $this->x * $cos - $this->y * $sin;
$yr = $this->x * $sin + $this->y * $cos;
return new self($xr, $yr);
}
}
-42
View File
@@ -1,42 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Move implements OperationInterface
{
public function __construct(private readonly float $x, private readonly float $y)
{
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self($this->x + $x, $this->y + $y);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$xr = $this->x * $cos - $this->y * $sin;
$yr = $this->x * $sin + $this->y * $cos;
return new self($xr, $yr);
}
}
@@ -1,17 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
interface OperationInterface
{
/**
* Translates the operation's coordinates.
*/
public function translate(float $x, float $y) : self;
/**
* Rotates the operation's coordinates.
*/
public function rotate(int $degrees) : self;
}
-117
View File
@@ -1,117 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
use IteratorAggregate;
use Traversable;
/**
* Internal Representation of a vector path.
*/
final class Path implements IteratorAggregate
{
/**
* @var OperationInterface[]
*/
private array $operations = [];
/**
* Moves the drawing operation to a certain position.
*/
public function move(float $x, float $y) : self
{
$path = clone $this;
$path->operations[] = new Move($x, $y);
return $path;
}
/**
* Draws a line from the current position to another position.
*/
public function line(float $x, float $y) : self
{
$path = clone $this;
$path->operations[] = new Line($x, $y);
return $path;
}
/**
* Draws an elliptic arc from the current position to another position.
*/
public function ellipticArc(
float $xRadius,
float $yRadius,
float $xAxisRotation,
bool $largeArc,
bool $sweep,
float $x,
float $y
) : self {
$path = clone $this;
$path->operations[] = new EllipticArc($xRadius, $yRadius, $xAxisRotation, $largeArc, $sweep, $x, $y);
return $path;
}
/**
* Draws a curve from the current position to another position.
*/
public function curve(float $x1, float $y1, float $x2, float $y2, float $x3, float $y3) : self
{
$path = clone $this;
$path->operations[] = new Curve($x1, $y1, $x2, $y2, $x3, $y3);
return $path;
}
/**
* Closes a sub-path.
*/
public function close() : self
{
$path = clone $this;
$path->operations[] = Close::instance();
return $path;
}
/**
* Appends another path to this one.
*/
public function append(self $other) : self
{
$path = clone $this;
$path->operations = array_merge($this->operations, $other->operations);
return $path;
}
public function translate(float $x, float $y) : self
{
$path = new self();
foreach ($this->operations as $operation) {
$path->operations[] = $operation->translate($x, $y);
}
return $path;
}
public function rotate(int $degrees) : self
{
$path = new self();
foreach ($this->operations as $operation) {
$path->operations[] = $operation->rotate($degrees);
}
return $path;
}
/**
* @return Traversable<int, OperationInterface>
*/
public function getIterator() : Traversable
{
foreach ($this->operations as $operation) {
yield $operation;
}
}
}
@@ -1,80 +0,0 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
final class PlainTextRenderer implements RendererInterface
{
/**
* UTF-8 full block (U+2588)
*/
private const FULL_BLOCK = "\xe2\x96\x88";
/**
* UTF-8 upper half block (U+2580)
*/
private const UPPER_HALF_BLOCK = "\xe2\x96\x80";
/**
* UTF-8 lower half block (U+2584)
*/
private const LOWER_HALF_BLOCK = "\xe2\x96\x84";
/**
* UTF-8 no-break space (U+00A0)
*/
private const EMPTY_BLOCK = "\xc2\xa0";
public function __construct(private readonly int $margin = 2)
{
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode) : string
{
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
$rows = $matrix->getArray()->toArray();
if (0 !== $matrixSize % 2) {
$rows[] = array_fill(0, $matrixSize, 0);
}
$horizontalMargin = str_repeat(self::EMPTY_BLOCK, $this->margin);
$result = str_repeat("\n", (int) ceil($this->margin / 2));
for ($i = 0; $i < $matrixSize; $i += 2) {
$result .= $horizontalMargin;
$upperRow = $rows[$i];
$lowerRow = $rows[$i + 1];
for ($j = 0; $j < $matrixSize; ++$j) {
$upperBit = $upperRow[$j];
$lowerBit = $lowerRow[$j];
if ($upperBit) {
$result .= $lowerBit ? self::FULL_BLOCK : self::UPPER_HALF_BLOCK;
} else {
$result .= $lowerBit ? self::LOWER_HALF_BLOCK : self::EMPTY_BLOCK;
}
}
$result .= $horizontalMargin . "\n";
}
$result .= str_repeat("\n", (int) ceil($this->margin / 2));
return $result;
}
}

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