Last minute changes.

This commit is contained in:
Vilyaem 2025-03-06 14:50:41 -05:00
commit 7f67f244f4
157 changed files with 46467 additions and 0 deletions

121
LICENSE Normal file
View file

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

179
README.TXT Normal file
View file

@ -0,0 +1,179 @@
------------------------------------------------------------------------------
/| //| |
//| // | | ___ __ ___ ___
// | // | | // ) ) // ) ) // ) ) // ) ) // / /
// | // | | // / / // / / //___/ / // / / ((___/ /
// |// | | ((___/ / // / / // ((___( ( / /
Monpay (now renamed to Neropay) is a simple GET-only PHP Monero & Wownero
cryptocurrency payment system. it uses the XMPP chat protocol for
notifications, receipts, and support.
Manifest
It is unnecessarily difficult to integrate a cryptocurrency payment system into
one or more online shops and businesses, one is pressured and tempted into
using foreign and bloated APIs or plugins into preexisting (and terrible)
Content Management Systems (CMS) such as 'Wordpress', and yet still not make
100% of the profit, as well as dealing with restrictions, such as a cap on
number of transactions per month. There is some libre-licensed projects that
allow individuals to host crypto payments, but these usually come with caveats
of their own, such as making Monero a second priority, using Javascript, having
a difficult setup, large number of lines, not handling multiple shops or
services, and not allowing for user instantiated services and businesses.
Features
Pros:
* Sale of digital items
* Sale of physical items
* XMPP notifications
* under 1K LOC
* No javascript
* Easy integration with websites
* Optionally redirect users after purchase
* Generates QR codes during payment
* No middle man (other than you)
* No fees
* Self hosted
* No docker
* Support for Wownero WOW
* Automatic documentation via doxygen
Cons:
* PHP sucks
* so does the 'composer' package manager
Setup
1. Install in a web directory
2. Install php-fpm, and php-curl if you havn't, check the output of phpinfo();
3. SQLite for PHP is usually default, check if so
4. Install the monero package, remote node RPC setup is below
monero-wallet-rpc --rpc-bind-port 18083 --disable-rpc-login --wallet-dir wallets --daemon-address soynero.net.org.com.edu.gov:18081
Consider making a service, the following is an example SysVInit service
script:
-------------------------------
#!/bin/sh
### BEGIN INIT INFO
# Provides: monero
# Required-Start: $local_fs $remote_fs $network $syslog $named
# Required-Stop: $local_fs $remote_fs $network $syslog $named
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start Monero Wallet RPC
# Description: Start Monero Wallet RPC
### END INIT INFO
NAME=monero
DESC=monero
. /lib/init/vars.sh
. /lib/lsb/init-functions
start_monero() {
# Start the daemon/service
#Change daemon address if necessary!
monero-wallet-rpc --rpc-bind-port 18083 --disable-rpc-login --wallet-dir /var/www/neropay/wallets --daemon-address 127.0.0.1:18081 --log-file /var/log/monero.log --detach
}
stop_monero() {
pkill monero-wallet
}
case "$1" in
start)
start_monero
;;
stop)
stop_monero
case "$?" in
0|1) log_end_msg 0 ;;
2) log_end_msg 1 ;;
esac
;;
*)
echo "Usage: $NAME {start|stop|restart}" >&2
exit 3
;;
esac
-------------------------------
5. Ensure www-data ownership/permissions, unix nonsense
6. Put items in data/items.txt according to the format
7. NGINX/httpd web server configuration
8. Test to see if transactions work
9. Turn off error reporting and benchmarking for live usage
10. Make 'data' directory inaccessible or out of web root
11. Enjoy
Specification
Monpay uses an SQLite database to store concurrent and completed payments,
this was done because Vilyaem wanted to experiment with SQLite, and to prevent
race-condition problems that result in the loss of funds for both parties.
Monpay uses a simple CSV formatted plain-text file as a 'database' for items,
each entry defining the name (or filename) of the item, the type of the item
(physical or digital), the price in XMR of the item, the seller's XMPP address
(for notifications), the Monero address, an optional entry for a 'ledger path'
which would be used to communicate to a locally hosted site what transactions
have occured, and finally, an optional 'redirect' entry, when the transaction
is complete, the user will be redirected to the page specified in the
'redirect' field.
Payments:
In the 'payments' table in 'payments.db'
IP Address (ip)
Temporary monero wallet name (walletname)
Item Name (item)
Amount due (dues)
Status (status)
Items:
name,type,price,seller xmpp,seller payment address,ledgerdir,redirect
Dependencies
* xmpp-php by norgul
* php-qrcode by chillerlan
* monerophp by Monero Integrations
Roadmap
1. Successful digital transactions X
2. Successful physical transactions X
3. Successful merchant support
4. Support Wownero? X
5. Support I2P/Tor?
6. Complete rewrite in C CGI?
License
Public Domain CC0
-----------------------------------------------------------------------------

7
composer.json Normal file
View file

@ -0,0 +1,7 @@
{
"require": {
"norgul/xmpp-php": "^2.2",
"chillerlan/php-qrcode": "^5.0",
"monero-integrations/monerophp": "@dev"
}
}

92
data/Doxyfile Executable file
View file

@ -0,0 +1,92 @@
# Neropay Doxyfile (a configuration for automatic docs generator Doxygen)
PROJECT_NAME = "Neropay"
DOXYFILE_ENCODING = UTF-8
INPUT_ENCODING = UTF-8
PROJECT_BRIEF = "'Nero payment system"
OUTPUT_DIRECTORY = ./doc
CREATE_SUBDIRS = NO
ALLOW_UNICODE_NAMES = NO
OUTPUT_LANGUAGE = English
OPTIMIZE_OUTPUT_JAVA = NO
OPTIMIZE_FOR_FORTRAN = NO
OPTIMIZE_OUTPUT_VHDL = NO
OPTIMIZE_OUTPUT_FOR_C = YES
HTML_OUTPUT = html
HTML_FILE_EXTENSION = .html
HTML_COLORSTYLE_HUE = 220
HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80
HTML_TIMESTAMP = YES
HTML_DYNAMIC_SECTIONS = NO
HTML_INDEX_NUM_ENTRIES = 100
GENERATE_HTML = YES
GENERATE_DOCSET = NO
GENERATE_LATEX = NO
GENERATE_AUTOGEN_DEF = NO
GENERATE_HTMLHELP = NO
GENERATE_CHI = NO
BRIEF_MEMBER_DESC = YES
REPEAT_BRIEF = YES
FULL_PATH_NAMES = YES
INHERIT_DOCS = YES
SEPARATE_MEMBER_PAGES = NO
TAB_SIZE = 4
MARKDOWN_SUPPORT = YES
SUBGROUPING = YES
TYPEDEF_HIDES_STRUCT = NO
HIDE_UNDOC_MEMBERS = NO
HIDE_UNDOC_CLASSES = NO
INTERNAL_DOCS = NO
CASE_SENSE_NAMES = YES
HIDE_SCOPE_NAMES = NO
HIDE_COMPOUND_REFERENCE = NO
SHOW_INCLUDE_FILES = YES
INLINE_INFO = YES
SORT_MEMBER_DOCS = YES
SHOW_USED_FILES = YES
SHOW_FILES = YES
SHOW_NAMESPACES = YES
QUIET = NO
WARNINGS = YES
WARN_IF_UNDOCUMENTED = YES
WARN_IF_DOC_ERROR = YES
EXTRACT_ALL = YES
EXTRACT_PACKAGE = NO
EXTRACT_PRIVATE = YES
EXTRACT_LOCAL_CLASSES = YES
EXTRACT_LOCAL_METHODS = NO
EXTRACT_STATIC = YES
EXTRACT_ANON_NSPACES = NO
RECURSIVE = NO
EXCLUDE_SYMLINKS = NO
EXAMPLE_RECURSIVE = NO
FILTER_SOURCE_FILES = NO
SOURCE_BROWSER = NO
INLINE_SOURCES = NO
STRIP_CODE_COMMENTS = YES
REFERENCED_BY_RELATION = NO
REFERENCES_RELATION = NO
REFERENCES_LINK_SOURCE = YES
SOURCE_TOOLTIPS = YES
USE_HTAGS = NO
VERBATIM_HEADERS = YES
CLANG_ASSISTED_PARSING = NO
ALPHABETICAL_INDEX = YES
COLS_IN_ALPHA_INDEX = 5
BINARY_TOC = NO
TOC_EXPAND = NO
DISABLE_INDEX = NO
ENUM_VALUES_PER_LINE = 4
EXT_LINKS_IN_WINDOW = NO
SEARCHENGINE = YES
SEARCHDATA_FILE = searchdata.xml
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = NO
EXPAND_ONLY_PREDEF = NO
SEARCH_INCLUDES = YES
SKIP_FUNCTION_MACROS = NO
ALLEXTERNALS = NO
EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES
HAVE_DOT = NO

2
data/items.txt Normal file
View file

@ -0,0 +1,2 @@
IndustrialSocietyItsFuture.pdf,digital,0.000005,kenyaz@vilyaem.xyz,48Sxa8J6518gqp4WeGtQ4rLe6SctPrEnnCqm6v6ydjLwRPi9Uh9gvVuUsU2AEDw75meTHCNY8KfU6Txysom4Bn5qPKMJ75w,NULL,NULL
Example,physical,0.000005,kenyaz@vilyaem.xyz,48Sxa8J6518gqp4WeGtQ4rLe6SctPrEnnCqm6v6ydjLwRPi9Uh9gvVuUsU2AEDw75meTHCNY8KfU6Txysom4Bn5qPKMJ75w,NULL,NULL

0
data/payments.db Normal file
View file

Binary file not shown.

560
index.php Normal file
View file

@ -0,0 +1,560 @@
<?php
/*********************************************
* Description - Neropay payment system
*
* index.php -- The main script responsible for
* everything.
* Author - Vilyaem
* *******************************************/
/*CONSTANTS*/
define("VILXMPPADDR", "YOUR XMPP ADDRESS");
define("XMPPHOST", "YOUR XMPP HOST (something.xyz)");
define("XMPPUSER", "YOUR XMPP USERNAME (vilyaem)");
define("XMPPPASS", "YOUR XMPP PASSWORD");
define("SQLDBPATH", "data/payments.db");
/*GET THE BLOAT*/
require __DIR__ . '/vendor/autoload.php';
require_once('vendor/monero-integrations/monerophp/src/jsonRPCClient.php');
require_once('vendor/monero-integrations/monerophp/src/walletRPC.php');
use MoneroIntegrations\MoneroPhp\walletRPC;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\Output\QRStringText;
use chillerlan\QRCode\{QRCode, QROptions};
/*
ini_set('display_errors',1);
ini_set('display_startup_errors',1);
error_reporting(E_ALL);
*/
$db = new SQLite3(SQLDBPATH);
$db->enableExceptions(true);
$db->exec("PRAGMA busy_timeout = 60000;"); /*Hold your horses*/
$db->exec("PRAGMA journal_mode=WAL;");
$open = $db->query('CREATE TABLE IF NOT EXISTS "payments" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"ip" TEXT,
"walletname" TEXT,
"item" TEXT,
"dues" TEXT,
"status" TEXT,
"address" TEXT,
"date" TEXT
)');
if($open === false){
echo 'Failed to create database!';
exit;
}
/*wallet address string we use to avoid
redundant open_wallet calls*/
$newwaladdr = NULL;
/*Benchmarking*/
$time = microtime();
$time = explode(' ', $time);
$time = $time[1] + $time[0];
$start = $time;
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Neropay</title>
<meta http-equiv="refresh" content="25">
</head>
<body>
<center>
<h1>Neropay</h1>
<small>A tiny Monero payment system</small></br>
<?php
/*Is this a Wownero transaction?*/
/*Get out*/
if (!isset($_SERVER['REMOTE_ADDR'])) {
exit;
}
if($_SERVER['REQUEST_METHOD'] === 'GET'){
if(!isset($_GET['item'])){
echo '<p>Welcome to Neropay, a tiny payment processsing system for Monero</p>';
exit;
}
if(isset($_GET['wow'])){
$wow = true;
echo '<p style="color:pink;">You are making a Wownero transaction!</p>';
}
else{
$wow = false;
}
/* Globals */
$ip = $_SERVER['REMOTE_ADDR'];
/*Nasty sanitization*/
$item = substr(str_replace([",", "#", "$", "%", "*", "~", "'", "=", "{", "[", "|", "`", "^", "]", "}", ":", ";", "<", ">", "/", "?", "&"], "",htmlspecialchars($_GET['item'])),0,256);
if(isset($_GET['addr'])){
$buyinfo = substr(str_replace([",", "#", "$", "%", "*", "~", "'", "=", "{", "[", "|", "`", "^", "]", "}", ":", ";", "<", ">", "/", "?", "&"], "",htmlspecialchars($_GET['addr'])),0,512);
}
/*Setup XMPP*/
/*
$xmppsettings = new Norgul\Xmpp\Options();
$xmppsettings
->setHost(XMPPHOST)
->setUsername(XMPPUSER)
->setPassword(XMPPPASS);
$xmppclient = new Norgul\Xmpp\XmppClient($xmppsettings);
$xmppclient->connect();
* /
/*Setup Monero or Wownero*/
if(!$wow){
$walletRPC = new walletRPC('127.0.0.1', 18083, false);
}
else{
$walletRPC = new walletRPC('127.0.0.1', 18084, false);
}
/*********************************************
* Description - Error messages to give to consoomers
* Author - Vilyaem
* *******************************************/
function NeropayError($msg){
echo '<p style="color:red;">Error, please report the following to kenyaz [at] vilyaem.xyz via Email or XMPP: ' . $msg . '</p>';
SendXMPP(VILXMPPADDR,$msg);
/*$xmppclient->disconnect();*/
exit;
}
/*********************************************
* Description - Retarded, PHP does scary stuff
* when you least expect it
* Author - Vilyaem
* *******************************************/
function IsBad($val){
if(!isset($val) || $val === false || $val === NULL){
return true;
}
else{
return false;
}
}
/*********************************************
* Description - Send an XMPP message
* Author - Vilyaem
* Date - Jul 21 2024
* *******************************************/
function SendXMPP($user,$msg){
/*
global $xmppsettings,$xmppclient;
$xmppclient->message->send($msg, $user);
*/
/*Just write to a file for the moment,
is XMPP abhorrently slow?*/
$date = date('Y-m-d h:i:s A');
$file = fopen("data/notes.txt","a") or NeropayError("10013");
fwrite($file,"$date: $msg\n");
fclose($file);
}
/*********************************************
* Description - Get property of a list.
* Author - Vilyaem
* *******************************************/
function GetListProp($object,$prop,$file){
$scanitems = file($file,FILE_IGNORE_NEW_LINES);
/*var_dump($object);*/
/*var_dump($file);*/
/*var_dump($prop);*/
foreach($scanitems as $scanitem){
$itemdata = explode(",",$scanitem);
/*var_dump($itemdata);*/
/*var_dump($itemdata[0]);*/
if($itemdata[0] === $object){
return $itemdata[$prop];/*return type*/
}
}
return NULL; /*failed*/
}
/*********************************************
* Description - Find payment
* Used when checking if a payment is already ongoing
* Author - Vilyaem
* *******************************************/
function FindPayment(){
global $db,$item,$ip;
$stm = $db->prepare('SELECT * FROM payments WHERE ip = :ip AND item = :item AND date = :date AND status = :status');
$stm->bindValue(':ip',$ip);
$stm->bindValue(':item',$item);
$stm->bindValue(':date',date('Y-m-d'));
$stm->bindValue(':status',"unpaid");
$res = $stm->execute();
$row = $res->fetchArray(SQLITE3_NUM);
/*Can't find it*/
if(IsBad($row)){
return false;
}
if(IsBad($res)){
return false; /*failed*/
}
else{
return true; /*There is one*/
}
}
/*********************************************
* Description - Ask for payment
* Author - Vilyaem
* *******************************************/
function AskPayment(){
global $db,$item,$ip,$walletRPC,$newwaladdr,$wow;
/*Access the database and and get the dues and wallet name*/
$stm = $db->prepare('SELECT * FROM payments WHERE ip = :ip AND item = :item AND date = :date AND status = :status');
if($stm === false){
NeropayError("Error code 1002");
}
$stm->bindValue(":ip",$ip);
$stm->bindValue(":item",$item);
$stm->bindValue(":date",date('Y-m-d'));
$stm->bindValue(":status","unpaid");
$res = $stm->execute();
if($res === false){
NeropayError("Error code 1003");
}
/*echo var_dump($stm) . "</br>";*/
/*echo var_dump($res) . "</br>";*/
/*echo var_dump($res->numColumns()) . "</br>";*/
/*echo var_dump($res->columnType(0)) . "</br>";*/
if ($res->numColumns() && $res->columnType(0) === SQLITE3_NULL) {
NeropayError("Error code 1004");
}
if($res->reset() === false){
NeropayError("Error code 1005");
}
$row = $res->fetchArray(SQLITE3_NUM);
/*echo var_dump($stm) . "</br>";*/
/*echo var_dump($res) . "</br>";*/
/*echo var_dump($row) . "</br>";*/
if($row === false){
NeropayError("Error code 1006");
}
/*
else{
echo "<p>AskPayment got a valid row!</p>";
}
*/
/*$db = null;*/
if($newwaladdr === NULL){
$wallet = $walletRPC->open_wallet($row[2],'');
$addr = $walletRPC->get_address();
}
else{
$addr = $newwaladdr;
}
$amnt = $row[4];
/*echo "</br></br>" . var_dump($addr) . "</br>";*/
echo '<img src="'.(new QRCode)->render($addr['address']).'" width=25% height=25% alt="QR Code" />';
/*var_dump($wow);*/
if(!$wow){
echo '<p>Please pay ' . $amnt . ' XMR to ' . $addr['address'] . '</p>';
}
else{
echo '<p>Please pay ' . $amnt * 4005 . ' WOW to ' . $addr['address'] . '</p>';
}
}
/*********************************************
* Description - Get an Item given a payment ID
* Author - Vilyaem
* *******************************************/
function GetItemByPayment(){
global $db,$ip,$item;
$stm = $db->prepare('SELECT * FROM payments WHERE ip = ? AND item = ? AND date = ? AND status = ?');
$stm->bindValue(":ip",$ip);
$stm->bindValue(":item",$item);
$stm->bindValue(":date",date('Y-m-d'));
$stm->bindValue(":status","unpaid");
$res = $stm->execute();
if($res === false){
NeropayError("Error code 1007");
}
$row = $res->fetchArray(SQLITE3_NUM);
return $row[2]; /*This should be the item*/
}
/*********************************************
* Description - Create a new concurrent payment
* in the database. This also creates a new wallet
* Author - Vilyaem
* *******************************************/
function NewPayment(){
global $db,$item,$ip,$walletRPC,$newwaladdr;
/*$db = null;*/
/*Create the wallet*/
$namenum=rand();
$name=md5($namenum);
$makewallet = $walletRPC->create_wallet($name, '');
$newwallet = $walletRPC->open_wallet($name,'');
$newaddress = $walletRPC->get_address();
$newwaladdr = $newaddress;
$newprice = GetListProp($item,2,"data/items.txt");
/*If item does not exist, stop!*/
if($newprice === NULL){
NeropayError("Error code 1008");
}
/*$db = new SQLite3(SQLDBPATH);*/
/*Add the payment to the database*/
$stm = $db->prepare('INSERT INTO "payments" ("ip", "walletname",
"item", "dues", "status", "address", "date")
VALUES (:ip, :walletname, :item, :dues, :status, :address, :date)');
if($stm === false){
NeropayError("Error code 1009");
}
$stm->bindValue(':ip', $ip);
$stm->bindValue(':walletname', $name);
$stm->bindValue(':item', $item);
$stm->bindValue(':dues', $newprice);
$stm->bindValue(':status', 'unpaid');
$stm->bindValue(':address', 'NULL');
$stm->bindValue(':date', date('Y-m-d'));
$res = $stm->execute();
if($stm === false || $res === false){
NeropayError("Error code 1010");
}
/*echo "newpayment: $ip,$name,$item";*/
/*Notify Vilyaem that a new payment process has begun*/
SendXMPP(VILXMPPADDR,'!!! A new payment has begun: ' . $ip . ',' . $item . ',' . $name);
/*Return wallet's address*/
return $newaddress;
}
/*********************************************
* Description - Success
* Author - Vilyaem
* *******************************************/
function Success(){
global $ip,$item,$buyinfo;
$date = date("Y-m-d");
$itemtype = GetListProp($item,1,"data/items.txt");
$profit = GetListProp($item,2,"data/items.txt");
/*Notify via XMPP of the success*/
SendXMPP(VILXMPPADDR,'Congratulations! CLIENT: ' . $ip . ' ITEM: ' . $item . ' DATE: ' . $date . ' PROFIT: ' . $profit);
/*If the item is digital, give the user an immediate download*/
if($itemtype === "digital"){ /*digital*/
ob_clean();
flush();
$filename = "files/$item";
$mimetype = mime_content_type($filename);
header("Content-Type: ".$mimetype );
header("Content-Disposition: attachment; filename=\"$item\"");
/*echo readfile($filename); */
readfile($filename);
}
else{ /*physical*/
/*NeropayError("Error code 1009");*/
if(isset($buyinfo)){
SendXMPP(VILXMMRADDR,$buyinfo);
echo "<p>Your shipping/contact information has been sent.</p>";
}
}
/*Congratulate the user*/
echo "<p style=\"color:green\">Congratulations on your purchase of $item!</p>";
}
/*********************************************
* Description - Check if payment is complete
* Author - Vilyaem
* *******************************************/
function CheckPayment(){
global $db,$item,$ip,$walletRPC,$wow;
/*Get payment from database*/
$stm = $db->prepare('SELECT * FROM payments WHERE ip = :ip AND item = :item AND date = :date');
$stm->bindValue(':ip',$ip);
$stm->bindValue(':item',$item);
$stm->bindValue(':date',date('Y-m-d'));
$res = $stm->execute();
if(IsBad($res)){
NeropayError("Error code 1011");
}
$row = $res->fetchArray(SQLITE3_NUM);
if(IsBad($row)){
NeropayError("Error code 1012");
}
/*echo var_dump($row) . "<br>";*/
/*If payment is already done succeed*/
if($row[4] === "finished"){
return true;
}
/*If not, check it*/
/*Open the wallet and see if the payment has been met*/
$mywallet = $walletRPC->open_wallet($row[2]);
$mybalance = $walletRPC->get_balance()['balance'];
/*echo var_dump($mywallet) . "<br>";*/
/*echo var_dump($mybalance) . "<br>";*/
if(($mybalance >= $row[4] && $wow === false) || ($mybalance >= $row[4] * 4005 && $wow === true)){
/*if(true){*/
/*transfer the money TODO this API call's args are confusing*/
/*echo '<p>' . $walletRPC->transfer() . '</p>'*/
/*Mark as finished*/
$stm = $db->prepare("UPDATE payments SET status='finished' WHERE ip = :ip AND item = :item AND date = :date");
$stm->bindValue(':ip',$ip);
$stm->bindValue(':item',$item);
$stm->bindValue(':date',date('Y-m-d'));
$res = $stm->execute();
if($res === false){
NeropayError("Error code 1013");
}
$row = $res->fetchArray(SQLITE3_NUM);
return true;
}
else{ /*Payment has not succeeded, and the balance has not changed.*/
return false;
}
}
/*********************************************
* Description - Main
* Author - Vilyaem
* *******************************************/
function Main(){
/*Check if this payment is already ongoing*/
if(FindPayment() === true){
/*echo "<p>Found the payment!</p>";*/
/*If payment is already ongoing, check if it has been paid,
if so, show success, otherwise ask for payment*/
if(CheckPayment() === true){
/*echo "<p>Check payment succeeded</p>";*/
Success();
}
else{
/*echo "<p>Check payment falied, ask for payment</p>";*/
AskPayment();
}
}
/*If it is not, create it and ask for payment*/
else{
/*echo "<p>Failed to find the payment!</p>";*/
NewPayment();
AskPayment();
}
}
Main();
/*$xmppclient->disconnect();*/
}
?>
<p>
<small>Note: Payments may take a couple minutes to go through.
</br>Report irregularities or problems to kenyaz [at] vilyaem.xyz
</br>The page should refresh automatically, if not, refresh manually</small>
</br><strong>WARNING: Monero might liberate you.</strong>
</p>
<?php
/*Benchmarking*/
$time = microtime();
$time = explode(' ', $time);
$time = $time[1] + $time[0];
$finish = $time;
$total_time = round(($finish - $start), 4);
echo '<em>Page generated in '.$total_time.' seconds.</em>';
?>
</center>
</body>
</html>

51
physprep.php Normal file
View file

@ -0,0 +1,51 @@
<?php
/*********************************************
* Description - Neropay payment system
*
* physorder.php -- Ask the user for extra information
* such as shipping details or XMPP contact
* Author - Vilyaem
* *******************************************/
if($_SERVER['REQUEST_METHOD'] != 'GET'){
header("Location: index.php");
exit;
}
else{
$item = isset($_GET['item']) ? $_GET['item'] : '';
$item = isset($_GET['wow']) ? $_GET['wow'] : '';
$addr = isset($_GET['addr']) ? $_GET['addr'] : '';
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Neropay</title>
</head>
<style>
body{font-family:Courier;};
</style>
<body>
<center>
<h1>Neropay</h1>
<small>A tiny Monero payment system</small></br>
<form action="index.php" method="get">
<textarea type="text" id="addr" name="addr" rows=16 cols=64 maxlength=1024 autofocus required placeholder="Please put contact or shipping information here, don't make a fool of yourself."></textarea>
<input type="hidden" id="item" name="item" value="<?php echo htmlspecialchars($item) ?>"></textarea>
<input type="hidden" id="wow" name="wow" value="<?php echo htmlspecialchars($wow) ?>"></textarea>
</br>
<input type="submit" value="Submit">
</form>
<p>
<small>Note: Payments may take a couple minutes to go through.
</br>Report irregularities or problems to kenyaz [at] vilyaem.xyz</small>
</br><strong>WARNING: Monero might liberate you.</strong>
</p>
</center>
</body>
</html>

25
vendor/autoload.php vendored Normal file
View file

@ -0,0 +1,25 @@
<?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;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit039ef714d06adc1d2f797455add3d2e5::getLoader();

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Smiley <smiley@chillerlan.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

40
vendor/chillerlan/php-qrcode/NOTICE vendored Normal file
View file

@ -0,0 +1,40 @@
Parts of this code are ported to php from the ZXing project
and licensed under the Apache License, Version 2.0.
Copyright 2007 ZXing authors (https://github.com/zxing/zxing),
Copyright (c) Ashot Khanamiryan (https://github.com/khanamiryan/php-qrcode-detector-decoder)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
List of affected files:
src/Common/ECICharset.php
src/Common/GenericGFPoly.php
src/Common/GF256.php
src/Common/LuminanceSourceAbstract.php
src/Common/MaskPattern.php
src/Decoder/Binarizer.php
src/Decoder/BitMatrix.php
src/Decoder/Decoder.php
src/Decoder/DecoderResult.php
src/Decoder/ReedSolomonDecoder.php
src/Detector/AlignmentPattern.php
src/Detector/AlignmentPatternFinder.php
src/Detector/Detector.php
src/Detector/FinderPattern.php
src/Detector/FinderPatternFinder.php
src/Detector/GridSampler.php
src/Detector/PerspectiveTransform.php
src/Detector/ResultPoint.php
tests/Common/MaskPatternTest.php

168
vendor/chillerlan/php-qrcode/README.md vendored Normal file
View file

@ -0,0 +1,168 @@
# chillerlan/php-qrcode
A PHP QR Code generator based on the [implementation by Kazuhiko Arase](https://github.com/kazuhikoarase/qrcode-generator), namespaced, cleaned up, improved and other stuff. <br>
It also features a QR Code reader based on a [PHP port](https://github.com/khanamiryan/php-qrcode-detector-decoder) of the [ZXing library](https://github.com/zxing/zxing).
**Attention:** there is now also a javascript port: [chillerlan/js-qrcode](https://github.com/chillerlan/js-qrcode).
[![PHP Version Support][php-badge]][php]
[![Packagist version][packagist-badge]][packagist]
[![Continuous Integration][gh-action-badge]][gh-action]
[![CodeCov][coverage-badge]][coverage]
[![Codacy][codacy-badge]][codacy]
[![Packagist downloads][downloads-badge]][downloads]
[![Documentation][readthedocs-badge]][readthedocs]
[php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF
[php]: https://www.php.net/supported-versions.php
[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg?logo=packagist
[packagist]: https://packagist.org/packages/chillerlan/php-qrcode
[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-qrcode/ci.yml?branch=v5.0.x&logo=github
[gh-action]: https://github.com/chillerlan/php-qrcode/actions/workflows/ci.yml?query=branch%3Amain
[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-qrcode/v5.0.x?logo=codecov
[coverage]: https://app.codecov.io/gh/chillerlan/php-qrcode/tree/v5.0.x
[codacy-badge]: https://img.shields.io/codacy/grade/edccfc4fe5a34b74b1c53ee03f097b8d/v5.0.x?logo=codacy
[codacy]: https://app.codacy.com/gh/chillerlan/php-qrcode/dashboard?branch=v5.0.x
[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode?logo=packagist
[downloads]: https://packagist.org/packages/chillerlan/php-qrcode/stats
[readthedocs-badge]: https://img.shields.io/readthedocs/php-qrcode/v5.0.x?logo=readthedocs
[readthedocs]: https://php-qrcode.readthedocs.io/en/v5.0.x/
## Overview
### Features
- Creation of [Model 2 QR Codes](https://www.qrcode.com/en/codes/model12.html), [Version 1 to 40](https://www.qrcode.com/en/about/version.html)
- [ECC Levels](https://www.qrcode.com/en/about/error_correction.html) L/M/Q/H supported
- Mixed mode support (encoding modes can be combined within a QR symbol). Supported modes:
- numeric
- alphanumeric
- 8-bit binary
- [ECI support](https://en.wikipedia.org/wiki/Extended_Channel_Interpretation)
- 13-bit double-byte:
- kanji (Japanese, Shift-JIS)
- hanzi (simplified Chinese, GB2312/GB18030) as [defined in GBT18284-2000](https://www.chinesestandard.net/PDF/English.aspx/GBT18284-2000)
- Flexible, easily extensible output modules, built-in support for the following output formats:
- [GdImage](https://www.php.net/manual/book.image) (raster graphics: bmp, gif, jpeg, png, webp)
- [ImageMagick](https://www.php.net/manual/book.imagick) ([multiple supported image formats](https://imagemagick.org/script/formats.php))
- Markup types: SVG, HTML, etc.
- String types: JSON, plain text, etc.
- Encapsulated Postscript (EPS)
- PDF via [FPDF](https://github.com/setasign/fpdf)
- QR Code reader (via GD and ImageMagick)
### Requirements
- PHP 7.4+
- [`ext-mbstring`](https://www.php.net/manual/book.mbstring.php)
- optional:
- [`ext-gd`](https://www.php.net/manual/book.image)
- [`ext-imagick`](https://github.com/Imagick/imagick) with [ImageMagick](https://imagemagick.org) installed
- [`ext-fileinfo`](https://www.php.net/manual/book.fileinfo.php) (required by `QRImagick` output)
- [`setasign/fpdf`](https://github.com/setasign/fpdf) for the PDF output module
For the QRCode reader, either `ext-gd` or `ext-imagick` is required!
## Documentation
- The user manual is at https://php-qrcode.readthedocs.io/ ([sources](https://github.com/chillerlan/php-qrcode/tree/v5.0.x/docs))
- An API documentation created with [phpDocumentor](https://www.phpdoc.org/) can be found at https://chillerlan.github.io/php-qrcode/
- The documentation for the `QROptions` container can be found here: [chillerlan/php-settings-container](https://github.com/chillerlan/php-settings-container#readme)
## Installation with [composer](https://getcomposer.org)
See [the installation guide](https://php-qrcode.readthedocs.io/en/v5.0.x/Usage/Installation.html) for more info!
### Terminal
```
composer require chillerlan/php-qrcode
```
### composer.json
```json
{
"require": {
"php": "^7.4 || ^8.0",
"chillerlan/php-qrcode": "v5.0.x-dev#<commit_hash>"
}
}
```
Note: replace `v5.0.x-dev` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^4.3` - see [releases](https://github.com/chillerlan/php-qrcode/releases) for valid versions.
## Quickstart
We want to encode this URI for a mobile authenticator into a QRcode image:
```php
$data = 'otpauth://totp/test?secret=B3JX4VCVJDVNXNZ5&issuer=chillerlan.net';
// quick and simple:
echo '<img src="'.(new QRCode)->render($data).'" alt="QR Code" />';
```
Wait, what was that? Please again, slower! See [Advanced usage](https://php-qrcode.readthedocs.io/en/v5.0.x/Usage/Advanced-usage.html) in the manual.
Also, have a look [in the examples folder](https://github.com/chillerlan/php-qrcode/tree/v5.0.x/examples) for some more usage examples.
<p align="center">
<img alt="QR codes are awesome!" style="width: auto; height: 530px;" src="https://raw.githubusercontent.com/chillerlan/php-qrcode/v5.0.x/.github/images/example.svg">
</p>
### Reading QR Codes
Using the built-in QR Code reader is pretty straight-forward:
```php
// it's generally a good idea to wrap the reader in a try/catch block because it WILL throw eventually
try{
$result = (new QRCode)->readFromFile('path/to/file.png'); // -> DecoderResult
// you can now use the result instance...
$content = $result->data;
$matrix = $result->getMatrix(); // -> QRMatrix
// ...or simply cast it to string to get the content:
$content = (string)$result;
}
catch(Throwable $e){
// oopsies!
}
```
## Shameless advertising
Hi, please check out some of my other projects that are way cooler than qrcodes!
- [js-qrcode](https://github.com/chillerlan/js-qrcode) - a javascript port of this library
- [php-authenticator](https://github.com/chillerlan/php-authenticator) - a Google Authenticator implementation (see [authenticator example](https://github.com/chillerlan/php-qrcode/blob/v5.0.x/examples/authenticator.php))
- [php-httpinterface](https://github.com/chillerlan/php-httpinterface) - a PSR-7/15/17/18 implemetation
- [php-oauth-core](https://github.com/chillerlan/php-oauth-core) - an OAuth 1/2 client library along with a bunch of [providers](https://github.com/chillerlan/php-oauth-providers)
- [php-database](https://github.com/chillerlan/php-database) - a database client & querybuilder for MySQL, Postgres, SQLite, MSSQL, Firebird
- [php-tootbot](https://github.com/php-tootbot/tootbot-template) - a Mastodon bot library (see [@dwil](https://github.com/php-tootbot/dwil))
## Disclaimer!
I don't take responsibility for molten CPUs, misled applications, failed log-ins etc.. Use at your own risk!
### License notice
- Parts of this code are [ported to PHP](https://github.com/codemasher/php-qrcode-decoder) from the [ZXing project](https://github.com/zxing/zxing) and licensed under the [Apache License, Version 2.0](./NOTICE).
- [The documentation](https://github.com/chillerlan/php-qrcode/tree/v5.0.x/docs) is licensed under the [Creative Commons Attribution 4.0 International (CC BY 4.0) License](https://creativecommons.org/licenses/by/4.0/).
### Trademark Notice
The word "QR Code" is a registered trademark of *DENSO WAVE INCORPORATED*<br>
https://www.qrcode.com/en/faq.html#patentH2Title

View file

@ -0,0 +1,79 @@
{
"name": "chillerlan/php-qrcode",
"description": "A QR code generator and reader with a user friendly API. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-qrcode",
"license": [
"MIT", "Apache-2.0"
],
"type": "library",
"keywords": [
"QR code", "qrcode", "qr", "qrcode-generator", "phpqrcode", "qrcode-reader", "qr-reader"
],
"authors": [
{
"name": "Kazuhiko Arase",
"homepage": "https://github.com/kazuhikoarase/qrcode-generator"
},
{
"name":"ZXing Authors",
"homepage": "https://github.com/zxing/zxing"
},
{
"name": "Ashot Khanamiryan",
"homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder"
},
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage":"https://github.com/chillerlan/php-qrcode/graphs/contributors"
}
],
"support": {
"docs": "https://php-qrcode.readthedocs.io",
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode"
},
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": "^7.4 || ^8.0",
"ext-mbstring": "*",
"chillerlan/php-settings-container": "^2.1.4 || ^3.1"
},
"require-dev": {
"chillerlan/php-authenticator": "^4.1 || ^5.1",
"phan/phan": "^5.4",
"phpunit/phpunit": "^9.6",
"phpmd/phpmd": "^2.15",
"setasign/fpdf": "^1.8.2",
"squizlabs/php_codesniffer": "^3.8"
},
"suggest": {
"chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
"setasign/fpdf": "Required to use the QR FPDF output.",
"simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code"
},
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\QRCodeTest\\": "tests/"
}
},
"scripts": {
"phpunit": "@php vendor/bin/phpunit",
"phan": "@php vendor/bin/phan"
},
"config": {
"lock": false,
"sort-packages": true,
"platform-check": true
}
}

View file

@ -0,0 +1,180 @@
<?php
/**
* Class BitBuffer
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function count, floor, min;
/**
* Holds the raw binary data
*/
final class BitBuffer{
/**
* The buffer content
*
* @var int[]
*/
private array $buffer;
/**
* Length of the content (bits)
*/
private int $length;
/**
* Read count (bytes)
*/
private int $bytesRead = 0;
/**
* Read count (bits)
*/
private int $bitsRead = 0;
/**
* BitBuffer constructor.
*
* @param int[] $bytes
*/
public function __construct(array $bytes = []){
$this->buffer = $bytes;
$this->length = count($this->buffer);
}
/**
* appends a sequence of bits
*/
public function put(int $bits, int $length):self{
for($i = 0; $i < $length; $i++){
$this->putBit((($bits >> ($length - $i - 1)) & 1) === 1);
}
return $this;
}
/**
* appends a single bit
*/
public function putBit(bool $bit):self{
$bufIndex = (int)floor($this->length / 8);
if(count($this->buffer) <= $bufIndex){
$this->buffer[] = 0;
}
if($bit === true){
$this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8));
}
$this->length++;
return $this;
}
/**
* returns the current buffer length
*/
public function getLength():int{
return $this->length;
}
/**
* returns the buffer content
*
* to debug: array_map(fn($v) => sprintf('%08b', $v), $bitBuffer->getBuffer())
*/
public function getBuffer():array{
return $this->buffer;
}
/**
* @return int number of bits that can be read successfully
*/
public function available():int{
return ((8 * ($this->length - $this->bytesRead)) - $this->bitsRead);
}
/**
* @author Sean Owen, ZXing
*
* @param int $numBits number of bits to read
*
* @return int representing the bits read. The bits will appear as the least-significant bits of the int
* @throws \chillerlan\QRCode\QRCodeException if numBits isn't in [1,32] or more than is available
*/
public function read(int $numBits):int{
if($numBits < 1 || $numBits > $this->available()){
throw new QRCodeException('invalid $numBits: '.$numBits);
}
$result = 0;
// First, read remainder from current byte
if($this->bitsRead > 0){
$bitsLeft = (8 - $this->bitsRead);
$toRead = min($numBits, $bitsLeft);
$bitsToNotRead = ($bitsLeft - $toRead);
$mask = ((0xff >> (8 - $toRead)) << $bitsToNotRead);
$result = (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead);
$numBits -= $toRead;
$this->bitsRead += $toRead;
if($this->bitsRead === 8){
$this->bitsRead = 0;
$this->bytesRead++;
}
}
// Next read whole bytes
if($numBits > 0){
while($numBits >= 8){
$result = (($result << 8) | ($this->buffer[$this->bytesRead] & 0xff));
$this->bytesRead++;
$numBits -= 8;
}
// Finally read a partial byte
if($numBits > 0){
$bitsToNotRead = (8 - $numBits);
$mask = ((0xff >> $bitsToNotRead) << $bitsToNotRead);
$result = (($result << $numBits) | (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead));
$this->bitsRead += $numBits;
}
}
return $result;
}
/**
* Clears the buffer and resets the stats
*/
public function clear():self{
$this->buffer = [];
$this->length = 0;
return $this->rewind();
}
/**
* Resets the read-counters
*/
public function rewind():self{
$this->bytesRead = 0;
$this->bitsRead = 0;
return $this;
}
}

View file

@ -0,0 +1,125 @@
<?php
/**
* Class ECICharset
*
* @created 21.01.2021
* @author ZXing Authors
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function sprintf;
/**
* ISO/IEC 18004:2000 - 8.4.1 Extended Channel Interpretation (ECI) Mode
*/
final class ECICharset{
public const CP437 = 0; // Code page 437, DOS Latin US
public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1
public const CP437_WO_GLI = 2; // An equivalent code table to CP437, without the return-to-GLI 0 logic
public const ISO_IEC_8859_1 = 3; // Latin-1 (Default)
public const ISO_IEC_8859_2 = 4; // Latin-2
public const ISO_IEC_8859_3 = 5; // Latin-3
public const ISO_IEC_8859_4 = 6; // Latin-4
public const ISO_IEC_8859_5 = 7; // Latin/Cyrillic
public const ISO_IEC_8859_6 = 8; // Latin/Arabic
public const ISO_IEC_8859_7 = 9; // Latin/Greek
public const ISO_IEC_8859_8 = 10; // Latin/Hebrew
public const ISO_IEC_8859_9 = 11; // Latin-5
public const ISO_IEC_8859_10 = 12; // Latin-6
public const ISO_IEC_8859_11 = 13; // Latin/Thai
// 14 reserved
public const ISO_IEC_8859_13 = 15; // Latin-7 (Baltic Rim)
public const ISO_IEC_8859_14 = 16; // Latin-8 (Celtic)
public const ISO_IEC_8859_15 = 17; // Latin-9
public const ISO_IEC_8859_16 = 18; // Latin-10
// 19 reserved
public const SHIFT_JIS = 20; // JIS X 0208 Annex 1 + JIS X 0201
public const WINDOWS_1250_LATIN_2 = 21; // Superset of Latin-2, Central Europe
public const WINDOWS_1251_CYRILLIC = 22; // Latin/Cyrillic
public const WINDOWS_1252_LATIN_1 = 23; // Superset of Latin-1
public const WINDOWS_1256_ARABIC = 24;
public const ISO_IEC_10646_UCS_2 = 25; // High order byte first (UTF-16BE)
public const ISO_IEC_10646_UTF_8 = 26; // UTF-8
public const ISO_IEC_646_1991 = 27; // International Reference Version of ISO 7-bit coded character set (US-ASCII)
public const BIG5 = 28; // Big 5 (Taiwan) Chinese Character Set
public const GB18030 = 29; // GB (PRC) Chinese Character Set
public const EUC_KR = 30; // Korean Character Set
/**
* map of charset id -> name
*
* @see \mb_list_encodings()
*/
public const MB_ENCODINGS = [
self::CP437 => null,
self::ISO_IEC_8859_1_GLI => null,
self::CP437_WO_GLI => null,
self::ISO_IEC_8859_1 => 'ISO-8859-1',
self::ISO_IEC_8859_2 => 'ISO-8859-2',
self::ISO_IEC_8859_3 => 'ISO-8859-3',
self::ISO_IEC_8859_4 => 'ISO-8859-4',
self::ISO_IEC_8859_5 => 'ISO-8859-5',
self::ISO_IEC_8859_6 => 'ISO-8859-6',
self::ISO_IEC_8859_7 => 'ISO-8859-7',
self::ISO_IEC_8859_8 => 'ISO-8859-8',
self::ISO_IEC_8859_9 => 'ISO-8859-9',
self::ISO_IEC_8859_10 => 'ISO-8859-10',
self::ISO_IEC_8859_11 => null,
self::ISO_IEC_8859_13 => 'ISO-8859-13',
self::ISO_IEC_8859_14 => 'ISO-8859-14',
self::ISO_IEC_8859_15 => 'ISO-8859-15',
self::ISO_IEC_8859_16 => 'ISO-8859-16',
self::SHIFT_JIS => 'SJIS',
self::WINDOWS_1250_LATIN_2 => null, // @see https://www.php.net/manual/en/function.mb-convert-encoding.php#112547
self::WINDOWS_1251_CYRILLIC => 'Windows-1251',
self::WINDOWS_1252_LATIN_1 => 'Windows-1252',
self::WINDOWS_1256_ARABIC => null, // @see https://stackoverflow.com/a/8592995
self::ISO_IEC_10646_UCS_2 => 'UTF-16BE',
self::ISO_IEC_10646_UTF_8 => 'UTF-8',
self::ISO_IEC_646_1991 => 'ASCII',
self::BIG5 => 'BIG-5',
self::GB18030 => 'GB18030',
self::EUC_KR => 'EUC-KR',
];
/**
* The current ECI character set ID
*/
private int $charsetID;
/**
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $charsetID){
if($charsetID < 0 || $charsetID > 999999){
throw new QRCodeException(sprintf('invalid charset id: "%s"', $charsetID));
}
$this->charsetID = $charsetID;
}
/**
* Returns the current character set ID
*/
public function getID():int{
return $this->charsetID;
}
/**
* Returns the name of the current character set or null if no name is available
*
* @see \mb_convert_encoding()
* @see \iconv()
*/
public function getName():?string{
return (self::MB_ENCODINGS[$this->charsetID] ?? null);
}
}

View file

@ -0,0 +1,223 @@
<?php
/**
* Class EccLevel
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function array_column;
/**
* This class encapsulates the four error correction levels defined by the QR code standard.
*/
final class EccLevel{
// ISO/IEC 18004:2000 Tables 12, 25
/** @var int */
public const L = 0b01; // 7%.
/** @var int */
public const M = 0b00; // 15%.
/** @var int */
public const Q = 0b11; // 25%.
/** @var int */
public const H = 0b10; // 30%.
/**
* ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
*
* @var int[][]
*/
private const MAX_BITS = [
// [ L, M, Q, H] // v => modules
[ 0, 0, 0, 0], // 0 => will be ignored, index starts at 1
[ 152, 128, 104, 72], // 1 => 21
[ 272, 224, 176, 128], // 2 => 25
[ 440, 352, 272, 208], // 3 => 29
[ 640, 512, 384, 288], // 4 => 33
[ 864, 688, 496, 368], // 5 => 37
[ 1088, 864, 608, 480], // 6 => 41
[ 1248, 992, 704, 528], // 7 => 45
[ 1552, 1232, 880, 688], // 8 => 49
[ 1856, 1456, 1056, 800], // 9 => 53
[ 2192, 1728, 1232, 976], // 10 => 57
[ 2592, 2032, 1440, 1120], // 11 => 61
[ 2960, 2320, 1648, 1264], // 12 => 65
[ 3424, 2672, 1952, 1440], // 13 => 69 NICE!
[ 3688, 2920, 2088, 1576], // 14 => 73
[ 4184, 3320, 2360, 1784], // 15 => 77
[ 4712, 3624, 2600, 2024], // 16 => 81
[ 5176, 4056, 2936, 2264], // 17 => 85
[ 5768, 4504, 3176, 2504], // 18 => 89
[ 6360, 5016, 3560, 2728], // 19 => 93
[ 6888, 5352, 3880, 3080], // 20 => 97
[ 7456, 5712, 4096, 3248], // 21 => 101
[ 8048, 6256, 4544, 3536], // 22 => 105
[ 8752, 6880, 4912, 3712], // 23 => 109
[ 9392, 7312, 5312, 4112], // 24 => 113
[10208, 8000, 5744, 4304], // 25 => 117
[10960, 8496, 6032, 4768], // 26 => 121
[11744, 9024, 6464, 5024], // 27 => 125
[12248, 9544, 6968, 5288], // 28 => 129
[13048, 10136, 7288, 5608], // 29 => 133
[13880, 10984, 7880, 5960], // 30 => 137
[14744, 11640, 8264, 6344], // 31 => 141
[15640, 12328, 8920, 6760], // 32 => 145
[16568, 13048, 9368, 7208], // 33 => 149
[17528, 13800, 9848, 7688], // 34 => 153
[18448, 14496, 10288, 7888], // 35 => 157
[19472, 15312, 10832, 8432], // 36 => 161
[20528, 15936, 11408, 8768], // 37 => 165
[21616, 16816, 12016, 9136], // 38 => 169
[22496, 17728, 12656, 9776], // 39 => 173
[23648, 18672, 13328, 10208], // 40 => 177
];
/**
* ISO/IEC 18004:2000 Section 8.9 - Format Information
*
* ECC level -> mask pattern
*
* @var int[][]
*/
private const FORMAT_PATTERN = [
[ // L
0b111011111000100,
0b111001011110011,
0b111110110101010,
0b111100010011101,
0b110011000101111,
0b110001100011000,
0b110110001000001,
0b110100101110110,
],
[ // M
0b101010000010010,
0b101000100100101,
0b101111001111100,
0b101101101001011,
0b100010111111001,
0b100000011001110,
0b100111110010111,
0b100101010100000,
],
[ // Q
0b011010101011111,
0b011000001101000,
0b011111100110001,
0b011101000000110,
0b010010010110100,
0b010000110000011,
0b010111011011010,
0b010101111101101,
],
[ // H
0b001011010001001,
0b001001110111110,
0b001110011100111,
0b001100111010000,
0b000011101100010,
0b000001001010101,
0b000110100001100,
0b000100000111011,
],
];
/**
* The current ECC level value
*
* L: 0b01
* M: 0b00
* Q: 0b11
* H: 0b10
*/
private int $eccLevel;
/**
* @param int $eccLevel containing the two bits encoding a QR Code's error correction level
*
* @todo: accept string values (PHP8+)
* @see https://github.com/chillerlan/php-qrcode/discussions/160
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $eccLevel){
if((0b11 & $eccLevel) !== $eccLevel){
throw new QRCodeException('invalid ECC level');
}
$this->eccLevel = $eccLevel;
}
/**
* returns the string representation of the current ECC level
*/
public function __toString():string{
return [
self::L => 'L',
self::M => 'M',
self::Q => 'Q',
self::H => 'H',
][$this->eccLevel];
}
/**
* returns the current ECC level
*/
public function getLevel():int{
return $this->eccLevel;
}
/**
* returns the ordinal value of the current ECC level
*
* references to the keys of the following tables:
*
* @see \chillerlan\QRCode\Common\EccLevel::MAX_BITS
* @see \chillerlan\QRCode\Common\EccLevel::FORMAT_PATTERN
* @see \chillerlan\QRCode\Common\Version::RSBLOCKS
*/
public function getOrdinal():int{
return [
self::L => 0,
self::M => 1,
self::Q => 2,
self::H => 3,
][$this->eccLevel];
}
/**
* returns the format pattern for the given $eccLevel and $maskPattern
*/
public function getformatPattern(MaskPattern $maskPattern):int{
return self::FORMAT_PATTERN[$this->getOrdinal()][$maskPattern->getPattern()];
}
/**
* returns an array with the max bit lengths for version 1-40 and the current ECC level
*
* @return int[]
*/
public function getMaxBits():array{
$col = array_column(self::MAX_BITS, $this->getOrdinal());
unset($col[0]); // remove the inavlid index 0
return $col;
}
/**
* Returns the maximum bit length for the given version and current ECC level
*/
public function getMaxBitsForVersion(Version $version):int{
return self::MAX_BITS[$version->getVersionNumber()][$this->getOrdinal()];
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* Class GDLuminanceSource
*
* @created 17.01.2021
* @author Ashot Khanamiryan
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\Decoder\QRCodeDecoderException;
use chillerlan\Settings\SettingsContainerInterface;
use function file_get_contents, get_resource_type, imagecolorat, imagecolorsforindex,
imagecreatefromstring, imagefilter, imagesx, imagesy, is_resource;
use const IMG_FILTER_BRIGHTNESS, IMG_FILTER_CONTRAST, IMG_FILTER_GRAYSCALE, IMG_FILTER_NEGATE, PHP_MAJOR_VERSION;
/**
* This class is used to help decode images from files which arrive as GD Resource
* It does not support rotation.
*/
class GDLuminanceSource extends LuminanceSourceAbstract{
/**
* @var resource|\GdImage
*/
protected $gdImage;
/**
* GDLuminanceSource constructor.
*
* @param resource|\GdImage $gdImage
* @param \chillerlan\Settings\SettingsContainerInterface|null $options
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function __construct($gdImage, SettingsContainerInterface $options = null){
/** @noinspection PhpFullyQualifiedNameUsageInspection */
if(
(PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage) // @todo: remove version check in v6
|| (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd'))
){
throw new QRCodeDecoderException('Invalid GD image source.'); // @codeCoverageIgnore
}
parent::__construct(imagesx($gdImage), imagesy($gdImage), $options);
$this->gdImage = $gdImage;
if($this->options->readerGrayscale){
imagefilter($this->gdImage, IMG_FILTER_GRAYSCALE);
}
if($this->options->readerInvertColors){
imagefilter($this->gdImage, IMG_FILTER_NEGATE);
}
if($this->options->readerIncreaseContrast){
imagefilter($this->gdImage, IMG_FILTER_BRIGHTNESS, -100);
imagefilter($this->gdImage, IMG_FILTER_CONTRAST, -100);
}
$this->setLuminancePixels();
}
/**
*
*/
protected function setLuminancePixels():void{
for($j = 0; $j < $this->height; $j++){
for($i = 0; $i < $this->width; $i++){
$argb = imagecolorat($this->gdImage, $i, $j);
$pixel = imagecolorsforindex($this->gdImage, $argb);
$this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']);
}
}
}
/** @inheritDoc */
public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
return new self(imagecreatefromstring(file_get_contents(self::checkFile($path))), $options);
}
/** @inheritDoc */
public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
return new self(imagecreatefromstring($blob), $options);
}
}

View file

@ -0,0 +1,154 @@
<?php
/**
* Class GF256
*
* @created 16.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function array_fill;
/**
* This class contains utility methods for performing mathematical operations over
* the Galois Fields. Operations use a given primitive polynomial in calculations.
*
* Throughout this package, elements of the GF are represented as an int
* for convenience and speed (but at the cost of memory).
*
*
* @author Sean Owen
* @author David Olivier
*/
final class GF256{
/**
* irreducible polynomial whose coefficients are represented by the bits of an int,
* where the least-significant bit represents the constant coefficient
*/
# private int $primitive = 0x011D;
private const logTable = [
0, // the first value is never returned, index starts at 1
0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75,
4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113,
5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69,
29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166,
6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136,
54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64,
30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61,
202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87,
7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24,
227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46,
55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97,
242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162,
31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246,
108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90,
203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215,
79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175,
];
private const expTable = [
1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38,
76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192,
157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35,
70, 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161,
95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240,
253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226,
217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206,
129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204,
133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84,
168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115,
230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255,
227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65,
130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166,
81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9,
18, 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22,
44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1,
];
/**
* Implements both addition and subtraction -- they are the same in GF(size).
*
* @return int sum/difference of a and b
*/
public static function addOrSubtract(int $a, int $b):int{
return ($a ^ $b);
}
/**
* @return GenericGFPoly the monomial representing coefficient * x^degree
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function buildMonomial(int $degree, int $coefficient):GenericGFPoly{
if($degree < 0){
throw new QRCodeException('degree < 0');
}
$coefficients = array_fill(0, ($degree + 1), 0);
$coefficients[0] = $coefficient;
return new GenericGFPoly($coefficients);
}
/**
* @return int 2 to the power of $a in GF(size)
*/
public static function exp(int $a):int{
if($a < 0){
$a += 255;
}
elseif($a >= 256){
$a -= 255;
}
return self::expTable[$a];
}
/**
* @return int base 2 log of $a in GF(size)
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function log(int $a):int{
if($a < 1){
throw new QRCodeException('$a < 1');
}
return self::logTable[$a];
}
/**
* @return int multiplicative inverse of a
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function inverse(int $a):int{
if($a === 0){
throw new QRCodeException('$a === 0');
}
return self::expTable[(256 - self::logTable[$a] - 1)];
}
/**
* @return int product of a and b in GF(size)
*/
public static function multiply(int $a, int $b):int{
if($a === 0 || $b === 0){
return 0;
}
return self::expTable[((self::logTable[$a] + self::logTable[$b]) % 255)];
}
}

View file

@ -0,0 +1,263 @@
<?php
/**
* Class GenericGFPoly
*
* @created 16.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function array_fill, array_slice, array_splice, count;
/**
* Represents a polynomial whose coefficients are elements of a GF.
* Instances of this class are immutable.
*
* Much credit is due to William Rucklidge since portions of this code are an indirect
* port of his C++ Reed-Solomon implementation.
*
* @author Sean Owen
*/
final class GenericGFPoly{
private array $coefficients;
/**
* @param array $coefficients array coefficients as ints representing elements of GF(size), arranged
* from most significant (highest-power term) coefficient to the least significant
* @param int|null $degree
*
* @throws \chillerlan\QRCode\QRCodeException if argument is null or empty, or if leading coefficient is 0 and this
* is not a constant polynomial (that is, it is not the monomial "0")
*/
public function __construct(array $coefficients, int $degree = null){
$degree ??= 0;
if(empty($coefficients)){
throw new QRCodeException('arg $coefficients is empty');
}
if($degree < 0){
throw new QRCodeException('negative degree');
}
$coefficientsLength = count($coefficients);
// Leading term must be non-zero for anything except the constant polynomial "0"
$firstNonZero = 0;
while($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] === 0){
$firstNonZero++;
}
$this->coefficients = [0];
if($firstNonZero !== $coefficientsLength){
$this->coefficients = array_fill(0, ($coefficientsLength - $firstNonZero + $degree), 0);
for($i = 0; $i < ($coefficientsLength - $firstNonZero); $i++){
$this->coefficients[$i] = $coefficients[($i + $firstNonZero)];
}
}
}
/**
* @return int $coefficient of x^degree term in this polynomial
*/
public function getCoefficient(int $degree):int{
return $this->coefficients[(count($this->coefficients) - 1 - $degree)];
}
/**
* @return int[]
*/
public function getCoefficients():array{
return $this->coefficients;
}
/**
* @return int $degree of this polynomial
*/
public function getDegree():int{
return (count($this->coefficients) - 1);
}
/**
* @return bool true if this polynomial is the monomial "0"
*/
public function isZero():bool{
return $this->coefficients[0] === 0;
}
/**
* @return int evaluation of this polynomial at a given point
*/
public function evaluateAt(int $a):int{
if($a === 0){
// Just return the x^0 coefficient
return $this->getCoefficient(0);
}
$result = 0;
foreach($this->coefficients as $c){
// if $a === 1 just the sum of the coefficients
$result = GF256::addOrSubtract((($a === 1) ? $result : GF256::multiply($a, $result)), $c);
}
return $result;
}
/**
*
*/
public function multiply(GenericGFPoly $other):self{
if($this->isZero() || $other->isZero()){
return new self([0]);
}
$product = array_fill(0, (count($this->coefficients) + count($other->coefficients) - 1), 0);
foreach($this->coefficients as $i => $aCoeff){
foreach($other->coefficients as $j => $bCoeff){
$product[($i + $j)] ^= GF256::multiply($aCoeff, $bCoeff);
}
}
return new self($product);
}
/**
* @return \chillerlan\QRCode\Common\GenericGFPoly[] [quotient, remainder]
* @throws \chillerlan\QRCode\QRCodeException
*/
public function divide(GenericGFPoly $other):array{
if($other->isZero()){
throw new QRCodeException('Division by 0');
}
$quotient = new self([0]);
$remainder = clone $this;
$denominatorLeadingTerm = $other->getCoefficient($other->getDegree());
$inverseDenominatorLeadingTerm = GF256::inverse($denominatorLeadingTerm);
while($remainder->getDegree() >= $other->getDegree() && !$remainder->isZero()){
$scale = GF256::multiply($remainder->getCoefficient($remainder->getDegree()), $inverseDenominatorLeadingTerm);
$diff = ($remainder->getDegree() - $other->getDegree());
$quotient = $quotient->addOrSubtract(GF256::buildMonomial($diff, $scale));
$remainder = $remainder->addOrSubtract($other->multiplyByMonomial($diff, $scale));
}
return [$quotient, $remainder];
}
/**
*
*/
public function multiplyInt(int $scalar):self{
if($scalar === 0){
return new self([0]);
}
if($scalar === 1){
return $this;
}
$product = array_fill(0, count($this->coefficients), 0);
foreach($this->coefficients as $i => $c){
$product[$i] = GF256::multiply($c, $scalar);
}
return new self($product);
}
/**
* @throws \chillerlan\QRCode\QRCodeException
*/
public function multiplyByMonomial(int $degree, int $coefficient):self{
if($degree < 0){
throw new QRCodeException('degree < 0');
}
if($coefficient === 0){
return new self([0]);
}
$product = array_fill(0, (count($this->coefficients) + $degree), 0);
foreach($this->coefficients as $i => $c){
$product[$i] = GF256::multiply($c, $coefficient);
}
return new self($product);
}
/**
*
*/
public function mod(GenericGFPoly $other):self{
if((count($this->coefficients) - count($other->coefficients)) < 0){
return $this;
}
$ratio = (GF256::log($this->coefficients[0]) - GF256::log($other->coefficients[0]));
foreach($other->coefficients as $i => $c){
$this->coefficients[$i] ^= GF256::exp(GF256::log($c) + $ratio);
}
return (new self($this->coefficients))->mod($other);
}
/**
*
*/
public function addOrSubtract(GenericGFPoly $other):self{
if($this->isZero()){
return $other;
}
if($other->isZero()){
return $this;
}
$smallerCoefficients = $this->coefficients;
$largerCoefficients = $other->coefficients;
if(count($smallerCoefficients) > count($largerCoefficients)){
$temp = $smallerCoefficients;
$smallerCoefficients = $largerCoefficients;
$largerCoefficients = $temp;
}
$sumDiff = array_fill(0, count($largerCoefficients), 0);
$lengthDiff = (count($largerCoefficients) - count($smallerCoefficients));
// Copy high-order terms only found in higher-degree polynomial's coefficients
array_splice($sumDiff, 0, $lengthDiff, array_slice($largerCoefficients, 0, $lengthDiff));
$countLargerCoefficients = count($largerCoefficients);
for($i = $lengthDiff; $i < $countLargerCoefficients; $i++){
$sumDiff[$i] = GF256::addOrSubtract($smallerCoefficients[($i - $lengthDiff)], $largerCoefficients[$i]);
}
return new self($sumDiff);
}
}

View file

@ -0,0 +1,78 @@
<?php
/**
* Class IMagickLuminanceSource
*
* @created 17.01.2021
* @author Ashot Khanamiryan
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Common;
use chillerlan\Settings\SettingsContainerInterface;
use Imagick;
use function count;
/**
* This class is used to help decode images from files which arrive as Imagick Resource
* It does not support rotation.
*/
class IMagickLuminanceSource extends LuminanceSourceAbstract{
protected Imagick $imagick;
/**
* IMagickLuminanceSource constructor.
*/
public function __construct(Imagick $imagick, SettingsContainerInterface $options = null){
parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight(), $options);
$this->imagick = $imagick;
if($this->options->readerGrayscale){
$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
}
if($this->options->readerInvertColors){
$this->imagick->negateImage($this->options->readerGrayscale);
}
if($this->options->readerIncreaseContrast){
for($i = 0; $i < 10; $i++){
$this->imagick->contrastImage(false); // misleading docs
}
}
$this->setLuminancePixels();
}
/**
*
*/
protected function setLuminancePixels():void{
$pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
$count = count($pixels);
for($i = 0; $i < $count; $i += 3){
$this->setLuminancePixel(($pixels[$i] & 0xff), ($pixels[($i + 1)] & 0xff), ($pixels[($i + 2)] & 0xff));
}
}
/** @inheritDoc */
public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
return new self(new Imagick(self::checkFile($path)), $options);
}
/** @inheritDoc */
public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
$im = new Imagick;
$im->readImageBlob($blob);
return new self($im, $options);
}
}

View file

@ -0,0 +1,104 @@
<?php
/**
* Class LuminanceSourceAbstract
*
* @created 24.01.2021
* @author ZXing Authors
* @author Ashot Khanamiryan
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\Decoder\QRCodeDecoderException;
use chillerlan\QRCode\QROptions;
use chillerlan\Settings\SettingsContainerInterface;
use function array_slice, array_splice, file_exists, is_file, is_readable, realpath;
/**
* The purpose of this class hierarchy is to abstract different bitmap implementations across
* platforms into a standard interface for requesting greyscale luminance values.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{
/** @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface */
protected SettingsContainerInterface $options;
protected array $luminances;
protected int $width;
protected int $height;
/**
*
*/
public function __construct(int $width, int $height, SettingsContainerInterface $options = null){
$this->width = $width;
$this->height = $height;
$this->options = ($options ?? new QROptions);
$this->luminances = [];
}
/** @inheritDoc */
public function getLuminances():array{
return $this->luminances;
}
/** @inheritDoc */
public function getWidth():int{
return $this->width;
}
/** @inheritDoc */
public function getHeight():int{
return $this->height;
}
/** @inheritDoc */
public function getRow(int $y):array{
if($y < 0 || $y >= $this->getHeight()){
throw new QRCodeDecoderException('Requested row is outside the image: '.$y);
}
$arr = [];
array_splice($arr, 0, $this->width, array_slice($this->luminances, ($y * $this->width), $this->width));
return $arr;
}
/**
*
*/
protected function setLuminancePixel(int $r, int $g, int $b):void{
$this->luminances[] = ($r === $g && $g === $b)
// Image is already greyscale, so pick any channel.
? $r // (($r + 128) % 256) - 128;
// Calculate luminance cheaply, favoring green.
: (($r + 2 * $g + $b) / 4); // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128;
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
protected static function checkFile(string $path):string{
$path = trim($path);
if(!file_exists($path) || !is_file($path) || !is_readable($path)){
throw new QRCodeDecoderException('invalid file: '.$path);
}
$realpath = realpath($path);
if($realpath === false){
throw new QRCodeDecoderException('unable to resolve path: '.$path);
}
return $realpath;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* Interface LuminanceSourceInterface
*
* @created 18.11.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
/**
*/
interface LuminanceSourceInterface{
/**
* Fetches luminance data for the underlying bitmap. Values should be fetched using:
* `int luminance = array[y * width + x] & 0xff`
*
* @return array A row-major 2D array of luminance values. Do not use result $length as it may be
* larger than $width * $height bytes on some platforms. Do not modify the contents
* of the result.
*/
public function getLuminances():array;
/**
* @return int The width of the bitmap.
*/
public function getWidth():int;
/**
* @return int The height of the bitmap.
*/
public function getHeight():int;
/**
* Fetches one row of luminance data from the underlying platform's bitmap. Values range from
* 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have
* to bitwise and with 0xff for each value. It is preferable for implementations of this method
* to only fetch this row rather than the whole image, since no 2D Readers may be installed and
* getLuminances() may never be called.
*
* @param int $y The row to fetch, which must be in [0,getHeight())
*
* @return array An array containing the luminance data.
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function getRow(int $y):array;
/**
* Creates a LuminanceSource instance from the given file
*/
public static function fromFile(string $path):self;
/**
* Creates a LuminanceSource instance from the given data blob
*/
public static function fromBlob(string $blob):self;
}

View file

@ -0,0 +1,329 @@
<?php
/**
* Class MaskPattern
*
* @created 19.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use chillerlan\QRCode\Data\QRMatrix;
use Closure;
use function abs, array_column, array_search, intdiv, min;
/**
* ISO/IEC 18004:2000 Section 8.8.1
* ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
*
* @see http://www.thonky.com/qr-code-tutorial/data-masking
* @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java
*/
final class MaskPattern{
/**
* @see \chillerlan\QRCode\QROptionsTrait::$maskPattern
*
* @var int
*/
public const AUTO = -1;
public const PATTERN_000 = 0b000;
public const PATTERN_001 = 0b001;
public const PATTERN_010 = 0b010;
public const PATTERN_011 = 0b011;
public const PATTERN_100 = 0b100;
public const PATTERN_101 = 0b101;
public const PATTERN_110 = 0b110;
public const PATTERN_111 = 0b111;
/**
* @var int[]
*/
public const PATTERNS = [
self::PATTERN_000,
self::PATTERN_001,
self::PATTERN_010,
self::PATTERN_011,
self::PATTERN_100,
self::PATTERN_101,
self::PATTERN_110,
self::PATTERN_111,
];
/*
* Penalty scores
*
* ISO/IEC 18004:2000 Section 8.8.1 - Table 24
*/
private const PENALTY_N1 = 3;
private const PENALTY_N2 = 3;
private const PENALTY_N3 = 40;
private const PENALTY_N4 = 10;
/**
* The current mask pattern value (0-7)
*/
private int $maskPattern;
/**
* MaskPattern constructor.
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $maskPattern){
if((0b111 & $maskPattern) !== $maskPattern){
throw new QRCodeException('invalid mask pattern');
}
$this->maskPattern = $maskPattern;
}
/**
* Returns the current mask pattern
*/
public function getPattern():int{
return $this->maskPattern;
}
/**
* Returns a closure that applies the mask for the chosen mask pattern.
*
* Note that the diagram in section 6.8.1 is misleading since it indicates that $i is column position
* and $j is row position. In fact, as the text says, $i is row position and $j is column position.
*
* @see https://www.thonky.com/qr-code-tutorial/mask-patterns
* @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117
*/
public function getMask():Closure{
// $x = column (width), $y = row (height)
return [
self::PATTERN_000 => fn(int $x, int $y):bool => (($x + $y) % 2) === 0,
self::PATTERN_001 => fn(int $x, int $y):bool => ($y % 2) === 0,
self::PATTERN_010 => fn(int $x, int $y):bool => ($x % 3) === 0,
self::PATTERN_011 => fn(int $x, int $y):bool => (($x + $y) % 3) === 0,
self::PATTERN_100 => fn(int $x, int $y):bool => ((intdiv($y, 2) + intdiv($x, 3)) % 2) === 0,
self::PATTERN_101 => fn(int $x, int $y):bool => (($x * $y) % 6) === 0,
self::PATTERN_110 => fn(int $x, int $y):bool => (($x * $y) % 6) < 3,
self::PATTERN_111 => fn(int $x, int $y):bool => (($x + $y + (($x * $y) % 3)) % 2) === 0,
][$this->maskPattern];
}
/**
* Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result
*/
public static function getBestPattern(QRMatrix $QRMatrix):self{
$penalties = [];
$size = $QRMatrix->getSize();
foreach(self::PATTERNS as $pattern){
$mp = new self($pattern);
$matrix = (clone $QRMatrix)->setFormatInfo($mp)->mask($mp)->getMatrix(true);
$penalty = 0;
for($level = 1; $level <= 4; $level++){
$penalty += self::{'testRule'.$level}($matrix, $size, $size);
}
$penalties[$pattern] = (int)$penalty;
}
return new self(array_search(min($penalties), $penalties, true));
}
/**
* Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and
* give penalty to them. Example: 00000 or 11111.
*/
public static function testRule1(array $matrix, int $height, int $width):int{
$penalty = 0;
// horizontal
foreach($matrix as $row){
$penalty += self::applyRule1($row);
}
// vertical
for($x = 0; $x < $width; $x++){
$penalty += self::applyRule1(array_column($matrix, $x));
}
return $penalty;
}
/**
*
*/
private static function applyRule1(array $rc):int{
$penalty = 0;
$numSameBitCells = 0;
$prevBit = null;
foreach($rc as $val){
if($val === $prevBit){
$numSameBitCells++;
}
else{
if($numSameBitCells >= 5){
$penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
}
$numSameBitCells = 1; // Include the cell itself.
$prevBit = $val;
}
}
if($numSameBitCells >= 5){
$penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
}
return $penalty;
}
/**
* Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give
* 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 testRule2(array $matrix, int $height, int $width):int{
$penalty = 0;
foreach($matrix as $y => $row){
if($y > ($height - 2)){
break;
}
foreach($row as $x => $val){
if($x > ($width - 2)){
break;
}
if(
$val === $row[($x + 1)]
&& $val === $matrix[($y + 1)][$x]
&& $val === $matrix[($y + 1)][($x + 1)]
){
$penalty++;
}
}
}
return (self::PENALTY_N2 * $penalty);
}
/**
* Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4
* starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we
* find patterns like 000010111010000, we give penalty once.
*/
public static function testRule3(array $matrix, int $height, int $width):int{
$penalties = 0;
foreach($matrix as $y => $row){
foreach($row as $x => $val){
if(
($x + 6) < $width
&& $val
&& !$row[($x + 1)]
&& $row[($x + 2)]
&& $row[($x + 3)]
&& $row[($x + 4)]
&& !$row[($x + 5)]
&& $row[($x + 6)]
&& (
self::isWhiteHorizontal($row, $width, ($x - 4), $x)
|| self::isWhiteHorizontal($row, $width, ($x + 7), ($x + 11))
)
){
$penalties++;
}
if(
($y + 6) < $height
&& $val
&& !$matrix[($y + 1)][$x]
&& $matrix[($y + 2)][$x]
&& $matrix[($y + 3)][$x]
&& $matrix[($y + 4)][$x]
&& !$matrix[($y + 5)][$x]
&& $matrix[($y + 6)][$x]
&& (
self::isWhiteVertical($matrix, $height, $x, ($y - 4), $y)
|| self::isWhiteVertical($matrix, $height, $x, ($y + 7), ($y + 11))
)
){
$penalties++;
}
}
}
return ($penalties * self::PENALTY_N3);
}
/**
*
*/
private static function isWhiteHorizontal(array $row, int $width, int $from, int $to):bool{
if($from < 0 || $width < $to){
return false;
}
for($x = $from; $x < $to; $x++){
if($row[$x]){
return false;
}
}
return true;
}
/**
*
*/
private static function isWhiteVertical(array $matrix, int $height, int $x, int $from, int $to):bool{
if($from < 0 || $height < $to){
return false;
}
for($y = $from; $y < $to; $y++){
if($matrix[$y][$x] === true){
return false;
}
}
return true;
}
/**
* Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give
* penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance.
*/
public static function testRule4(array $matrix, int $height, int $width):int{
$darkCells = 0;
$totalCells = ($height * $width);
foreach($matrix as $row){
foreach($row as $val){
if($val === true){
$darkCells++;
}
}
}
return (intdiv((abs($darkCells * 2 - $totalCells) * 10), $totalCells) * self::PENALTY_N4);
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
* Class Mode
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\Data\{AlphaNum, Byte, Hanzi, Kanji, Number};
use chillerlan\QRCode\QRCodeException;
/**
* Data mode information - ISO 18004:2006, 6.4.1, Tables 2 and 3
*/
final class Mode{
// ISO/IEC 18004:2000 Table 2
/** @var int */
public const TERMINATOR = 0b0000;
/** @var int */
public const NUMBER = 0b0001;
/** @var int */
public const ALPHANUM = 0b0010;
/** @var int */
public const BYTE = 0b0100;
/** @var int */
public const KANJI = 0b1000;
/** @var int */
public const HANZI = 0b1101;
/** @var int */
public const STRCTURED_APPEND = 0b0011;
/** @var int */
public const FNC1_FIRST = 0b0101;
/** @var int */
public const FNC1_SECOND = 0b1001;
/** @var int */
public const ECI = 0b0111;
/**
* mode length bits for the version breakpoints 1-9, 10-26 and 27-40
*
* ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator
*/
public const LENGTH_BITS = [
self::NUMBER => [10, 12, 14],
self::ALPHANUM => [ 9, 11, 13],
self::BYTE => [ 8, 16, 16],
self::KANJI => [ 8, 10, 12],
self::HANZI => [ 8, 10, 12],
self::ECI => [ 0, 0, 0],
];
/**
* Map of data mode => interface (detection order)
*
* @var string[]
*/
public const INTERFACES = [
self::NUMBER => Number::class,
self::ALPHANUM => AlphaNum::class,
self::KANJI => Kanji::class,
self::HANZI => Hanzi::class,
self::BYTE => Byte::class,
];
/**
* returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function getLengthBitsForVersion(int $mode, int $version):int{
if(!isset(self::LENGTH_BITS[$mode])){
throw new QRCodeException('invalid mode given');
}
$minVersion = 0;
foreach([9, 26, 40] as $key => $breakpoint){
if($version > $minVersion && $version <= $breakpoint){
return self::LENGTH_BITS[$mode][$key];
}
$minVersion = $breakpoint;
}
throw new QRCodeException(sprintf('invalid version number: %d', $version));
}
}

View file

@ -0,0 +1,287 @@
<?php
/**
* Class Version
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
/**
* Version related tables and methods
*/
final class Version{
/**
* Enable version auto detection
*
* @see \chillerlan\QRCode\QROptionsTrait::$version
*
* @var int
*/
public const AUTO = -1;
/**
* ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns
*
* version -> pattern
*
* @var int[][]
*/
private const ALIGNMENT_PATTERN = [
1 => [],
2 => [6, 18],
3 => [6, 22],
4 => [6, 26],
5 => [6, 30],
6 => [6, 34],
7 => [6, 22, 38],
8 => [6, 24, 42],
9 => [6, 26, 46],
10 => [6, 28, 50],
11 => [6, 30, 54],
12 => [6, 32, 58],
13 => [6, 34, 62],
14 => [6, 26, 46, 66],
15 => [6, 26, 48, 70],
16 => [6, 26, 50, 74],
17 => [6, 30, 54, 78],
18 => [6, 30, 56, 82],
19 => [6, 30, 58, 86],
20 => [6, 34, 62, 90],
21 => [6, 28, 50, 72, 94],
22 => [6, 26, 50, 74, 98],
23 => [6, 30, 54, 78, 102],
24 => [6, 28, 54, 80, 106],
25 => [6, 32, 58, 84, 110],
26 => [6, 30, 58, 86, 114],
27 => [6, 34, 62, 90, 118],
28 => [6, 26, 50, 74, 98, 122],
29 => [6, 30, 54, 78, 102, 126],
30 => [6, 26, 52, 78, 104, 130],
31 => [6, 30, 56, 82, 108, 134],
32 => [6, 34, 60, 86, 112, 138],
33 => [6, 30, 58, 86, 114, 142],
34 => [6, 34, 62, 90, 118, 146],
35 => [6, 30, 54, 78, 102, 126, 150],
36 => [6, 24, 50, 76, 102, 128, 154],
37 => [6, 28, 54, 80, 106, 132, 158],
38 => [6, 32, 58, 84, 110, 136, 162],
39 => [6, 26, 54, 82, 110, 138, 166],
40 => [6, 30, 58, 86, 114, 142, 170],
];
/**
* ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version
*
* no version pattern for QR Codes < 7
*
* @var int[]
*/
private const VERSION_PATTERN = [
7 => 0b000111110010010100,
8 => 0b001000010110111100,
9 => 0b001001101010011001,
10 => 0b001010010011010011,
11 => 0b001011101111110110,
12 => 0b001100011101100010,
13 => 0b001101100001000111,
14 => 0b001110011000001101,
15 => 0b001111100100101000,
16 => 0b010000101101111000,
17 => 0b010001010001011101,
18 => 0b010010101000010111,
19 => 0b010011010100110010,
20 => 0b010100100110100110,
21 => 0b010101011010000011,
22 => 0b010110100011001001,
23 => 0b010111011111101100,
24 => 0b011000111011000100,
25 => 0b011001000111100001,
26 => 0b011010111110101011,
27 => 0b011011000010001110,
28 => 0b011100110000011010,
29 => 0b011101001100111111,
30 => 0b011110110101110101,
31 => 0b011111001001010000,
32 => 0b100000100111010101,
33 => 0b100001011011110000,
34 => 0b100010100010111010,
35 => 0b100011011110011111,
36 => 0b100100101100001011,
37 => 0b100101010000101110,
38 => 0b100110101001100100,
39 => 0b100111010101000001,
40 => 0b101000110001101001,
];
/**
* ISO/IEC 18004:2000 Tables 13-22 - Error correction characteristics
*
* @see http://www.thonky.com/qr-code-tutorial/error-correction-table
*/
private const RSBLOCKS = [
1 => [[ 7, [[ 1, 19], [ 0, 0]]], [10, [[ 1, 16], [ 0, 0]]], [13, [[ 1, 13], [ 0, 0]]], [17, [[ 1, 9], [ 0, 0]]]],
2 => [[10, [[ 1, 34], [ 0, 0]]], [16, [[ 1, 28], [ 0, 0]]], [22, [[ 1, 22], [ 0, 0]]], [28, [[ 1, 16], [ 0, 0]]]],
3 => [[15, [[ 1, 55], [ 0, 0]]], [26, [[ 1, 44], [ 0, 0]]], [18, [[ 2, 17], [ 0, 0]]], [22, [[ 2, 13], [ 0, 0]]]],
4 => [[20, [[ 1, 80], [ 0, 0]]], [18, [[ 2, 32], [ 0, 0]]], [26, [[ 2, 24], [ 0, 0]]], [16, [[ 4, 9], [ 0, 0]]]],
5 => [[26, [[ 1, 108], [ 0, 0]]], [24, [[ 2, 43], [ 0, 0]]], [18, [[ 2, 15], [ 2, 16]]], [22, [[ 2, 11], [ 2, 12]]]],
6 => [[18, [[ 2, 68], [ 0, 0]]], [16, [[ 4, 27], [ 0, 0]]], [24, [[ 4, 19], [ 0, 0]]], [28, [[ 4, 15], [ 0, 0]]]],
7 => [[20, [[ 2, 78], [ 0, 0]]], [18, [[ 4, 31], [ 0, 0]]], [18, [[ 2, 14], [ 4, 15]]], [26, [[ 4, 13], [ 1, 14]]]],
8 => [[24, [[ 2, 97], [ 0, 0]]], [22, [[ 2, 38], [ 2, 39]]], [22, [[ 4, 18], [ 2, 19]]], [26, [[ 4, 14], [ 2, 15]]]],
9 => [[30, [[ 2, 116], [ 0, 0]]], [22, [[ 3, 36], [ 2, 37]]], [20, [[ 4, 16], [ 4, 17]]], [24, [[ 4, 12], [ 4, 13]]]],
10 => [[18, [[ 2, 68], [ 2, 69]]], [26, [[ 4, 43], [ 1, 44]]], [24, [[ 6, 19], [ 2, 20]]], [28, [[ 6, 15], [ 2, 16]]]],
11 => [[20, [[ 4, 81], [ 0, 0]]], [30, [[ 1, 50], [ 4, 51]]], [28, [[ 4, 22], [ 4, 23]]], [24, [[ 3, 12], [ 8, 13]]]],
12 => [[24, [[ 2, 92], [ 2, 93]]], [22, [[ 6, 36], [ 2, 37]]], [26, [[ 4, 20], [ 6, 21]]], [28, [[ 7, 14], [ 4, 15]]]],
13 => [[26, [[ 4, 107], [ 0, 0]]], [22, [[ 8, 37], [ 1, 38]]], [24, [[ 8, 20], [ 4, 21]]], [22, [[12, 11], [ 4, 12]]]],
14 => [[30, [[ 3, 115], [ 1, 116]]], [24, [[ 4, 40], [ 5, 41]]], [20, [[11, 16], [ 5, 17]]], [24, [[11, 12], [ 5, 13]]]],
15 => [[22, [[ 5, 87], [ 1, 88]]], [24, [[ 5, 41], [ 5, 42]]], [30, [[ 5, 24], [ 7, 25]]], [24, [[11, 12], [ 7, 13]]]],
16 => [[24, [[ 5, 98], [ 1, 99]]], [28, [[ 7, 45], [ 3, 46]]], [24, [[15, 19], [ 2, 20]]], [30, [[ 3, 15], [13, 16]]]],
17 => [[28, [[ 1, 107], [ 5, 108]]], [28, [[10, 46], [ 1, 47]]], [28, [[ 1, 22], [15, 23]]], [28, [[ 2, 14], [17, 15]]]],
18 => [[30, [[ 5, 120], [ 1, 121]]], [26, [[ 9, 43], [ 4, 44]]], [28, [[17, 22], [ 1, 23]]], [28, [[ 2, 14], [19, 15]]]],
19 => [[28, [[ 3, 113], [ 4, 114]]], [26, [[ 3, 44], [11, 45]]], [26, [[17, 21], [ 4, 22]]], [26, [[ 9, 13], [16, 14]]]],
20 => [[28, [[ 3, 107], [ 5, 108]]], [26, [[ 3, 41], [13, 42]]], [30, [[15, 24], [ 5, 25]]], [28, [[15, 15], [10, 16]]]],
21 => [[28, [[ 4, 116], [ 4, 117]]], [26, [[17, 42], [ 0, 0]]], [28, [[17, 22], [ 6, 23]]], [30, [[19, 16], [ 6, 17]]]],
22 => [[28, [[ 2, 111], [ 7, 112]]], [28, [[17, 46], [ 0, 0]]], [30, [[ 7, 24], [16, 25]]], [24, [[34, 13], [ 0, 0]]]],
23 => [[30, [[ 4, 121], [ 5, 122]]], [28, [[ 4, 47], [14, 48]]], [30, [[11, 24], [14, 25]]], [30, [[16, 15], [14, 16]]]],
24 => [[30, [[ 6, 117], [ 4, 118]]], [28, [[ 6, 45], [14, 46]]], [30, [[11, 24], [16, 25]]], [30, [[30, 16], [ 2, 17]]]],
25 => [[26, [[ 8, 106], [ 4, 107]]], [28, [[ 8, 47], [13, 48]]], [30, [[ 7, 24], [22, 25]]], [30, [[22, 15], [13, 16]]]],
26 => [[28, [[10, 114], [ 2, 115]]], [28, [[19, 46], [ 4, 47]]], [28, [[28, 22], [ 6, 23]]], [30, [[33, 16], [ 4, 17]]]],
27 => [[30, [[ 8, 122], [ 4, 123]]], [28, [[22, 45], [ 3, 46]]], [30, [[ 8, 23], [26, 24]]], [30, [[12, 15], [28, 16]]]],
28 => [[30, [[ 3, 117], [10, 118]]], [28, [[ 3, 45], [23, 46]]], [30, [[ 4, 24], [31, 25]]], [30, [[11, 15], [31, 16]]]],
29 => [[30, [[ 7, 116], [ 7, 117]]], [28, [[21, 45], [ 7, 46]]], [30, [[ 1, 23], [37, 24]]], [30, [[19, 15], [26, 16]]]],
30 => [[30, [[ 5, 115], [10, 116]]], [28, [[19, 47], [10, 48]]], [30, [[15, 24], [25, 25]]], [30, [[23, 15], [25, 16]]]],
31 => [[30, [[13, 115], [ 3, 116]]], [28, [[ 2, 46], [29, 47]]], [30, [[42, 24], [ 1, 25]]], [30, [[23, 15], [28, 16]]]],
32 => [[30, [[17, 115], [ 0, 0]]], [28, [[10, 46], [23, 47]]], [30, [[10, 24], [35, 25]]], [30, [[19, 15], [35, 16]]]],
33 => [[30, [[17, 115], [ 1, 116]]], [28, [[14, 46], [21, 47]]], [30, [[29, 24], [19, 25]]], [30, [[11, 15], [46, 16]]]],
34 => [[30, [[13, 115], [ 6, 116]]], [28, [[14, 46], [23, 47]]], [30, [[44, 24], [ 7, 25]]], [30, [[59, 16], [ 1, 17]]]],
35 => [[30, [[12, 121], [ 7, 122]]], [28, [[12, 47], [26, 48]]], [30, [[39, 24], [14, 25]]], [30, [[22, 15], [41, 16]]]],
36 => [[30, [[ 6, 121], [14, 122]]], [28, [[ 6, 47], [34, 48]]], [30, [[46, 24], [10, 25]]], [30, [[ 2, 15], [64, 16]]]],
37 => [[30, [[17, 122], [ 4, 123]]], [28, [[29, 46], [14, 47]]], [30, [[49, 24], [10, 25]]], [30, [[24, 15], [46, 16]]]],
38 => [[30, [[ 4, 122], [18, 123]]], [28, [[13, 46], [32, 47]]], [30, [[48, 24], [14, 25]]], [30, [[42, 15], [32, 16]]]],
39 => [[30, [[20, 117], [ 4, 118]]], [28, [[40, 47], [ 7, 48]]], [30, [[43, 24], [22, 25]]], [30, [[10, 15], [67, 16]]]],
40 => [[30, [[19, 118], [ 6, 119]]], [28, [[18, 47], [31, 48]]], [30, [[34, 24], [34, 25]]], [30, [[20, 15], [61, 16]]]],
];
/**
* ISO/IEC 18004:2000 Table 1 - Data capacity of all versions of QR Code
*/
private const TOTAL_CODEWORDS = [
1 => 26,
2 => 44,
3 => 70,
4 => 100,
5 => 134,
6 => 172,
7 => 196,
8 => 242,
9 => 292,
10 => 346,
11 => 404,
12 => 466,
13 => 532,
14 => 581,
15 => 655,
16 => 733,
17 => 815,
18 => 901,
19 => 991,
20 => 1085,
21 => 1156,
22 => 1258,
23 => 1364,
24 => 1474,
25 => 1588,
26 => 1706,
27 => 1828,
28 => 1921,
29 => 2051,
30 => 2185,
31 => 2323,
32 => 2465,
33 => 2611,
34 => 2761,
35 => 2876,
36 => 3034,
37 => 3196,
38 => 3362,
39 => 3532,
40 => 3706,
];
/**
* QR Code version number
*/
private int $version;
/**
* Version constructor.
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $version){
if($version < 1 || $version > 40){
throw new QRCodeException('invalid version given');
}
$this->version = $version;
}
/**
* returns the current version number as string
*/
public function __toString():string{
return (string)$this->version;
}
/**
* returns the current version number
*/
public function getVersionNumber():int{
return $this->version;
}
/**
* the matrix size for the given version
*/
public function getDimension():int{
return (($this->version * 4) + 17);
}
/**
* the version pattern for the given version
*/
public function getVersionPattern():?int{
return (self::VERSION_PATTERN[$this->version] ?? null);
}
/**
* the alignment patterns for the current version
*
* @return int[]
*/
public function getAlignmentPattern():array{
return self::ALIGNMENT_PATTERN[$this->version];
}
/**
* returns ECC block information for the given $version and $eccLevel
*/
public function getRSBlocks(EccLevel $eccLevel):array{
return self::RSBLOCKS[$this->version][$eccLevel->getOrdinal()];
}
/**
* returns the maximum codewords for the current version
*/
public function getTotalCodewords():int{
return self::TOTAL_CODEWORDS[$this->version];
}
}

View file

@ -0,0 +1,137 @@
<?php
/**
* Class AlphaNum
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use function array_flip, ceil, intdiv, str_split;
/**
* Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / :
*
* ISO/IEC 18004:2000 Section 8.3.3
* ISO/IEC 18004:2000 Section 8.4.3
*/
final class AlphaNum extends QRDataModeAbstract{
/**
* ISO/IEC 18004:2000 Table 5
*
* @var int[]
*/
private const CHAR_TO_ORD = [
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7,
'8' => 8, '9' => 9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15,
'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23,
'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31,
'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39,
'+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44,
];
/**
* @inheritDoc
*/
public const DATAMODE = Mode::ALPHANUM;
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return (int)ceil($this->getCharCount() * (11 / 2));
}
/**
* @inheritDoc
*/
public static function validateString(string $string):bool{
if($string === ''){
return false;
}
foreach(str_split($string) as $chr){
if(!isset(self::CHAR_TO_ORD[$chr])){
return false;
}
}
return true;
}
/**
* @inheritDoc
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$len = $this->getCharCount();
$bitBuffer
->put(self::DATAMODE, 4)
->put($len, $this::getLengthBits($versionNumber))
;
// encode 2 characters in 11 bits
for($i = 0; ($i + 1) < $len; $i += 2){
$bitBuffer->put((self::CHAR_TO_ORD[$this->data[$i]] * 45 + self::CHAR_TO_ORD[$this->data[($i + 1)]]), 11);
}
// encode a remaining character in 6 bits
if($i < $len){
$bitBuffer->put(self::CHAR_TO_ORD[$this->data[$i]], 6);
}
return $this;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
$charmap = array_flip(self::CHAR_TO_ORD);
// @todo
$toAlphaNumericChar = function(int $ord) use ($charmap):string{
if(isset($charmap[$ord])){
return $charmap[$ord];
}
throw new QRCodeDataException('invalid character value: '.$ord);
};
$result = '';
// Read two characters at a time
while($length > 1){
if($bitBuffer->available() < 11){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$nextTwoCharsBits = $bitBuffer->read(11);
$result .= $toAlphaNumericChar(intdiv($nextTwoCharsBits, 45));
$result .= $toAlphaNumericChar($nextTwoCharsBits % 45);
$length -= 2;
}
if($length === 1){
// special case: one character left
if($bitBuffer->available() < 6){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$result .= $toAlphaNumericChar($bitBuffer->read(6));
}
return $result;
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* Class Byte
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use function chr, ord;
/**
* 8-bit Byte mode, ISO-8859-1 or UTF-8
*
* ISO/IEC 18004:2000 Section 8.3.4
* ISO/IEC 18004:2000 Section 8.4.4
*/
final class Byte extends QRDataModeAbstract{
/**
* @inheritDoc
*/
public const DATAMODE = Mode::BYTE;
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return ($this->getCharCount() * 8);
}
/**
* @inheritDoc
*/
public static function validateString(string $string):bool{
return $string !== '';
}
/**
* @inheritDoc
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$len = $this->getCharCount();
$bitBuffer
->put(self::DATAMODE, 4)
->put($len, $this::getLengthBits($versionNumber))
;
$i = 0;
while($i < $len){
$bitBuffer->put(ord($this->data[$i]), 8);
$i++;
}
return $this;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
if($bitBuffer->available() < (8 * $length)){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$readBytes = '';
for($i = 0; $i < $length; $i++){
$readBytes .= chr($bitBuffer->read(8));
}
return $readBytes;
}
}

View file

@ -0,0 +1,155 @@
<?php
/**
* Class ECI
*
* @created 20.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode};
use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf;
/**
* Adds an ECI Designator
*
* ISO/IEC 18004:2000 8.4.1.1
*
* Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment()
*/
final class ECI extends QRDataModeAbstract{
/**
* @inheritDoc
*/
public const DATAMODE = Mode::ECI;
/**
* The current ECI encoding id
*/
private int $encoding;
/**
* @inheritDoc
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(int $encoding){
if($encoding < 0 || $encoding > 999999){
throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding));
}
$this->encoding = $encoding;
}
/**
* @inheritDoc
*/
public function getLengthInBits():int{
if($this->encoding < 128){
return 8;
}
if($this->encoding < 16384){
return 16;
}
return 24;
}
/**
* Writes an ECI designator to the bitbuffer
*
* @inheritDoc
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$bitBuffer->put(self::DATAMODE, 4);
if($this->encoding < 128){
$bitBuffer->put($this->encoding, 8);
}
elseif($this->encoding < 16384){
$bitBuffer->put(($this->encoding | 0x8000), 16);
}
elseif($this->encoding < 1000000){
$bitBuffer->put(($this->encoding | 0xC00000), 24);
}
else{
throw new QRCodeDataException('invalid ECI ID');
}
return $this;
}
/**
* Reads and parses the value of an ECI designator
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function parseValue(BitBuffer $bitBuffer):ECICharset{
$firstByte = $bitBuffer->read(8);
// just one byte
if(($firstByte & 0b10000000) === 0){
$id = ($firstByte & 0b01111111);
}
// two bytes
elseif(($firstByte & 0b11000000) === 0b10000000){
$id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8));
}
// three bytes
elseif(($firstByte & 0b11100000) === 0b11000000){
$id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16));
}
else{
throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte)); // @codeCoverageIgnore
}
return new ECICharset($id);
}
/**
* @codeCoverageIgnore Unused, but required as per interface
*/
public static function validateString(string $string):bool{
return true;
}
/**
* Reads and decodes the ECI designator including the following byte sequence
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$eciCharset = self::parseValue($bitBuffer);
$nextMode = $bitBuffer->read(4);
if($nextMode !== Mode::BYTE){
throw new QRCodeDataException(sprintf('ECI designator followed by invalid mode: "%04b"', $nextMode));
}
$data = Byte::decodeSegment($bitBuffer, $versionNumber);
$encoding = $eciCharset->getName();
if($encoding === null){
// The spec isn't clear on this mode; see
// section 6.4.5: t does not say which encoding to assuming
// upon decoding. I have seen ISO-8859-1 used as well as
// Shift_JIS -- without anything like an ECI designator to
// give a hint.
$encoding = mb_detect_encoding($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true);
if($encoding === false){
throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore
}
}
return mb_convert_encoding($data, mb_internal_encoding(), $encoding);
}
}

View file

@ -0,0 +1,205 @@
<?php
/**
* Class Hanzi
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use Throwable;
use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding,
mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen;
/**
* Hanzi (simplified Chinese) mode, GBT18284-2000: 13-bit double-byte characters from the GB2312/GB18030 character set
*
* Please note that this is not part of the QR Code specification and may not be supported by all readers (ZXing-based ones do).
*
* @see https://en.wikipedia.org/wiki/GB_2312
* @see http://www.herongyang.com/GB2312/Introduction-of-GB2312.html
* @see https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
* @see https://gist.github.com/codemasher/91da33c44bfb48a81a6c1426bb8e4338
* @see https://github.com/zxing/zxing/blob/dfb06fa33b17a9e68321be151c22846c7b78048f/core/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java#L172-L209
* @see https://www.chinesestandard.net/PDF/English.aspx/GBT18284-2000
*/
final class Hanzi extends QRDataModeAbstract{
/**
* possible values: GB2312, GB18030
*
* @var string
*/
public const ENCODING = 'GB18030';
/**
* @todo: other subsets???
*
* @var int
*/
public const GB2312_SUBSET = 0b0001;
/**
* @inheritDoc
*/
public const DATAMODE = Mode::HANZI;
/**
* @inheritDoc
*/
protected function getCharCount():int{
return mb_strlen($this->data, self::ENCODING);
}
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return ($this->getCharCount() * 13);
}
/**
* @inheritDoc
*/
public static function convertEncoding(string $string):string{
mb_detect_order([mb_internal_encoding(), 'UTF-8', 'GB2312', 'GB18030', 'CP936', 'EUC-CN', 'HZ']);
$detected = mb_detect_encoding($string, null, true);
if($detected === false){
throw new QRCodeDataException('mb_detect_encoding error');
}
if($detected === self::ENCODING){
return $string;
}
$string = mb_convert_encoding($string, self::ENCODING, $detected);
if(!is_string($string)){
throw new QRCodeDataException('mb_convert_encoding error');
}
return $string;
}
/**
* checks if a string qualifies as Hanzi/GB2312
*/
public static function validateString(string $string):bool{
try{
$string = self::convertEncoding($string);
}
catch(Throwable $e){
return false;
}
$len = strlen($string);
if($len < 2 || ($len % 2) !== 0){
return false;
}
for($i = 0; $i < $len; $i += 2){
$byte1 = ord($string[$i]);
$byte2 = ord($string[($i + 1)]);
// byte 1 unused ranges
if($byte1 < 0xa1 || ($byte1 > 0xa9 && $byte1 < 0xb0) || $byte1 > 0xf7){
return false;
}
// byte 2 unused ranges
if($byte2 < 0xa1 || $byte2 > 0xfe){
return false;
}
}
return true;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$bitBuffer
->put(self::DATAMODE, 4)
->put($this::GB2312_SUBSET, 4)
->put($this->getCharCount(), $this::getLengthBits($versionNumber))
;
$len = strlen($this->data);
for($i = 0; ($i + 1) < $len; $i += 2){
$c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)])));
if($c >= 0xa1a1 && $c <= 0xaafe){
$c -= 0x0a1a1;
}
elseif($c >= 0xb0a1 && $c <= 0xfafe){
$c -= 0x0a6a1;
}
else{
throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c));
}
$bitBuffer->put((((($c >> 8) & 0xff) * 0x060) + ($c & 0xff)), 13);
}
if($i < $len){
throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1)));
}
return $this;
}
/**
* See specification GBT 18284-2000
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
// Hanzi mode contains a subset indicator right after mode indicator
if($bitBuffer->read(4) !== self::GB2312_SUBSET){
throw new QRCodeDataException('ecpected subset indicator for Hanzi mode');
}
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
if($bitBuffer->available() < ($length * 13)){
throw new QRCodeDataException('not enough bits available');
}
// Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as GB2312 afterwards
$buffer = [];
$offset = 0;
while($length > 0){
// Each 13 bits encodes a 2-byte character
$twoBytes = $bitBuffer->read(13);
$assembledTwoBytes = ((intdiv($twoBytes, 0x060) << 8) | ($twoBytes % 0x060));
$assembledTwoBytes += ($assembledTwoBytes < 0x00a00) // 0x003BF
? 0x0a1a1 // In the 0xA1A1 to 0xAAFE range
: 0x0a6a1; // In the 0xB0A1 to 0xFAFE range
$buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8));
$buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes);
$offset += 2;
$length--;
}
return mb_convert_encoding(implode($buffer), mb_internal_encoding(), self::ENCODING);
}
}

View file

@ -0,0 +1,191 @@
<?php
/**
* Class Kanji
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use Throwable;
use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding,
mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen;
/**
* Kanji mode: 13-bit double-byte characters from the Shift-JIS character set
*
* ISO/IEC 18004:2000 Section 8.3.5
* ISO/IEC 18004:2000 Section 8.4.5
*
* @see https://en.wikipedia.org/wiki/Shift_JIS#As_defined_in_JIS_X_0208:1997
* @see http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml
* @see https://gist.github.com/codemasher/d07d3e6e9346c08e7a41b8b978784952
*/
final class Kanji extends QRDataModeAbstract{
/**
* possible values: SJIS, SJIS-2004
*
* SJIS-2004 may produce errors in PHP < 8
*
* @var string
*/
public const ENCODING = 'SJIS';
/**
* @inheritDoc
*/
public const DATAMODE = Mode::KANJI;
/**
* @inheritDoc
*/
protected function getCharCount():int{
return mb_strlen($this->data, self::ENCODING);
}
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return ($this->getCharCount() * 13);
}
/**
* @inheritDoc
*/
public static function convertEncoding(string $string):string{
mb_detect_order([mb_internal_encoding(), 'UTF-8', 'SJIS', 'SJIS-2004']);
$detected = mb_detect_encoding($string, null, true);
if($detected === false){
throw new QRCodeDataException('mb_detect_encoding error');
}
if($detected === self::ENCODING){
return $string;
}
$string = mb_convert_encoding($string, self::ENCODING, $detected);
if(!is_string($string)){
throw new QRCodeDataException(sprintf('invalid encoding: %s', $detected));
}
return $string;
}
/**
* checks if a string qualifies as SJIS Kanji
*/
public static function validateString(string $string):bool{
try{
$string = self::convertEncoding($string);
}
catch(Throwable $e){
return false;
}
$len = strlen($string);
if($len < 2 || ($len % 2) !== 0){
return false;
}
for($i = 0; $i < $len; $i += 2){
$byte1 = ord($string[$i]);
$byte2 = ord($string[($i + 1)]);
// byte 1 unused and vendor ranges
if($byte1 < 0x81 || ($byte1 > 0x84 && $byte1 < 0x88) || ($byte1 > 0x9f && $byte1 < 0xe0) || $byte1 > 0xea){
return false;
}
// byte 2 unused ranges
if($byte2 < 0x40 || $byte2 === 0x7f || $byte2 > 0xfc){
return false;
}
}
return true;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$bitBuffer
->put(self::DATAMODE, 4)
->put($this->getCharCount(), $this::getLengthBits($versionNumber))
;
$len = strlen($this->data);
for($i = 0; ($i + 1) < $len; $i += 2){
$c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)])));
if($c >= 0x8140 && $c <= 0x9ffc){
$c -= 0x8140;
}
elseif($c >= 0xe040 && $c <= 0xebbf){
$c -= 0xc140;
}
else{
throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c));
}
$bitBuffer->put((((($c >> 8) & 0xff) * 0xc0) + ($c & 0xff)), 13);
}
if($i < $len){
throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1)));
}
return $this;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
if($bitBuffer->available() < ($length * 13)){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
// Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as SJIS afterwards
$buffer = [];
$offset = 0;
while($length > 0){
// Each 13 bits encodes a 2-byte character
$twoBytes = $bitBuffer->read(13);
$assembledTwoBytes = ((intdiv($twoBytes, 0x0c0) << 8) | ($twoBytes % 0x0c0));
$assembledTwoBytes += ($assembledTwoBytes < 0x01f00)
? 0x08140 // In the 0x8140 to 0x9FFC range
: 0x0c140; // In the 0xE040 to 0xEBBF range
$buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8));
$buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes);
$offset += 2;
$length--;
}
return mb_convert_encoding(implode($buffer), mb_internal_encoding(), self::ENCODING);
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* Class Number
*
* @created 26.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use function array_flip, ceil, intdiv, str_split, substr, unpack;
/**
* Numeric mode: decimal digits 0 to 9
*
* ISO/IEC 18004:2000 Section 8.3.2
* ISO/IEC 18004:2000 Section 8.4.2
*/
final class Number extends QRDataModeAbstract{
/**
* @var int[]
*/
private const NUMBER_TO_ORD = [
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
];
/**
* @inheritDoc
*/
public const DATAMODE = Mode::NUMBER;
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return (int)ceil($this->getCharCount() * (10 / 3));
}
/**
* @inheritDoc
*/
public static function validateString(string $string):bool{
if($string === ''){
return false;
}
foreach(str_split($string) as $chr){
if(!isset(self::NUMBER_TO_ORD[$chr])){
return false;
}
}
return true;
}
/**
* @inheritDoc
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$len = $this->getCharCount();
$bitBuffer
->put(self::DATAMODE, 4)
->put($len, $this::getLengthBits($versionNumber))
;
$i = 0;
// encode numeric triplets in 10 bits
while(($i + 2) < $len){
$bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10);
$i += 3;
}
if($i < $len){
// encode 2 remaining numbers in 7 bits
if(($len - $i) === 2){
$bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7);
}
// encode one remaining number in 4 bits
elseif(($len - $i) === 1){
$bitBuffer->put($this->parseInt(substr($this->data, $i, 1)), 4);
}
}
return $this;
}
/**
* get the code for the given numeric string
*/
private function parseInt(string $string):int{
$num = 0;
foreach(unpack('C*', $string) as $chr){
$num = ($num * 10 + $chr - 48);
}
return $num;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
$charmap = array_flip(self::NUMBER_TO_ORD);
// @todo
$toNumericChar = function(int $ord) use ($charmap):string{
if(isset($charmap[$ord])){
return $charmap[$ord];
}
throw new QRCodeDataException('invalid character value: '.$ord);
};
$result = '';
// Read three digits at a time
while($length >= 3){
// Each 10 bits encodes three digits
if($bitBuffer->available() < 10){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$threeDigitsBits = $bitBuffer->read(10);
if($threeDigitsBits >= 1000){
throw new QRCodeDataException('error decoding numeric value');
}
$result .= $toNumericChar(intdiv($threeDigitsBits, 100));
$result .= $toNumericChar(intdiv($threeDigitsBits, 10) % 10);
$result .= $toNumericChar($threeDigitsBits % 10);
$length -= 3;
}
if($length === 2){
// Two digits left over to read, encoded in 7 bits
if($bitBuffer->available() < 7){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$twoDigitsBits = $bitBuffer->read(7);
if($twoDigitsBits >= 100){
throw new QRCodeDataException('error decoding numeric value');
}
$result .= $toNumericChar(intdiv($twoDigitsBits, 10));
$result .= $toNumericChar($twoDigitsBits % 10);
}
elseif($length === 1){
// One digit left over to read
if($bitBuffer->available() < 4){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$digitBits = $bitBuffer->read(4);
if($digitBits >= 10){
throw new QRCodeDataException('error decoding numeric value');
}
$result .= $toNumericChar($digitBits);
}
return $result;
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeDataException
*
* @created 09.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeDataException extends QRCodeException{
}

View file

@ -0,0 +1,263 @@
<?php
/**
* Class QRData
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, Version};
use chillerlan\Settings\SettingsContainerInterface;
use function sprintf;
/**
* Processes the binary data and maps it on a QRMatrix which is then being returned
*/
final class QRData{
/**
* the options instance
*
* @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
*/
private SettingsContainerInterface $options;
/**
* a BitBuffer instance
*/
private BitBuffer $bitBuffer;
/**
* an EccLevel instance
*/
private EccLevel $eccLevel;
/**
* current QR Code version
*/
private Version $version;
/**
* @var \chillerlan\QRCode\Data\QRDataModeInterface[]
*/
private array $dataSegments = [];
/**
* Max bits for the current ECC mode
*
* @var int[]
*/
private array $maxBitsForEcc;
/**
* QRData constructor.
*/
public function __construct(SettingsContainerInterface $options, array $dataSegments = []){
$this->options = $options;
$this->bitBuffer = new BitBuffer;
$this->eccLevel = new EccLevel($this->options->eccLevel);
$this->maxBitsForEcc = $this->eccLevel->getMaxBits();
$this->setData($dataSegments);
}
/**
* Sets the data string (internally called by the constructor)
*
* Subsequent calls will overwrite the current state - use the QRCode::add*Segement() method instead
*
* @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments
*/
public function setData(array $dataSegments):self{
$this->dataSegments = $dataSegments;
$this->version = $this->getMinimumVersion();
$this->bitBuffer->clear();
$this->writeBitBuffer();
return $this;
}
/**
* Returns the current BitBuffer instance
*
* @codeCoverageIgnore
*/
public function getBitBuffer():BitBuffer{
return $this->bitBuffer;
}
/**
* Sets a BitBuffer object
*
* This can be used instead of setData(), however, the version auto-detection is not available in this case.
* The version needs to match the length bits range for the data mode the data has been encoded with,
* additionally the bit array needs to contain enough pad bits.
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setBitBuffer(BitBuffer $bitBuffer):self{
if($this->options->version === Version::AUTO){
throw new QRCodeDataException('version auto detection is not available');
}
if($bitBuffer->getLength() === 0){
throw new QRCodeDataException('the given BitBuffer is empty');
}
$this->dataSegments = [];
$this->bitBuffer = $bitBuffer;
$this->version = new Version($this->options->version);
return $this;
}
/**
* returns a fresh matrix object with the data written and masked with the given $maskPattern
*/
public function writeMatrix():QRMatrix{
return (new QRMatrix($this->version, $this->eccLevel))
->initFunctionalPatterns()
->writeCodewords($this->bitBuffer)
;
}
/**
* estimates the total length of the several mode segments in order to guess the minimum version
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function estimateTotalBitLength():int{
$length = 0;
foreach($this->dataSegments as $segment){
// data length of the current segment
$length += $segment->getLengthInBits();
// +4 bits for the mode descriptor
$length += 4;
// Hanzi mode sets an additional 4 bit long subset identifier
if($segment instanceof Hanzi){
$length += 4;
}
}
$provisionalVersion = null;
foreach($this->maxBitsForEcc as $version => $maxBits){
if($length <= $maxBits){
$provisionalVersion = $version;
}
}
if($provisionalVersion !== null){
// add character count indicator bits for the provisional version
foreach($this->dataSegments as $segment){
$length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
}
// it seems that in some cases the estimated total length is not 100% accurate,
// so we substract 4 bits from the total when not in mixed mode
if(count($this->dataSegments) <= 1){
$length -= 4;
}
// we've got a match!
// or let's see if there's a higher version number available
if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){
return $length;
}
}
throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
}
/**
* returns the minimum version number for the given string
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function getMinimumVersion():Version{
if($this->options->version !== Version::AUTO){
return new Version($this->options->version);
}
$total = $this->estimateTotalBitLength();
// guess the version number within the given range
for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){
if($total <= ($this->maxBitsForEcc[$version] - 4)){
return new Version($version);
}
}
// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
}
/**
* creates a BitBuffer and writes the string data to it
*
* @throws \chillerlan\QRCode\QRCodeException on data overflow
*/
private function writeBitBuffer():void{
$MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);
foreach($this->dataSegments as $segment){
$segment->write($this->bitBuffer, $this->version->getVersionNumber());
}
// overflow, likely caused due to invalid version setting
if($this->bitBuffer->getLength() > $MAX_BITS){
throw new QRCodeDataException(
sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
);
}
// add terminator (ISO/IEC 18004:2000 Table 2)
if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){
$this->bitBuffer->put(Mode::TERMINATOR, 4);
}
// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
// by the addition of padding bits with binary value 0
while(($this->bitBuffer->getLength() % 8) !== 0){
if($this->bitBuffer->getLength() === $MAX_BITS){
break;
}
$this->bitBuffer->putBit(false);
}
// The message bit stream shall then be extended to fill the data capacity of the symbol
// corresponding to the Version and Error Correction Level, by the addition of the Pad
// Codewords 11101100 and 00010001 alternately.
$alternate = false;
while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){
$this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);
$alternate = !$alternate;
}
// In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
// to the end of the message in order exactly to fill the symbol capacity
while($this->bitBuffer->getLength() <= $MAX_BITS){
$this->bitBuffer->putBit(false);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* Class QRDataModeAbstract
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\Mode;
/**
* abstract methods for the several data modes
*/
abstract class QRDataModeAbstract implements QRDataModeInterface{
/**
* The data to write
*/
protected string $data;
/**
* QRDataModeAbstract constructor.
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function __construct(string $data){
$data = $this::convertEncoding($data);
if(!$this::validateString($data)){
throw new QRCodeDataException('invalid data');
}
$this->data = $data;
}
/**
* returns the character count of the $data string
*/
protected function getCharCount():int{
return strlen($this->data);
}
/**
* @inheritDoc
*/
public static function convertEncoding(string $string):string{
return $string;
}
/**
* shortcut
*/
protected static function getLengthBits(int $versionNumber):int{
return Mode::getLengthBitsForVersion(static::DATAMODE, $versionNumber);
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* Interface QRDataModeInterface
*
* @created 01.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\BitBuffer;
/**
* Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
*/
interface QRDataModeInterface{
/**
* the current data mode: Number, Alphanum, Kanji, Hanzi, Byte, ECI
*
* tbh I hate this constant here, but it's part of the interface, so I can't just declare it in the abstract class.
* (phan will complain about a PhanAccessOverridesFinalConstant)
*
* @see https://wiki.php.net/rfc/final_class_const
*
* @var int
* @see \chillerlan\QRCode\Common\Mode
* @internal do not call this constant from the interface, but rather from one of the child classes
*/
public const DATAMODE = -1;
/**
* retruns the length in bits of the data string
*/
public function getLengthInBits():int;
/**
* encoding conversion helper
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function convertEncoding(string $string):string;
/**
* checks if the given string qualifies for the encoder module
*/
public static function validateString(string $string):bool;
/**
* writes the actual data string to the BitBuffer, uses the given version to determine the length bits
*
* @see \chillerlan\QRCode\Data\QRData::writeBitBuffer()
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface;
/**
* reads a segment from the BitBuffer and decodes in the current data mode
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string;
}

View file

@ -0,0 +1,812 @@
<?php
/**
* Class QRMatrix
*
* @created 15.11.2017
* @author Smiley <smiley@chillerlan.net>
* @copyright 2017 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version};
use function array_fill, array_map, array_reverse, count, intdiv;
/**
* Holds an array representation of the final QR Code that contains numerical values for later output modifications;
* maps the ECC coded binary data and applies the mask pattern
*
* @see http://www.thonky.com/qr-code-tutorial/format-version-information
*/
class QRMatrix{
/*
* special values
*/
/** @var int */
public const IS_DARK = 0b100000000000;
/** @var int */
public const M_NULL = 0b000000000000;
/** @var int */
public const M_LOGO = 0b001000000000;
/** @var int */
public const M_LOGO_DARK = 0b101000000000;
/*
* light values
*/
/** @var int */
public const M_DATA = 0b000000000010;
/** @var int */
public const M_FINDER = 0b000000000100;
/** @var int */
public const M_SEPARATOR = 0b000000001000;
/** @var int */
public const M_ALIGNMENT = 0b000000010000;
/** @var int */
public const M_TIMING = 0b000000100000;
/** @var int */
public const M_FORMAT = 0b000001000000;
/** @var int */
public const M_VERSION = 0b000010000000;
/** @var int */
public const M_QUIETZONE = 0b000100000000;
/*
* dark values
*/
/** @var int */
public const M_DARKMODULE = 0b100000000001;
/** @var int */
public const M_DATA_DARK = 0b100000000010;
/** @var int */
public const M_FINDER_DARK = 0b100000000100;
/** @var int */
public const M_ALIGNMENT_DARK = 0b100000010000;
/** @var int */
public const M_TIMING_DARK = 0b100000100000;
/** @var int */
public const M_FORMAT_DARK = 0b100001000000;
/** @var int */
public const M_VERSION_DARK = 0b100010000000;
/** @var int */
public const M_FINDER_DOT = 0b110000000000;
/*
* values used for reversed reflectance
*/
/** @var int */
public const M_DARKMODULE_LIGHT = 0b000000000001;
/** @var int */
public const M_FINDER_DOT_LIGHT = 0b010000000000;
/** @var int */
public const M_SEPARATOR_DARK = 0b100000001000;
/** @var int */
public const M_QUIETZONE_DARK = 0b100100000000;
/**
* Map of flag => coord
*
* @see \chillerlan\QRCode\Data\QRMatrix::checkNeighbours()
*
* @var array
*/
protected const neighbours = [
0b00000001 => [-1, -1],
0b00000010 => [ 0, -1],
0b00000100 => [ 1, -1],
0b00001000 => [ 1, 0],
0b00010000 => [ 1, 1],
0b00100000 => [ 0, 1],
0b01000000 => [-1, 1],
0b10000000 => [-1, 0],
];
/**
* the matrix version - always set in QRMatrix, may be null in BitMatrix
*/
protected ?Version $version = null;
/**
* the current ECC level - always set in QRMatrix, may be null in BitMatrix
*/
protected ?EccLevel $eccLevel = null;
/**
* the mask pattern that was used in the most recent operation, set via:
*
* - QRMatrix::setFormatInfo()
* - QRMatrix::mask()
* - BitMatrix::readFormatInformation()
*/
protected ?MaskPattern $maskPattern = null;
/**
* the size (side length) of the matrix, including quiet zone (if created)
*/
protected int $moduleCount;
/**
* the actual matrix data array
*
* @var int[][]
*/
protected array $matrix;
/**
* QRMatrix constructor.
*/
public function __construct(Version $version, EccLevel $eccLevel){
$this->version = $version;
$this->eccLevel = $eccLevel;
$this->moduleCount = $this->version->getDimension();
$this->matrix = $this->createMatrix($this->moduleCount, $this::M_NULL);
}
/**
* Creates a 2-dimensional array (square) of the given $size
*/
protected function createMatrix(int $size, int $value):array{
return array_fill(0, $size, array_fill(0, $size, $value));
}
/**
* shortcut to initialize the functional patterns
*/
public function initFunctionalPatterns():self{
return $this
->setFinderPattern()
->setSeparators()
->setAlignmentPattern()
->setTimingPattern()
->setDarkModule()
->setVersionNumber()
->setFormatInfo()
;
}
/**
* Returns the data matrix, returns a pure boolean representation if $boolean is set to true
*
* @return int[][]|bool[][]
*/
public function getMatrix(bool $boolean = null):array{
if($boolean !== true){
return $this->matrix;
}
$matrix = $this->matrix;
foreach($matrix as &$row){
$row = array_map([$this, 'isDark'], $row);
}
return $matrix;
}
/**
* @deprecated 5.0.0 use QRMatrix::getMatrix() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getMatrix()
* @codeCoverageIgnore
*/
public function matrix(bool $boolean = null):array{
return $this->getMatrix($boolean);
}
/**
* Returns the current version number
*/
public function getVersion():?Version{
return $this->version;
}
/**
* @deprecated 5.0.0 use QRMatrix::getVersion() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getVersion()
* @codeCoverageIgnore
*/
public function version():?Version{
return $this->getVersion();
}
/**
* Returns the current ECC level
*/
public function getEccLevel():?EccLevel{
return $this->eccLevel;
}
/**
* @deprecated 5.0.0 use QRMatrix::getEccLevel() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getEccLevel()
* @codeCoverageIgnore
*/
public function eccLevel():?EccLevel{
return $this->getEccLevel();
}
/**
* Returns the current mask pattern
*/
public function getMaskPattern():?MaskPattern{
return $this->maskPattern;
}
/**
* @deprecated 5.0.0 use QRMatrix::getMaskPattern() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getMaskPattern()
* @codeCoverageIgnore
*/
public function maskPattern():?MaskPattern{
return $this->getMaskPattern();
}
/**
* Returns the absoulute size of the matrix, including quiet zone (after setting it).
*
* size = version * 4 + 17 [ + 2 * quietzone size]
*/
public function getSize():int{
return $this->moduleCount;
}
/**
* @deprecated 5.0.0 use QRMatrix::getSize() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getSize()
* @codeCoverageIgnore
*/
public function size():int{
return $this->getSize();
}
/**
* Returns the value of the module at position [$x, $y] or -1 if the coordinate is outside the matrix
*/
public function get(int $x, int $y):int{
if(!isset($this->matrix[$y][$x])){
return -1;
}
return $this->matrix[$y][$x];
}
/**
* Sets the $M_TYPE value for the module at position [$x, $y]
*
* true => $M_TYPE | 0x800
* false => $M_TYPE
*/
public function set(int $x, int $y, bool $value, int $M_TYPE):self{
if(isset($this->matrix[$y][$x])){
// we don't know whether the input is dark, so we remove the dark bit
$M_TYPE &= ~$this::IS_DARK;
if($value === true){
$M_TYPE |= $this::IS_DARK;
}
$this->matrix[$y][$x] = $M_TYPE;
}
return $this;
}
/**
* Fills an area of $width * $height, from the given starting point [$startX, $startY] (top left) with $value for $M_TYPE.
*/
public function setArea(int $startX, int $startY, int $width, int $height, bool $value, int $M_TYPE):self{
for($y = $startY; $y < ($startY + $height); $y++){
for($x = $startX; $x < ($startX + $width); $x++){
$this->set($x, $y, $value, $M_TYPE);
}
}
return $this;
}
/**
* Flips the value of the module at ($x, $y)
*/
public function flip(int $x, int $y):self{
if(isset($this->matrix[$y][$x])){
$this->matrix[$y][$x] ^= $this::IS_DARK;
}
return $this;
}
/**
* Checks whether the module at ($x, $y) is of the given $M_TYPE
*
* true => $value & $M_TYPE === $M_TYPE
*
* Also, returns false if the given coordinates are out of range.
*/
public function checkType(int $x, int $y, int $M_TYPE):bool{
if(isset($this->matrix[$y][$x])){
return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE;
}
return false;
}
/**
* Checks whether the module at ($x, $y) is in the given array of $M_TYPES,
* returns true if a match is found, otherwise false.
*/
public function checkTypeIn(int $x, int $y, array $M_TYPES):bool{
foreach($M_TYPES as $type){
if($this->checkType($x, $y, $type)){
return true;
}
}
return false;
}
/**
* Checks whether the module at ($x, $y) is true (dark) or false (light)
*
* Also, returns false if the given coordinates are out of range.
*/
public function check(int $x, int $y):bool{
if(isset($this->matrix[$y][$x])){
return $this->isDark($this->matrix[$y][$x]);
}
return false;
}
/**
* Checks whether the given $M_TYPE is a dark value
*/
public function isDark(int $M_TYPE):bool{
return ($M_TYPE & $this::IS_DARK) === $this::IS_DARK;
}
/**
* Checks the status of the neighbouring modules for the module at ($x, $y) and returns a bitmask with the results.
*
* The 8 flags of the bitmask represent the status of each of the neighbouring fields,
* starting with the lowest bit for top left, going clockwise:
*
* 0 1 2
* 7 # 3
* 6 5 4
*/
public function checkNeighbours(int $x, int $y, int $M_TYPE = null):int{
$bits = 0;
foreach($this::neighbours as $bit => [$ix, $iy]){
$ix += $x;
$iy += $y;
// $M_TYPE is given, skip if the field is not the same type
if($M_TYPE !== null && !$this->checkType($ix, $iy, $M_TYPE)){
continue;
}
if($this->checkType($ix, $iy, $this::IS_DARK)){
$bits |= $bit;
}
}
return $bits;
}
/**
* Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder
*
* 4 * version + 9 or moduleCount - 8
*/
public function setDarkModule():self{
$this->set(8, ($this->moduleCount - 8), true, $this::M_DARKMODULE);
return $this;
}
/**
* Draws the 7x7 finder patterns in the corners top left/right and bottom left
*
* ISO/IEC 18004:2000 Section 7.3.2
*/
public function setFinderPattern():self{
$pos = [
[0, 0], // top left
[($this->moduleCount - 7), 0], // top right
[0, ($this->moduleCount - 7)], // bottom left
];
foreach($pos as $c){
$this
->setArea( $c[0] , $c[1] , 7, 7, true, $this::M_FINDER)
->setArea(($c[0] + 1), ($c[1] + 1), 5, 5, false, $this::M_FINDER)
->setArea(($c[0] + 2), ($c[1] + 2), 3, 3, true, $this::M_FINDER_DOT)
;
}
return $this;
}
/**
* Draws the separator lines around the finder patterns
*
* ISO/IEC 18004:2000 Section 7.3.3
*/
public function setSeparators():self{
$h = [
[7, 0],
[($this->moduleCount - 8), 0],
[7, ($this->moduleCount - 8)],
];
$v = [
[7, 7],
[($this->moduleCount - 1), 7],
[7, ($this->moduleCount - 8)],
];
for($c = 0; $c < 3; $c++){
for($i = 0; $i < 8; $i++){
$this->set( $h[$c][0] , ($h[$c][1] + $i), false, $this::M_SEPARATOR);
$this->set(($v[$c][0] - $i), $v[$c][1] , false, $this::M_SEPARATOR);
}
}
return $this;
}
/**
* Draws the 5x5 alignment patterns
*
* ISO/IEC 18004:2000 Section 7.3.5
*/
public function setAlignmentPattern():self{
$alignmentPattern = $this->version->getAlignmentPattern();
foreach($alignmentPattern as $y){
foreach($alignmentPattern as $x){
// skip existing patterns
if($this->matrix[$y][$x] !== $this::M_NULL){
continue;
}
$this
->setArea(($x - 2), ($y - 2), 5, 5, true, $this::M_ALIGNMENT)
->setArea(($x - 1), ($y - 1), 3, 3, false, $this::M_ALIGNMENT)
->set($x, $y, true, $this::M_ALIGNMENT)
;
}
}
return $this;
}
/**
* Draws the timing pattern (h/v checkered line between the finder patterns)
*
* ISO/IEC 18004:2000 Section 7.3.4
*/
public function setTimingPattern():self{
for($i = 8; $i < ($this->moduleCount - 8); $i++){
if($this->matrix[6][$i] !== $this::M_NULL || $this->matrix[$i][6] !== $this::M_NULL){
continue;
}
$v = ($i % 2) === 0;
$this->set($i, 6, $v, $this::M_TIMING); // h
$this->set(6, $i, $v, $this::M_TIMING); // v
}
return $this;
}
/**
* Draws the version information, 2x 3x6 pixel
*
* ISO/IEC 18004:2000 Section 8.10
*/
public function setVersionNumber():self{
$bits = $this->version->getVersionPattern();
if($bits !== null){
for($i = 0; $i < 18; $i++){
$a = intdiv($i, 3);
$b = (($i % 3) + ($this->moduleCount - 8 - 3));
$v = (($bits >> $i) & 1) === 1;
$this->set($b, $a, $v, $this::M_VERSION); // ne
$this->set($a, $b, $v, $this::M_VERSION); // sw
}
}
return $this;
}
/**
* Draws the format info along the finder patterns. If no $maskPattern, all format info modules will be set to false.
*
* ISO/IEC 18004:2000 Section 8.9
*/
public function setFormatInfo(MaskPattern $maskPattern = null):self{
$this->maskPattern = $maskPattern;
$bits = 0; // sets all format fields to false (test mode)
if($this->maskPattern instanceof MaskPattern){
$bits = $this->eccLevel->getformatPattern($this->maskPattern);
}
for($i = 0; $i < 15; $i++){
$v = (($bits >> $i) & 1) === 1;
if($i < 6){
$this->set(8, $i, $v, $this::M_FORMAT);
}
elseif($i < 8){
$this->set(8, ($i + 1), $v, $this::M_FORMAT);
}
else{
$this->set(8, ($this->moduleCount - 15 + $i), $v, $this::M_FORMAT);
}
if($i < 8){
$this->set(($this->moduleCount - $i - 1), 8, $v, $this::M_FORMAT);
}
elseif($i < 9){
$this->set(((15 - $i)), 8, $v, $this::M_FORMAT);
}
else{
$this->set((15 - $i - 1), 8, $v, $this::M_FORMAT);
}
}
return $this;
}
/**
* Draws the "quiet zone" of $size around the matrix
*
* ISO/IEC 18004:2000 Section 7.3.7
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setQuietZone(int $quietZoneSize):self{
// early exit if there's nothing to add
if($quietZoneSize < 1){
return $this;
}
if($this->matrix[($this->moduleCount - 1)][($this->moduleCount - 1)] === $this::M_NULL){
throw new QRCodeDataException('use only after writing data');
}
// create a matrix with the new size
$newSize = ($this->moduleCount + ($quietZoneSize * 2));
$newMatrix = $this->createMatrix($newSize, $this::M_QUIETZONE);
// copy over the current matrix
foreach($this->matrix as $y => $row){
foreach($row as $x => $val){
$newMatrix[($y + $quietZoneSize)][($x + $quietZoneSize)] = $val;
}
}
// set the new values
$this->moduleCount = $newSize;
$this->matrix = $newMatrix;
return $this;
}
/**
* Rotates the matrix by 90 degrees clock wise
*/
public function rotate90():self{
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
$this->matrix = array_map((fn(int ...$a):array => array_reverse($a)), ...$this->matrix);
return $this;
}
/**
* Inverts the values of the whole matrix
*
* ISO/IEC 18004:2015 Section 6.2 - Reflectance reversal
*/
public function invert():self{
foreach($this->matrix as $y => $row){
foreach($row as $x => $val){
// skip null fields
if($val === $this::M_NULL){
continue;
}
$this->flip($x, $y);
}
}
return $this;
}
/**
* Clears a space of $width * $height in order to add a logo or text.
* If no $height is given, the space will be assumed a square of $width.
*
* Additionally, the logo space can be positioned within the QR Code using $startX and $startY.
* If either of these are null, the logo space will be centered in that direction.
* ECC level "H" (30%) is required.
*
* The coordinates of $startX and $startY do not include the quiet zone:
* [0, 0] is always the top left module of the top left finder pattern, negative values go into the quiet zone top and left.
*
* Please note that adding a logo space minimizes the error correction capacity of the QR Code and
* created images may become unreadable, especially when printed with a chance to receive damage.
* Please test thoroughly before using this feature in production.
*
* This method should be called from within an output module (after the matrix has been filled with data).
* Note that there is no restiction on how many times this method could be called on the same matrix instance.
*
* @link https://github.com/chillerlan/php-qrcode/issues/52
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setLogoSpace(int $width, int $height = null, int $startX = null, int $startY = null):self{
$height ??= $width;
// if width and height happen to be negative or 0 (default value), just return - nothing to do
if($width <= 0 || $height <= 0){
return $this; // @codeCoverageIgnore
}
// for logos, we operate in ECC H (30%) only
if($this->eccLevel->getLevel() !== EccLevel::H){
throw new QRCodeDataException('ECC level "H" required to add logo space');
}
// $this->moduleCount includes the quiet zone (if created), we need the QR size here
$dimension = $this->version->getDimension();
// throw if the size exceeds the qrcode size
if($width > $dimension || $height > $dimension){
throw new QRCodeDataException('logo dimensions exceed matrix size');
}
// we need uneven sizes to center the logo space, adjust if needed
if($startX === null && ($width % 2) === 0){
$width++;
}
if($startY === null && ($height % 2) === 0){
$height++;
}
// throw if the logo space exceeds the maximum error correction capacity
if(($width * $height) > (int)($dimension * $dimension * 0.25)){
throw new QRCodeDataException('logo space exceeds the maximum error correction capacity');
}
$quietzone = (($this->moduleCount - $dimension) / 2);
$end = ($this->moduleCount - $quietzone);
// determine start coordinates
$startX ??= (($dimension - $width) / 2);
$startY ??= (($dimension - $height) / 2);
$endX = ($quietzone + $startX + $width);
$endY = ($quietzone + $startY + $height);
// clear the space
for($y = ($quietzone + $startY); $y < $endY; $y++){
for($x = ($quietzone + $startX); $x < $endX; $x++){
// out of bounds, skip
if($x < $quietzone || $y < $quietzone ||$x >= $end || $y >= $end){
continue;
}
$this->set($x, $y, false, $this::M_LOGO);
}
}
return $this;
}
/**
* Maps the interleaved binary $data on the matrix
*/
public function writeCodewords(BitBuffer $bitBuffer):self{
$data = (new ReedSolomonEncoder($this->version, $this->eccLevel))->interleaveEcBytes($bitBuffer);
$byteCount = count($data);
$iByte = 0;
$iBit = 7;
$direction = true;
for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){
// skip vertical alignment pattern
if($i === 6){
$i--;
}
for($count = 0; $count < $this->moduleCount; $count++){
$y = $count;
if($direction){
$y = ($this->moduleCount - 1 - $count);
}
for($col = 0; $col < 2; $col++){
$x = ($i - $col);
// skip functional patterns
if($this->matrix[$y][$x] !== $this::M_NULL){
continue;
}
$this->matrix[$y][$x] = $this::M_DATA;
if($iByte < $byteCount && (($data[$iByte] >> $iBit--) & 1) === 1){
$this->matrix[$y][$x] |= $this::IS_DARK;
}
if($iBit === -1){
$iByte++;
$iBit = 7;
}
}
}
$direction = !$direction; // switch directions
}
return $this;
}
/**
* Applies/reverses the mask pattern
*
* ISO/IEC 18004:2000 Section 8.8.1
*/
public function mask(MaskPattern $maskPattern):self{
$this->maskPattern = $maskPattern;
$mask = $this->maskPattern->getMask();
foreach($this->matrix as $y => $row){
foreach($row as $x => $val){
// skip non-data modules
if(($val & $this::M_DATA) === $this::M_DATA && $mask($x, $y)){
$this->flip($x, $y);
}
}
}
return $this;
}
}

View file

@ -0,0 +1,127 @@
<?php
/**
* Class ReedSolomonEncoder
*
* @created 07.01.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version};
use function array_fill, array_merge, count, max;
/**
* Reed-Solomon encoding - ISO/IEC 18004:2000 Section 8.5 ff
*
* @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
*/
final class ReedSolomonEncoder{
private Version $version;
private EccLevel $eccLevel;
private array $interleavedData;
private int $interleavedDataIndex;
/**
* ReedSolomonDecoder constructor
*/
public function __construct(Version $version, EccLevel $eccLevel){
$this->version = $version;
$this->eccLevel = $eccLevel;
}
/**
* ECC encoding and interleaving
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function interleaveEcBytes(BitBuffer $bitBuffer):array{
[$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->eccLevel);
$rsBlocks = array_fill(0, $l1, [($numEccCodewords + $b1), $b1]);
if($l2 > 0){
$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [($numEccCodewords + $b2), $b2]));
}
$bitBufferData = $bitBuffer->getBuffer();
$dataBytes = [];
$ecBytes = [];
$maxDataBytes = 0;
$maxEcBytes = 0;
$dataByteOffset = 0;
foreach($rsBlocks as $key => [$rsBlockTotal, $dataByteCount]){
$dataBytes[$key] = [];
for($i = 0; $i < $dataByteCount; $i++){
$dataBytes[$key][$i] = ($bitBufferData[($i + $dataByteOffset)] & 0xff);
}
$ecByteCount = ($rsBlockTotal - $dataByteCount);
$ecBytes[$key] = $this->encode($dataBytes[$key], $ecByteCount);
$maxDataBytes = max($maxDataBytes, $dataByteCount);
$maxEcBytes = max($maxEcBytes, $ecByteCount);
$dataByteOffset += $dataByteCount;
}
$this->interleavedData = array_fill(0, $this->version->getTotalCodewords(), 0);
$this->interleavedDataIndex = 0;
$numRsBlocks = ($l1 + $l2);
$this->interleave($dataBytes, $maxDataBytes, $numRsBlocks);
$this->interleave($ecBytes, $maxEcBytes, $numRsBlocks);
return $this->interleavedData;
}
/**
*
*/
private function encode(array $dataBytes, int $ecByteCount):array{
$rsPoly = new GenericGFPoly([1]);
for($i = 0; $i < $ecByteCount; $i++){
$rsPoly = $rsPoly->multiply(new GenericGFPoly([1, GF256::exp($i)]));
}
$rsPolyDegree = $rsPoly->getDegree();
$modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree))
->mod($rsPoly)
->getCoefficients()
;
$ecBytes = array_fill(0, $rsPolyDegree, 0);
$count = (count($modCoefficients) - $rsPolyDegree);
foreach($ecBytes as $i => &$val){
$modIndex = ($i + $count);
$val = 0;
if($modIndex >= 0){
$val = $modCoefficients[$modIndex];
}
}
return $ecBytes;
}
/**
*
*/
private function interleave(array $byteArray, int $maxBytes, int $numRsBlocks):void{
for($x = 0; $x < $maxBytes; $x++){
for($y = 0; $y < $numRsBlocks; $y++){
if($x < count($byteArray[$y])){
$this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x];
}
}
}
}
}

View file

@ -0,0 +1,361 @@
<?php
/**
* Class Binarizer
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\LuminanceSourceInterface;
use chillerlan\QRCode\Data\QRMatrix;
use function array_fill, count, intdiv, max;
/**
* This class implements a local thresholding algorithm, which while slower than the
* GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for
* high frequency images of barcodes with black data on white backgrounds. For this application,
* it does a much better job than a global blackpoint with severe shadows and gradients.
* However, it tends to produce artifacts on lower frequency images and is therefore not
* a good general purpose binarizer for uses outside ZXing.
*
* This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers,
* and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already
* inherently local, and only fails for horizontal gradients. We can revisit that problem later,
* but for now it was not a win to use local blocks for 1D.
*
* This Binarizer is the default for the unit tests and the recommended class for library users.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
final class Binarizer{
// This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels.
// So this is the smallest dimension in each axis we can accept.
private const BLOCK_SIZE_POWER = 3;
private const BLOCK_SIZE = 8; // ...0100...00
private const BLOCK_SIZE_MASK = 7; // ...0011...11
private const MINIMUM_DIMENSION = 40;
private const MIN_DYNAMIC_RANGE = 24;
# private const LUMINANCE_BITS = 5;
private const LUMINANCE_SHIFT = 3;
private const LUMINANCE_BUCKETS = 32;
private LuminanceSourceInterface $source;
private array $luminances;
/**
*
*/
public function __construct(LuminanceSourceInterface $source){
$this->source = $source;
$this->luminances = $this->source->getLuminances();
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function estimateBlackPoint(array $buckets):int{
// Find the tallest peak in the histogram.
$numBuckets = count($buckets);
$maxBucketCount = 0;
$firstPeak = 0;
$firstPeakSize = 0;
for($x = 0; $x < $numBuckets; $x++){
if($buckets[$x] > $firstPeakSize){
$firstPeak = $x;
$firstPeakSize = $buckets[$x];
}
if($buckets[$x] > $maxBucketCount){
$maxBucketCount = $buckets[$x];
}
}
// Find the second-tallest peak which is somewhat far from the tallest peak.
$secondPeak = 0;
$secondPeakScore = 0;
for($x = 0; $x < $numBuckets; $x++){
$distanceToBiggest = ($x - $firstPeak);
// Encourage more distant second peaks by multiplying by square of distance.
$score = ($buckets[$x] * $distanceToBiggest * $distanceToBiggest);
if($score > $secondPeakScore){
$secondPeak = $x;
$secondPeakScore = $score;
}
}
// Make sure firstPeak corresponds to the black peak.
if($firstPeak > $secondPeak){
$temp = $firstPeak;
$firstPeak = $secondPeak;
$secondPeak = $temp;
}
// If there is too little contrast in the image to pick a meaningful black point, throw rather
// than waste time trying to decode the image, and risk false positives.
if(($secondPeak - $firstPeak) <= ($numBuckets / 16)){
throw new QRCodeDecoderException('no meaningful dark point found'); // @codeCoverageIgnore
}
// Find a valley between them that is low and closer to the white peak.
$bestValley = ($secondPeak - 1);
$bestValleyScore = -1;
for($x = ($secondPeak - 1); $x > $firstPeak; $x--){
$fromFirst = ($x - $firstPeak);
$score = ($fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]));
if($score > $bestValleyScore){
$bestValley = $x;
$bestValleyScore = $score;
}
}
return ($bestValley << self::LUMINANCE_SHIFT);
}
/**
* Calculates the final BitMatrix once for all requests. This could be called once from the
* constructor instead, but there are some advantages to doing it lazily, such as making
* profiling easier, and not doing heavy lifting when callers don't expect it.
*
* Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive
* and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or
* may not apply sharpening. Therefore, a row from this matrix may not be identical to one
* fetched using getBlackRow(), so don't mix and match between them.
*
* @return \chillerlan\QRCode\Decoder\BitMatrix The 2D array of bits for the image (true means black).
*/
public function getBlackMatrix():BitMatrix{
$width = $this->source->getWidth();
$height = $this->source->getHeight();
if($width >= self::MINIMUM_DIMENSION && $height >= self::MINIMUM_DIMENSION){
$subWidth = ($width >> self::BLOCK_SIZE_POWER);
if(($width & self::BLOCK_SIZE_MASK) !== 0){
$subWidth++;
}
$subHeight = ($height >> self::BLOCK_SIZE_POWER);
if(($height & self::BLOCK_SIZE_MASK) !== 0){
$subHeight++;
}
return $this->calculateThresholdForBlock($subWidth, $subHeight, $width, $height);
}
// If the image is too small, fall back to the global histogram approach.
return $this->getHistogramBlackMatrix($width, $height);
}
/**
*
*/
private function getHistogramBlackMatrix(int $width, int $height):BitMatrix{
// Quickly calculates the histogram by sampling four rows from the image. This proved to be
// more robust on the blackbox tests than sampling a diagonal as we used to do.
$buckets = array_fill(0, self::LUMINANCE_BUCKETS, 0);
$right = intdiv(($width * 4), 5);
$x = intdiv($width, 5);
for($y = 1; $y < 5; $y++){
$row = intdiv(($height * $y), 5);
$localLuminances = $this->source->getRow($row);
for(; $x < $right; $x++){
$pixel = ($localLuminances[$x] & 0xff);
$buckets[($pixel >> self::LUMINANCE_SHIFT)]++;
}
}
$blackPoint = $this->estimateBlackPoint($buckets);
// We delay reading the entire image luminance until the black point estimation succeeds.
// Although we end up reading four rows twice, it is consistent with our motto of
// "fail quickly" which is necessary for continuous scanning.
$matrix = new BitMatrix(max($width, $height));
for($y = 0; $y < $height; $y++){
$offset = ($y * $width);
for($x = 0; $x < $width; $x++){
$matrix->set($x, $y, (($this->luminances[($offset + $x)] & 0xff) < $blackPoint), QRMatrix::M_DATA);
}
}
return $matrix;
}
/**
* Calculates a single black point for each block of pixels and saves it away.
* See the following thread for a discussion of this algorithm:
*
* @see http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0
*/
private function calculateBlackPoints(int $subWidth, int $subHeight, int $width, int $height):array{
$blackPoints = array_fill(0, $subHeight, array_fill(0, $subWidth, 0));
for($y = 0; $y < $subHeight; $y++){
$yoffset = ($y << self::BLOCK_SIZE_POWER);
$maxYOffset = ($height - self::BLOCK_SIZE);
if($yoffset > $maxYOffset){
$yoffset = $maxYOffset;
}
for($x = 0; $x < $subWidth; $x++){
$xoffset = ($x << self::BLOCK_SIZE_POWER);
$maxXOffset = ($width - self::BLOCK_SIZE);
if($xoffset > $maxXOffset){
$xoffset = $maxXOffset;
}
$sum = 0;
$min = 255;
$max = 0;
for($yy = 0, $offset = ($yoffset * $width + $xoffset); $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
$pixel = ((int)($this->luminances[(int)($offset + $xx)]) & 0xff);
$sum += $pixel;
// still looking for good contrast
if($pixel < $min){
$min = $pixel;
}
if($pixel > $max){
$max = $pixel;
}
}
// short-circuit min/max tests once dynamic range is met
if(($max - $min) > self::MIN_DYNAMIC_RANGE){
// finish the rest of the rows quickly
for($yy++, $offset += $width; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
$sum += ((int)($this->luminances[(int)($offset + $xx)]) & 0xff);
}
}
}
}
// The default estimate is the average of the values in the block.
$average = ($sum >> (self::BLOCK_SIZE_POWER * 2));
if(($max - $min) <= self::MIN_DYNAMIC_RANGE){
// If variation within the block is low, assume this is a block with only light or only
// dark pixels. In that case we do not want to use the average, as it would divide this
// low contrast area into black and white pixels, essentially creating data out of noise.
//
// The default assumption is that the block is light/background. Since no estimate for
// the level of dark pixels exists locally, use half the min for the block.
$average = ($min / 2);
if($y > 0 && $x > 0){
// Correct the "white background" assumption for blocks that have neighbors by comparing
// the pixels in this block to the previously calculated black points. This is based on
// the fact that dark barcode symbology is always surrounded by some amount of light
// background for which reasonable black point estimates were made. The bp estimated at
// the boundaries is used for the interior.
// The (min < bp) is arbitrary but works better than other heuristics that were tried.
$averageNeighborBlackPoint = (
($blackPoints[($y - 1)][$x] + (2 * $blackPoints[$y][($x - 1)]) + $blackPoints[($y - 1)][($x - 1)]) / 4
);
if($min < $averageNeighborBlackPoint){
$average = $averageNeighborBlackPoint;
}
}
}
$blackPoints[$y][$x] = $average;
}
}
return $blackPoints;
}
/**
* For each block in the image, calculate the average black point using a 5x5 grid
* of the surrounding blocks. Also handles the corner cases (fractional blocks are computed based
* on the last pixels in the row/column which are also used in the previous block).
*/
private function calculateThresholdForBlock(int $subWidth, int $subHeight, int $width, int $height):BitMatrix{
$matrix = new BitMatrix(max($width, $height));
$blackPoints = $this->calculateBlackPoints($subWidth, $subHeight, $width, $height);
for($y = 0; $y < $subHeight; $y++){
$yoffset = ($y << self::BLOCK_SIZE_POWER);
$maxYOffset = ($height - self::BLOCK_SIZE);
if($yoffset > $maxYOffset){
$yoffset = $maxYOffset;
}
for($x = 0; $x < $subWidth; $x++){
$xoffset = ($x << self::BLOCK_SIZE_POWER);
$maxXOffset = ($width - self::BLOCK_SIZE);
if($xoffset > $maxXOffset){
$xoffset = $maxXOffset;
}
$left = $this->cap($x, 2, ($subWidth - 3));
$top = $this->cap($y, 2, ($subHeight - 3));
$sum = 0;
for($z = -2; $z <= 2; $z++){
$br = $blackPoints[($top + $z)];
$sum += ($br[($left - 2)] + $br[($left - 1)] + $br[$left] + $br[($left + 1)] + $br[($left + 2)]);
}
$average = (int)($sum / 25);
// Applies a single threshold to a block of pixels.
for($j = 0, $o = ($yoffset * $width + $xoffset); $j < self::BLOCK_SIZE; $j++, $o += $width){
for($i = 0; $i < self::BLOCK_SIZE; $i++){
// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
$v = (((int)($this->luminances[($o + $i)]) & 0xff) <= $average);
$matrix->set(($xoffset + $i), ($yoffset + $j), $v, QRMatrix::M_DATA);
}
}
}
}
return $matrix;
}
/**
* @noinspection PhpSameParameterValueInspection
*/
private function cap(int $value, int $min, int $max):int{
if($value < $min){
return $min;
}
if($value > $max){
return $max;
}
return $value;
}
}

View file

@ -0,0 +1,430 @@
<?php
/**
* Class BitMatrix
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
use function array_fill, array_reverse, count;
use const PHP_INT_MAX, PHP_INT_SIZE;
/**
* Extended QRMatrix to map read data from the Binarizer
*/
final class BitMatrix extends QRMatrix{
/**
* See ISO 18004:2006, Annex C, Table C.1
*
* [data bits, sequence after masking]
*/
private const DECODE_LOOKUP = [
0x5412, // 0101010000010010
0x5125, // 0101000100100101
0x5E7C, // 0101111001111100
0x5B4B, // 0101101101001011
0x45F9, // 0100010111111001
0x40CE, // 0100000011001110
0x4F97, // 0100111110010111
0x4AA0, // 0100101010100000
0x77C4, // 0111011111000100
0x72F3, // 0111001011110011
0x7DAA, // 0111110110101010
0x789D, // 0111100010011101
0x662F, // 0110011000101111
0x6318, // 0110001100011000
0x6C41, // 0110110001000001
0x6976, // 0110100101110110
0x1689, // 0001011010001001
0x13BE, // 0001001110111110
0x1CE7, // 0001110011100111
0x19D0, // 0001100111010000
0x0762, // 0000011101100010
0x0255, // 0000001001010101
0x0D0C, // 0000110100001100
0x083B, // 0000100000111011
0x355F, // 0011010101011111
0x3068, // 0011000001101000
0x3F31, // 0011111100110001
0x3A06, // 0011101000000110
0x24B4, // 0010010010110100
0x2183, // 0010000110000011
0x2EDA, // 0010111011011010
0x2BED, // 0010101111101101
];
private const FORMAT_INFO_MASK_QR = 0x5412; // 0101010000010010
/**
* This flag has effect only on the copyVersionBit() method.
* Before proceeding with readCodewords() the resetInfo() method should be called.
*/
private bool $mirror = false;
/**
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(int $dimension){
$this->moduleCount = $dimension;
$this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
}
/**
* Resets the current version info in order to attempt another reading
*/
public function resetVersionInfo():self{
$this->version = null;
$this->eccLevel = null;
$this->maskPattern = null;
return $this;
}
/**
* Mirror the bit matrix diagonally in order to attempt a second reading.
*/
public function mirrorDiagonal():self{
$this->mirror = !$this->mirror;
// mirror vertically
$this->matrix = array_reverse($this->matrix);
// rotate by 90 degrees clockwise
/** @phan-suppress-next-line PhanTypeMismatchReturnSuperType */
return $this->rotate90();
}
/**
* Reads the bits in the BitMatrix representing the finder pattern in the
* correct order in order to reconstruct the codewords bytes contained within the
* QR Code. Throws if the exact number of bytes expected is not read.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function readCodewords():array{
$this
->readFormatInformation()
->readVersion()
->mask($this->maskPattern) // reverse the mask pattern
;
// invoke a fresh matrix with only the function & format patterns to compare against
$matrix = (new QRMatrix($this->version, $this->eccLevel))
->initFunctionalPatterns()
->setFormatInfo($this->maskPattern)
;
$result = [];
$byte = 0;
$bitsRead = 0;
$direction = true;
// Read columns in pairs, from right to left
for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){
// Skip whole column with vertical alignment pattern;
// saves time and makes the other code proceed more cleanly
if($i === 6){
$i--;
}
// Read alternatingly from bottom to top then top to bottom
for($count = 0; $count < $this->moduleCount; $count++){
$y = ($direction) ? ($this->moduleCount - 1 - $count) : $count;
for($col = 0; $col < 2; $col++){
$x = ($i - $col);
// Ignore bits covered by the function pattern
if($matrix->get($x, $y) !== $this::M_NULL){
continue;
}
$bitsRead++;
$byte <<= 1;
if($this->check($x, $y)){
$byte |= 1;
}
// If we've made a whole byte, save it off
if($bitsRead === 8){
$result[] = $byte;
$bitsRead = 0;
$byte = 0;
}
}
}
$direction = !$direction; // switch directions
}
if(count($result) !== $this->version->getTotalCodewords()){
throw new QRCodeDecoderException('result count differs from total codewords for version');
}
// bytes encoded within the QR Code
return $result;
}
/**
* Reads format information from one of its two locations within the QR Code.
* Throws if both format information locations cannot be parsed as the valid encoding of format information.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function readFormatInformation():self{
if($this->eccLevel !== null && $this->maskPattern !== null){
return $this;
}
// Read top-left format info bits
$formatInfoBits1 = 0;
for($i = 0; $i < 6; $i++){
$formatInfoBits1 = $this->copyVersionBit($i, 8, $formatInfoBits1);
}
// ... and skip a bit in the timing pattern ...
$formatInfoBits1 = $this->copyVersionBit(7, 8, $formatInfoBits1);
$formatInfoBits1 = $this->copyVersionBit(8, 8, $formatInfoBits1);
$formatInfoBits1 = $this->copyVersionBit(8, 7, $formatInfoBits1);
// ... and skip a bit in the timing pattern ...
for($j = 5; $j >= 0; $j--){
$formatInfoBits1 = $this->copyVersionBit(8, $j, $formatInfoBits1);
}
// Read the top-right/bottom-left pattern too
$formatInfoBits2 = 0;
$jMin = ($this->moduleCount - 7);
for($j = ($this->moduleCount - 1); $j >= $jMin; $j--){
$formatInfoBits2 = $this->copyVersionBit(8, $j, $formatInfoBits2);
}
for($i = ($this->moduleCount - 8); $i < $this->moduleCount; $i++){
$formatInfoBits2 = $this->copyVersionBit($i, 8, $formatInfoBits2);
}
$formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
if($formatInfo === null){
// Should return null, but, some QR codes apparently do not mask this info.
// Try again by actually masking the pattern first.
$formatInfo = $this->doDecodeFormatInformation(
($formatInfoBits1 ^ $this::FORMAT_INFO_MASK_QR),
($formatInfoBits2 ^ $this::FORMAT_INFO_MASK_QR)
);
// still nothing???
if($formatInfo === null){
throw new QRCodeDecoderException('failed to read format info'); // @codeCoverageIgnore
}
}
$this->eccLevel = new EccLevel(($formatInfo >> 3) & 0x03); // Bits 3,4
$this->maskPattern = new MaskPattern($formatInfo & 0x07); // Bottom 3 bits
return $this;
}
/**
*
*/
private function copyVersionBit(int $i, int $j, int $versionBits):int{
$bit = $this->mirror
? $this->check($j, $i)
: $this->check($i, $j);
return ($bit) ? (($versionBits << 1) | 0x1) : ($versionBits << 1);
}
/**
* Returns information about the format it specifies, or null if it doesn't seem to match any known pattern
*/
private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?int{
$bestDifference = PHP_INT_MAX;
$bestFormatInfo = 0;
// Find the int in FORMAT_INFO_DECODE_LOOKUP with the fewest bits differing
foreach($this::DECODE_LOOKUP as $maskedBits => $dataBits){
if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
// Found an exact match
return $maskedBits;
}
$bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits);
if($bitsDifference < $bestDifference){
$bestFormatInfo = $maskedBits;
$bestDifference = $bitsDifference;
}
if($maskedFormatInfo1 !== $maskedFormatInfo2){
// also try the other option
$bitsDifference = $this->numBitsDiffering($maskedFormatInfo2, $dataBits);
if($bitsDifference < $bestDifference){
$bestFormatInfo = $maskedBits;
$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 $bestFormatInfo;
}
return null;
}
/**
* Reads version information from one of its two locations within the QR Code.
* Throws if both version information locations cannot be parsed as the valid encoding of version information.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
* @noinspection DuplicatedCode
*/
private function readVersion():self{
if($this->version !== null){
return $this;
}
$provisionalVersion = (($this->moduleCount - 17) / 4);
// no version info if v < 7
if($provisionalVersion < 7){
$this->version = new Version($provisionalVersion);
return $this;
}
// Read top-right version info: 3 wide by 6 tall
$versionBits = 0;
$ijMin = ($this->moduleCount - 11);
for($y = 5; $y >= 0; $y--){
for($x = ($this->moduleCount - 9); $x >= $ijMin; $x--){
$versionBits = $this->copyVersionBit($x, $y, $versionBits);
}
}
$this->version = $this->decodeVersionInformation($versionBits);
if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
return $this;
}
// Hmm, failed. Try bottom left: 6 wide by 3 tall
$versionBits = 0;
for($x = 5; $x >= 0; $x--){
for($y = ($this->moduleCount - 9); $y >= $ijMin; $y--){
$versionBits = $this->copyVersionBit($x, $y, $versionBits);
}
}
$this->version = $this->decodeVersionInformation($versionBits);
if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
return $this;
}
throw new QRCodeDecoderException('failed to read version');
}
/**
* Decodes the version information from the given bit sequence, returns null if no valid match is found.
*/
private function decodeVersionInformation(int $versionBits):?Version{
$bestDifference = PHP_INT_MAX;
$bestVersion = 0;
for($i = 7; $i <= 40; $i++){
$targetVersion = new Version($i);
$targetVersionPattern = $targetVersion->getVersionPattern();
// Do the version info bits match exactly? done.
if($targetVersionPattern === $versionBits){
return $targetVersion;
}
// Otherwise see if this is the closest to a real version info bit string
// we have seen so far
/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */
$bitsDifference = $this->numBitsDiffering($versionBits, $targetVersionPattern);
if($bitsDifference < $bestDifference){
$bestVersion = $i;
$bestDifference = $bitsDifference;
}
}
// We can tolerate up to 3 bits of error since no two version info codewords will
// differ in less than 8 bits.
if($bestDifference <= 3){
return new Version($bestVersion);
}
// If we didn't find a close enough match, fail
return null;
}
/**
*
*/
private function uRShift(int $a, int $b):int{
if($b === 0){
return $a;
}
return (($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)));
}
/**
*
*/
private function numBitsDiffering(int $a, int $b):int{
// a now has a 1 bit exactly where its bit differs with b's
$a ^= $b;
// Offset $i holds the number of 1-bits in the binary representation of $i
$BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
// Count bits set quickly with a series of lookups:
$count = 0;
for($i = 0; $i < 32; $i += 4){
$count += $BITS_SET_IN_HALF_BYTE[($this->uRShift($a, $i) & 0x0F)];
}
return $count;
}
/**
* @codeCoverageIgnore
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setQuietZone(int $quietZoneSize = null):self{
throw new QRCodeDataException('not supported');
}
/**
* @codeCoverageIgnore
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setLogoSpace(int $width, int $height = null, int $startX = null, int $startY = null):self{
throw new QRCodeDataException('not supported');
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Class Decoder
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, LuminanceSourceInterface, MaskPattern, Mode, Version};
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number};
use chillerlan\QRCode\Detector\Detector;
use Throwable;
use function chr, str_replace;
/**
* The main class which implements QR Code decoding -- as opposed to locating and extracting
* the QR Code from an image.
*
* @author Sean Owen
*/
final class Decoder{
private ?Version $version = null;
private ?EccLevel $eccLevel = null;
private ?MaskPattern $maskPattern = null;
private BitBuffer $bitBuffer;
/**
* Decodes a QR Code represented as a BitMatrix.
* A 1 or "true" is taken to mean a black module.
*
* @throws \Throwable|\chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function decode(LuminanceSourceInterface $source):DecoderResult{
$matrix = (new Detector($source))->detect();
try{
// clone the BitMatrix to avoid errors in case we run into mirroring
return $this->decodeMatrix(clone $matrix);
}
catch(Throwable $e){
try{
/*
* Prepare for a mirrored reading.
*
* Since we're here, this means we have successfully detected some kind
* of version and format information when mirrored. This is a good sign,
* that the QR code may be mirrored, and we should try once more with a
* mirrored content.
*/
return $this->decodeMatrix($matrix->resetVersionInfo()->mirrorDiagonal());
}
catch(Throwable $f){
// Throw the exception from the original reading
throw $e;
}
}
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function decodeMatrix(BitMatrix $matrix):DecoderResult{
// Read raw codewords
$rawCodewords = $matrix->readCodewords();
$this->version = $matrix->getVersion();
$this->eccLevel = $matrix->getEccLevel();
$this->maskPattern = $matrix->getMaskPattern();
if($this->version === null || $this->eccLevel === null || $this->maskPattern === null){
throw new QRCodeDecoderException('unable to read version or format info'); // @codeCoverageIgnore
}
$resultBytes = (new ReedSolomonDecoder($this->version, $this->eccLevel))->decode($rawCodewords);
return $this->decodeBitStream($resultBytes);
}
/**
* Decode the contents of that stream of bytes
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function decodeBitStream(BitBuffer $bitBuffer):DecoderResult{
$this->bitBuffer = $bitBuffer;
$versionNumber = $this->version->getVersionNumber();
$symbolSequence = -1;
$parityData = -1;
$fc1InEffect = false;
$result = '';
// While still another segment to read...
while($this->bitBuffer->available() >= 4){
$datamode = $this->bitBuffer->read(4); // mode is encoded by 4 bits
// OK, assume we're done
if($datamode === Mode::TERMINATOR){
break;
}
elseif($datamode === Mode::NUMBER){
$result .= Number::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::ALPHANUM){
$result .= $this->decodeAlphanumSegment($versionNumber, $fc1InEffect);
}
elseif($datamode === Mode::BYTE){
$result .= Byte::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::KANJI){
$result .= Kanji::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::STRCTURED_APPEND){
if($this->bitBuffer->available() < 16){
throw new QRCodeDecoderException('structured append: not enough bits left');
}
// sequence number and parity is added later to the result metadata
// Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
$symbolSequence = $this->bitBuffer->read(8);
$parityData = $this->bitBuffer->read(8);
}
elseif($datamode === Mode::FNC1_FIRST || $datamode === Mode::FNC1_SECOND){
// We do little with FNC1 except alter the parsed result a bit according to the spec
$fc1InEffect = true;
}
elseif($datamode === Mode::ECI){
$result .= ECI::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::HANZI){
$result .= Hanzi::decodeSegment($this->bitBuffer, $versionNumber);
}
else{
throw new QRCodeDecoderException('invalid data mode');
}
}
return new DecoderResult([
'rawBytes' => $this->bitBuffer,
'data' => $result,
'version' => $this->version,
'eccLevel' => $this->eccLevel,
'maskPattern' => $this->maskPattern,
'structuredAppendParity' => $parityData,
'structuredAppendSequence' => $symbolSequence,
]);
}
/**
*
*/
private function decodeAlphanumSegment(int $versionNumber, bool $fc1InEffect):string{
$str = AlphaNum::decodeSegment($this->bitBuffer, $versionNumber);
// See section 6.4.8.1, 6.4.8.2
if($fc1InEffect){ // ???
// We need to massage the result a bit if in an FNC1 mode:
$str = str_replace(chr(0x1d), '%', $str);
$str = str_replace('%%', '%', $str);
}
return $str;
}
}

View file

@ -0,0 +1,99 @@
<?php
/**
* Class DecoderResult
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version};
use chillerlan\QRCode\Data\QRMatrix;
use function property_exists;
/**
* Encapsulates the result of decoding a matrix of bits. This typically
* applies to 2D barcode formats. For now, it contains the raw bytes obtained
* as well as a String interpretation of those bytes, if applicable.
*
* @property \chillerlan\QRCode\Common\BitBuffer $rawBytes
* @property string $data
* @property \chillerlan\QRCode\Common\Version $version
* @property \chillerlan\QRCode\Common\EccLevel $eccLevel
* @property \chillerlan\QRCode\Common\MaskPattern $maskPattern
* @property int $structuredAppendParity
* @property int $structuredAppendSequence
*/
final class DecoderResult{
private BitBuffer $rawBytes;
private Version $version;
private EccLevel $eccLevel;
private MaskPattern $maskPattern;
private string $data = '';
private int $structuredAppendParity = -1;
private int $structuredAppendSequence = -1;
/**
* DecoderResult constructor.
*/
public function __construct(iterable $properties = null){
if(!empty($properties)){
foreach($properties as $property => $value){
if(!property_exists($this, $property)){
continue;
}
$this->{$property} = $value;
}
}
}
/**
* @return mixed|null
*/
public function __get(string $property){
if(property_exists($this, $property)){
return $this->{$property};
}
return null;
}
/**
*
*/
public function __toString():string{
return $this->data;
}
/**
*
*/
public function hasStructuredAppend():bool{
return $this->structuredAppendParity >= 0 && $this->structuredAppendSequence >= 0;
}
/**
* Returns a QRMatrix instance with the settings and data of the reader result
*/
public function getQRMatrix():QRMatrix{
return (new QRMatrix($this->version, $this->eccLevel))
->initFunctionalPatterns()
->writeCodewords($this->rawBytes)
->setFormatInfo($this->maskPattern)
->mask($this->maskPattern)
;
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeDecoderException
*
* @created 01.12.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeDecoderException extends QRCodeException{
}

View file

@ -0,0 +1,313 @@
<?php
/**
* Class ReedSolomonDecoder
*
* @created 24.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version};
use function array_fill, array_reverse, count;
/**
* Implements Reed-Solomon decoding
*
* The algorithm will not be explained here, but the following references were helpful
* in creating this implementation:
*
* - Bruce Maggs "Decoding Reed-Solomon Codes" (see discussion of Forney's Formula)
* http://www.cs.cmu.edu/afs/cs.cmu.edu/project/pscico-guyb/realworld/www/rs_decode.ps
* - J.I. Hall. "Chapter 5. Generalized Reed-Solomon Codes" (see discussion of Euclidean algorithm)
* https://users.math.msu.edu/users/halljo/classes/codenotes/GRS.pdf
*
* Much credit is due to William Rucklidge since portions of this code are an indirect
* port of his C++ Reed-Solomon implementation.
*
* @author Sean Owen
* @author William Rucklidge
* @author sanfordsquires
*/
final class ReedSolomonDecoder{
private Version $version;
private EccLevel $eccLevel;
/**
* ReedSolomonDecoder constructor
*/
public function __construct(Version $version, EccLevel $eccLevel){
$this->version = $version;
$this->eccLevel = $eccLevel;
}
/**
* Error-correct and copy data blocks together into a stream of bytes
*/
public function decode(array $rawCodewords):BitBuffer{
$dataBlocks = $this->deinterleaveRawBytes($rawCodewords);
$dataBytes = [];
foreach($dataBlocks as [$numDataCodewords, $codewordBytes]){
$corrected = $this->correctErrors($codewordBytes, $numDataCodewords);
for($i = 0; $i < $numDataCodewords; $i++){
$dataBytes[] = $corrected[$i];
}
}
return new BitBuffer($dataBytes);
}
/**
* When QR Codes use multiple data blocks, they are actually interleaved.
* That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This
* method will separate the data into original blocks.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function deinterleaveRawBytes(array $rawCodewords):array{
// Figure out the number and size of data blocks used by this version and
// error correction level
[$numEccCodewords, $eccBlocks] = $this->version->getRSBlocks($this->eccLevel);
// Now establish DataBlocks of the appropriate size and number of data codewords
$result = [];//new DataBlock[$totalBlocks];
$numResultBlocks = 0;
foreach($eccBlocks as [$numEccBlocks, $eccPerBlock]){
for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){
$result[$numResultBlocks] = [$eccPerBlock, array_fill(0, ($numEccCodewords + $eccPerBlock), 0)];
}
}
// All blocks have the same amount of data, except that the last n
// (where n may be 0) have 1 more byte. Figure out where these start.
/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
$shorterBlocksTotalCodewords = count($result[0][1]);
$longerBlocksStartAt = (count($result) - 1);
while($longerBlocksStartAt >= 0){
$numCodewords = count($result[$longerBlocksStartAt][1]);
if($numCodewords == $shorterBlocksTotalCodewords){
break;
}
$longerBlocksStartAt--;
}
$longerBlocksStartAt++;
$shorterBlocksNumDataCodewords = ($shorterBlocksTotalCodewords - $numEccCodewords);
// The last elements of result may be 1 element longer;
// first fill out as many elements as all of them have
$rawCodewordsOffset = 0;
for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){
for($j = 0; $j < $numResultBlocks; $j++){
$result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++];
}
}
// Fill out the last data block in the longer ones
for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){
$result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++];
}
// Now add in error correction blocks
/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
$max = count($result[0][1]);
for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){
for($j = 0; $j < $numResultBlocks; $j++){
$iOffset = ($j < $longerBlocksStartAt) ? $i : ($i + 1);
$result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++];
}
}
// DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code
return $result;
}
/**
* Given data and error-correction codewords received, possibly corrupted by errors, attempts to
* correct the errors in-place using Reed-Solomon error correction.
*/
private function correctErrors(array $codewordBytes, int $numDataCodewords):array{
// First read into an array of ints
$codewordsInts = [];
foreach($codewordBytes as $codewordByte){
$codewordsInts[] = ($codewordByte & 0xFF);
}
$decoded = $this->decodeWords($codewordsInts, (count($codewordBytes) - $numDataCodewords));
// Copy back into array of bytes -- only need to worry about the bytes that were data
// We don't care about errors in the error-correction codewords
for($i = 0; $i < $numDataCodewords; $i++){
$codewordBytes[$i] = $decoded[$i];
}
return $codewordBytes;
}
/**
* Decodes given set of received codewords, which include both data and error-correction
* codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place,
* in the input.
*
* @param array $received data and error-correction codewords
* @param int $numEccCodewords number of error-correction codewords available
*
* @return int[]
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException if decoding fails for any reason
*/
private function decodeWords(array $received, int $numEccCodewords):array{
$poly = new GenericGFPoly($received);
$syndromeCoefficients = [];
$error = false;
for($i = 0; $i < $numEccCodewords; $i++){
$syndromeCoefficients[$i] = $poly->evaluateAt(GF256::exp($i));
if($syndromeCoefficients[$i] !== 0){
$error = true;
}
}
if(!$error){
return $received;
}
[$sigma, $omega] = $this->runEuclideanAlgorithm(
GF256::buildMonomial($numEccCodewords, 1),
new GenericGFPoly(array_reverse($syndromeCoefficients)),
$numEccCodewords
);
$errorLocations = $this->findErrorLocations($sigma);
$errorMagnitudes = $this->findErrorMagnitudes($omega, $errorLocations);
$errorLocationsCount = count($errorLocations);
$receivedCount = count($received);
for($i = 0; $i < $errorLocationsCount; $i++){
$position = ($receivedCount - 1 - GF256::log($errorLocations[$i]));
if($position < 0){
throw new QRCodeDecoderException('Bad error location');
}
$received[$position] ^= $errorMagnitudes[$i];
}
return $received;
}
/**
* @return \chillerlan\QRCode\Common\GenericGFPoly[] [sigma, omega]
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function runEuclideanAlgorithm(GenericGFPoly $a, GenericGFPoly $b, int $z):array{
// Assume a's degree is >= b's
if($a->getDegree() < $b->getDegree()){
$temp = $a;
$a = $b;
$b = $temp;
}
$rLast = $a;
$r = $b;
$tLast = new GenericGFPoly([0]);
$t = new GenericGFPoly([1]);
// Run Euclidean algorithm until r's degree is less than z/2
while((2 * $r->getDegree()) >= $z){
$rLastLast = $rLast;
$tLastLast = $tLast;
$rLast = $r;
$tLast = $t;
// Divide rLastLast by rLast, with quotient in q and remainder in r
[$q, $r] = $rLastLast->divide($rLast);
$t = $q->multiply($tLast)->addOrSubtract($tLastLast);
if($r->getDegree() >= $rLast->getDegree()){
throw new QRCodeDecoderException('Division algorithm failed to reduce polynomial?');
}
}
$sigmaTildeAtZero = $t->getCoefficient(0);
if($sigmaTildeAtZero === 0){
throw new QRCodeDecoderException('sigmaTilde(0) was zero');
}
$inverse = GF256::inverse($sigmaTildeAtZero);
return [$t->multiplyInt($inverse), $r->multiplyInt($inverse)];
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function findErrorLocations(GenericGFPoly $errorLocator):array{
// This is a direct application of Chien's search
$numErrors = $errorLocator->getDegree();
if($numErrors === 1){ // shortcut
return [$errorLocator->getCoefficient(1)];
}
$result = array_fill(0, $numErrors, 0);
$e = 0;
for($i = 1; $i < 256 && $e < $numErrors; $i++){
if($errorLocator->evaluateAt($i) === 0){
$result[$e] = GF256::inverse($i);
$e++;
}
}
if($e !== $numErrors){
throw new QRCodeDecoderException('Error locator degree does not match number of roots');
}
return $result;
}
/**
*
*/
private function findErrorMagnitudes(GenericGFPoly $errorEvaluator, array $errorLocations):array{
// This is directly applying Forney's Formula
$s = count($errorLocations);
$result = [];
for($i = 0; $i < $s; $i++){
$xiInverse = GF256::inverse($errorLocations[$i]);
$denominator = 1;
for($j = 0; $j < $s; $j++){
if($i !== $j){
# $denominator = GF256::multiply($denominator, GF256::addOrSubtract(1, GF256::multiply($errorLocations[$j], $xiInverse)));
// Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug.
// Below is a funny-looking workaround from Steven Parkes
$term = GF256::multiply($errorLocations[$j], $xiInverse);
$denominator = GF256::multiply($denominator, ((($term & 0x1) === 0) ? ($term | 1) : ($term & ~1)));
}
}
$result[$i] = GF256::multiply($errorEvaluator->evaluateAt($xiInverse), GF256::inverse($denominator));
}
return $result;
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* Class AlignmentPattern
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
/**
* Encapsulates an alignment pattern, which are the smaller square patterns found in
* all but the simplest QR Codes.
*
* @author Sean Owen
*/
final class AlignmentPattern extends ResultPoint{
/**
* Combines this object's current estimate of a finder pattern position and module size
* with a new estimate. It returns a new FinderPattern containing an average of the two.
*/
public function combineEstimate(float $i, float $j, float $newModuleSize):self{
return new self(
(($this->x + $j) / 2.0),
(($this->y + $i) / 2.0),
(($this->estimatedModuleSize + $newModuleSize) / 2.0)
);
}
}

View file

@ -0,0 +1,283 @@
<?php
/**
* Class AlignmentPatternFinder
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Decoder\BitMatrix;
use function abs, count;
/**
* This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder
* patterns but are smaller and appear at regular intervals throughout the image.
*
* At the moment this only looks for the bottom-right alignment pattern.
*
* This is mostly a simplified copy of FinderPatternFinder. It is copied,
* pasted and stripped down here for maximum performance but does unfortunately duplicate
* some code.
*
* This class is thread-safe but not reentrant. Each thread must allocate its own object.
*
* @author Sean Owen
*/
final class AlignmentPatternFinder{
private BitMatrix $matrix;
private float $moduleSize;
/** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */
private array $possibleCenters;
/**
* Creates a finder that will look in a portion of the whole image.
*
* @param \chillerlan\QRCode\Decoder\BitMatrix $matrix image to search
* @param float $moduleSize estimated module size so far
*/
public function __construct(BitMatrix $matrix, float $moduleSize){
$this->matrix = $matrix;
$this->moduleSize = $moduleSize;
$this->possibleCenters = [];
}
/**
* This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since
* it's pretty performance-critical and so is written to be fast foremost.
*
* @param int $startX left column from which to start searching
* @param int $startY top row from which to start searching
* @param int $width width of region to search
* @param int $height height of region to search
*
* @return \chillerlan\QRCode\Detector\AlignmentPattern|null
*/
public function find(int $startX, int $startY, int $width, int $height):?AlignmentPattern{
$maxJ = ($startX + $width);
$middleI = ($startY + ($height / 2));
$stateCount = [];
// We are looking for black/white/black modules in 1:1:1 ratio;
// this tracks the number of black/white/black modules seen so far
for($iGen = 0; $iGen < $height; $iGen++){
// Search from middle outwards
$i = (int)($middleI + ((($iGen & 0x01) === 0) ? ($iGen + 1) / 2 : -(($iGen + 1) / 2)));
$stateCount[0] = 0;
$stateCount[1] = 0;
$stateCount[2] = 0;
$j = $startX;
// Burn off leading white pixels before anything else; if we start in the middle of
// a white run, it doesn't make sense to count its length, since we don't know if the
// white run continued to the left of the start point
while($j < $maxJ && !$this->matrix->check($j, $i)){
$j++;
}
$currentState = 0;
while($j < $maxJ){
if($this->matrix->check($j, $i)){
// Black pixel
if($currentState === 1){ // Counting black pixels
$stateCount[$currentState]++;
}
// Counting white pixels
else{
// A winner?
if($currentState === 2){
// Yes
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
if($confirmed !== null){
return $confirmed;
}
}
$stateCount[0] = $stateCount[2];
$stateCount[1] = 1;
$stateCount[2] = 0;
$currentState = 1;
}
else{
$stateCount[++$currentState]++;
}
}
}
// White pixel
else{
// Counting black pixels
if($currentState === 1){
$currentState++;
}
$stateCount[$currentState]++;
}
$j++;
}
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ);
if($confirmed !== null){
return $confirmed;
}
}
}
// Hmm, nothing we saw was observed and confirmed twice. If we had
// any guess at all, return it.
if(count($this->possibleCenters)){
return $this->possibleCenters[0];
}
return null;
}
/**
* @param int[] $stateCount count of black/white/black pixels just read
*
* @return bool true if the proportions of the counts is close enough to the 1/1/1 ratios
* used by alignment patterns to be considered a match
*/
private function foundPatternCross(array $stateCount):bool{
$maxVariance = ($this->moduleSize / 2.0);
for($i = 0; $i < 3; $i++){
if(abs($this->moduleSize - $stateCount[$i]) >= $maxVariance){
return false;
}
}
return true;
}
/**
* This is called when a horizontal scan finds a possible alignment pattern. It will
* cross-check with a vertical scan, and if successful, will see if this pattern had been
* found on a previous horizontal scan. If so, we consider it confirmed and conclude we have
* found the alignment pattern.
*
* @param int[] $stateCount reading state module counts from horizontal scan
* @param int $i row where alignment pattern may be found
* @param int $j end of possible alignment pattern in row
*
* @return \chillerlan\QRCode\Detector\AlignmentPattern|null if we have found the same pattern twice, or null if not
*/
private function handlePossibleCenter(array $stateCount, int $i, int $j):?AlignmentPattern{
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2]);
$centerJ = $this->centerFromEnd($stateCount, $j);
$centerI = $this->crossCheckVertical($i, (int)$centerJ, (2 * $stateCount[1]), $stateCountTotal);
if($centerI !== null){
$estimatedModuleSize = (($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0);
foreach($this->possibleCenters as $center){
// Look for about the same center and module size:
if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
}
}
// Hadn't found this before; save it
$point = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize);
$this->possibleCenters[] = $point;
}
return null;
}
/**
* Given a count of black/white/black pixels just seen and an end position,
* figures the location of the center of this black/white/black run.
*
* @param int[] $stateCount
* @param int $end
*
* @return float
*/
private function centerFromEnd(array $stateCount, int $end):float{
return (float)(($end - $stateCount[2]) - $stateCount[1] / 2);
}
/**
* After a horizontal scan finds a potential alignment pattern, this method
* "cross-checks" by scanning down vertically through the center of the possible
* alignment pattern to see if the same proportion is detected.
*
* @param int $startI row where an alignment pattern was detected
* @param int $centerJ center of the section that appears to cross an alignment pattern
* @param int $maxCount maximum reasonable number of modules that should be
* observed in any reading state, based on the results of the horizontal scan
* @param int $originalStateCountTotal
*
* @return float|null vertical center of alignment pattern, or null if not found
*/
private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
$maxI = $this->matrix->getSize();
$stateCount = [];
$stateCount[0] = 0;
$stateCount[1] = 0;
$stateCount[2] = 0;
// Start counting up from center
$i = $startI;
while($i >= 0 && $this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$i--;
}
// If already too many modules in this state or ran off the edge:
if($i < 0 || $stateCount[1] > $maxCount){
return null;
}
while($i >= 0 && !$this->matrix->check($centerJ, $i) && $stateCount[0] <= $maxCount){
$stateCount[0]++;
$i--;
}
if($stateCount[0] > $maxCount){
return null;
}
// Now also count down from center
$i = ($startI + 1);
while($i < $maxI && $this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$i++;
}
if($i == $maxI || $stateCount[1] > $maxCount){
return null;
}
while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[2] <= $maxCount){
$stateCount[2]++;
$i++;
}
if($stateCount[2] > $maxCount){
return null;
}
if((5 * abs(($stateCount[0] + $stateCount[1] + $stateCount[2]) - $originalStateCountTotal)) >= (2 * $originalStateCountTotal)){
return null;
}
if(!$this->foundPatternCross($stateCount)){
return null;
}
return $this->centerFromEnd($stateCount, $i);
}
}

View file

@ -0,0 +1,350 @@
<?php
/**
* Class Detector
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Common\{LuminanceSourceInterface, Version};
use chillerlan\QRCode\Decoder\{Binarizer, BitMatrix};
use function abs, intdiv, is_nan, max, min, round;
use const NAN;
/**
* Encapsulates logic that can detect a QR Code in an image, even if the QR Code
* is rotated or skewed, or partially obscured.
*
* @author Sean Owen
*/
final class Detector{
private BitMatrix $matrix;
/**
* Detector constructor.
*/
public function __construct(LuminanceSourceInterface $source){
$this->matrix = (new Binarizer($source))->getBlackMatrix();
}
/**
* Detects a QR Code in an image.
*/
public function detect():BitMatrix{
[$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->matrix))->find();
$moduleSize = $this->calculateModuleSize($topLeft, $topRight, $bottomLeft);
$dimension = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize);
$provisionalVersion = new Version(intdiv(($dimension - 17), 4));
$alignmentPattern = null;
// Anything above version 1 has an alignment pattern
if(!empty($provisionalVersion->getAlignmentPattern())){
// Guess where a "bottom right" finder pattern would have been
$bottomRightX = ($topRight->getX() - $topLeft->getX() + $bottomLeft->getX());
$bottomRightY = ($topRight->getY() - $topLeft->getY() + $bottomLeft->getY());
// Estimate that alignment pattern is closer by 3 modules
// from "bottom right" to known top left location
$correctionToTopLeft = (1.0 - 3.0 / (float)($provisionalVersion->getDimension() - 7));
$estAlignmentX = (int)($topLeft->getX() + $correctionToTopLeft * ($bottomRightX - $topLeft->getX()));
$estAlignmentY = (int)($topLeft->getY() + $correctionToTopLeft * ($bottomRightY - $topLeft->getY()));
// Kind of arbitrary -- expand search radius before giving up
for($i = 4; $i <= 16; $i <<= 1){//??????????
$alignmentPattern = $this->findAlignmentInRegion($moduleSize, $estAlignmentX, $estAlignmentY, (float)$i);
if($alignmentPattern !== null){
break;
}
}
// If we didn't find alignment pattern... well try anyway without it
}
$transform = $this->createTransform($topLeft, $topRight, $bottomLeft, $dimension, $alignmentPattern);
return (new GridSampler)->sampleGrid($this->matrix, $dimension, $transform);
}
/**
* Computes an average estimated module size based on estimated derived from the positions
* of the three finder patterns.
*
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException
*/
private function calculateModuleSize(FinderPattern $topLeft, FinderPattern $topRight, FinderPattern $bottomLeft):float{
// Take the average
$moduleSize = ((
$this->calculateModuleSizeOneWay($topLeft, $topRight) +
$this->calculateModuleSizeOneWay($topLeft, $bottomLeft)
) / 2.0);
if($moduleSize < 1.0){
throw new QRCodeDetectorException('module size < 1.0');
}
return $moduleSize;
}
/**
* Estimates module size based on two finder patterns -- it uses
* #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int) to figure the
* width of each, measuring along the axis between their centers.
*/
private function calculateModuleSizeOneWay(FinderPattern $a, FinderPattern $b):float{
$moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays($a->getX(), $a->getY(), $b->getX(), $b->getY());
$moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays($b->getX(), $b->getY(), $a->getX(), $a->getY());
if(is_nan($moduleSizeEst1)){
return ($moduleSizeEst2 / 7.0);
}
if(is_nan($moduleSizeEst2)){
return ($moduleSizeEst1 / 7.0);
}
// Average them, and divide by 7 since we've counted the width of 3 black modules,
// and 1 white and 1 black module on either side. Ergo, divide sum by 14.
return (($moduleSizeEst1 + $moduleSizeEst2) / 14.0);
}
/**
* See #sizeOfBlackWhiteBlackRun(int, int, int, int); computes the total width of
* a finder pattern by looking for a black-white-black run from the center in the direction
* of another po$(another finder pattern center), and in the opposite direction too.
*
* @noinspection DuplicatedCode
*/
private function sizeOfBlackWhiteBlackRunBothWays(float $fromX, float $fromY, float $toX, float $toY):float{
$result = $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$toX, (int)$toY);
$dimension = $this->matrix->getSize();
// Now count other way -- don't run off image though of course
$scale = 1.0;
$otherToX = ($fromX - ($toX - $fromX));
if($otherToX < 0){
$scale = ($fromX / ($fromX - $otherToX));
$otherToX = 0;
}
elseif($otherToX >= $dimension){
$scale = (($dimension - 1 - $fromX) / ($otherToX - $fromX));
$otherToX = ($dimension - 1);
}
$otherToY = (int)($fromY - ($toY - $fromY) * $scale);
$scale = 1.0;
if($otherToY < 0){
$scale = ($fromY / ($fromY - $otherToY));
$otherToY = 0;
}
elseif($otherToY >= $dimension){
$scale = (($dimension - 1 - $fromY) / ($otherToY - $fromY));
$otherToY = ($dimension - 1);
}
$otherToX = (int)($fromX + ($otherToX - $fromX) * $scale);
$result += $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, $otherToX, $otherToY);
// Middle pixel is double-counted this way; subtract 1
return ($result - 1.0);
}
/**
* This method traces a line from a po$in the image, in the direction towards another point.
* It begins in a black region, and keeps going until it finds white, then black, then white again.
* It reports the distance from the start to this point.
*
* This is used when figuring out how wide a finder pattern is, when the finder pattern
* may be skewed or rotated.
*/
private function sizeOfBlackWhiteBlackRun(int $fromX, int $fromY, int $toX, int $toY):float{
// Mild variant of Bresenham's algorithm;
// @see https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
$steep = abs($toY - $fromY) > abs($toX - $fromX);
if($steep){
$temp = $fromX;
$fromX = $fromY;
$fromY = $temp;
$temp = $toX;
$toX = $toY;
$toY = $temp;
}
$dx = abs($toX - $fromX);
$dy = abs($toY - $fromY);
$error = (-$dx / 2);
$xstep = (($fromX < $toX) ? 1 : -1);
$ystep = (($fromY < $toY) ? 1 : -1);
// In black pixels, looking for white, first or second time.
$state = 0;
// Loop up until x == toX, but not beyond
$xLimit = ($toX + $xstep);
for($x = $fromX, $y = $fromY; $x !== $xLimit; $x += $xstep){
$realX = ($steep) ? $y : $x;
$realY = ($steep) ? $x : $y;
// Does current pixel mean we have moved white to black or vice versa?
// Scanning black in state 0,2 and white in state 1, so if we find the wrong
// color, advance to next state or end if we are in state 2 already
if(($state === 1) === $this->matrix->check($realX, $realY)){
if($state === 2){
return FinderPattern::distance($x, $y, $fromX, $fromY);
}
$state++;
}
$error += $dy;
if($error > 0){
if($y === $toY){
break;
}
$y += $ystep;
$error -= $dx;
}
}
// Found black-white-black; give the benefit of the doubt that the next pixel outside the image
// is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a
// small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this.
if($state === 2){
return FinderPattern::distance(($toX + $xstep), $toY, $fromX, $fromY);
}
// else we didn't find even black-white-black; no estimate is really possible
return NAN;
}
/**
* Computes the dimension (number of modules on a size) of the QR Code based on the position
* of the finder patterns and estimated module size.
*
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException
*/
private function computeDimension(FinderPattern $nw, FinderPattern $ne, FinderPattern $sw, float $size):int{
$tltrCentersDimension = (int)round($nw->getDistance($ne) / $size);
$tlblCentersDimension = (int)round($nw->getDistance($sw) / $size);
$dimension = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7);
switch($dimension % 4){
case 0:
$dimension++;
break;
// 1? do nothing
case 2:
$dimension--;
break;
case 3:
throw new QRCodeDetectorException('estimated dimension: '.$dimension);
}
if(($dimension % 4) !== 1){
throw new QRCodeDetectorException('dimension mod 4 is not 1');
}
return $dimension;
}
/**
* Attempts to locate an alignment pattern in a limited region of the image, which is
* guessed to contain it.
*
* @param float $overallEstModuleSize estimated module size so far
* @param int $estAlignmentX x coordinate of center of area probably containing alignment pattern
* @param int $estAlignmentY y coordinate of above
* @param float $allowanceFactor number of pixels in all directions to search from the center
*
* @return \chillerlan\QRCode\Detector\AlignmentPattern|null if found, or null otherwise
*/
private function findAlignmentInRegion(
float $overallEstModuleSize,
int $estAlignmentX,
int $estAlignmentY,
float $allowanceFactor
):?AlignmentPattern{
// Look for an alignment pattern (3 modules in size) around where it should be
$dimension = $this->matrix->getSize();
$allowance = (int)($allowanceFactor * $overallEstModuleSize);
$alignmentAreaLeftX = max(0, ($estAlignmentX - $allowance));
$alignmentAreaRightX = min(($dimension - 1), ($estAlignmentX + $allowance));
if(($alignmentAreaRightX - $alignmentAreaLeftX) < ($overallEstModuleSize * 3)){
return null;
}
$alignmentAreaTopY = max(0, ($estAlignmentY - $allowance));
$alignmentAreaBottomY = min(($dimension - 1), ($estAlignmentY + $allowance));
if(($alignmentAreaBottomY - $alignmentAreaTopY) < ($overallEstModuleSize * 3)){
return null;
}
return (new AlignmentPatternFinder($this->matrix, $overallEstModuleSize))->find(
$alignmentAreaLeftX,
$alignmentAreaTopY,
($alignmentAreaRightX - $alignmentAreaLeftX),
($alignmentAreaBottomY - $alignmentAreaTopY),
);
}
/**
*
*/
private function createTransform(
FinderPattern $nw,
FinderPattern $ne,
FinderPattern $sw,
int $size,
AlignmentPattern $ap = null
):PerspectiveTransform{
$dimMinusThree = ($size - 3.5);
if($ap instanceof AlignmentPattern){
$bottomRightX = $ap->getX();
$bottomRightY = $ap->getY();
$sourceBottomRightX = ($dimMinusThree - 3.0);
$sourceBottomRightY = $sourceBottomRightX;
}
else{
// Don't have an alignment pattern, just make up the bottom-right point
$bottomRightX = ($ne->getX() - $nw->getX() + $sw->getX());
$bottomRightY = ($ne->getY() - $nw->getY() + $sw->getY());
$sourceBottomRightX = $dimMinusThree;
$sourceBottomRightY = $dimMinusThree;
}
return (new PerspectiveTransform)->quadrilateralToQuadrilateral(
3.5,
3.5,
$dimMinusThree,
3.5,
$sourceBottomRightX,
$sourceBottomRightY,
3.5,
$dimMinusThree,
$nw->getX(),
$nw->getY(),
$ne->getX(),
$ne->getY(),
$bottomRightX,
$bottomRightY,
$sw->getX(),
$sw->getY()
);
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* Class FinderPattern
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use function sqrt;
/**
* Encapsulates a finder pattern, which are the three square patterns found in
* the corners of QR Codes. It also encapsulates a count of similar finder patterns,
* as a convenience to the finder's bookkeeping.
*
* @author Sean Owen
*/
final class FinderPattern extends ResultPoint{
private int $count;
/**
*
*/
public function __construct(float $posX, float $posY, float $estimatedModuleSize, int $count = null){
parent::__construct($posX, $posY, $estimatedModuleSize);
$this->count = ($count ?? 1);
}
/**
*
*/
public function getCount():int{
return $this->count;
}
/**
* @param \chillerlan\QRCode\Detector\FinderPattern $b second pattern
*
* @return float distance between two points
*/
public function getDistance(FinderPattern $b):float{
return self::distance($this->x, $this->y, $b->x, $b->y);
}
/**
* Get square of distance between a and b.
*/
public function getSquaredDistance(FinderPattern $b):float{
return self::squaredDistance($this->x, $this->y, $b->x, $b->y);
}
/**
* Combines this object's current estimate of a finder pattern position and module size
* with a new estimate. It returns a new FinderPattern containing a weighted average
* based on count.
*/
public function combineEstimate(float $i, float $j, float $newModuleSize):self{
$combinedCount = ($this->count + 1);
return new self(
($this->count * $this->x + $j) / $combinedCount,
($this->count * $this->y + $i) / $combinedCount,
($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount,
$combinedCount
);
}
/**
*
*/
private static function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{
$xDiff = ($aX - $bX);
$yDiff = ($aY - $bY);
return ($xDiff * $xDiff + $yDiff * $yDiff);
}
/**
*
*/
public static function distance(float $aX, float $aY, float $bX, float $bY):float{
return sqrt(self::squaredDistance($aX, $aY, $bX, $bY));
}
}

View file

@ -0,0 +1,770 @@
<?php
/**
* Class FinderPatternFinder
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*
* @phan-file-suppress PhanTypePossiblyInvalidDimOffset
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Decoder\BitMatrix;
use function abs, count, intdiv, usort;
use const PHP_FLOAT_MAX;
/**
* This class attempts to find finder patterns in a QR Code. Finder patterns are the square
* markers at three corners of a QR Code.
*
* This class is thread-safe but not reentrant. Each thread must allocate its own object.
*
* @author Sean Owen
*/
final class FinderPatternFinder{
private const MIN_SKIP = 2;
private const MAX_MODULES = 177; // 1 pixel/module times 3 modules/center
private const CENTER_QUORUM = 2; // support up to version 10 for mobile clients
private BitMatrix $matrix;
/** @var \chillerlan\QRCode\Detector\FinderPattern[] */
private array $possibleCenters;
private bool $hasSkipped = false;
/**
* Creates a finder that will search the image for three finder patterns.
*
* @param BitMatrix $matrix image to search
*/
public function __construct(BitMatrix $matrix){
$this->matrix = $matrix;
$this->possibleCenters = [];
}
/**
* @return \chillerlan\QRCode\Detector\FinderPattern[]
*/
public function find():array{
$dimension = $this->matrix->getSize();
// We are looking for black/white/black/white/black modules in
// 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
// Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
// image, and then account for the center being 3 modules in size. This gives the smallest
// number of pixels the center could be, so skip this often.
$iSkip = intdiv((3 * $dimension), (4 * self::MAX_MODULES));
if($iSkip < self::MIN_SKIP){
$iSkip = self::MIN_SKIP;
}
$done = false;
for($i = ($iSkip - 1); ($i < $dimension) && !$done; $i += $iSkip){
// Get a row of black/white values
$stateCount = $this->getCrossCheckStateCount();
$currentState = 0;
for($j = 0; $j < $dimension; $j++){
// Black pixel
if($this->matrix->check($j, $i)){
// Counting white pixels
if(($currentState & 1) === 1){
$currentState++;
}
$stateCount[$currentState]++;
}
// White pixel
else{
// Counting black pixels
if(($currentState & 1) === 0){
// A winner?
if($currentState === 4){
// Yes
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
if($confirmed){
// Start examining every other line. Checking each line turned out to be too
// expensive and didn't improve performance.
$iSkip = 3;
if($this->hasSkipped){
$done = $this->haveMultiplyConfirmedCenters();
}
else{
$rowSkip = $this->findRowSkip();
if($rowSkip > $stateCount[2]){
// Skip rows between row of lower confirmed center
// and top of presumed third confirmed center
// but back up a bit to get a full chance of detecting
// it, entire width of center of finder pattern
// Skip by rowSkip, but back off by $stateCount[2] (size of last center
// of pattern we saw) to be conservative, and also back off by iSkip which
// is about to be re-added
$i += ($rowSkip - $stateCount[2] - $iSkip);
$j = ($dimension - 1);
}
}
}
else{
$stateCount = $this->doShiftCounts2($stateCount);
$currentState = 3;
continue;
}
// Clear state to start looking again
$currentState = 0;
$stateCount = $this->getCrossCheckStateCount();
}
// No, shift counts back by two
else{
$stateCount = $this->doShiftCounts2($stateCount);
$currentState = 3;
}
}
else{
$stateCount[++$currentState]++;
}
}
// Counting white pixels
else{
$stateCount[$currentState]++;
}
}
}
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension);
if($confirmed){
$iSkip = $stateCount[0];
if($this->hasSkipped){
// Found a third one
$done = $this->haveMultiplyConfirmedCenters();
}
}
}
}
return $this->orderBestPatterns($this->selectBestPatterns());
}
/**
* @return int[]
*/
private function getCrossCheckStateCount():array{
return [0, 0, 0, 0, 0];
}
/**
* @param int[] $stateCount
*
* @return int[]
*/
private function doShiftCounts2(array $stateCount):array{
$stateCount[0] = $stateCount[2];
$stateCount[1] = $stateCount[3];
$stateCount[2] = $stateCount[4];
$stateCount[3] = 1;
$stateCount[4] = 0;
return $stateCount;
}
/**
* Given a count of black/white/black/white/black pixels just seen and an end position,
* figures the location of the center of this run.
*
* @param int[] $stateCount
*/
private function centerFromEnd(array $stateCount, int $end):float{
return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2);
}
/**
* @param int[] $stateCount
*/
private function foundPatternCross(array $stateCount):bool{
// Allow less than 50% variance from 1-1-3-1-1 proportions
return $this->foundPatternVariance($stateCount, 2.0);
}
/**
* @param int[] $stateCount
*/
private function foundPatternDiagonal(array $stateCount):bool{
// Allow less than 75% variance from 1-1-3-1-1 proportions
return $this->foundPatternVariance($stateCount, 1.333);
}
/**
* @param int[] $stateCount count of black/white/black/white/black pixels just read
*
* @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios
* used by finder patterns to be considered a match
*/
private function foundPatternVariance(array $stateCount, float $variance):bool{
$totalModuleSize = 0;
for($i = 0; $i < 5; $i++){
$count = $stateCount[$i];
if($count === 0){
return false;
}
$totalModuleSize += $count;
}
if($totalModuleSize < 7){
return false;
}
$moduleSize = ($totalModuleSize / 7.0);
$maxVariance = ($moduleSize / $variance);
return
abs($moduleSize - $stateCount[0]) < $maxVariance
&& abs($moduleSize - $stateCount[1]) < $maxVariance
&& abs(3.0 * $moduleSize - $stateCount[2]) < (3 * $maxVariance)
&& abs($moduleSize - $stateCount[3]) < $maxVariance
&& abs($moduleSize - $stateCount[4]) < $maxVariance;
}
/**
* After a vertical and horizontal scan finds a potential finder pattern, this method
* "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
* finder pattern to see if the same proportion is detected.
*
* @param int $centerI row where a finder pattern was detected
* @param int $centerJ center of the section that appears to cross a finder pattern
*
* @return bool true if proportions are withing expected limits
*/
private function crossCheckDiagonal(int $centerI, int $centerJ):bool{
$stateCount = $this->getCrossCheckStateCount();
// Start counting up, left from center finding black center mass
$i = 0;
while($centerI >= $i && $centerJ >= $i && $this->matrix->check(($centerJ - $i), ($centerI - $i))){
$stateCount[2]++;
$i++;
}
if($stateCount[2] === 0){
return false;
}
// Continue up, left finding white space
while($centerI >= $i && $centerJ >= $i && !$this->matrix->check(($centerJ - $i), ($centerI - $i))){
$stateCount[1]++;
$i++;
}
if($stateCount[1] === 0){
return false;
}
// Continue up, left finding black border
while($centerI >= $i && $centerJ >= $i && $this->matrix->check(($centerJ - $i), ($centerI - $i))){
$stateCount[0]++;
$i++;
}
if($stateCount[0] === 0){
return false;
}
$dimension = $this->matrix->getSize();
// Now also count down, right from center
$i = 1;
while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && $this->matrix->check(($centerJ + $i), ($centerI + $i))){
$stateCount[2]++;
$i++;
}
while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && !$this->matrix->check(($centerJ + $i), ($centerI + $i))){
$stateCount[3]++;
$i++;
}
if($stateCount[3] === 0){
return false;
}
while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && $this->matrix->check(($centerJ + $i), ($centerI + $i))){
$stateCount[4]++;
$i++;
}
if($stateCount[4] === 0){
return false;
}
return $this->foundPatternDiagonal($stateCount);
}
/**
* After a horizontal scan finds a potential finder pattern, this method
* "cross-checks" by scanning down vertically through the center of the possible
* finder pattern to see if the same proportion is detected.
*
* @param int $startI row where a finder pattern was detected
* @param int $centerJ center of the section that appears to cross a finder pattern
* @param int $maxCount maximum reasonable number of modules that should be
* observed in any reading state, based on the results of the horizontal scan
* @param int $originalStateCountTotal
*
* @return float|null vertical center of finder pattern, or null if not found
* @noinspection DuplicatedCode
*/
private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
$maxI = $this->matrix->getSize();
$stateCount = $this->getCrossCheckStateCount();
// Start counting up from center
$i = $startI;
while($i >= 0 && $this->matrix->check($centerJ, $i)){
$stateCount[2]++;
$i--;
}
if($i < 0){
return null;
}
while($i >= 0 && !$this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$i--;
}
// If already too many modules in this state or ran off the edge:
if($i < 0 || $stateCount[1] > $maxCount){
return null;
}
while($i >= 0 && $this->matrix->check($centerJ, $i) && $stateCount[0] <= $maxCount){
$stateCount[0]++;
$i--;
}
if($stateCount[0] > $maxCount){
return null;
}
// Now also count down from center
$i = ($startI + 1);
while($i < $maxI && $this->matrix->check($centerJ, $i)){
$stateCount[2]++;
$i++;
}
if($i === $maxI){
return null;
}
while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[3] < $maxCount){
$stateCount[3]++;
$i++;
}
if($i === $maxI || $stateCount[3] >= $maxCount){
return null;
}
while($i < $maxI && $this->matrix->check($centerJ, $i) && $stateCount[4] < $maxCount){
$stateCount[4]++;
$i++;
}
if($stateCount[4] >= $maxCount){
return null;
}
// If we found a finder-pattern-like section, but its size is more than 40% different from
// the original, assume it's a false positive
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]);
if((5 * abs($stateCountTotal - $originalStateCountTotal)) >= (2 * $originalStateCountTotal)){
return null;
}
if(!$this->foundPatternCross($stateCount)){
return null;
}
return $this->centerFromEnd($stateCount, $i);
}
/**
* Like #crossCheckVertical(int, int, int, int), and in fact is basically identical,
* except it reads horizontally instead of vertically. This is used to cross-cross
* check a vertical cross-check and locate the real center of the alignment pattern.
* @noinspection DuplicatedCode
*/
private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{
$maxJ = $this->matrix->getSize();
$stateCount = $this->getCrossCheckStateCount();
$j = $startJ;
while($j >= 0 && $this->matrix->check($j, $centerI)){
$stateCount[2]++;
$j--;
}
if($j < 0){
return null;
}
while($j >= 0 && !$this->matrix->check($j, $centerI) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$j--;
}
if($j < 0 || $stateCount[1] > $maxCount){
return null;
}
while($j >= 0 && $this->matrix->check($j, $centerI) && $stateCount[0] <= $maxCount){
$stateCount[0]++;
$j--;
}
if($stateCount[0] > $maxCount){
return null;
}
$j = ($startJ + 1);
while($j < $maxJ && $this->matrix->check($j, $centerI)){
$stateCount[2]++;
$j++;
}
if($j === $maxJ){
return null;
}
while($j < $maxJ && !$this->matrix->check($j, $centerI) && $stateCount[3] < $maxCount){
$stateCount[3]++;
$j++;
}
if($j === $maxJ || $stateCount[3] >= $maxCount){
return null;
}
while($j < $maxJ && $this->matrix->check($j, $centerI) && $stateCount[4] < $maxCount){
$stateCount[4]++;
$j++;
}
if($stateCount[4] >= $maxCount){
return null;
}
// If we found a finder-pattern-like section, but its size is significantly different from
// the original, assume it's a false positive
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]);
if((5 * abs($stateCountTotal - $originalStateCountTotal)) >= $originalStateCountTotal){
return null;
}
if(!$this->foundPatternCross($stateCount)){
return null;
}
return $this->centerFromEnd($stateCount, $j);
}
/**
* This is called when a horizontal scan finds a possible alignment pattern. It will
* cross-check with a vertical scan, and if successful, will, ah, cross-cross-check
* with another horizontal scan. This is needed primarily to locate the real horizontal
* center of the pattern in cases of extreme skew.
* And then we cross-cross-cross check with another diagonal scan.
*
* If that succeeds the finder pattern location is added to a list that tracks
* the number of times each location has been nearly-matched as a finder pattern.
* Each additional find is more evidence that the location is in fact a finder
* pattern center
*
* @param int[] $stateCount reading state module counts from horizontal scan
* @param int $i row where finder pattern may be found
* @param int $j end of possible finder pattern in row
*
* @return bool if a finder pattern candidate was found this time
*/
private function handlePossibleCenter(array $stateCount, int $i, int $j):bool{
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]);
$centerJ = $this->centerFromEnd($stateCount, $j);
$centerI = $this->crossCheckVertical($i, (int)$centerJ, $stateCount[2], $stateCountTotal);
if($centerI !== null){
// Re-cross check
$centerJ = $this->crossCheckHorizontal((int)$centerJ, (int)$centerI, $stateCount[2], $stateCountTotal);
if($centerJ !== null && ($this->crossCheckDiagonal((int)$centerI, (int)$centerJ))){
$estimatedModuleSize = ($stateCountTotal / 7.0);
$found = false;
// cautious (was in for fool in which $this->possibleCenters is updated)
$count = count($this->possibleCenters);
for($index = 0; $index < $count; $index++){
$center = $this->possibleCenters[$index];
// Look for about the same center and module size:
if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
$this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
$found = true;
break;
}
}
if(!$found){
$point = new FinderPattern($centerJ, $centerI, $estimatedModuleSize);
$this->possibleCenters[] = $point;
}
return true;
}
}
return false;
}
/**
* @return int number of rows we could safely skip during scanning, based on the first
* two finder patterns that have been located. In some cases their position will
* allow us to infer that the third pattern must lie below a certain point farther
* down in the image.
*/
private function findRowSkip():int{
$max = count($this->possibleCenters);
if($max <= 1){
return 0;
}
$firstConfirmedCenter = null;
foreach($this->possibleCenters as $center){
if($center->getCount() >= self::CENTER_QUORUM){
if($firstConfirmedCenter === null){
$firstConfirmedCenter = $center;
}
else{
// We have two confirmed centers
// How far down can we skip before resuming looking for the next
// pattern? In the worst case, only the difference between the
// difference in the x / y coordinates of the two centers.
// This is the case where you find top left last.
$this->hasSkipped = true;
return (int)((abs($firstConfirmedCenter->getX() - $center->getX()) -
abs($firstConfirmedCenter->getY() - $center->getY())) / 2);
}
}
}
return 0;
}
/**
* @return bool true if we have found at least 3 finder patterns that have been detected
* at least #CENTER_QUORUM times each, and, the estimated module size of the
* candidates is "pretty similar"
*/
private function haveMultiplyConfirmedCenters():bool{
$confirmedCount = 0;
$totalModuleSize = 0.0;
$max = count($this->possibleCenters);
foreach($this->possibleCenters as $pattern){
if($pattern->getCount() >= self::CENTER_QUORUM){
$confirmedCount++;
$totalModuleSize += $pattern->getEstimatedModuleSize();
}
}
if($confirmedCount < 3){
return false;
}
// OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive"
// and that we need to keep looking. We detect this by asking if the estimated module sizes
// vary too much. We arbitrarily say that when the total deviation from average exceeds
// 5% of the total module size estimates, it's too much.
$average = ($totalModuleSize / (float)$max);
$totalDeviation = 0.0;
foreach($this->possibleCenters as $pattern){
$totalDeviation += abs($pattern->getEstimatedModuleSize() - $average);
}
return $totalDeviation <= (0.05 * $totalModuleSize);
}
/**
* @return \chillerlan\QRCode\Detector\FinderPattern[] the 3 best FinderPatterns from our list of candidates. The "best" are
* those that have been detected at least #CENTER_QUORUM times, and whose module
* size differs from the average among those patterns the least
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if 3 such finder patterns do not exist
*/
private function selectBestPatterns():array{
$startSize = count($this->possibleCenters);
if($startSize < 3){
throw new QRCodeDetectorException('could not find enough finder patterns');
}
usort(
$this->possibleCenters,
fn(FinderPattern $a, FinderPattern $b) => ($a->getEstimatedModuleSize() <=> $b->getEstimatedModuleSize())
);
$distortion = PHP_FLOAT_MAX;
$bestPatterns = [];
for($i = 0; $i < ($startSize - 2); $i++){
$fpi = $this->possibleCenters[$i];
$minModuleSize = $fpi->getEstimatedModuleSize();
for($j = ($i + 1); $j < ($startSize - 1); $j++){
$fpj = $this->possibleCenters[$j];
$squares0 = $fpi->getSquaredDistance($fpj);
for($k = ($j + 1); $k < $startSize; $k++){
$fpk = $this->possibleCenters[$k];
$maxModuleSize = $fpk->getEstimatedModuleSize();
// module size is not similar
if($maxModuleSize > ($minModuleSize * 1.4)){
continue;
}
$a = $squares0;
$b = $fpj->getSquaredDistance($fpk);
$c = $fpi->getSquaredDistance($fpk);
// sorts ascending - inlined
if($a < $b){
if($b > $c){
if($a < $c){
$temp = $b;
$b = $c;
$c = $temp;
}
else{
$temp = $a;
$a = $c;
$c = $b;
$b = $temp;
}
}
}
else{
if($b < $c){
if($a < $c){
$temp = $a;
$a = $b;
$b = $temp;
}
else{
$temp = $a;
$a = $b;
$b = $c;
$c = $temp;
}
}
else{
$temp = $a;
$a = $c;
$c = $temp;
}
}
// a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).
// Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,
// we need to check both two equal sides separately.
// The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity
// from isosceles right triangle.
$d = (abs($c - 2 * $b) + abs($c - 2 * $a));
if($d < $distortion){
$distortion = $d;
$bestPatterns = [$fpi, $fpj, $fpk];
}
}
}
}
if($distortion === PHP_FLOAT_MAX){
throw new QRCodeDetectorException('finder patterns may be too distorted');
}
return $bestPatterns;
}
/**
* Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC
* and BC is less than AC, and the angle between BC and BA is less than 180 degrees.
*
* @param \chillerlan\QRCode\Detector\FinderPattern[] $patterns array of three FinderPattern to order
*
* @return \chillerlan\QRCode\Detector\FinderPattern[]
*/
private function orderBestPatterns(array $patterns):array{
// Find distances between pattern centers
$zeroOneDistance = $patterns[0]->getDistance($patterns[1]);
$oneTwoDistance = $patterns[1]->getDistance($patterns[2]);
$zeroTwoDistance = $patterns[0]->getDistance($patterns[2]);
// Assume one closest to other two is B; A and C will just be guesses at first
if($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance){
[$pointB, $pointA, $pointC] = $patterns;
}
elseif($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance){
[$pointA, $pointB, $pointC] = $patterns;
}
else{
[$pointA, $pointC, $pointB] = $patterns;
}
// Use cross product to figure out whether A and C are correct or flipped.
// This asks whether BC x BA has a positive z component, which is the arrangement
// we want for A, B, C. If it's negative, then we've got it flipped around and
// should swap A and C.
if($this->crossProductZ($pointA, $pointB, $pointC) < 0.0){
$temp = $pointA;
$pointA = $pointC;
$pointC = $temp;
}
return [$pointA, $pointB, $pointC];
}
/**
* Returns the z component of the cross product between vectors BC and BA.
*/
private function crossProductZ(FinderPattern $pointA, FinderPattern $pointB, FinderPattern $pointC):float{
$bX = $pointB->getX();
$bY = $pointB->getY();
return ((($pointC->getX() - $bX) * ($pointA->getY() - $bY)) - (($pointC->getY() - $bY) * ($pointA->getX() - $bX)));
}
}

View file

@ -0,0 +1,181 @@
<?php
/**
* Class GridSampler
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\Decoder\BitMatrix;
use function array_fill, count, intdiv, sprintf;
/**
* Implementations of this class can, given locations of finder patterns for a QR code in an
* image, sample the right points in the image to reconstruct the QR code, accounting for
* perspective distortion. It is abstracted since it is relatively expensive and should be allowed
* to take advantage of platform-specific optimized implementations, like Sun's Java Advanced
* Imaging library, but which may not be available in other environments such as J2ME, and vice
* versa.
*
* The implementation used can be controlled by calling #setGridSampler(GridSampler)
* with an instance of a class which implements this interface.
*
* @author Sean Owen
*/
final class GridSampler{
private array $points;
/**
* Checks a set of points that have been transformed to sample points on an image against
* the image's dimensions to see if the point are even within the image.
*
* This method will actually "nudge" the endpoints back onto the image if they are found to be
* barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder
* patterns in an image where the QR Code runs all the way to the image border.
*
* For efficiency, the method will check points from either end of the line until one is found
* to be within the image. Because the set of points are assumed to be linear, this is valid.
*
* @param int $dimension matrix width/height
*
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if an endpoint is lies outside the image boundaries
*/
private function checkAndNudgePoints(int $dimension):void{
$nudged = true;
$max = count($this->points);
// Check and nudge points from start until we see some that are OK:
for($offset = 0; $offset < $max && $nudged; $offset += 2){
$x = (int)$this->points[$offset];
$y = (int)$this->points[($offset + 1)];
if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 1, x: %s, y: %s, d: %s', $x, $y, $dimension));
}
$nudged = false;
if($x === -1){
$this->points[$offset] = 0.0;
$nudged = true;
}
elseif($x === $dimension){
$this->points[$offset] = ($dimension - 1);
$nudged = true;
}
if($y === -1){
$this->points[($offset + 1)] = 0.0;
$nudged = true;
}
elseif($y === $dimension){
$this->points[($offset + 1)] = ($dimension - 1);
$nudged = true;
}
}
// Check and nudge points from end:
$nudged = true;
for($offset = ($max - 2); $offset >= 0 && $nudged; $offset -= 2){
$x = (int)$this->points[$offset];
$y = (int)$this->points[($offset + 1)];
if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 2, x: %s, y: %s, d: %s', $x, $y, $dimension));
}
$nudged = false;
if($x === -1){
$this->points[$offset] = 0.0;
$nudged = true;
}
elseif($x === $dimension){
$this->points[$offset] = ($dimension - 1);
$nudged = true;
}
if($y === -1){
$this->points[($offset + 1)] = 0.0;
$nudged = true;
}
elseif($y === $dimension){
$this->points[($offset + 1)] = ($dimension - 1);
$nudged = true;
}
}
}
/**
* Samples an image for a rectangular matrix of bits of the given dimension. The sampling
* transformation is determined by the coordinates of 4 points, in the original and transformed
* image space.
*
* @return \chillerlan\QRCode\Decoder\BitMatrix representing a grid of points sampled from the image within a region
* defined by the "from" parameters
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if image can't be sampled, for example, if the transformation defined
* by the given points is invalid or results in sampling outside the image boundaries
*/
public function sampleGrid(BitMatrix $matrix, int $dimension, PerspectiveTransform $transform):BitMatrix{
if($dimension <= 0){
throw new QRCodeDetectorException('invalid matrix size');
}
$bits = new BitMatrix($dimension);
$this->points = array_fill(0, (2 * $dimension), 0.0);
for($y = 0; $y < $dimension; $y++){
$max = count($this->points);
$iValue = ($y + 0.5);
for($x = 0; $x < $max; $x += 2){
$this->points[$x] = (($x / 2) + 0.5);
$this->points[($x + 1)] = $iValue;
}
// phpcs:ignore
[$this->points, ] = $transform->transformPoints($this->points);
// Quick check to see if points transformed to something inside the image;
// sufficient to check the endpoints
$this->checkAndNudgePoints($matrix->getSize());
// no need to try/catch as QRMatrix::set() will silently discard out of bounds values
# try{
for($x = 0; $x < $max; $x += 2){
// Black(-ish) pixel
$bits->set(
intdiv($x, 2),
$y,
$matrix->check((int)$this->points[$x], (int)$this->points[($x + 1)]),
QRMatrix::M_DATA
);
}
# }
# catch(\Throwable $aioobe){//ArrayIndexOutOfBoundsException
// This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting
// transform gets "twisted" such that it maps a straight line of points to a set of points
// whose endpoints are in bounds, but others are not. There is probably some mathematical
// way to detect this about the transformation that I don't know yet.
// This results in an ugly runtime exception despite our clever checks above -- can't have
// that. We could check each point's coordinates but that feels duplicative. We settle for
// catching and wrapping ArrayIndexOutOfBoundsException.
# throw new QRCodeDetectorException('ArrayIndexOutOfBoundsException');
# }
}
return $bits;
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* Class PerspectiveTransform
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use function count;
/**
* This class implements a perspective transform in two dimensions. Given four source and four
* destination points, it will compute the transformation implied between them. The code is based
* directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.
*
* @author Sean Owen
*/
final class PerspectiveTransform{
private float $a11;
private float $a12;
private float $a13;
private float $a21;
private float $a22;
private float $a23;
private float $a31;
private float $a32;
private float $a33;
/**
*
*/
private function set(
float $a11, float $a21, float $a31,
float $a12, float $a22, float $a32,
float $a13, float $a23, float $a33
):self{
$this->a11 = $a11;
$this->a12 = $a12;
$this->a13 = $a13;
$this->a21 = $a21;
$this->a22 = $a22;
$this->a23 = $a23;
$this->a31 = $a31;
$this->a32 = $a32;
$this->a33 = $a33;
return $this;
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function quadrilateralToQuadrilateral(
float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3,
float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p
):self{
return (new self)
->squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p)
->times($this->quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3));
}
/**
*
*/
private function quadrilateralToSquare(
float $x0, float $y0, float $x1, float $y1,
float $x2, float $y2, float $x3, float $y3
):self{
// Here, the adjoint serves as the inverse:
return $this
->squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
->buildAdjoint();
}
/**
*
*/
private function buildAdjoint():self{
// Adjoint is the transpose of the cofactor matrix:
return $this->set(
($this->a22 * $this->a33 - $this->a23 * $this->a32),
($this->a23 * $this->a31 - $this->a21 * $this->a33),
($this->a21 * $this->a32 - $this->a22 * $this->a31),
($this->a13 * $this->a32 - $this->a12 * $this->a33),
($this->a11 * $this->a33 - $this->a13 * $this->a31),
($this->a12 * $this->a31 - $this->a11 * $this->a32),
($this->a12 * $this->a23 - $this->a13 * $this->a22),
($this->a13 * $this->a21 - $this->a11 * $this->a23),
($this->a11 * $this->a22 - $this->a12 * $this->a21)
);
}
/**
*
*/
private function squareToQuadrilateral(
float $x0, float $y0, float $x1, float $y1,
float $x2, float $y2, float $x3, float $y3
):self{
$dx3 = ($x0 - $x1 + $x2 - $x3);
$dy3 = ($y0 - $y1 + $y2 - $y3);
if($dx3 === 0.0 && $dy3 === 0.0){
// Affine
return $this->set(($x1 - $x0), ($x2 - $x1), $x0, ($y1 - $y0), ($y2 - $y1), $y0, 0.0, 0.0, 1.0);
}
$dx1 = ($x1 - $x2);
$dx2 = ($x3 - $x2);
$dy1 = ($y1 - $y2);
$dy2 = ($y3 - $y2);
$denominator = ($dx1 * $dy2 - $dx2 * $dy1);
$a13 = (($dx3 * $dy2 - $dx2 * $dy3) / $denominator);
$a23 = (($dx1 * $dy3 - $dx3 * $dy1) / $denominator);
return $this->set(
($x1 - $x0 + $a13 * $x1),
($x3 - $x0 + $a23 * $x3),
$x0,
($y1 - $y0 + $a13 * $y1),
($y3 - $y0 + $a23 * $y3),
$y0,
$a13,
$a23,
1.0
);
}
/**
*
*/
private function times(PerspectiveTransform $other):self{
return $this->set(
($this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13),
($this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23),
($this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33),
($this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13),
($this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23),
($this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33),
($this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13),
($this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23),
($this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33)
);
}
/**
* @return array[] [$xValues, $yValues]
*/
public function transformPoints(array $xValues, array $yValues = null):array{
$max = count($xValues);
if($yValues !== null){ // unused
for($i = 0; $i < $max; $i++){
$x = $xValues[$i];
$y = $yValues[$i];
$denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33);
$xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator);
$yValues[$i] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator);
}
return [$xValues, $yValues];
}
for($i = 0; $i < $max; $i += 2){
$x = $xValues[$i];
$y = $xValues[($i + 1)];
$denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33);
$xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator);
$xValues[($i + 1)] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator);
}
return [$xValues, []];
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeDetectorException
*
* @created 01.12.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeDetectorException extends QRCodeException{
}

View file

@ -0,0 +1,73 @@
<?php
/**
* Class ResultPoint
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use function abs;
/**
* Encapsulates a point of interest in an image containing a barcode. Typically, this
* would be the location of a finder pattern or the corner of the barcode, for example.
*
* @author Sean Owen
*/
abstract class ResultPoint{
protected float $x;
protected float $y;
protected float $estimatedModuleSize;
/**
*
*/
public function __construct(float $x, float $y, float $estimatedModuleSize){
$this->x = $x;
$this->y = $y;
$this->estimatedModuleSize = $estimatedModuleSize;
}
/**
*
*/
public function getX():float{
return $this->x;
}
/**
*
*/
public function getY():float{
return $this->y;
}
/**
*
*/
public function getEstimatedModuleSize():float{
return $this->estimatedModuleSize;
}
/**
* Determines if this finder pattern "about equals" a finder pattern at the stated
* position and size -- meaning, it is at nearly the same center with nearly the same size.
*/
public function aboutEquals(float $moduleSize, float $i, float $j):bool{
if(abs($i - $this->y) <= $moduleSize && abs($j - $this->x) <= $moduleSize){
$moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize);
return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize;
}
return false;
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeOutputException
*
* @created 09.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeOutputException extends QRCodeException{
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Class QREps
*
* @created 09.05.2022
* @author smiley <smiley@chillerlan.net>
* @copyright 2022 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function array_values, count, date, implode, is_array, is_numeric, max, min, round, sprintf;
/**
* Encapsulated Postscript (EPS) output
*
* @see https://github.com/t0k4rt/phpqrcode/blob/bb29e6eb77e0a2a85bb0eb62725e0adc11ff5a90/qrvect.php#L52-L137
* @see https://web.archive.org/web/20170818010030/http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/postscript/pdfs/5002.EPSF_Spec.pdf
* @see https://web.archive.org/web/20210419003859/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf
* @see https://github.com/chillerlan/php-qrcode/discussions/148
*/
class QREps extends QROutputAbstract{
public const MIME_TYPE = 'application/postscript';
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_array($value) || count($value) < 3){
return false;
}
// check the first values of the array
foreach(array_values($value) as $i => $val){
if($i > 3){
break;
}
if(!is_numeric($val)){
return false;
}
}
return true;
}
/**
* @param array $value
*
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
$values = [];
foreach(array_values($value) as $i => $val){
if($i > 3){
break;
}
// clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range
$values[] = round((max(0, min(255, intval($val))) / 255), 6);
}
return $this->formatColor($values);
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return $this->formatColor(($isDark) ? [0.0, 0.0, 0.0] : [1.0, 1.0, 1.0]);
}
/**
* Set the color format string
*
* 4 values in the color array will be interpreted as CMYK, 3 as RGB
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function formatColor(array $values):string{
$count = count($values);
if($count < 3){
throw new QRCodeOutputException('invalid color value');
}
$format = ($count === 4)
// CMYK
? '%f %f %f %f C'
// RGB
:'%f %f %f R';
return sprintf($format, ...$values);
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
[$width, $height] = $this->getOutputDimensions();
$eps = [
// main header
'%!PS-Adobe-3.0 EPSF-3.0',
'%%Creator: php-qrcode (https://github.com/chillerlan/php-qrcode)',
'%%Title: QR Code',
sprintf('%%%%CreationDate: %1$s', date('c')),
'%%DocumentData: Clean7Bit',
'%%LanguageLevel: 3',
sprintf('%%%%BoundingBox: 0 0 %s %s', $width, $height),
'%%EndComments',
// function definitions
'%%BeginProlog',
'/F { rectfill } def',
'/R { setrgbcolor } def',
'/C { setcmykcolor } def',
'%%EndProlog',
];
if($this::moduleValueIsValid($this->options->bgColor)){
$eps[] = $this->prepareModuleValue($this->options->bgColor);
$eps[] = sprintf('0 0 %s %s F', $width, $height);
}
// create the path elements
$paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE));
foreach($paths as $M_TYPE => $path){
if(empty($path)){
continue;
}
$eps[] = $this->getModuleValue($M_TYPE);
$eps[] = implode("\n", $path);
}
// end file
$eps[] = '%%EOF';
$data = implode("\n", $eps);
$this->saveToFile($data, $file);
return $data;
}
/**
* Returns a path segment for a single module
*/
protected function module(int $x, int $y, int $M_TYPE):string{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return '';
}
$outputX = ($x * $this->scale);
// Actual size - one block = Topmost y pos.
$top = ($this->length - $this->scale);
// Apparently y-axis is inverted (y0 is at bottom and not top) in EPS, so we have to switch the y-axis here
$outputY = ($top - ($y * $this->scale));
return sprintf('%d %d %d %d F', $outputX, $outputY, $this->scale, $this->scale);
}
}

View file

@ -0,0 +1,177 @@
<?php
/**
* Class QRFpdf
*
* @created 03.06.2020
* @author Maximilian Kresse
* @license MIT
*
* @see https://github.com/chillerlan/php-qrcode/pull/49
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use FPDF;
use function array_values, class_exists, count, intval, is_array, is_numeric, max, min;
/**
* QRFpdf output module (requires fpdf)
*
* @see https://github.com/Setasign/FPDF
* @see http://www.fpdf.org/
*/
class QRFpdf extends QROutputAbstract{
public const MIME_TYPE = 'application/pdf';
protected FPDF $fpdf;
protected ?array $prevColor = null;
/**
* QRFpdf constructor.
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
if(!class_exists(FPDF::class)){
// @codeCoverageIgnoreStart
throw new QRCodeOutputException(
'The QRFpdf output requires FPDF (https://github.com/Setasign/FPDF)'.
' as dependency but the class "\\FPDF" couldn\'t be found.'
);
// @codeCoverageIgnoreEnd
}
parent::__construct($options, $matrix);
}
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_array($value) || count($value) < 3){
return false;
}
// check the first 3 values of the array
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
if(!is_numeric($val)){
return false;
}
}
return true;
}
/**
* @param array $value
*
* @inheritDoc
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function prepareModuleValue($value):array{
$values = [];
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
$values[] = max(0, min(255, intval($val)));
}
if(count($values) !== 3){
throw new QRCodeOutputException('invalid color value');
}
return $values;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):array{
return ($isDark) ? [0, 0, 0] : [255, 255, 255];
}
/**
* Initializes an FPDF instance
*/
protected function initFPDF():FPDF{
return new FPDF('P', $this->options->fpdfMeasureUnit, $this->getOutputDimensions());
}
/**
* @inheritDoc
*
* @return string|\FPDF
*/
public function dump(string $file = null){
$this->fpdf = $this->initFPDF();
$this->fpdf->AddPage();
if($this::moduleValueIsValid($this->options->bgColor)){
$bgColor = $this->prepareModuleValue($this->options->bgColor);
[$width, $height] = $this->getOutputDimensions();
/** @phan-suppress-next-line PhanParamTooFewUnpack */
$this->fpdf->SetFillColor(...$bgColor);
$this->fpdf->Rect(0, 0, $width, $height, 'F');
}
$this->prevColor = null;
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$this->module($x, $y, $M_TYPE);
}
}
if($this->options->returnResource){
return $this->fpdf;
}
$pdfData = $this->fpdf->Output('S');
$this->saveToFile($pdfData, $file);
if($this->options->outputBase64){
$pdfData = $this->toBase64DataURI($pdfData);
}
return $pdfData;
}
/**
* Renders a single module
*/
protected function module(int $x, int $y, int $M_TYPE):void{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return;
}
$color = $this->getModuleValue($M_TYPE);
if($color !== null && $color !== $this->prevColor){
/** @phan-suppress-next-line PhanParamTooFewUnpack */
$this->fpdf->SetFillColor(...$color);
$this->prevColor = $color;
}
$this->fpdf->Rect(($x * $this->scale), ($y * $this->scale), $this->scale, $this->scale, 'F');
}
}

View file

@ -0,0 +1,400 @@
<?php
/**
* Class QRGdImage
*
* @created 05.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use ErrorException;
use Throwable;
use function array_values, count, extension_loaded, imagebmp, imagecolorallocate, imagecolortransparent,
imagecreatetruecolor, imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng,
imagescale, imagetypes, imagewebp, intdiv, intval, is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start,
restore_error_handler, set_error_handler, sprintf;
use const IMG_BMP, IMG_GIF, IMG_JPG, IMG_PNG, IMG_WEBP;
/**
* Converts the matrix into GD images, raw or base64 output (requires ext-gd)
*
* @see https://php.net/manual/book.image.php
*
* @deprecated 5.0.0 this class will be made abstract in future versions,
* calling it directly is deprecated - use one of the child classes instead
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
class QRGdImage extends QROutputAbstract{
/**
* The GD image resource
*
* @see imagecreatetruecolor()
* @var resource|\GdImage
*
* @todo: add \GdImage type in v6
*/
protected $image;
/**
* The allocated background color
*
* @see \imagecolorallocate()
*/
protected int $background;
/**
* Whether we're running in upscale mode (scale < 20)
*
* @see \chillerlan\QRCode\QROptions::$drawCircularModules
*/
protected bool $upscaled = false;
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
$this->options = $options;
$this->matrix = $matrix;
$this->checkGD();
if($this->options->invertMatrix){
$this->matrix->invert();
}
$this->copyVars();
$this->setMatrixDimensions();
}
/**
* Checks whether GD is installed and if the given mode is supported
*
* @return void
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
* @codeCoverageIgnore
*/
protected function checkGD():void{
if(!extension_loaded('gd')){
throw new QRCodeOutputException('ext-gd not loaded');
}
$modes = [
self::GDIMAGE_BMP => IMG_BMP,
self::GDIMAGE_GIF => IMG_GIF,
self::GDIMAGE_JPG => IMG_JPG,
self::GDIMAGE_PNG => IMG_PNG,
self::GDIMAGE_WEBP => IMG_WEBP,
];
// likely using default or custom output
if(!isset($modes[$this->options->outputType])){
return;
}
$mode = $modes[$this->options->outputType];
if((imagetypes() & $mode) !== $mode){
throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputType));
}
}
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_array($value) || count($value) < 3){
return false;
}
// check the first 3 values of the array
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
if(!is_numeric($val)){
return false;
}
}
return true;
}
/**
* @param array $value
*
* @inheritDoc
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function prepareModuleValue($value):int{
$values = [];
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
$values[] = max(0, min(255, intval($val)));
}
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
$color = imagecolorallocate($this->image, ...$values);
if($color === false){
throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
}
return $color;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):int{
return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
}
/**
* @inheritDoc
*
* @return string|resource|\GdImage
*
* @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
* @throws \ErrorException
*/
public function dump(string $file = null){
set_error_handler(function(int $errno, string $errstr):bool{
throw new ErrorException($errstr, $errno);
});
$this->image = $this->createImage();
// set module values after image creation because we need the GdImage instance
$this->setModuleValues();
$this->setBgColor();
imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
$this->drawImage();
if($this->upscaled){
// scale down to the expected size
$this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10));
$this->upscaled = false;
}
// set transparency after scaling, otherwise it would be undone
// @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099
$this->setTransparencyColor();
if($this->options->returnResource){
restore_error_handler();
return $this->image;
}
$imageData = $this->dumpImage();
$this->saveToFile($imageData, $file);
if($this->options->outputBase64){
// @todo: remove mime parameter in v6
$imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType);
}
restore_error_handler();
return $imageData;
}
/**
* Creates a new GdImage resource and scales it if necessary
*
* we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
*
* @see https://github.com/chillerlan/php-qrcode/issues/23
*
* @return \GdImage|resource
*/
protected function createImage(){
if($this->drawCircularModules && $this->options->gdImageUseUpscale && $this->options->scale < 20){
// increase the initial image size by 10
$this->length *= 10;
$this->scale *= 10;
$this->upscaled = true;
}
return imagecreatetruecolor($this->length, $this->length);
}
/**
* Sets the background color
*/
protected function setBgColor():void{
if(isset($this->background)){
return;
}
if($this::moduleValueIsValid($this->options->bgColor)){
$this->background = $this->prepareModuleValue($this->options->bgColor);
return;
}
$this->background = $this->prepareModuleValue([255, 255, 255]);
}
/**
* Sets the transparency color
*/
protected function setTransparencyColor():void{
// @todo: the jpg skip can be removed in v6
if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){
return;
}
$transparencyColor = $this->background;
if($this::moduleValueIsValid($this->options->transparencyColor)){
$transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
}
imagecolortransparent($this->image, $transparencyColor);
}
/**
* Draws the QR image
*/
protected function drawImage():void{
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$this->module($x, $y, $M_TYPE);
}
}
}
/**
* Creates a single QR pixel with the given settings
*/
protected function module(int $x, int $y, int $M_TYPE):void{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return;
}
$color = $this->getModuleValue($M_TYPE);
if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
imagefilledellipse(
$this->image,
(($x * $this->scale) + intdiv($this->scale, 2)),
(($y * $this->scale) + intdiv($this->scale, 2)),
(int)($this->circleDiameter * $this->scale),
(int)($this->circleDiameter * $this->scale),
$color
);
return;
}
imagefilledrectangle(
$this->image,
($x * $this->scale),
($y * $this->scale),
(($x + 1) * $this->scale),
(($y + 1) * $this->scale),
$color
);
}
/**
* Renders the image with the gdimage function for the desired output
*
* @see \imagebmp()
* @see \imagegif()
* @see \imagejpeg()
* @see \imagepng()
* @see \imagewebp()
*
* @todo: v6.0: make abstract and call from child classes
* @see https://github.com/chillerlan/php-qrcode/issues/223
* @codeCoverageIgnore
*/
protected function renderImage():void{
switch($this->options->outputType){
case QROutputInterface::GDIMAGE_BMP:
imagebmp($this->image, null, ($this->options->quality > 0));
break;
case QROutputInterface::GDIMAGE_GIF:
imagegif($this->image);
break;
case QROutputInterface::GDIMAGE_JPG:
imagejpeg($this->image, null, max(-1, min(100, $this->options->quality)));
break;
case QROutputInterface::GDIMAGE_WEBP:
imagewebp($this->image, null, max(-1, min(100, $this->options->quality)));
break;
// silently default to png output
case QROutputInterface::GDIMAGE_PNG:
default:
imagepng($this->image, null, max(-1, min(9, $this->options->quality)));
}
}
/**
* Creates the final image by calling the desired GD output function
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function dumpImage():string{
$exception = null;
$imageData = null;
ob_start();
try{
$this->renderImage();
$imageData = ob_get_contents();
imagedestroy($this->image);
}
// not going to cover edge cases
// @codeCoverageIgnoreStart
catch(Throwable $e){
$exception = $e;
}
// @codeCoverageIgnoreEnd
ob_end_clean();
// throw here in case an exception happened within the output buffer
if($exception instanceof Throwable){
throw new QRCodeOutputException($exception->getMessage());
}
return $imageData;
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImageBMP
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagebmp;
/**
* GdImage bmp output
*
* @see \imagebmp()
*/
class QRGdImageBMP extends QRGdImage{
public const MIME_TYPE = 'image/bmp';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagebmp($this->image, null, ($this->options->quality > 0));
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImageGIF
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagegif;
/**
* GdImage gif output
*
* @see \imagegif()
*/
class QRGdImageGIF extends QRGdImage{
public const MIME_TYPE = 'image/gif';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagegif($this->image);
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* Class QRGdImageJPEG
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagejpeg, max, min;
/**
* GdImage jpeg output
*
* @see \imagejpeg()
*/
class QRGdImageJPEG extends QRGdImage{
public const MIME_TYPE = 'image/jpg';
/**
* @inheritDoc
*/
protected function setTransparencyColor():void{
// noop - transparency is not supported
}
/**
* @inheritDoc
*/
protected function renderImage():void{
imagejpeg($this->image, null, max(-1, min(100, $this->options->quality)));
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImagePNG
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagepng, max, min;
/**
* GdImage png output
*
* @see \imagepng()
*/
class QRGdImagePNG extends QRGdImage{
public const MIME_TYPE = 'image/png';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagepng($this->image, null, max(-1, min(9, $this->options->quality)));
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImageWEBP
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagewebp, max, min;
/**
* GdImage webp output
*
* @see \imagewebp()
*/
class QRGdImageWEBP extends QRGdImage{
public const MIME_TYPE = 'image/webp';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagewebp($this->image, null, max(-1, min(100, $this->options->quality)));
}
}

View file

@ -0,0 +1,19 @@
<?php
/**
* Class QRImage
*
* @created 14.12.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
/**
* @deprecated 5.0.0 backward compatibility, use QRGdImage instead
* @see \chillerlan\QRCode\Output\QRGdImage
*/
class QRImage extends QRGdImage{
}

View file

@ -0,0 +1,235 @@
<?php
/**
* Class QRImagick
*
* @created 04.07.2018
* @author smiley <smiley@chillerlan.net>
* @copyright 2018 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use finfo, Imagick, ImagickDraw, ImagickPixel;
use function extension_loaded, in_array, is_string, max, min, preg_match, strlen;
use const FILEINFO_MIME_TYPE;
/**
* ImageMagick output module (requires ext-imagick)
*
* @see https://php.net/manual/book.imagick.php
* @see https://phpimagick.com
*/
class QRImagick extends QROutputAbstract{
/**
* The main image instance
*/
protected Imagick $imagick;
/**
* The main draw instance
*/
protected ImagickDraw $imagickDraw;
/**
* The allocated background color
*/
protected ImagickPixel $backgroundColor;
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
foreach(['fileinfo', 'imagick'] as $ext){
if(!extension_loaded($ext)){
throw new QRCodeOutputException(sprintf('ext-%s not loaded', $ext)); // @codeCoverageIgnore
}
}
parent::__construct($options, $matrix);
}
/**
* note: we're not necessarily validating the several values, just checking the general syntax
*
* @see https://www.php.net/manual/imagickpixel.construct.php
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_string($value)){
return false;
}
$value = trim($value);
// hex notation
// #rgb(a)
// #rrggbb(aa)
// #rrrrggggbbbb(aaaa)
// ...
if(preg_match('/^#[a-f\d]+$/i', $value) && in_array((strlen($value) - 1), [3, 4, 6, 8, 9, 12, 16, 24, 32], true)){
return true;
}
// css (-like) func(...values)
if(preg_match('#^(graya?|hs(b|la?)|rgba?)\([\d .,%]+\)$#i', $value)){
return true;
}
// predefined css color
if(preg_match('/^[a-z]+$/i', $value)){
return true;
}
return false;
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):ImagickPixel{
return new ImagickPixel($value);
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):ImagickPixel{
return $this->prepareModuleValue(($isDark) ? '#000' : '#fff');
}
/**
* @inheritDoc
*
* @return string|\Imagick
*/
public function dump(string $file = null){
$this->setBgColor();
$this->imagick = $this->createImage();
$this->drawImage();
// set transparency color after all operations
$this->setTransparencyColor();
if($this->options->returnResource){
return $this->imagick;
}
$imageData = $this->imagick->getImageBlob();
$this->imagick->destroy();
$this->saveToFile($imageData, $file);
if($this->options->outputBase64){
$imageData = $this->toBase64DataURI($imageData, (new finfo(FILEINFO_MIME_TYPE))->buffer($imageData));
}
return $imageData;
}
/**
* Sets the background color
*/
protected function setBgColor():void{
if($this::moduleValueIsValid($this->options->bgColor)){
$this->backgroundColor = $this->prepareModuleValue($this->options->bgColor);
return;
}
$this->backgroundColor = $this->prepareModuleValue('white');
}
/**
* Creates a new Imagick instance
*/
protected function createImage():Imagick{
$imagick = new Imagick;
[$width, $height] = $this->getOutputDimensions();
$imagick->newImage($width, $height, $this->backgroundColor, $this->options->imagickFormat);
if($this->options->quality > -1){
$imagick->setImageCompressionQuality(max(0, min(100, $this->options->quality)));
}
return $imagick;
}
/**
* Sets the transparency color
*/
protected function setTransparencyColor():void{
if(!$this->options->imageTransparent){
return;
}
$transparencyColor = $this->backgroundColor;
if($this::moduleValueIsValid($this->options->transparencyColor)){
$transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
}
$this->imagick->transparentPaintImage($transparencyColor, 0.0, 10, false);
}
/**
* Creates the QR image via ImagickDraw
*/
protected function drawImage():void{
$this->imagickDraw = new ImagickDraw;
$this->imagickDraw->setStrokeWidth(0);
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$this->module($x, $y, $M_TYPE);
}
}
$this->imagick->drawImage($this->imagickDraw);
}
/**
* draws a single pixel at the given position
*/
protected function module(int $x, int $y, int $M_TYPE):void{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return;
}
$this->imagickDraw->setFillColor($this->getModuleValue($M_TYPE));
if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
$this->imagickDraw->circle(
(($x + 0.5) * $this->scale),
(($y + 0.5) * $this->scale),
(($x + 0.5 + $this->circleRadius) * $this->scale),
(($y + 0.5) * $this->scale)
);
return;
}
$this->imagickDraw->rectangle(
($x * $this->scale),
($y * $this->scale),
((($x + 1) * $this->scale) - 1),
((($y + 1) * $this->scale) - 1)
);
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Class QRMarkup
*
* @created 17.12.2016
* @author Smiley <smiley@chillerlan.net>
* @copyright 2016 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function is_string, preg_match, strip_tags, trim;
/**
* Abstract for markup types: HTML, SVG, ... XML anyone?
*/
abstract class QRMarkup extends QROutputAbstract{
/**
* note: we're not necessarily validating the several values, just checking the general syntax
* note: css4 colors are not included
*
* @todo: XSS proof
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_string($value)){
return false;
}
$value = trim(strip_tags($value), " '\"\r\n\t");
// hex notation
// #rgb(a)
// #rrggbb(aa)
if(preg_match('/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i', $value)){
return true;
}
// css: hsla/rgba(...values)
if(preg_match('#^(hsla?|rgba?)\([\d .,%/]+\)$#i', $value)){
return true;
}
// predefined css color
if(preg_match('/^[a-z]+$/i', $value)){
return true;
}
return false;
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
return trim(strip_tags($value), " '\"\r\n\t");
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return ($isDark) ? '#000' : '#fff';
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
$data = $this->createMarkup($file !== null);
$this->saveToFile($data, $file);
return $data;
}
/**
* returns a string with all css classes for the current element
*/
protected function getCssClass(int $M_TYPE = 0):string{
return $this->options->cssClass;
}
/**
* returns the fully parsed and rendered markup string for the given input
*/
abstract protected function createMarkup(bool $saveToFile):string;
}

View file

@ -0,0 +1,51 @@
<?php
/**
* Class QRMarkupHTML
*
* @created 06.06.2022
* @author smiley <smiley@chillerlan.net>
* @copyright 2022 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function implode, sprintf;
/**
* HTML output (a cheap markup substitute when SVG is not available or not an option)
*/
class QRMarkupHTML extends QRMarkup{
public const MIME_TYPE = 'text/html';
/**
* @inheritDoc
*/
protected function createMarkup(bool $saveToFile):string{
$rows = [];
$cssClass = $this->getCssClass();
foreach($this->matrix->getMatrix() as $row){
$element = '<span style="background: %s;"></span>';
$modules = array_map(fn(int $M_TYPE):string => sprintf($element, $this->getModuleValue($M_TYPE)), $row);
$rows[] = sprintf('<div>%s</div>%s', implode('', $modules), $this->eol);
}
$html = sprintf('<div class="%1$s">%3$s%2$s</div>%3$s', $cssClass, implode('', $rows), $this->eol);
// wrap the snippet into a body when saving to file
if($saveToFile){
$html = sprintf(
'<!DOCTYPE html><html lang="none">%2$s<head>%2$s<meta charset="UTF-8">%2$s'.
'<title>QR Code</title></head>%2$s<body>%1$s</body>%2$s</html>',
$html,
$this->eol
);
}
return $html;
}
}

View file

@ -0,0 +1,200 @@
<?php
/**
* Class QRMarkupSVG
*
* @created 06.06.2022
* @author smiley <smiley@chillerlan.net>
* @copyright 2022 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function array_chunk, implode, is_string, preg_match, sprintf, trim;
/**
* SVG output
*
* @see https://github.com/codemasher/php-qrcode/pull/5
* @see https://developer.mozilla.org/en-US/docs/Web/SVG
* @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/
* @see https://lea.verou.me/blog/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/
* @see https://codepen.io/leaverou/full/RmwzKv
* @see https://jakearchibald.github.io/svgomg/
* @see https://web.archive.org/web/20200220211445/http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html
*/
class QRMarkupSVG extends QRMarkup{
public const MIME_TYPE = 'image/svg+xml';
/**
* @todo: XSS proof
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_string($value)){
return false;
}
$value = trim($value);
// url(...)
if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){
return true;
}
// otherwise check for standard css notation
return parent::moduleValueIsValid($value);
}
/**
* @inheritDoc
*/
protected function getOutputDimensions():array{
return [$this->moduleCount, $this->moduleCount];
}
/**
* @inheritDoc
*/
protected function getCssClass(int $M_TYPE = 0):string{
return implode(' ', [
'qr-'.($this::LAYERNAMES[$M_TYPE] ?? $M_TYPE),
$this->matrix->isDark($M_TYPE) ? 'dark' : 'light',
$this->options->cssClass,
]);
}
/**
* @inheritDoc
*/
protected function createMarkup(bool $saveToFile):string{
$svg = $this->header();
if(!empty($this->options->svgDefs)){
$svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->eol);
}
$svg .= $this->paths();
// close svg
$svg .= sprintf('%1$s</svg>%1$s', $this->eol);
// transform to data URI only when not saving to file
if(!$saveToFile && $this->options->outputBase64){
$svg = $this->toBase64DataURI($svg);
}
return $svg;
}
/**
* returns the value for the SVG viewBox attribute
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox
* @see https://css-tricks.com/scale-svg/#article-header-id-3
*/
protected function getViewBox():string{
[$width, $height] = $this->getOutputDimensions();
return sprintf('0 0 %s %s', $width, $height);
}
/**
* returns the <svg> header with the given options parsed
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg
*/
protected function header():string{
$header = sprintf(
'<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" viewBox="%2$s" preserveAspectRatio="%3$s">%4$s',
$this->options->cssClass,
$this->getViewBox(),
$this->options->svgPreserveAspectRatio,
$this->eol
);
if($this->options->svgAddXmlHeader){
$header = sprintf('<?xml version="1.0" encoding="UTF-8"?>%s%s', $this->eol, $header);
}
return $header;
}
/**
* returns one or more SVG <path> elements
*/
protected function paths():string{
$paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE));
$svg = [];
// create the path elements
foreach($paths as $M_TYPE => $modules){
// limit the total line length
$chunks = array_chunk($modules, 100);
$chonks = [];
foreach($chunks as $chunk){
$chonks[] = implode(' ', $chunk);
}
$path = implode($this->eol, $chonks);
if(empty($path)){
continue;
}
$svg[] = $this->path($path, $M_TYPE);
}
return implode($this->eol, $svg);
}
/**
* renders and returns a single <path> element
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
*/
protected function path(string $path, int $M_TYPE):string{
if($this->options->svgUseFillAttributes){
return sprintf(
'<path class="%s" fill="%s" d="%s"/>',
$this->getCssClass($M_TYPE),
$this->getModuleValue($M_TYPE),
$path
);
}
return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
}
/**
* returns a path segment for a single module
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
*/
protected function module(int $x, int $y, int $M_TYPE):string{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return '';
}
if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
// string interpolation: ugly and fast
$ix = ($x + 0.5 - $this->circleRadius);
$iy = ($y + 0.5);
// phpcs:ignore
return "M$ix $iy a$this->circleRadius $this->circleRadius 0 1 0 $this->circleDiameter 0 a$this->circleRadius $this->circleRadius 0 1 0 -$this->circleDiameter 0Z";
}
// phpcs:ignore
return "M$x $y h1 v1 h-1Z";
}
}

View file

@ -0,0 +1,261 @@
<?php
/**
* Class QROutputAbstract
*
* @created 09.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use Closure;
use function base64_encode, dirname, file_put_contents, is_writable, ksort, sprintf;
/**
* common output abstract
*/
abstract class QROutputAbstract implements QROutputInterface{
/**
* the current size of the QR matrix
*
* @see \chillerlan\QRCode\Data\QRMatrix::getSize()
*/
protected int $moduleCount;
/**
* the side length of the QR image (modules * scale)
*/
protected int $length;
/**
* an (optional) array of color values for the several QR matrix parts
*/
protected array $moduleValues;
/**
* the (filled) data matrix object
*/
protected QRMatrix $matrix;
/**
* @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
*/
protected SettingsContainerInterface $options;
/** @see \chillerlan\QRCode\QROptions::$scale */
protected int $scale;
/** @see \chillerlan\QRCode\QROptions::$connectPaths */
protected bool $connectPaths;
/** @see \chillerlan\QRCode\QROptions::$excludeFromConnect */
protected array $excludeFromConnect;
/** @see \chillerlan\QRCode\QROptions::$eol */
protected string $eol;
/** @see \chillerlan\QRCode\QROptions::$drawLightModules */
protected bool $drawLightModules;
/** @see \chillerlan\QRCode\QROptions::$drawCircularModules */
protected bool $drawCircularModules;
/** @see \chillerlan\QRCode\QROptions::$keepAsSquare */
protected array $keepAsSquare;
/** @see \chillerlan\QRCode\QROptions::$circleRadius */
protected float $circleRadius;
protected float $circleDiameter;
/**
* QROutputAbstract constructor.
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
$this->options = $options;
$this->matrix = $matrix;
if($this->options->invertMatrix){
$this->matrix->invert();
}
$this->copyVars();
$this->setMatrixDimensions();
$this->setModuleValues();
}
/**
* Creates copies of several QROptions values to avoid calling the magic getters
* in long loops for a significant performance increase.
*
* These variables are usually used in the "module" methods and are called up to 31329 times (at version 40).
*/
protected function copyVars():void{
$vars = [
'connectPaths',
'excludeFromConnect',
'eol',
'drawLightModules',
'drawCircularModules',
'keepAsSquare',
'circleRadius',
];
foreach($vars as $property){
$this->{$property} = $this->options->{$property};
}
$this->circleDiameter = ($this->circleRadius * 2);
}
/**
* Sets/updates the matrix dimensions
*
* Call this method if you modify the matrix from within your custom module in case the dimensions have been changed
*/
protected function setMatrixDimensions():void{
$this->moduleCount = $this->matrix->getSize();
$this->scale = $this->options->scale;
$this->length = ($this->moduleCount * $this->scale);
}
/**
* Returns a 2 element array with the current output width and height
*
* The type and units of the values depend on the output class. The default value is the current module count * scale.
*/
protected function getOutputDimensions():array{
return [$this->length, $this->length];
}
/**
* Sets the initial module values
*/
protected function setModuleValues():void{
// first fill the map with the default values
foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){
$this->moduleValues[$M_TYPE] = $this->getDefaultModuleValue($defaultValue);
}
// now loop over the options values to replace defaults and add extra values
foreach($this->options->moduleValues as $M_TYPE => $value){
if($this::moduleValueIsValid($value)){
$this->moduleValues[$M_TYPE] = $this->prepareModuleValue($value);
}
}
}
/**
* Prepares the value for the given input (return value depends on the output class)
*
* @param mixed $value
*
* @return mixed|null
*/
abstract protected function prepareModuleValue($value);
/**
* Returns a default value for either dark or light modules (return value depends on the output class)
*
* @return mixed|null
*/
abstract protected function getDefaultModuleValue(bool $isDark);
/**
* Returns the prepared value for the given $M_TYPE
*
* @return mixed return value depends on the output class
* @throws \chillerlan\QRCode\Output\QRCodeOutputException if $moduleValues[$M_TYPE] doesn't exist
*/
protected function getModuleValue(int $M_TYPE){
if(!isset($this->moduleValues[$M_TYPE])){
throw new QRCodeOutputException(sprintf('$M_TYPE %012b not found in module values map', $M_TYPE));
}
return $this->moduleValues[$M_TYPE];
}
/**
* Returns the prepared module value at the given coordinate [$x, $y] (convenience)
*
* @return mixed|null
*/
protected function getModuleValueAt(int $x, int $y){
return $this->getModuleValue($this->matrix->get($x, $y));
}
/**
* Returns a base64 data URI for the given string and mime type
*/
protected function toBase64DataURI(string $data, string $mime = null):string{
return sprintf('data:%s;base64,%s', ($mime ?? $this::MIME_TYPE), base64_encode($data));
}
/**
* Saves the qr $data to a $file. If $file is null, nothing happens.
*
* @see file_put_contents()
* @see \chillerlan\QRCode\QROptions::$cachefile
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function saveToFile(string $data, string $file = null):void{
if($file === null){
return;
}
if(!is_writable(dirname($file))){
throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s', $file));
}
if(file_put_contents($file, $data) === false){
throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s (file_put_contents error)', $file));
}
}
/**
* collects the modules per QRMatrix::M_* type and runs a $transform function on each module and
* returns an array with the transformed modules
*
* The transform callback is called with the following parameters:
*
* $x - current column
* $y - current row
* $M_TYPE - field value
* $M_TYPE_LAYER - (possibly modified) field value that acts as layer id
*/
protected function collectModules(Closure $transform):array{
$paths = [];
// collect the modules for each type
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$M_TYPE_LAYER = $M_TYPE;
if($this->connectPaths && !$this->matrix->checkTypeIn($x, $y, $this->excludeFromConnect)){
// to connect paths we'll redeclare the $M_TYPE_LAYER to data only
$M_TYPE_LAYER = QRMatrix::M_DATA;
if($this->matrix->isDark($M_TYPE)){
$M_TYPE_LAYER = QRMatrix::M_DATA_DARK;
}
}
// collect the modules per $M_TYPE
$module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER);
if(!empty($module)){
$paths[$M_TYPE_LAYER][] = $module;
}
}
}
// beautify output
ksort($paths);
return $paths;
}
}

View file

@ -0,0 +1,226 @@
<?php
/**
* Interface QROutputInterface,
*
* @created 02.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
/**
* Converts the data matrix into readable output
*/
interface QROutputInterface{
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const MARKUP_HTML = 'html';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const MARKUP_SVG = 'svg';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_BMP = 'bmp';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_GIF = 'gif';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_JPG = 'jpg';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_PNG = 'png';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_WEBP = 'webp';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const STRING_JSON = 'json';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const STRING_TEXT = 'text';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const IMAGICK = 'imagick';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const FPDF = 'fpdf';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const EPS = 'eps';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const CUSTOM = 'custom';
/**
* Map of built-in output modes => class FQN
*
* @var string[]
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const MODES = [
self::MARKUP_SVG => QRMarkupSVG::class,
self::MARKUP_HTML => QRMarkupHTML::class,
self::GDIMAGE_BMP => QRGdImageBMP::class,
self::GDIMAGE_GIF => QRGdImageGIF::class,
self::GDIMAGE_JPG => QRGdImageJPEG::class,
self::GDIMAGE_PNG => QRGdImagePNG::class,
self::GDIMAGE_WEBP => QRGdImageWEBP::class,
self::STRING_JSON => QRStringJSON::class,
self::STRING_TEXT => QRStringText::class,
self::IMAGICK => QRImagick::class,
self::FPDF => QRFpdf::class,
self::EPS => QREps::class,
];
/**
* Map of module type => default value
*
* @var bool[]
*/
public const DEFAULT_MODULE_VALUES = [
// light
QRMatrix::M_NULL => false,
QRMatrix::M_DARKMODULE_LIGHT => false,
QRMatrix::M_DATA => false,
QRMatrix::M_FINDER => false,
QRMatrix::M_SEPARATOR => false,
QRMatrix::M_ALIGNMENT => false,
QRMatrix::M_TIMING => false,
QRMatrix::M_FORMAT => false,
QRMatrix::M_VERSION => false,
QRMatrix::M_QUIETZONE => false,
QRMatrix::M_LOGO => false,
QRMatrix::M_FINDER_DOT_LIGHT => false,
// dark
QRMatrix::M_DARKMODULE => true,
QRMatrix::M_DATA_DARK => true,
QRMatrix::M_FINDER_DARK => true,
QRMatrix::M_SEPARATOR_DARK => true,
QRMatrix::M_ALIGNMENT_DARK => true,
QRMatrix::M_TIMING_DARK => true,
QRMatrix::M_FORMAT_DARK => true,
QRMatrix::M_VERSION_DARK => true,
QRMatrix::M_QUIETZONE_DARK => true,
QRMatrix::M_LOGO_DARK => true,
QRMatrix::M_FINDER_DOT => true,
];
/**
* Map of module type => readable name (for CSS etc.)
*
* @var string[]
*/
public const LAYERNAMES = [
// light
QRMatrix::M_NULL => 'null',
QRMatrix::M_DARKMODULE_LIGHT => 'darkmodule-light',
QRMatrix::M_DATA => 'data',
QRMatrix::M_FINDER => 'finder',
QRMatrix::M_SEPARATOR => 'separator',
QRMatrix::M_ALIGNMENT => 'alignment',
QRMatrix::M_TIMING => 'timing',
QRMatrix::M_FORMAT => 'format',
QRMatrix::M_VERSION => 'version',
QRMatrix::M_QUIETZONE => 'quietzone',
QRMatrix::M_LOGO => 'logo',
QRMatrix::M_FINDER_DOT_LIGHT => 'finder-dot-light',
// dark
QRMatrix::M_DARKMODULE => 'darkmodule',
QRMatrix::M_DATA_DARK => 'data-dark',
QRMatrix::M_FINDER_DARK => 'finder-dark',
QRMatrix::M_SEPARATOR_DARK => 'separator-dark',
QRMatrix::M_ALIGNMENT_DARK => 'alignment-dark',
QRMatrix::M_TIMING_DARK => 'timing-dark',
QRMatrix::M_FORMAT_DARK => 'format-dark',
QRMatrix::M_VERSION_DARK => 'version-dark',
QRMatrix::M_QUIETZONE_DARK => 'quietzone-dark',
QRMatrix::M_LOGO_DARK => 'logo-dark',
QRMatrix::M_FINDER_DOT => 'finder-dot',
];
/**
* @var string
* @see \chillerlan\QRCode\Output\QROutputAbstract::toBase64DataURI()
* @internal do not call this constant from the interface, but rather from one of the child classes
*/
public const MIME_TYPE = '';
/**
* Determines whether the given value is valid
*
* @param mixed $value
*/
public static function moduleValueIsValid($value):bool;
/**
* Generates the output, optionally dumps it to a file, and returns it
*
* please note that the value of QROptions::$cachefile is already evaluated at this point.
* if the output module is invoked manually, it has no effect at all.
* you need to supply the $file parameter here in that case (or handle the option value in your custom output module).
*
* @see \chillerlan\QRCode\QRCode::renderMatrix()
*
* @return mixed
*/
public function dump(string $file = null);
}

View file

@ -0,0 +1,111 @@
<?php
/**
* Class QRString
*
* @created 05.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function implode, is_string, json_encode, max, min, sprintf;
use const JSON_THROW_ON_ERROR;
/**
* Converts the matrix data into string types
*
* @deprecated 5.0.0 this class will be removed in future versions, use one of QRStringText or QRStringJSON instead
*/
class QRString extends QROutputAbstract{
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
return is_string($value);
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
return $value;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return ($isDark) ? '██' : '░░';
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
switch($this->options->outputType){
case QROutputInterface::STRING_TEXT:
$data = $this->text();
break;
case QROutputInterface::STRING_JSON:
default:
$data = $this->json();
}
$this->saveToFile($data, $file);
return $data;
}
/**
* string output
*/
protected function text():string{
$lines = [];
$linestart = $this->options->textLineStart;
for($y = 0; $y < $this->moduleCount; $y++){
$r = [];
for($x = 0; $x < $this->moduleCount; $x++){
$r[] = $this->getModuleValueAt($x, $y);
}
$lines[] = $linestart.implode('', $r);
}
return implode($this->eol, $lines);
}
/**
* JSON output
*
* @throws \JsonException
*/
protected function json():string{
return json_encode($this->matrix->getMatrix($this->options->jsonAsBooleans), JSON_THROW_ON_ERROR);
}
//
/**
* a little helper to create a proper ANSI 8-bit color escape sequence
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
* @see https://en.wikipedia.org/wiki/Block_Elements
*
* @codeCoverageIgnore
*/
public static function ansi8(string $str, int $color, bool $background = null):string{
$color = max(0, min($color, 255));
$background = ($background === true) ? 48 : 38;
return sprintf("\x1b[%s;5;%sm%s\x1b[0m", $background, $color, $str);
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* Class QRStringJSON
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function json_encode;
/**
*
*/
class QRStringJSON extends QROutputAbstract{
public const MIME_TYPE = 'application/json';
/**
* @inheritDoc
* @throws \JsonException
*/
public function dump(string $file = null):string{
$matrix = $this->matrix->getMatrix($this->options->jsonAsBooleans);
$data = json_encode($matrix, $this->options->jsonFlags);;
$this->saveToFile($data, $file);
return $data;
}
/**
* unused - required by interface
*
* @inheritDoc
* @codeCoverageIgnore
*/
protected function prepareModuleValue($value):string{
return '';
}
/**
* unused - required by interface
*
* @inheritDoc
* @codeCoverageIgnore
*/
protected function getDefaultModuleValue(bool $isDark):string{
return '';
}
/**
* unused - required by interface
*
* @inheritDoc
* @codeCoverageIgnore
*/
public static function moduleValueIsValid($value):bool{
return true;
}
}

View file

@ -0,0 +1,76 @@
<?php
/**
* Class QRStringText
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function array_map, implode, is_string, max, min, sprintf;
/**
*
*/
class QRStringText extends QROutputAbstract{
public const MIME_TYPE = 'text/plain';
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
return is_string($value);
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
return $value;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return ($isDark) ? '██' : '░░';
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
$lines = [];
$linestart = $this->options->textLineStart;
foreach($this->matrix->getMatrix() as $row){
$lines[] = $linestart.implode('', array_map([$this, 'getModuleValue'], $row));
}
$data = implode($this->eol, $lines);
$this->saveToFile($data, $file);
return $data;
}
/**
* a little helper to create a proper ANSI 8-bit color escape sequence
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
* @see https://en.wikipedia.org/wiki/Block_Elements
*
* @codeCoverageIgnore
*/
public static function ansi8(string $str, int $color, bool $background = null):string{
$color = max(0, min($color, 255));
$background = ($background === true) ? 48 : 38;
return sprintf("\x1b[%s;5;%sm%s\x1b[0m", $background, $color, $str);
}
}

488
vendor/chillerlan/php-qrcode/src/QRCode.php vendored Executable file
View file

@ -0,0 +1,488 @@
<?php
/**
* Class QRCode
*
* @created 26.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
namespace chillerlan\QRCode;
use chillerlan\QRCode\Common\{
EccLevel, ECICharset, GDLuminanceSource, IMagickLuminanceSource, LuminanceSourceInterface, MaskPattern, Mode, Version
};
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number, QRData, QRDataModeInterface, QRMatrix};
use chillerlan\QRCode\Decoder\{Decoder, DecoderResult};
use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface};
use chillerlan\Settings\SettingsContainerInterface;
use function class_exists, class_implements, in_array, mb_convert_encoding, mb_internal_encoding;
/**
* Turns a text string into a Model 2 QR Code
*
* @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
* @see https://www.qrcode.com/en/codes/model12.html
* @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf
* @see https://en.wikipedia.org/wiki/QR_code
* @see https://www.thonky.com/qr-code-tutorial/
*/
class QRCode{
/**
* @deprecated 5.0.0 use Version::AUTO instead
* @see \chillerlan\QRCode\Common\Version::AUTO
* @var int
*/
public const VERSION_AUTO = Version::AUTO;
/**
* @deprecated 5.0.0 use MaskPattern::AUTO instead
* @see \chillerlan\QRCode\Common\MaskPattern::AUTO
* @var int
*/
public const MASK_PATTERN_AUTO = MaskPattern::AUTO;
/**
* @deprecated 5.0.0 use EccLevel::L instead
* @see \chillerlan\QRCode\Common\EccLevel::L
* @var int
*/
public const ECC_L = EccLevel::L;
/**
* @deprecated 5.0.0 use EccLevel::M instead
* @see \chillerlan\QRCode\Common\EccLevel::M
* @var int
*/
public const ECC_M = EccLevel::M;
/**
* @deprecated 5.0.0 use EccLevel::Q instead
* @see \chillerlan\QRCode\Common\EccLevel::Q
* @var int
*/
public const ECC_Q = EccLevel::Q;
/**
* @deprecated 5.0.0 use EccLevel::H instead
* @see \chillerlan\QRCode\Common\EccLevel::H
* @var int
*/
public const ECC_H = EccLevel::H;
/**
* @deprecated 5.0.0 use QROutputInterface::MARKUP_HTML instead
* @see \chillerlan\QRCode\Output\QROutputInterface::MARKUP_HTML
* @var string
*/
public const OUTPUT_MARKUP_HTML = QROutputInterface::MARKUP_HTML;
/**
* @deprecated 5.0.0 use QROutputInterface::MARKUP_SVG instead
* @see \chillerlan\QRCode\Output\QROutputInterface::MARKUP_SVG
* @var string
*/
public const OUTPUT_MARKUP_SVG = QROutputInterface::MARKUP_SVG;
/**
* @deprecated 5.0.0 use QROutputInterface::GDIMAGE_PNG instead
* @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_PNG
* @var string
*/
public const OUTPUT_IMAGE_PNG = QROutputInterface::GDIMAGE_PNG;
/**
* @deprecated 5.0.0 use QROutputInterface::GDIMAGE_JPG instead
* @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_JPG
* @var string
*/
public const OUTPUT_IMAGE_JPG = QROutputInterface::GDIMAGE_JPG;
/**
* @deprecated 5.0.0 use QROutputInterface::GDIMAGE_GIF instead
* @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_GIF
* @var string
*/
public const OUTPUT_IMAGE_GIF = QROutputInterface::GDIMAGE_GIF;
/**
* @deprecated 5.0.0 use QROutputInterface::STRING_JSON instead
* @see \chillerlan\QRCode\Output\QROutputInterface::STRING_JSON
* @var string
*/
public const OUTPUT_STRING_JSON = QROutputInterface::STRING_JSON;
/**
* @deprecated 5.0.0 use QROutputInterface::STRING_TEXT instead
* @see \chillerlan\QRCode\Output\QROutputInterface::STRING_TEXT
* @var string
*/
public const OUTPUT_STRING_TEXT = QROutputInterface::STRING_TEXT;
/**
* @deprecated 5.0.0 use QROutputInterface::IMAGICK instead
* @see \chillerlan\QRCode\Output\QROutputInterface::IMAGICK
* @var string
*/
public const OUTPUT_IMAGICK = QROutputInterface::IMAGICK;
/**
* @deprecated 5.0.0 use QROutputInterface::FPDF instead
* @see \chillerlan\QRCode\Output\QROutputInterface::FPDF
* @var string
*/
public const OUTPUT_FPDF = QROutputInterface::FPDF;
/**
* @deprecated 5.0.0 use QROutputInterface::EPS instead
* @see \chillerlan\QRCode\Output\QROutputInterface::EPS
* @var string
*/
public const OUTPUT_EPS = QROutputInterface::EPS;
/**
* @deprecated 5.0.0 use QROutputInterface::CUSTOM instead
* @see \chillerlan\QRCode\Output\QROutputInterface::CUSTOM
* @var string
*/
public const OUTPUT_CUSTOM = QROutputInterface::CUSTOM;
/**
* @deprecated 5.0.0 use QROutputInterface::MODES instead
* @see \chillerlan\QRCode\Output\QROutputInterface::MODES
* @var string[]
*/
public const OUTPUT_MODES = QROutputInterface::MODES;
/**
* The settings container
*
* @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
*/
protected SettingsContainerInterface $options;
/**
* A collection of one or more data segments of QRDataModeInterface instances to write
*
* @var \chillerlan\QRCode\Data\QRDataModeInterface[]
*/
protected array $dataSegments = [];
/**
* The luminance source for the reader
*/
protected string $luminanceSourceFQN = GDLuminanceSource::class;
/**
* QRCode constructor.
*
* PHP8: accept iterable
*/
public function __construct(SettingsContainerInterface $options = null){
$this->setOptions(($options ?? new QROptions));
}
/**
* Sets an options instance
*/
public function setOptions(SettingsContainerInterface $options):self{
$this->options = $options;
if($this->options->readerUseImagickIfAvailable){
$this->luminanceSourceFQN = IMagickLuminanceSource::class;
}
return $this;
}
/**
* Renders a QR Code for the given $data and QROptions, saves $file optionally
*
* Note: it is possible to add several data segments before calling this method with a valid $data string
* which will result in a mixed-mode QR Code with the given parameter as last element.
*
* @see https://github.com/chillerlan/php-qrcode/issues/246
*
* @return mixed
*/
public function render(string $data = null, string $file = null){
if($data !== null){
/** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
foreach(Mode::INTERFACES as $dataInterface){
if($dataInterface::validateString($data)){
$this->addSegment(new $dataInterface($data));
break;
}
}
}
return $this->renderMatrix($this->getQRMatrix(), $file);
}
/**
* Renders a QR Code for the given QRMatrix and QROptions, saves $file optionally
*
* @return mixed
*/
public function renderMatrix(QRMatrix $matrix, string $file = null){
return $this->initOutputInterface($matrix)->dump($file ?? $this->options->cachefile);
}
/**
* Returns a QRMatrix object for the given $data and current QROptions
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function getQRMatrix():QRMatrix{
$matrix = (new QRData($this->options, $this->dataSegments))->writeMatrix();
$maskPattern = $this->options->maskPattern === MaskPattern::AUTO
? MaskPattern::getBestPattern($matrix)
: new MaskPattern($this->options->maskPattern);
$matrix->setFormatInfo($maskPattern)->mask($maskPattern);
return $this->addMatrixModifications($matrix);
}
/**
* add matrix modifications after mask pattern evaluation and before handing over to output
*/
protected function addMatrixModifications(QRMatrix $matrix):QRMatrix{
if($this->options->addLogoSpace){
// check whether one of the dimensions was omitted
$logoSpaceWidth = ($this->options->logoSpaceWidth ?? $this->options->logoSpaceHeight ?? 0);
$logoSpaceHeight = ($this->options->logoSpaceHeight ?? $logoSpaceWidth);
$matrix->setLogoSpace(
$logoSpaceWidth,
$logoSpaceHeight,
$this->options->logoSpaceStartX,
$this->options->logoSpaceStartY
);
}
if($this->options->addQuietzone){
$matrix->setQuietZone($this->options->quietzoneSize);
}
return $matrix;
}
/**
* @deprecated 5.0.0 use QRCode::getQRMatrix() instead
* @see \chillerlan\QRCode\QRCode::getQRMatrix()
* @codeCoverageIgnore
*/
public function getMatrix():QRMatrix{
return $this->getQRMatrix();
}
/**
* initializes a fresh built-in or custom QROutputInterface
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function initOutputInterface(QRMatrix $matrix):QROutputInterface{
// @todo: remove custom invocation in v6
$outputInterface = (QROutputInterface::MODES[$this->options->outputType] ?? null);
if($this->options->outputType === QROutputInterface::CUSTOM){
$outputInterface = $this->options->outputInterface;
}
if(!$outputInterface || !class_exists($outputInterface)){
throw new QRCodeOutputException('invalid output module');
}
if(!in_array(QROutputInterface::class, class_implements($outputInterface))){
throw new QRCodeOutputException('output module does not implement QROutputInterface');
}
return new $outputInterface($this->options, $matrix);
}
/**
* checks if a string qualifies as numeric (convenience method)
*
* @deprecated 5.0.0 use Number::validateString() instead
* @see \chillerlan\QRCode\Data\Number::validateString()
* @codeCoverageIgnore
*/
public function isNumber(string $string):bool{
return Number::validateString($string);
}
/**
* checks if a string qualifies as alphanumeric (convenience method)
*
* @deprecated 5.0.0 use AlphaNum::validateString() instead
* @see \chillerlan\QRCode\Data\AlphaNum::validateString()
* @codeCoverageIgnore
*/
public function isAlphaNum(string $string):bool{
return AlphaNum::validateString($string);
}
/**
* checks if a string qualifies as Kanji (convenience method)
*
* @deprecated 5.0.0 use Kanji::validateString() instead
* @see \chillerlan\QRCode\Data\Kanji::validateString()
* @codeCoverageIgnore
*/
public function isKanji(string $string):bool{
return Kanji::validateString($string);
}
/**
* a dummy (convenience method)
*
* @deprecated 5.0.0 use Byte::validateString() instead
* @see \chillerlan\QRCode\Data\Byte::validateString()
* @codeCoverageIgnore
*/
public function isByte(string $string):bool{
return Byte::validateString($string);
}
/**
* Adds a data segment
*
* ISO/IEC 18004:2000 8.3.6 - Mixing modes
* ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length
*/
public function addSegment(QRDataModeInterface $segment):self{
$this->dataSegments[] = $segment;
return $this;
}
/**
* Clears the data segments array
*
* @codeCoverageIgnore
*/
public function clearSegments():self{
$this->dataSegments = [];
return $this;
}
/**
* Adds a numeric data segment
*
* ISO/IEC 18004:2000 8.3.2 - Numeric Mode
*/
public function addNumericSegment(string $data):self{
return $this->addSegment(new Number($data));
}
/**
* Adds an alphanumeric data segment
*
* ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode
*/
public function addAlphaNumSegment(string $data):self{
return $this->addSegment(new AlphaNum($data));
}
/**
* Adds a Kanji data segment (Japanese 13-bit double-byte characters, Shift-JIS)
*
* ISO/IEC 18004:2000 8.3.5 - Kanji Mode
*/
public function addKanjiSegment(string $data):self{
return $this->addSegment(new Kanji($data));
}
/**
* Adds a Hanzi data segment (simplified Chinese 13-bit double-byte characters, GB2312/GB18030)
*
* GBT18284-2000 Hanzi Mode
*/
public function addHanziSegment(string $data):self{
return $this->addSegment(new Hanzi($data));
}
/**
* Adds an 8-bit byte data segment
*
* ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode
*/
public function addByteSegment(string $data):self{
return $this->addSegment(new Byte($data));
}
/**
* Adds a standalone ECI designator
*
* The ECI designator must be followed by a Byte segment that contains the string encoded according to the given ECI charset
*
* ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode
*/
public function addEciDesignator(int $encoding):self{
return $this->addSegment(new ECI($encoding));
}
/**
* Adds an ECI data segment (including designator)
*
* The given string will be encoded from mb_internal_encoding() to the given ECI character set
*
* I hate this somehow, but I'll leave it for now
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function addEciSegment(int $encoding, string $data):self{
// validate the encoding id
$eciCharset = new ECICharset($encoding);
// get charset name
$eciCharsetName = $eciCharset->getName();
// convert the string to the given charset
if($eciCharsetName !== null){
$data = mb_convert_encoding($data, $eciCharsetName, mb_internal_encoding());
return $this
->addEciDesignator($eciCharset->getID())
->addByteSegment($data)
;
}
throw new QRCodeException('unable to add ECI segment');
}
/**
* Reads a QR Code from a given file
*
* @noinspection PhpUndefinedMethodInspection
*/
public function readFromFile(string $path):DecoderResult{
return $this->readFromSource($this->luminanceSourceFQN::fromFile($path, $this->options));
}
/**
* Reads a QR Code from the given data blob
*
* @noinspection PhpUndefinedMethodInspection
*/
public function readFromBlob(string $blob):DecoderResult{
return $this->readFromSource($this->luminanceSourceFQN::fromBlob($blob, $this->options));
}
/**
* Reads a QR Code from the given luminance source
*/
public function readFromSource(LuminanceSourceInterface $source):DecoderResult{
return (new Decoder)->decode($source);
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeException
*
* @created 27.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode;
use Exception;
/**
* An exception container
*/
class QRCodeException extends Exception{
}

View file

@ -0,0 +1,20 @@
<?php
/**
* Class QROptions
*
* @created 08.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode;
use chillerlan\Settings\SettingsContainerAbstract;
/**
* The QRCode settings container
*/
class QROptions extends SettingsContainerAbstract{
use QROptionsTrait;
}

View file

@ -0,0 +1,729 @@
<?php
/**
* Trait QROptionsTrait
*
* Note: the docblocks in this file are optimized for readability in PhpStorm ond on readthedocs.io
*
* @created 10.03.2018
* @author smiley <smiley@chillerlan.net>
* @copyright 2018 smiley
* @license MIT
*
* @noinspection PhpUnused, PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode;
use chillerlan\QRCode\Output\QROutputInterface;
use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
use function extension_loaded, in_array, max, min, strtolower;
use const JSON_THROW_ON_ERROR, PHP_EOL;
/**
* The QRCode plug-in settings & setter functionality
*/
trait QROptionsTrait{
/*
* QR Code specific settings
*/
/**
* QR Code version number
*
* `1 ... 40` or `Version::AUTO` (default)
*
* @see \chillerlan\QRCode\Common\Version
*/
protected int $version = Version::AUTO;
/**
* Minimum QR version
*
* if `QROptions::$version` is set to `Version::AUTO` (default: 1)
*/
protected int $versionMin = 1;
/**
* Maximum QR version
*
* if `QROptions::$version` is set to `Version::AUTO` (default: 40)
*/
protected int $versionMax = 40;
/**
* Error correct level
*
* `EccLevel::X` where `X` is:
*
* - `L` => 7% (default)
* - `M` => 15%
* - `Q` => 25%
* - `H` => 30%
*
* @todo: accept string values (PHP8+)
* @see \chillerlan\QRCode\Common\EccLevel
* @see https://github.com/chillerlan/php-qrcode/discussions/160
*/
protected int $eccLevel = EccLevel::L;
/**
* Mask Pattern to use (no value in using, mostly for unit testing purposes)
*
* `0 ... 7` or `MaskPattern::PATTERN_AUTO` (default)
*
* @see \chillerlan\QRCode\Common\MaskPattern
*/
protected int $maskPattern = MaskPattern::AUTO;
/**
* Add a "quiet zone" (margin) according to the QR code spec
*
* @see https://www.qrcode.com/en/howto/code.html
*/
protected bool $addQuietzone = true;
/**
* Size of the quiet zone
*
* internally clamped to `0 ... $moduleCount / 2` (default: 4)
*/
protected int $quietzoneSize = 4;
/*
* General output settings
*/
/**
* The built-in output type
*
* - `QROutputInterface::MARKUP_SVG` (default)
* - `QROutputInterface::MARKUP_HTML`
* - `QROutputInterface::GDIMAGE_BMP`
* - `QROutputInterface::GDIMAGE_GIF`
* - `QROutputInterface::GDIMAGE_JPG`
* - `QROutputInterface::GDIMAGE_PNG`
* - `QROutputInterface::GDIMAGE_WEBP`
* - `QROutputInterface::STRING_TEXT`
* - `QROutputInterface::STRING_JSON`
* - `QROutputInterface::IMAGICK`
* - `QROutputInterface::EPS`
* - `QROutputInterface::FPDF`
* - `QROutputInterface::CUSTOM`
*
* @see \chillerlan\QRCode\Output\QREps
* @see \chillerlan\QRCode\Output\QRFpdf
* @see \chillerlan\QRCode\Output\QRGdImage
* @see \chillerlan\QRCode\Output\QRImagick
* @see \chillerlan\QRCode\Output\QRMarkupHTML
* @see \chillerlan\QRCode\Output\QRMarkupSVG
* @see \chillerlan\QRCode\Output\QRString
* @see https://github.com/chillerlan/php-qrcode/issues/223
*
* @deprecated 5.0.0 see issue #223
*/
protected string $outputType = QROutputInterface::MARKUP_SVG;
/**
* The FQCN of the custom `QROutputInterface`
*
* if `QROptions::$outputType` is set to `QROutputInterface::CUSTOM` (default: `null`)
*
* @deprecated 5.0.0 the nullable type will be removed in future versions
* and the default value will be set to `QRMarkupSVG::class`
*/
protected ?string $outputInterface = null;
/**
* Return the image resource instead of a render if applicable.
*
* - `QRGdImage`: `resource` (PHP < 8), `GdImage`
* - `QRImagick`: `Imagick`
* - `QRFpdf`: `FPDF`
*
* This option overrides/ignores other output settings, such as `QROptions::$cachefile`
* and `QROptions::$outputBase64`. (default: `false`)
*
* @see \chillerlan\QRCode\Output\QROutputInterface::dump()
*/
protected bool $returnResource = false;
/**
* Optional cache file path `/path/to/cache.file`
*
* Please note that the `$file` parameter in `QRCode::render()` and `QRCode::renderMatrix()`
* takes precedence over the `QROptions::$cachefile` value. (default: `null`)
*
* @see \chillerlan\QRCode\QRCode::render()
* @see \chillerlan\QRCode\QRCode::renderMatrix()
*/
protected ?string $cachefile = null;
/**
* Toggle base64 data URI or raw data output (if applicable)
*
* (default: `true`)
*
* @see \chillerlan\QRCode\Output\QROutputAbstract::toBase64DataURI()
*/
protected bool $outputBase64 = true;
/**
* Newline string
*
* (default: `PHP_EOL`)
*/
protected string $eol = PHP_EOL;
/*
* Common visual modifications
*/
/**
* Sets the image background color (if applicable)
*
* - `QRImagick`: defaults to `"white"`
* - `QRGdImage`: defaults to `[255, 255, 255]`
* - `QRFpdf`: defaults to blank internally (white page)
*
* @var mixed|null
*/
protected $bgColor = null;
/**
* Whether to invert the matrix (reflectance reversal)
*
* (default: `false`)
*
* @see \chillerlan\QRCode\Data\QRMatrix::invert()
*/
protected bool $invertMatrix = false;
/**
* Whether to draw the light (false) modules
*
* (default: `true`)
*/
protected bool $drawLightModules = true;
/**
* Specify whether to draw the modules as filled circles
*
* a note for `GdImage` output:
*
* if `QROptions::$scale` is less than 20, the image will be upscaled internally, then the modules will be drawn
* using `imagefilledellipse()` and then scaled back to the expected size
*
* No effect in: `QREps`, `QRFpdf`, `QRMarkupHTML`
*
* @see \imagefilledellipse()
* @see https://github.com/chillerlan/php-qrcode/issues/23
* @see https://github.com/chillerlan/php-qrcode/discussions/122
*/
protected bool $drawCircularModules = false;
/**
* Specifies the radius of the modules when `QROptions::$drawCircularModules` is set to `true`
*
* (default: 0.45)
*/
protected float $circleRadius = 0.45;
/**
* Specifies which module types to exclude when `QROptions::$drawCircularModules` is set to `true`
*
* (default: `[]`)
*/
protected array $keepAsSquare = [];
/**
* Whether to connect the paths for the several module types to avoid weird glitches when using gradients etc.
*
* This option is exclusive to output classes that use the module collector `QROutputAbstract::collectModules()`,
* which converts the `$M_TYPE` of all modules to `QRMatrix::M_DATA` and `QRMatrix::M_DATA_DARK` respectively.
*
* Module types that should not be added to the connected path can be excluded via `QROptions::$excludeFromConnect`.
*
* Currentty used in `QREps` and `QRMarkupSVG`.
*
* @see \chillerlan\QRCode\Output\QROutputAbstract::collectModules()
* @see \chillerlan\QRCode\QROptionsTrait::$excludeFromConnect
* @see https://github.com/chillerlan/php-qrcode/issues/57
*/
protected bool $connectPaths = false;
/**
* Specify which paths/patterns to exclude from connecting if `QROptions::$connectPaths` is set to `true`
*
* @see \chillerlan\QRCode\QROptionsTrait::$connectPaths
*/
protected array $excludeFromConnect = [];
/**
* Module values map
*
* - `QRImagick`, `QRMarkupHTML`, `QRMarkupSVG`: #ABCDEF, cssname, rgb(), rgba()...
* - `QREps`, `QRFpdf`, `QRGdImage`: `[R, G, B]` // 0-255
* - `QREps`: `[C, M, Y, K]` // 0-255
*
* @see \chillerlan\QRCode\Output\QROutputAbstract::setModuleValues()
*/
protected array $moduleValues = [];
/**
* Toggles logo space creation
*
* @see \chillerlan\QRCode\QRCode::addMatrixModifications()
* @see \chillerlan\QRCode\Data\QRMatrix::setLogoSpace()
*/
protected bool $addLogoSpace = false;
/**
* Width of the logo space
*
* if only `QROptions::$logoSpaceWidth` is given, the logo space is assumed a square of that size
*/
protected ?int $logoSpaceWidth = null;
/**
* Height of the logo space
*
* if only `QROptions::$logoSpaceHeight` is given, the logo space is assumed a square of that size
*/
protected ?int $logoSpaceHeight = null;
/**
* Optional horizontal start position of the logo space (top left corner)
*/
protected ?int $logoSpaceStartX = null;
/**
* Optional vertical start position of the logo space (top left corner)
*/
protected ?int $logoSpaceStartY = null;
/*
* Common raster image settings (QRGdImage, QRImagick)
*/
/**
* Pixel size of a QR code module
*/
protected int $scale = 5;
/**
* Toggle transparency
*
* - `QRGdImage` and `QRImagick`: the given `QROptions::$transparencyColor` is set as transparent
*
* @see https://github.com/chillerlan/php-qrcode/discussions/121
*/
protected bool $imageTransparent = false;
/**
* Sets a transparency color for when `QROptions::$imageTransparent` is set to `true`.
*
* Defaults to `QROptions::$bgColor`.
*
* - `QRGdImage`: `[R, G, B]`, this color is set as transparent in `imagecolortransparent()`
* - `QRImagick`: `"color_str"`, this color is set in `Imagick::transparentPaintImage()`
*
* @see \imagecolortransparent()
* @see \Imagick::transparentPaintImage()
*
* @var mixed|null
*/
protected $transparencyColor = null;
/**
* Compression quality
*
* The given value depends on the used output type:
*
* - `QRGdImageBMP`: `[0...1]`
* - `QRGdImageJPEG`: `[0...100]`
* - `QRGdImageWEBP`: `[0...9]`
* - `QRGdImagePNG`: `[0...100]`
* - `QRImagick`: `[0...100]`
*
* @see \imagebmp()
* @see \imagejpeg()
* @see \imagepng()
* @see \imagewebp()
* @see \Imagick::setImageCompressionQuality()
*/
protected int $quality = -1;
/*
* QRGdImage settings
*/
/**
* Toggles the usage of internal upscaling when `QROptions::$drawCircularModules` is set to `true` and
* `QROptions::$scale` is less than 20
*
* @see \chillerlan\QRCode\Output\QRGdImage::createImage()
* @see https://github.com/chillerlan/php-qrcode/issues/23
*/
protected bool $gdImageUseUpscale = true;
/*
* QRImagick settings
*/
/**
* Imagick output format
*
* @see \Imagick::setImageFormat()
* @see https://www.imagemagick.org/script/formats.php
*/
protected string $imagickFormat = 'png32';
/*
* Common markup output settings (QRMarkupSVG, QRMarkupHTML)
*/
/**
* A common css class
*/
protected string $cssClass = 'qrcode';
/*
* QRMarkupSVG settings
*/
/**
* Whether to add an XML header line or not, e.g. to embed the SVG directly in HTML
*
* `<?xml version="1.0" encoding="UTF-8"?>`
*/
protected bool $svgAddXmlHeader = true;
/**
* Anything in the SVG `<defs>` tag
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
*/
protected string $svgDefs = '';
/**
* Sets the value for the "preserveAspectRatio" on the `<svg>` element
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
*/
protected string $svgPreserveAspectRatio = 'xMidYMid';
/**
* Whether to use the SVG `fill` attributes
*
* If set to `true` (default), the `fill` attribute will be set with the module value for the `<path>` element's `$M_TYPE`.
* When set to `false`, the module values map will be ignored and the QR Code may be styled via CSS.
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
*/
protected bool $svgUseFillAttributes = true;
/*
* QRStringText settings
*/
/**
* An optional line prefix, e.g. empty space to align the QR Code in a console
*/
protected string $textLineStart = '';
/*
* QRStringJSON settings
*/
/**
* Sets the flags to use for the `json_encode()` call
*
* @see https://www.php.net/manual/json.constants.php
*/
protected int $jsonFlags = JSON_THROW_ON_ERROR;
/**
* Whether to return matrix values in JSON as booleans or `$M_TYPE` integers
*/
protected bool $jsonAsBooleans = false;
/*
* QRFpdf settings
*/
/**
* Measurement unit for `FPDF` output: `pt`, `mm`, `cm`, `in` (default: `pt`)
*
* @see FPDF::__construct()
*/
protected string $fpdfMeasureUnit = 'pt';
/*
* QR Code reader settings
*/
/**
* Use Imagick (if available) when reading QR Codes
*/
protected bool $readerUseImagickIfAvailable = false;
/**
* Grayscale the image before reading
*/
protected bool $readerGrayscale = false;
/**
* Invert the colors of the image
*/
protected bool $readerInvertColors = false;
/**
* Increase the contrast before reading
*
* note that applying contrast works different in GD and Imagick, so mileage may vary
*/
protected bool $readerIncreaseContrast = false;
/**
* clamp min/max version number
*/
protected function setMinMaxVersion(int $versionMin, int $versionMax):void{
$min = max(1, min(40, $versionMin));
$max = max(1, min(40, $versionMax));
$this->versionMin = min($min, $max);
$this->versionMax = max($min, $max);
}
/**
* sets the minimum version number
*/
protected function set_versionMin(int $version):void{
$this->setMinMaxVersion($version, $this->versionMax);
}
/**
* sets the maximum version number
*/
protected function set_versionMax(int $version):void{
$this->setMinMaxVersion($this->versionMin, $version);
}
/**
* sets/clamps the version number
*/
protected function set_version(int $version):void{
$this->version = ($version !== Version::AUTO) ? max(1, min(40, $version)) : Version::AUTO;
}
/**
* sets/clamps the quiet zone size
*/
protected function set_quietzoneSize(int $quietzoneSize):void{
$this->quietzoneSize = max(0, min($quietzoneSize, 75));
}
/**
* sets the FPDF measurement unit
*
* @codeCoverageIgnore
*/
protected function set_fpdfMeasureUnit(string $unit):void{
$unit = strtolower($unit);
if(in_array($unit, ['cm', 'in', 'mm', 'pt'], true)){
$this->fpdfMeasureUnit = $unit;
}
// @todo throw or ignore silently?
}
/**
* enables Imagick for the QR Code reader if the extension is available
*/
protected function set_readerUseImagickIfAvailable(bool $useImagickIfAvailable):void{
$this->readerUseImagickIfAvailable = ($useImagickIfAvailable && extension_loaded('imagick'));
}
/**
* clamp the logo space values between 0 and maximum length (177 modules at version 40)
*/
protected function clampLogoSpaceValue(?int $value):?int{
if($value === null){
return null;
}
return (int)max(0, min(177, $value));
}
/**
* clamp/set logo space width
*/
protected function set_logoSpaceWidth(?int $value):void{
$this->logoSpaceWidth = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set logo space height
*/
protected function set_logoSpaceHeight(?int $value):void{
$this->logoSpaceHeight = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set horizontal logo space start
*/
protected function set_logoSpaceStartX(?int $value):void{
$this->logoSpaceStartX = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set vertical logo space start
*/
protected function set_logoSpaceStartY(?int $value):void{
$this->logoSpaceStartY = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set SVG circle radius
*/
protected function set_circleRadius(float $circleRadius):void{
$this->circleRadius = max(0.1, min(0.75, $circleRadius));
}
/*
* redirect calls of deprecated variables to new/renamed property
*/
/**
* @deprecated 5.0.0 use QROptions::$outputBase64 instead
* @see \chillerlan\QRCode\QROptions::$outputBase64
*/
protected bool $imageBase64;
/**
* redirect call to the new variable
*
* @deprecated 5.0.0 use QROptions::$outputBase64 instead
* @see \chillerlan\QRCode\QROptions::$outputBase64
* @codeCoverageIgnore
*/
protected function set_imageBase64(bool $imageBase64):void{
$this->outputBase64 = $imageBase64;
}
/**
* redirect call to the new variable
*
* @deprecated 5.0.0 use QROptions::$outputBase64 instead
* @see \chillerlan\QRCode\QROptions::$outputBase64
* @codeCoverageIgnore
*/
protected function get_imageBase64():bool{
return $this->outputBase64;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
*/
protected int $jpegQuality;
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function set_jpegQuality(int $jpegQuality):void{
$this->quality = $jpegQuality;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function get_jpegQuality():int{
return $this->quality;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
*/
protected int $pngCompression;
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function set_pngCompression(int $pngCompression):void{
$this->quality = $pngCompression;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function get_pngCompression():int{
return $this->quality;
}
/**
* @deprecated 5.0.0 use QROptions::$transparencyColor instead
* @see \chillerlan\QRCode\QROptions::$transparencyColor
*/
protected array $imageTransparencyBG;
/**
* @deprecated 5.0.0 use QROptions::$transparencyColor instead
* @see \chillerlan\QRCode\QROptions::$transparencyColor
* @codeCoverageIgnore
*/
protected function set_imageTransparencyBG(?array $imageTransparencyBG):void{
$this->transparencyColor = $imageTransparencyBG;
}
/**
* @deprecated 5.0.0 use QROptions::$transparencyColor instead
* @see \chillerlan\QRCode\QROptions::$transparencyColor
* @codeCoverageIgnore
*/
protected function get_imageTransparencyBG():?array{
return $this->transparencyColor;
}
/**
* @deprecated 5.0.0 use QROptions::$bgColor instead
* @see \chillerlan\QRCode\QROptions::$bgColor
*/
protected string $imagickBG;
/**
* @deprecated 5.0.0 use QROptions::$bgColor instead
* @see \chillerlan\QRCode\QROptions::$bgColor
* @codeCoverageIgnore
*/
protected function set_imagickBG(?string $imagickBG):void{
$this->bgColor = $imagickBG;
}
/**
* @deprecated 5.0.0 use QROptions::$bgColor instead
* @see \chillerlan\QRCode\QROptions::$bgColor
* @codeCoverageIgnore
*/
protected function get_imagickBG():?string{
return $this->bgColor;
}
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Smiley <smiley@chillerlan.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,167 @@
# chillerlan/php-settings-container
A container class for settings objects - decouple configuration logic from your application! Not a DI container.
- [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerInterface.php) provides immutable properties with magic getter & setter and some fancy.
[![PHP Version Support][php-badge]][php]
[![version][packagist-badge]][packagist]
[![license][license-badge]][license]
[![Continuous Integration][gh-action-badge]][gh-action]
[![Coverage][coverage-badge]][coverage]
[![Codacy][codacy-badge]][codacy]
[![Packagist downloads][downloads-badge]][downloads]
[php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-settings-container?logo=php&color=8892BF
[php]: https://www.php.net/supported-versions.php
[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-settings-container.svg?logo=packagist
[packagist]: https://packagist.org/packages/chillerlan/php-settings-container
[license-badge]: https://img.shields.io/github/license/chillerlan/php-settings-container.svg
[license]: https://github.com/chillerlan/php-settings-container/blob/main/LICENSE
[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-settings-container.svg?logo=codecov
[coverage]: https://codecov.io/github/chillerlan/php-settings-container
[codacy-badge]: https://img.shields.io/codacy/grade/bd2467799e2943d2853ce3ebad5af490/main?logo=codacy
[codacy]: https://www.codacy.com/gh/chillerlan/php-settings-container/dashboard?branch=main
[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-settings-container.svg?logo=packagist
[downloads]: https://packagist.org/packages/chillerlan/php-settings-container/stats
[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-settings-container/ci.yml?branch=main&logo=github
[gh-action]: https://github.com/chillerlan/php-settings-container/actions/workflows/ci.yml?query=branch%3Amain
## Documentation
### Installation
**requires [composer](https://getcomposer.org)**
*composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^3.0` - see [releases](https://github.com/chillerlan/php-settings-container/releases) for valid versions)
```json
{
"require": {
"php": "^8.1",
"chillerlan/php-settings-container": "dev-main"
}
}
```
Profit!
## Usage
The `SettingsContainerInterface` (wrapped in`SettingsContainerAbstract`) provides plug-in functionality for immutable object properties and adds some fancy, like loading/saving JSON, arrays etc.
It takes an `iterable` as the only constructor argument and calls a method with the trait's name on invocation (`MyTrait::MyTrait()`) for each used trait.
A PHPStan ruleset to exclude errors generated by accessing magic properties on `SettingsContainerInterface` can be found in `rules-magic-access.neon`.
### Simple usage
```php
class MyContainer extends SettingsContainerAbstract{
protected string $foo;
protected string $bar;
}
```
```php
// use it just like a \stdClass (except the properties are fixed)
$container = new MyContainer;
$container->foo = 'what';
$container->bar = 'foo';
// which is equivalent to
$container = new MyContainer(['bar' => 'foo', 'foo' => 'what']);
// ...or try
$container->fromJSON('{"foo": "what", "bar": "foo"}');
// fetch all properties as array
$container->toArray(); // -> ['foo' => 'what', 'bar' => 'foo']
// or JSON
$container->toJSON(); // -> {"foo": "what", "bar": "foo"}
// JSON via JsonSerializable
$json = json_encode($container); // -> {"foo": "what", "bar": "foo"}
//non-existing properties will be ignored:
$container->nope = 'what';
var_dump($container->nope); // -> null
```
### Advanced usage
```php
// from library 1
trait SomeOptions{
protected string $foo;
protected string $what;
// this method will be called in SettingsContainerAbstract::construct()
// after the properties have been set
protected function SomeOptions():void{
// just some constructor stuff...
$this->foo = strtoupper($this->foo);
}
/*
* special prefixed magic setters & getters
*/
// this method will be called from __set() when property $what is set
protected function set_what(string $value):void{
$this->what = md5($value);
}
// this method is called on __get() for the property $what
protected function get_what():string{
return 'hash: '.$this->what;
}
}
// from library 2
trait MoreOptions{
protected string $bar = 'whatever'; // provide default values
}
```
```php
$commonOptions = [
// SomeOptions
'foo' => 'whatever',
// MoreOptions
'bar' => 'nothing',
];
// now plug the several library options together to a single object
$container = new class ($commonOptions) extends SettingsContainerAbstract{
use SomeOptions, MoreOptions;
};
var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value)
var_dump($container->bar); // -> nothing
$container->what = 'some value';
var_dump($container->what); // -> hash: 5946210c9e93ae37891dfe96c3e39614 (custom getter added "hash: ")
```
### API
#### [`SettingsContainerAbstract`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerAbstract.php)
| method | return | info |
|--------------------------------------------|------------------------------|---------------------------------------------------------------------------------------------------------------------|
| `__construct(iterable $properties = null)` | - | calls `construct()` internally after the properties have been set |
| (protected) `construct()` | void | calls a method with trait name as replacement constructor for each used trait |
| `__get(string $property)` | mixed | calls `$this->{'get_'.$property}()` if such a method exists |
| `__set(string $property, $value)` | void | calls `$this->{'set_'.$property}($value)` if such a method exists |
| `__isset(string $property)` | bool | |
| `__unset(string $property)` | void | |
| `__toString()` | string | a JSON string |
| `toArray()` | array | |
| `fromIterable(iterable $properties)` | `SettingsContainerInterface` | |
| `toJSON(int $jsonOptions = null)` | string | accepts [JSON options constants](http://php.net/manual/json.constants.php) |
| `fromJSON(string $json)` | `SettingsContainerInterface` | |
| `jsonSerialize()` | mixed | implements the [`JsonSerializable`](https://www.php.net/manual/en/jsonserializable.jsonserialize.php) interface |
| `serialize()` | string | implements the [`Serializable`](https://www.php.net/manual/en/serializable.serialize.php) interface |
| `unserialize(string $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/serializable.unserialize.php) interface |
| `__serialize()` | array | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.serialize) interface |
| `__unserialize(array $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize) interface |
## Disclaimer
This might be either an utterly genius or completely stupid idea - you decide. However, i like it and it works.
Also, this is not a dependency injection container. Stop using DI containers FFS.

View file

@ -0,0 +1,52 @@
{
"name": "chillerlan/php-settings-container",
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"license": "MIT",
"type": "library",
"minimum-stability": "stable",
"keywords": [
"helper", "container", "settings", "configuration"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"require": {
"php": "^8.1",
"ext-json": "*"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\SettingsTest\\": "tests"
}
},
"scripts": {
"phpunit": "@php vendor/bin/phpunit",
"phpstan": "@php vendor/bin/phpstan"
},
"config": {
"lock": false,
"sort-packages": true,
"platform-check": true
}
}

View file

@ -0,0 +1,4 @@
parameters:
ignoreErrors:
# yes, these are magic
- message: "#^Access to an undefined property chillerlan\\\\Settings\\\\SettingsContainerInterface\\:\\:\\$[\\w]+\\.$#"

View file

@ -0,0 +1,252 @@
<?php
/**
* Class SettingsContainerAbstract
*
* @created 28.08.2018
* @author Smiley <smiley@chillerlan.net>
* @copyright 2018 Smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Settings;
use InvalidArgumentException, JsonException, ReflectionClass, ReflectionProperty;
use function array_keys, get_object_vars, is_object, json_decode, json_encode,
json_last_error_msg, method_exists, property_exists, serialize, unserialize;
use const JSON_THROW_ON_ERROR;
abstract class SettingsContainerAbstract implements SettingsContainerInterface{
/**
* SettingsContainerAbstract constructor.
*
* @phpstan-param array<string, mixed> $properties
*/
public function __construct(iterable|null $properties = null){
if(!empty($properties)){
$this->fromIterable($properties);
}
$this->construct();
}
/**
* calls a method with trait name as replacement constructor for each used trait
* (remember pre-php5 classname constructors? yeah, basically this.)
*/
protected function construct():void{
$traits = (new ReflectionClass($this))->getTraits();
foreach($traits as $trait){
$method = $trait->getShortName();
if(method_exists($this, $method)){
$this->{$method}();
}
}
}
/**
* @inheritdoc
*/
public function __get(string $property):mixed{
if(!property_exists($this, $property) || $this->isPrivate($property)){
return null;
}
$method = 'get_'.$property;
if(method_exists($this, $method)){
return $this->{$method}();
}
return $this->{$property};
}
/**
* @inheritdoc
*/
public function __set(string $property, mixed $value):void{
if(!property_exists($this, $property) || $this->isPrivate($property)){
return;
}
$method = 'set_'.$property;
if(method_exists($this, $method)){
$this->{$method}($value);
return;
}
$this->{$property} = $value;
}
/**
* @inheritdoc
*/
public function __isset(string $property):bool{
return isset($this->{$property}) && !$this->isPrivate($property);
}
/**
* @internal Checks if a property is private
*/
protected function isPrivate(string $property):bool{
return (new ReflectionProperty($this, $property))->isPrivate();
}
/**
* @inheritdoc
*/
public function __unset(string $property):void{
if($this->__isset($property)){
unset($this->{$property});
}
}
/**
* @inheritdoc
*/
public function __toString():string{
return $this->toJSON();
}
/**
* @inheritdoc
*/
public function toArray():array{
$properties = [];
foreach(array_keys(get_object_vars($this)) as $key){
$properties[$key] = $this->__get($key);
}
return $properties;
}
/**
* @inheritdoc
*/
public function fromIterable(iterable $properties):static{
foreach($properties as $key => $value){
$this->__set($key, $value);
}
return $this;
}
/**
* @inheritdoc
*/
public function toJSON(int|null $jsonOptions = null):string{
$json = json_encode($this, ($jsonOptions ?? 0));
if($json === false){
throw new JsonException(json_last_error_msg());
}
return $json;
}
/**
* @inheritdoc
*/
public function fromJSON(string $json):static{
/** @phpstan-var array<string, mixed> $data */
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
return $this->fromIterable($data);
}
/**
* @inheritdoc
* @return array<string, mixed>
*/
public function jsonSerialize():array{
return $this->toArray();
}
/**
* Returns a serialized string representation of the object in its current state (except static/readonly properties)
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*/
public function serialize():string{
return serialize($this);
}
/**
* Restores the data (except static/readonly properties) from the given serialized object to the current instance
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*/
public function unserialize(string $data):void{
$obj = unserialize($data);
if($obj === false || !is_object($obj)){
throw new InvalidArgumentException('The given serialized string is invalid');
}
$reflection = new ReflectionClass($obj);
if(!$reflection->isInstance($this)){
throw new InvalidArgumentException('The unserialized object does not match the class of this container');
}
$properties = $reflection->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY));
foreach($properties as $reflectionProperty){
$this->{$reflectionProperty->name} = $reflectionProperty->getValue($obj);
}
}
/**
* Returns a serialized string representation of the object in its current state (except static/readonly properties)
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*/
public function __serialize():array{
$properties = (new ReflectionClass($this))
->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY))
;
$data = [];
foreach($properties as $reflectionProperty){
$data[$reflectionProperty->name] = $reflectionProperty->getValue($this);
}
return $data;
}
/**
* Restores the data from the given array to the current instance
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*
* @param array<string, mixed> $data
*/
public function __unserialize(array $data):void{
foreach($data as $key => $value){
$this->{$key} = $value;
}
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* Interface SettingsContainerInterface
*
* @created 28.08.2018
* @author Smiley <smiley@chillerlan.net>
* @copyright 2018 Smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Settings;
use JsonSerializable, Serializable;
/**
* a generic container with magic getter and setter
*/
interface SettingsContainerInterface extends JsonSerializable, Serializable{
/**
* Retrieve the value of $property
*
* @return mixed|null
*/
public function __get(string $property):mixed;
/**
* Set $property to $value while avoiding private and non-existing properties
*/
public function __set(string $property, mixed $value):void;
/**
* Checks if $property is set (aka. not null), excluding private properties
*/
public function __isset(string $property):bool;
/**
* Unsets $property while avoiding private and non-existing properties
*/
public function __unset(string $property):void;
/**
* @see \chillerlan\Settings\SettingsContainerInterface::toJSON()
*/
public function __toString():string;
/**
* Returns an array representation of the settings object
*
* The values will be run through the magic __get(), which may also call custom getters.
*
* @return array<string, mixed>
*/
public function toArray():array;
/**
* Sets properties from a given iterable
*
* The values will be run through the magic __set(), which may also call custom setters.
*
* @phpstan-param array<string, mixed> $properties
*/
public function fromIterable(iterable $properties):static;
/**
* Returns a JSON representation of the settings object
*
* @see \json_encode()
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*
* @throws \JsonException
*/
public function toJSON(int|null $jsonOptions = null):string;
/**
* Sets properties from a given JSON string
*
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*
* @throws \Exception
* @throws \JsonException
*/
public function fromJSON(string $json):static;
}

579
vendor/composer/ClassLoader.php vendored Normal file
View file

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

359
vendor/composer/InstalledVersions.php vendored Normal file
View file

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
vendor/composer/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

10
vendor/composer/autoload_classmap.php vendored Normal file
View file

@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View file

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

12
vendor/composer/autoload_psr4.php vendored Normal file
View file

@ -0,0 +1,12 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'chillerlan\\Settings\\' => array($vendorDir . '/chillerlan/php-settings-container/src'),
'chillerlan\\QRCode\\' => array($vendorDir . '/chillerlan/php-qrcode/src'),
'Norgul\\Xmpp\\' => array($vendorDir . '/norgul/xmpp-php/src'),
);

38
vendor/composer/autoload_real.php vendored Normal file
View file

@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit039ef714d06adc1d2f797455add3d2e5
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit039ef714d06adc1d2f797455add3d2e5', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit039ef714d06adc1d2f797455add3d2e5', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit039ef714d06adc1d2f797455add3d2e5::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

49
vendor/composer/autoload_static.php vendored Normal file
View file

@ -0,0 +1,49 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit039ef714d06adc1d2f797455add3d2e5
{
public static $prefixLengthsPsr4 = array (
'c' =>
array (
'chillerlan\\Settings\\' => 20,
'chillerlan\\QRCode\\' => 18,
),
'N' =>
array (
'Norgul\\Xmpp\\' => 12,
),
);
public static $prefixDirsPsr4 = array (
'chillerlan\\Settings\\' =>
array (
0 => __DIR__ . '/..' . '/chillerlan/php-settings-container/src',
),
'chillerlan\\QRCode\\' =>
array (
0 => __DIR__ . '/..' . '/chillerlan/php-qrcode/src',
),
'Norgul\\Xmpp\\' =>
array (
0 => __DIR__ . '/..' . '/norgul/xmpp-php/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit039ef714d06adc1d2f797455add3d2e5::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit039ef714d06adc1d2f797455add3d2e5::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit039ef714d06adc1d2f797455add3d2e5::$classMap;
}, null, ClassLoader::class);
}
}

272
vendor/composer/installed.json vendored Normal file
View file

@ -0,0 +1,272 @@
{
"packages": [
{
"name": "chillerlan/php-qrcode",
"version": "5.0.2",
"version_normalized": "5.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-qrcode.git",
"reference": "da5bdb82c8755f54de112b271b402aaa8df53269"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/da5bdb82c8755f54de112b271b402aaa8df53269",
"reference": "da5bdb82c8755f54de112b271b402aaa8df53269",
"shasum": ""
},
"require": {
"chillerlan/php-settings-container": "^2.1.4 || ^3.1",
"ext-mbstring": "*",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"chillerlan/php-authenticator": "^4.1 || ^5.1",
"phan/phan": "^5.4",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"setasign/fpdf": "^1.8.2",
"squizlabs/php_codesniffer": "^3.8"
},
"suggest": {
"chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
"setasign/fpdf": "Required to use the QR FPDF output.",
"simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code"
},
"time": "2024-02-27T14:37:26+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT",
"Apache-2.0"
],
"authors": [
{
"name": "Kazuhiko Arase",
"homepage": "https://github.com/kazuhikoarase/qrcode-generator"
},
{
"name": "ZXing Authors",
"homepage": "https://github.com/zxing/zxing"
},
{
"name": "Ashot Khanamiryan",
"homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder"
},
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors"
}
],
"description": "A QR code generator and reader with a user friendly API. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-qrcode",
"keywords": [
"phpqrcode",
"qr",
"qr code",
"qr-reader",
"qrcode",
"qrcode-generator",
"qrcode-reader"
],
"support": {
"docs": "https://php-qrcode.readthedocs.io",
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode"
},
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
"type": "custom"
},
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"install-path": "../chillerlan/php-qrcode"
},
{
"name": "chillerlan/php-settings-container",
"version": "3.2.1",
"version_normalized": "3.2.1.0",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.1"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"time": "2024-07-16T11:13:48+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"keywords": [
"Settings",
"configuration",
"container",
"helper"
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
"type": "custom"
},
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"install-path": "../chillerlan/php-settings-container"
},
{
"name": "monero-integrations/monerophp",
"version": "dev-Dev",
"version_normalized": "dev-Dev",
"source": {
"type": "git",
"url": "https://github.com/monero-integrations/monerophp.git",
"reference": "c45af18802312f6ea251c9adff642baa7a6ce965"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/monero-integrations/monerophp/zipball/c45af18802312f6ea251c9adff642baa7a6ce965",
"reference": "c45af18802312f6ea251c9adff642baa7a6ce965",
"shasum": ""
},
"require": {
"php": ">=5.6.0"
},
"time": "2018-02-14T02:49:13+00:00",
"type": "library",
"installation-source": "dist",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "SerHack",
"email": "support@monerointegrations.com"
}
],
"description": "A PHP library for the Monero Wallet RPC JSON-RPC interface.",
"homepage": "https://github.com/monero-integrations/monerophp",
"keywords": [
"Monero",
"XMR",
"cryptocurrency",
"json-rpc"
],
"support": {
"issues": "https://github.com/monero-integrations/monerophp/issues",
"source": "https://github.com/monero-integrations/monerophp/tree/Dev"
},
"install-path": "../monero-integrations/monerophp"
},
{
"name": "norgul/xmpp-php",
"version": "v2.2.3",
"version_normalized": "2.2.3.0",
"source": {
"type": "git",
"url": "https://github.com/Norgul/xmpp-php.git",
"reference": "f9d24ca167599e9040f5cc3f500f5ec90d28faf3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Norgul/xmpp-php/zipball/f9d24ca167599e9040f5cc3f500f5ec90d28faf3",
"reference": "f9d24ca167599e9040f5cc3f500f5ec90d28faf3",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.0.x-dev",
"phpmd/phpmd": "2.6.0",
"phpunit/phpunit": "6.*",
"squizlabs/php_codesniffer": "3.4.2"
},
"time": "2019-10-14T18:36:17+00:00",
"type": "project",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Norgul\\Xmpp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marko Dupor",
"email": "marko.dupor@gmail.com"
}
],
"description": "PHP library for XMPP.",
"keywords": [
"jabber",
"library",
"php",
"xmpp"
],
"support": {
"issues": "https://github.com/Norgul/xmpp-php/issues",
"source": "https://github.com/Norgul/xmpp-php/tree/v2.2.3"
},
"install-path": "../norgul/xmpp-php"
}
],
"dev": true,
"dev-package-names": []
}

59
vendor/composer/installed.php vendored Normal file
View file

@ -0,0 +1,59 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'chillerlan/php-qrcode' => array(
'pretty_version' => '5.0.2',
'version' => '5.0.2.0',
'reference' => 'da5bdb82c8755f54de112b271b402aaa8df53269',
'type' => 'library',
'install_path' => __DIR__ . '/../chillerlan/php-qrcode',
'aliases' => array(),
'dev_requirement' => false,
),
'chillerlan/php-settings-container' => array(
'pretty_version' => '3.2.1',
'version' => '3.2.1.0',
'reference' => '95ed3e9676a1d47cab2e3174d19b43f5dbf52681',
'type' => 'library',
'install_path' => __DIR__ . '/../chillerlan/php-settings-container',
'aliases' => array(),
'dev_requirement' => false,
),
'monero-integrations/monerophp' => array(
'pretty_version' => 'dev-Dev',
'version' => 'dev-Dev',
'reference' => 'c45af18802312f6ea251c9adff642baa7a6ce965',
'type' => 'library',
'install_path' => __DIR__ . '/../monero-integrations/monerophp',
'aliases' => array(),
'dev_requirement' => false,
),
'norgul/xmpp-php' => array(
'pretty_version' => 'v2.2.3',
'version' => '2.2.3.0',
'reference' => 'f9d24ca167599e9040f5cc3f500f5ec90d28faf3',
'type' => 'project',
'install_path' => __DIR__ . '/../norgul/xmpp-php',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

26
vendor/composer/platform_check.php vendored Normal file
View file

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 SerHack and 2018 Monero Integrations team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,45 @@
[![PHPCS PSR-12](https://img.shields.io/badge/PHPCS-PSR12-226146.svg)](https://www.php-fig.org/psr/psr-12/) [![PHPStan ](.github/phpstan.svg)](https://phpstan.org/)
# Monero Library
A Monero library written in PHP by the [Monero Integrations](https://monerointegrations.com) [team](https://github.com/monero-integrations/monerophp/graphs/contributors).
## How It Works
This library has 3 main parts:
1. A Monero daemon JSON RPC API wrapper, `daemonRPC.php`
2. A Monero wallet (`monero-wallet-rpc`) JSON RPC API wrapper, `walletRPC.php`
3. A Monero/Cryptonote toolbox, `cryptonote.php`, with both lower level functions used in Monero related cryptography and higher level methods for things like generating Monero private/public keys.
In addition to these features, there are other lower-level libraries included for portability, *eg.* an ed25519 library, a SHA3 library, *etc.*
## Preview
![Preview](https://user-images.githubusercontent.com/4107993/38056594-b6cd6e14-3291-11e8-96e2-a771b0e9cee3.png)
## Documentation
Documentation can be found in the [`/docs`](https://github.com/sneurlax/monerophp/tree/master/docs) folder.
## Configuration
### Requirements
- Monero daemon (`monerod`)
- Webserver with PHP, for example XMPP, Apache, or NGINX
- cURL PHP extension for JSON RPC API(s)
- GMP PHP extension for about 100x faster calculations (as opposed to BCMath)
Debian (or Ubuntu) are recommended.
### Getting Started
1. Start the Monero daemon (`monerod`) on testnet.
```bash
monerod --testnet --detach
```
2. Start the Monero wallet RPC interface (`monero-wallet-rpc`) on testnet.
```bash
monero-wallet-rpc --testnet --rpc-bind-port 28083 --disable-rpc-login --wallet-dir /path/to/wallet/directory
```
3. Edit `example.php` with your the IP address of `monerod` and `monero-wallet-rpc` (use `127.0.0.1:28081` and `127.0.0.1:28083`, respectively, for testnet.)
4. Serve `example.php` with your webserver (*eg.* XMPP, Apache/Apache2, NGINX, *etc.*) and navigate to it. If everything has been set up correctly, information from your Monero daemon and wallet will be displayed.

View file

@ -0,0 +1,66 @@
{
"name": "monero-integrations/monerophp",
"description": "A Monero library written in PHP by the Monero-Integrations team.",
"keywords": ["Monero", "XMR", "monerod", "monero-wallet-rpc", "cryptonote", "JSONRPC", "JSON-RPC", "cryptocurrency"],
"homepage": "https://github.com/monero-integrations/monerophp",
"type": "library",
"version" : "1.0.1",
"license": "MIT",
"authors": [
{
"name": "SerHack",
"email": "support@monerointegrations.com"
},
{
"name": "cryptochangements34",
"email": "bW9uZXJv@gmail.com"
},
{
"name": "sneurlax",
"email": "sneurlax@gmail.com"
}
],
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"require": {
"php": ">=7.3",
"ext-bcmath": "*",
"ext-curl": "*",
"ext-json": "*",
"kornrunner/keccak": "^1.1"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "*",
"phpstan/extension-installer": "*",
"brainmaestro/composer-git-hooks": "^2.8",
"squizlabs/php_codesniffer": "*"
},
"suggest": {
"ext-gmp": "Used to have a multiple math precision for generating address"
},
"autoload": {
"psr-4": {
"MoneroIntegrations\\MoneroPhp\\": "src/"
}
},
"extra": {
"hooks": {
"pre-commit": [
"vendor/bin/phpcbf"
]
}
},
"scripts": {
"post-install-cmd": "cghooks add --ignore-lock",
"post-update-cmd": "cghooks update",
"lint": [
"phpcbf || true",
"phpcs || true",
"phpstan analyse --memory-limit 1G"
]
}
}

1286
vendor/monero-integrations/monerophp/composer.lock generated vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,166 @@
# MoneroPHP documentation
This document lists the MoneroPHP classes and their methods. More detailed documentation (including parameters and their types and return formats and examples) can be found on each class' page.
### Classes
- [`cryptonote`](#cryptonote-class) ([`src/cryptonote.php`](https://github.com/monero-integrations/monerophp/tree/master/src/cryptonote.php))
- [`ed25519`](#ed25519-class) ([`src/ed25519.php`](https://github.com/monero-integrations/monerophp/tree/master/src/ed25519.php))
- [`SHA3`](#SHA3-class) ([`src/SHA3.php`](https://github.com/monero-integrations/monerophp/tree/master/src/SHA3.php))
- [`base58`](#base58-class) ([`src/base58.php`](https://github.com/monero-integrations/monerophp/tree/master/src/base58.php))
JSON RPC wrappers:
- [`daemonRPC`](#daemonRPC-class) ([`src/daemonRPC.php`](https://github.com/monero-integrations/monerophp/tree/master/src/daemonRPC.php))
- [`walletRPC`](#walletRPC-class) ([`src/walletRPC.php`](https://github.com/monero-integrations/monerophp/tree/master/src/walletRPC.php))
## [`cryptonote` class](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md)
*Documentation under development*
- [`keccak_256`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#keccak_256)
- [`gen_new_hex_seed`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#gen_new_hex_seed)
- [`sc_reduce`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#sc_reduce)
- [`hash_to_scalar`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#hash_to_scalar)
- [`derive_viewKey`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#derive_viewKey)
- [`gen_private_keys`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#gen_private_keys)
- [`pk_from_sk`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#pk_from_sk)
- [`gen_key_derivation`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#gen_key_derivation)
- [`encode_varint`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#encode_varint)
- [`derivation_to_scalar`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#derivation_to_scalar)
- [`stealth_payment_id`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#stealth_payment_id)
- [`txpub_from_extra`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#txpub_from_extra)
- [`derive_public_key`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#derive_public_key)
- [`is_output_mine`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#is_output_mine)
- [`encode_address`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#encode_address)
- [`verify_checksum`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#verify_checksum)
- [`decode_address`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#decode_address)
- [`integrated_addr_from_keys`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#integrated_addr_from_keys)
- [`address_from_seed`](https://github.com/monero-integrations/monerophp/tree/master/docs/cryptonote.md#address_from_seed)
## [`ed25519` class](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md)
*Documentation under development*
- [`H`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#H)
- [`pymod`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#pymod)
- [`expmod`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#expmod)
- [`inv`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#inv)
- [`xrecover`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#xrecover)
- [`edwards`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#edwards)
- [`scalarmult`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#scalarmult)
- [`scalarloop`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#scalarloop)
- [`bitsToString`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#bitsToString)
- [`dec2bin_i`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#dec2bin_i)
- [`encodeint`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#encodeint)
- [`encodepoint`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#encodepoint)
- [`bit`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#bit)
- [`publickey`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#publickey)
- [`Hint`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#Hint)
- [`signature`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#signature)
- [`isoncurve`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#isoncurve)
- [`decodeint`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#decodeint)
- [`decodepoint`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#decodepoint)
- [`checkvalid`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#checkvalid)
- [`scalarmult_base`](https://github.com/monero-integrations/monerophp/tree/master/docs/ed25519.md#scalarmult_base)
## [`SHA3` class](https://github.com/monero-integrations/monerophp/tree/master/docs/SHA3.md)
- [`init`](https://github.com/monero-integrations/monerophp/tree/master/docs/SHA3.md#init)
- [`absorb`](https://github.com/monero-integrations/monerophp/tree/master/docs/SHA3.md#absorb)
- [`absorb`](https://github.com/monero-integrations/monerophp/tree/master/docs/SHA3.md#absorb)
- [`squeeze`](https://github.com/monero-integrations/monerophp/tree/master/docs/SHA3.md#squeeze)
## [`base58` class](https://github.com/monero-integrations/monerophp/tree/master/docs/base58.md)
- [`encode`](https://github.com/monero-integrations/monerophp/tree/master/docs/base58.md##encode)
- [`decode`](https://github.com/monero-integrations/monerophp/tree/master/docs/base58.md##decode)
## [`daemonRPC` class](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md)
- [`getblockcount`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getblockcount)
- [`on_getblockhash`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#on_getblockhash)
- [`getblocktemplate`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getblocktemplate)
- [`submitblock`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#submitblock)
- [`getlastblockheader`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getlastblockheader)
- [`getblockheaderbyhash`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getblockheaderbyhash)
- [`getblockheaderbyheight`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getblockheaderbyheight)
- [`getblock_by_hash`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getblock_by_hash)
- [`getblock_by_height`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getblock_by_height)
- [`get_connections`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#get_connections)
- [`get_info`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#get_info)
- [`hardfork_info`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#hardfork_info)
- [`setbans`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#setbans)
- [`getbans`](https://github.com/monero-integrations/monerophp/tree/master/docs/daemonRPC.md#getbans)
## [`walletRPC` class](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md)
- [`_transform`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#_transform)
- [`get_balance`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_balance)
- [`get_address`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_address)
- [`create_address`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#create_address)
- [`label_address`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#label_address)
- [`get_accounts`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_accounts)
- [`create_account`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#create_account)
- [`label_account`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#label_account)
- [`get_account_tags`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_account_tags)
- [`tag_accounts`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#tag_accounts)
- [`untag_accounts`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#untag_accounts)
- [`set_account_tag_description`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#set_account_tag_description)
- [`get_height`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_height)
- [`transfer`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#transfer)
- [`transfer_split`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#transfer_split)
- [`sweep_dust`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#sweep_dust)
- [`sweep_unmixable`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#sweep_unmixable)
- [`sweep_all`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#sweep_all)
- [`sweep_single`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#sweep_single)
- [`relay_tx`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#relay_tx)
- [`store`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#store)
- [`get_payments`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_payments)
- [`get_bulk_payments`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_bulk_payments)
- [`incoming_transfers`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#incoming_transfers)
- [`query_key`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#query_key)
- [`view_key`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#view_key)
- [`spend_key`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#spend_key)
- [`mnemonic`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#mnemonic)
- [`make_integrated_address`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#make_integrated_address)
- [`split_integrated_address`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#split_integrated_address)
- [`stop_wallet`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#stop_wallet)
- [`rescan_blockchain`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#rescan_blockchain)
- [`set_tx_notes`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#set_tx_notes)
- [`get_tx_notes`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_tx_notes)
- [`set_attribute`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#set_attribute)
- [`get_attribute`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_attribute)
- [`get_tx_key`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_tx_key)
- [`check_tx_key`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#check_tx_key)
- [`get_tx_proof`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_tx_proof)
- [`check_tx_proof`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#check_tx_proof)
- [`get_spend_proof`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_spend_proof)
- [`check_spend_proof`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#check_spend_proof)
- [`get_reserve_proof`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_reserve_proof)
- [`check_reserve_proof`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#check_reserve_proof)
- [`get_transfers`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_transfers)
- [`get_transfer_by_txid`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_transfer_by_txid)
- [`sign`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#sign)
- [`verify`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#verify)
- [`export_key_images`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#export_key_images)
- [`import_key_images`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#import_key_images)
- [`make_uri`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#make_uri)
- [`parse_uri`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#parse_uri)
- [`get_address_book`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_address_book)
- [`add_address_book`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#add_address_book)
- [`delete_address_book`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#delete_address_book)
- [`rescan_spent`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#rescan_spent)
- [`start_mining`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#start_mining)
- [`stop_mining`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#stop_mining)
- [`get_languages`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#get_languages)
- [`create_wallet`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#create_wallet)
- [`open_wallet`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#open_wallet)
- [`is_multisig`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#is_multisig)
- [`prepare_multisig`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#prepare_multisig)
- [`make_multisig`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#make_multisig)
- [`export_multisig_info`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#export_multisig_info)
- [`import_multisig_info`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#import_multisig_info)
- [`finalize_multisig`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#finalize_multisig)
- [`sign_multisig`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#sign_multisig)
- [`submit_multisig`](https://github.com/monero-integrations/monerophp/tree/master/docs/walletRPC.md#submit_multisig)

View file

@ -0,0 +1,42 @@
# `base58` class
[`src/base58.php`](https://github.com/monero-integrations/monerophp/tree/master/src/base58.php)
A PHP Base58 codec
### Methods
- [`encode`](#encode)
- [`decode`](#decode)
#### `encode`
Encode a hexadecimal (Base16) string to Base58
Parameters:
- `$hex <String>` A hexadecimal (Base16) string to convert to Base58
Return: `<String>`
`"479cG5opa54beQWSyqNoWw5tna9sHUNmMTtiFqLPaUhDevpJ2YLwXAggSx5ePdeFrYF8cdbmVRSmp1Kn3t4Y9kFu7rZ7pFw"`
#### `decode`
Decode a Base58 string to hexadecimal (Base16)
Parameters:
- `$hex <String>` A Base58 string to convert to hexadecimal (Base16)
Return: `<String>`
`"0137F8F06C971B168745F562AA107B4D172F336271BC0F9D3B510C14D3460DFB27D8CEBE561E73AC1E11833D5EA40200EB3C82E9C66ACAF1AB1A6BB53C40537C0B7A22160B0E"`
### Credits
Written by the [Monero Integrations team](https://github.com/monero-integrations/monerophp/graphs/contributors) (<support@monerointegrations.com>)
Using work from:
- bigreddmachine [MoneroPy] (https://github.com/bigreddmachine)
- Paul Shapiro [mymonero-core-js] (https://github.com/paulshapiro)

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