init PHP rewrite

This commit is contained in:
Zoey
2024-04-20 18:38:03 +02:00
parent 18ae902a04
commit 2323650863
178 changed files with 25163 additions and 286 deletions

View File

@@ -6,18 +6,12 @@ on:
paths: paths:
- .github/workflows/docker.yml - .github/workflows/docker.yml
- Dockerfile - Dockerfile
- frontend/**
- backend/**
- global/**
- rootfs/** - rootfs/**
- src/** - src/**
pull_request: pull_request:
paths: paths:
- .github/workflows/docker.yml - .github/workflows/docker.yml
- Dockerfile - Dockerfile
- frontend/**
- backend/**
- global/**
- rootfs/** - rootfs/**
- src/** - src/**
workflow_dispatch: workflow_dispatch:
@@ -56,9 +50,7 @@ jobs:
- name: version - name: version
run: | run: |
version="$(cat .version)+$(git rev-parse --short HEAD)" version="$(cat .version)+$(git rev-parse --short HEAD)"
sed -i "s|\"0.0.0\"|\"$version\"|g" frontend/js/i18n/messages.json #sed -i "s|\"0.0.0\"|\"$version\"|g" src/
sed -i "s|\"0.0.0\"|\"$version\"|g" frontend/package.json
sed -i "s|\"0.0.0\"|\"$version\"|g" backend/package.json
- name: Build - name: Build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}

View File

@@ -3,6 +3,7 @@ on:
push: push:
branches: branches:
- develop - develop
- php
schedule: schedule:
- cron: "0 */6 * * *" - cron: "0 */6 * * *"
workflow_dispatch: workflow_dispatch:
@@ -13,21 +14,9 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: actions/setup-node@v4 - name: lint
with:
node-version: 21
- name: eslint
run: | run: |
cd backend cd src
yarn install --no-lockfile
yarn eslint . --fix
- name: update
run: |
curl -L https://unpkg.com/xregexp/xregexp-all.js -o rootfs/nftd/xregexp-all.js
curl -L https://unpkg.com/showdown/dist/showdown.min.js -o rootfs/nftd/showdown.min.js
curl -L https://code.jquery.com/jquery-"$(git ls-remote --tags https://github.com/jquery/jquery | cut -d/ -f3 | sort -V | tail -1)".min.js -o rootfs/nftd/jquery.min.js
curl -L https://cdn.jsdelivr.net/npm/bootstrap@"$(git ls-remote --tags https://github.com/twbs/bootstrap v3.3.* | cut -d/ -f3 | sort -V | tail -1)"/dist/css/bootstrap.min.css -o rootfs/html/404/bootstrap.min.css
curl -L https://cdn.jsdelivr.net/npm/bootstrap@"$(git ls-remote --tags https://github.com/twbs/bootstrap v3.3.* | cut -d/ -f3 | sort -V | tail -1)"/dist/css/bootstrap.min.css -o rootfs/html/default/bootstrap.min.css
- name: nginxbeautifier - name: nginxbeautifier
run: | run: |
yarn global add nginxbeautifier yarn global add nginxbeautifier

661
COPYING Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@@ -1,40 +1,3 @@
# syntax=docker/dockerfile:labs
FROM --platform="$BUILDPLATFORM" alpine:3.19.1 as frontend
COPY frontend /build/frontend
COPY global/certbot-dns-plugins.json /build/frontend/certbot-dns-plugins.json
ARG NODE_ENV=production \
NODE_OPTIONS=--openssl-legacy-provider
WORKDIR /build/frontend
RUN apk upgrade --no-cache -a && \
apk add --no-cache ca-certificates nodejs yarn git python3 build-base && \
yarn global add clean-modules && \
yarn --no-lockfile install && \
clean-modules --yes && \
yarn --no-lockfile build && \
yarn cache clean --all
COPY darkmode.css /build/frontend/dist/css/darkmode.css
COPY security.txt /build/frontend/dist/.well-known/security.txt
FROM --platform="$BUILDPLATFORM" alpine:3.19.1 as backend
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
COPY backend /build/backend
COPY global/certbot-dns-plugins.json /build/backend/certbot-dns-plugins.json
ARG NODE_ENV=production \
TARGETARCH
WORKDIR /build/backend
RUN apk upgrade --no-cache -a && \
apk add --no-cache ca-certificates nodejs-current yarn && \
yarn global add clean-modules && \
if [ "$TARGETARCH" = "amd64" ]; then \
npm_config_target_platform=linux npm_config_target_arch=x64 yarn install --no-lockfile; \
elif [ "$TARGETARCH" = "arm64" ]; then \
npm_config_target_platform=linux npm_config_target_arch=arm64 yarn install --no-lockfile; \
fi && \
clean-modules --yes && \
yarn cache clean --all
FROM --platform="$BUILDPLATFORM" alpine:3.19.1 as crowdsec FROM --platform="$BUILDPLATFORM" alpine:3.19.1 as crowdsec
SHELL ["/bin/ash", "-eo", "pipefail", "-c"] SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
@@ -64,18 +27,19 @@ SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
ARG CRS_VER=v4.1.0 ARG CRS_VER=v4.1.0
COPY rootfs / COPY rootfs /
COPY --from=zoeyvid/certbot-docker:34 /usr/local /usr/local COPY src /html/app
COPY --from=zoeyvid/curl-quic:380 /usr/local/bin/curl /usr/local/bin/curl COPY --from=zoeyvid/curl-quic:380 /usr/local/bin/curl /usr/local/bin/curl
RUN apk upgrade --no-cache -a && \ RUN apk upgrade --no-cache -a && \
apk add --no-cache ca-certificates tzdata tini \ apk add --no-cache ca-certificates tzdata tini \
bash nano \ bash nano \
nodejs-current \
openssl apache2-utils \ openssl apache2-utils \
lua5.1-lzlib lua5.1-socket \ lua5.1-lzlib lua5.1-socket \
coreutils grep findutils jq shadow su-exec \ coreutils grep findutils jq shadow su-exec \
luarocks5.1 lua5.1-dev lua5.1-sec build-base git yarn && \ luarocks5.1 lua5.1-dev lua5.1-sec build-base git \
fcgi php83-fpm && \
curl https://raw.githubusercontent.com/acmesh-official/acme.sh/master/acme.sh | sh -s -- --install-online --home /usr/local/acme.sh --nocron && \ curl https://raw.githubusercontent.com/acmesh-official/acme.sh/master/acme.sh | sh -s -- --install-online --home /usr/local/acme.sh --nocron && \
ln -s /usr/local/acme.sh/acme.sh /usr/local/bin/acme.sh && \
git clone https://github.com/coreruleset/coreruleset --branch "$CRS_VER" /tmp/coreruleset && \ git clone https://github.com/coreruleset/coreruleset --branch "$CRS_VER" /tmp/coreruleset && \
mkdir -v /usr/local/nginx/conf/conf.d/include/coreruleset && \ mkdir -v /usr/local/nginx/conf/conf.d/include/coreruleset && \
mv -v /tmp/coreruleset/crs-setup.conf.example /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf.example && \ mv -v /tmp/coreruleset/crs-setup.conf.example /usr/local/nginx/conf/conf.d/include/coreruleset/crs-setup.conf.example && \
@@ -86,11 +50,8 @@ RUN apk upgrade --no-cache -a && \
luarocks-5.1 install lua-resty-http && \ luarocks-5.1 install lua-resty-http && \
luarocks-5.1 install lua-resty-string && \ luarocks-5.1 install lua-resty-string && \
luarocks-5.1 install lua-resty-openssl && \ luarocks-5.1 install lua-resty-openssl && \
yarn global add nginxbeautifier && \ apk del --no-cache luarocks5.1 lua5.1-dev lua5.1-sec build-base git
apk del --no-cache luarocks5.1 lua5.1-dev lua5.1-sec build-base git yarn
COPY --from=backend /build/backend /app
COPY --from=frontend /build/frontend/dist /html/frontend
COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/lib/plugins /usr/local/nginx/lib/lua/plugins COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/lib/plugins /usr/local/nginx/lib/lua/plugins
COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/lib/crowdsec.lua /usr/local/nginx/lib/lua/crowdsec.lua COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/lib/crowdsec.lua /usr/local/nginx/lib/lua/crowdsec.lua
COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/templates/ban.html /usr/local/nginx/conf/conf.d/include/ban.html COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/templates/ban.html /usr/local/nginx/conf/conf.d/include/ban.html
@@ -98,18 +59,12 @@ COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/templates/captcha.html
COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/config_example.conf /usr/local/nginx/conf/conf.d/include/crowdsec.conf COPY --from=crowdsec /src/crowdsec-nginx-bouncer/lua-mod/config_example.conf /usr/local/nginx/conf/conf.d/include/crowdsec.conf
COPY --from=crowdsec /src/crowdsec-nginx-bouncer/nginx/crowdsec_nginx.conf /usr/local/nginx/conf/conf.d/include/crowdsec_nginx.conf COPY --from=crowdsec /src/crowdsec-nginx-bouncer/nginx/crowdsec_nginx.conf /usr/local/nginx/conf/conf.d/include/crowdsec_nginx.conf
RUN ln -s /usr/local/acme.sh/acme.sh /usr/local/bin/acme.sh && \
ln -s /app/password-reset.js /usr/local/bin/password-reset.js && \
ln -s /app/sqlite-vaccum.js /usr/local/bin/sqlite-vaccum.js && \
ln -s /app/index.js /usr/local/bin/index.js
ENV NODE_ENV=production \ ENV NODE_ENV=production \
NODE_CONFIG_DIR=/data/etc/npm \ NODE_CONFIG_DIR=/data/etc/npm \
DB_SQLITE_FILE=/data/etc/npm/database.sqlite DB_SQLITE_FILE=/data/etc/npm/database.sqlite
ENV PUID=0 \ ENV PUID=0 \
PGID=0 \ PGID=0 \
NIBEP=48693 \
GOAIWSP=48683 \ GOAIWSP=48683 \
NPM_PORT=81 \ NPM_PORT=81 \
GOA_PORT=91 \ GOA_PORT=91 \
@@ -141,8 +96,7 @@ ENV PUID=0 \
GOA=false \ GOA=false \
GOACLA="--agent-list --real-os --double-decode --anonymize-ip --anonymize-level=1 --keep-last=30 --with-output-resolver --no-query-string" \ GOACLA="--agent-list --real-os --double-decode --anonymize-ip --anonymize-level=1 --keep-last=30 --with-output-resolver --no-query-string" \
PHP81=false \ PHP81=false \
PHP82=false \ PHP82=false
PHP83=false
WORKDIR /app WORKDIR /app
ENTRYPOINT ["tini", "--", "entrypoint.sh"] ENTRYPOINT ["tini", "--", "entrypoint.sh"]

View File

@@ -1,14 +0,0 @@
services:
npmplus-caddy:
container_name: npmplus-caddy
image: zoeyvid/npmplus:caddy
restart: always
network_mode: bridge
ports:
- "80:80"
environment:
- "TZ=Europe/Berlin"
npmplus:
environment:
- "DISABLE_HTTP=true" # disables nginx to listen on port 80, default false

View File

@@ -12,7 +12,6 @@ services:
- "TZ=Europe/Berlin" # set timezone, required - "TZ=Europe/Berlin" # set timezone, required
# - "PUID=1000" # set group id, default 0 (root) # - "PUID=1000" # set group id, default 0 (root)
# - "PGID=1000" # set user id, default 0 (root) # - "PGID=1000" # set user id, default 0 (root)
# - "NIBEP=48694" # internal port of the NOMplus API, always bound to 127.0.0.1, default 48693, you need to change it, if you want to run multiple npm instances in network mode host
# - "GOAIWSP=48684" # internal port of goaccess, always bound to 127.0.0.1, default 48683, you need to change it, if you want to run multiple npm with goaccess instances in network mode host # - "GOAIWSP=48684" # internal port of goaccess, always bound to 127.0.0.1, default 48683, you need to change it, if you want to run multiple npm with goaccess instances in network mode host
# - "NPM_PORT=82" # Port the NPM UI should be bound to, default 81, you need to change it, if you want to run multiple npm instances in network mode host # - "NPM_PORT=82" # Port the NPM UI should be bound to, default 81, you need to change it, if you want to run multiple npm instances in network mode host
# - "NPM_PORT=92" # Port the goaccess should be bound to, default 91, you need to change it, if you want to run multiple npm with goaccess instances in network mode host # - "NPM_PORT=92" # Port the goaccess should be bound to, default 91, you need to change it, if you want to run multiple npm with goaccess instances in network mode host
@@ -42,10 +41,20 @@ services:
# - "IPRT=3" # Set how many hours should be between updating ip ranges from aws and cloudflare, default 1, ignored when SKIP_IP_RANGES is true # - "IPRT=3" # Set how many hours should be between updating ip ranges from aws and cloudflare, default 1, ignored when SKIP_IP_RANGES is true
# - "GOA=true" # Enables goaccess, overrides LOGROTATE, default false --- if you download the GeoLite2-Country.mmdb, GeoLite2-City.mmdb AND GeoLite2-ASN.mmdb file from MaxMind and place them in /opt/npm/etc/goaccess/geoip it will automatically enable GeoIP in goaccess after restarting NPMplus (no need to change GOACLA below), you may also use the compose.geoip.yaml # - "GOA=true" # Enables goaccess, overrides LOGROTATE, default false --- if you download the GeoLite2-Country.mmdb, GeoLite2-City.mmdb AND GeoLite2-ASN.mmdb file from MaxMind and place them in /opt/npm/etc/goaccess/geoip it will automatically enable GeoIP in goaccess after restarting NPMplus (no need to change GOACLA below), you may also use the compose.geoip.yaml
# - "GOACLA=--agent-list --real-os --double-decode --anonymize-ip --anonymize-level=2 --keep-last=7 --with-output-resolver --no-query-string" # Arguments that should be passed to goaccess, default: https://github.com/ZoeyVid/NPMplus/blob/develop/rootfs/usr/local/bin/launch.sh#L50 and: --agent-list --real-os --double-decode --anonymize-ip --anonymize-level=1 --keep-last=30 --with-output-resolver --no-query-string # - "GOACLA=--agent-list --real-os --double-decode --anonymize-ip --anonymize-level=2 --keep-last=7 --with-output-resolver --no-query-string" # Arguments that should be passed to goaccess, default: https://github.com/ZoeyVid/NPMplus/blob/develop/rootfs/usr/local/bin/launch.sh#L50 and: --agent-list --real-os --double-decode --anonymize-ip --anonymize-level=1 --keep-last=30 --with-output-resolver --no-query-string
# - "PHP_APKS=php-pecl-apcu php-pecl-redis" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php-*, default none
# - "PHP81=true" # Activate PHP81, default false # - "PHP81=true" # Activate PHP81, default false
# - "PHP81_APKS=php81-curl php81-openssl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php81-*, default none # - "PHP81_APKS=php81-curl php81-openssl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php81-*, default none
# - "PHP82=true" # Activate PHP82, default false # - "PHP82=true" # Activate PHP82, default false
# - "PHP82_APKS=php82-curl php-82-openssl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php82-*, default none # - "PHP82_APKS=php82-curl php-82-openssl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php82-*, default none
# - "PHP83=true" # Activate PHP83, default false
# - "PHP83_APKS=php83-curl php83-openssl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php83-*, default none # - "PHP83_APKS=php83-curl php83-openssl" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php83-*, default none
# - "PHP_APKS=php-pecl-apcu php-pecl-redis" # Add php extensions, see available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.19&repo=community&arch=x86_64&name=php-*, default none
# This can be used with DISABLE_HTTP=true, to force HTTPS redirects for every host
# npmplus-caddy:
# container_name: npmplus-caddy
# image: zoeyvid/npmplus:caddy
# restart: always
# network_mode: bridge
# ports:
# - "80:80"
# environment:
# - "TZ=Europe/Berlin"

View File

@@ -2,7 +2,7 @@
"extends": [ "extends": [
"config:base" "config:base"
], ],
"baseBranches": [], "baseBranches": ["develop", "php"],
"includeForks": true, "includeForks": true,
"automerge": false, "automerge": false,
"branchPrefix": "renovate-deps-update-", "branchPrefix": "renovate-deps-update-",

View File

@@ -1,15 +0,0 @@
agree-tos = true
non-interactive = true
webroot-path = /tmp/acme-challenge
new-key= true
key-type = ecdsa
must-staple = true
no-reuse-key = true
rsa-key-size = 4096
elliptic-curve = secp384r1
# An example of using an alternate ACME server that uses EAB credentials
# server = https://dv.acme-v02.api.pki.goog/directory
# eab-kid = somestringofstuffwithoutquotes
# eab-hmac-key = yaddayaddahexhexnotquoted

File diff suppressed because one or more lines are too long

View File

@@ -1,26 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 Not Found</title>
<link href="/bootstrap.min.css" rel="stylesheet">
<style>
.jumbotron {
margin-top: 50px;
}
</style>
</head>
<body>
<div class="container">
<div class="jumbotron">
<h1 class="text-center">404 Not Found</h1>
</div>
<p class="text-center">
<small>Powered by <a href="https://github.com/ZoeyVid/NPMplus" target="_blank">NPMplus</a>
</small>
</p>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Default Site</title>
<link href="/bootstrap.min.css" rel="stylesheet">
<style>
.jumbotron {
margin-top: 50px;
}
</style>
</head>
<body>
<div class="container">
<div class="jumbotron">
<h1 class="text-center">Congratulations!</h1>
<p>You've successfully started NPMplus.</p>
<p>If you're seeing this site then you're trying to access a host that isn't set up yet.</p>
<p>Log in to the Admin panel to get started.</p>
</div>
<p class="text-center">
<small>Powered by <a href="https://github.com/ZoeyVid/NPMplus" target="_blank">NPMplus</a>
</small>
</p>
</div>
</body>
</html>

View File

@@ -31,7 +31,7 @@ if [ "$GOA_IPV6_BINDING" != "[::]" ] && [ "$GOA_IPV4_BINDING" != "0.0.0.0" ]; th
fi fi
fi fi
if (if [ "$GOA" = "true" ]; then [ -f /tmp/goa/index.html ] && nc -z "$HCGOA_IP" "$GOA_PORT"; fi && if [ "$PHP81" = true ]; then cgi-fcgi -bind -connect /run/php81.sock > /dev/null 2>&1; fi && if [ "$PHP82" = true ]; then cgi-fcgi -bind -connect /run/php82.sock > /dev/null 2>&1; fi && if [ "$PHP83" = true ]; then cgi-fcgi -bind -connect /run/php83.sock > /dev/null 2>&1; fi && [ "$(curl -sk https://"$HCNPM_IP":"$NPM_PORT"/api/ | jq --raw-output .status)" = "OK" ]); then if (if [ "$GOA" = "true" ]; then [ -f /tmp/goa/index.html ] && nc -z "$HCGOA_IP" "$GOA_PORT"; fi && if [ "$PHP81" = true ]; then cgi-fcgi -bind -connect /run/php81.sock > /dev/null 2>&1; fi && if [ "$PHP82" = true ]; then cgi-fcgi -bind -connect /run/php82.sock > /dev/null 2>&1; fi && cgi-fcgi -bind -connect /run/php83.sock > /dev/null 2>&1 && [ "$(curl -sk https://"$HCNPM_IP":"$NPM_PORT"/status | jq -r .status)" = "ok" ]); then
echo "OK" echo "OK"
exit 0 exit 0
else else

View File

@@ -36,19 +36,18 @@ if [ "$PHP82" = "true" ]; then
fi fi
fi fi
if [ "$PHP83" = "true" ]; then
if ! PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FORt > /dev/null 2>&1; then if ! PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FORt > /dev/null 2>&1; then
PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FORt PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FORt
sleep inf sleep inf
fi fi
fi
nginx -e stderr &
if [ "$PHP81" = "true" ]; then PHP_INI_SCAN_DIR=/data/php/81/conf.d php-fpm81 -c /data/php/81 -y /data/php/81/php-fpm.conf -FOR; fi & if [ "$PHP81" = "true" ]; then PHP_INI_SCAN_DIR=/data/php/81/conf.d php-fpm81 -c /data/php/81 -y /data/php/81/php-fpm.conf -FOR; fi &
if [ "$PHP82" = "true" ]; then PHP_INI_SCAN_DIR=/data/php/82/conf.d php-fpm82 -c /data/php/82 -y /data/php/82/php-fpm.conf -FOR; fi & if [ "$PHP82" = "true" ]; then PHP_INI_SCAN_DIR=/data/php/82/conf.d php-fpm82 -c /data/php/82 -y /data/php/82/php-fpm.conf -FOR; fi &
if [ "$PHP83" = "true" ]; then PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FOR; fi & PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FOR &
if [ "$LOGROTATE" = "true" ]; then while true; do logrotate --verbose --state /data/etc/logrotate.status /etc/logrotate; sleep 25h; done; fi & if [ "$LOGROTATE" = "true" ]; then while true; do logrotate --verbose --state /data/etc/logrotate.status /etc/logrotate; sleep 25h; done; fi &
# shellcheck disable=SC2086 # shellcheck disable=SC2086
if [ "$GOA" = "true" ]; then while true; do goaccess --no-global-config --num-tests=0 --tz="$TZ" --date-format="%d/%b/%Y" --time-format="%H:%M:%S" --log-format='[%d:%t %^] %v %h %T "%r" %s %b %b %R %u' --no-ip-validation --addr=127.0.0.1 --port="$GOAIWSP" \ if [ "$GOA" = "true" ]; then while true; do goaccess --no-global-config --num-tests=0 --tz="$TZ" --date-format="%d/%b/%Y" --time-format="%H:%M:%S" --log-format='[%d:%t %^] %v %h %T "%r" %s %b %b %R %u' --no-ip-validation --addr=127.0.0.1 --port="$GOAIWSP" \
-f /data/nginx/access.log --real-time-html -o /tmp/goa/index.html --persist --restore --db-path=/data/etc/goaccess/data -b /etc/goaccess/browsers.list -b /etc/goaccess/podcast.list $GOACLA; done; fi & -f /data/nginx/access.log --real-time-html -o /tmp/goa/index.html --persist --restore --db-path=/data/etc/goaccess/data -b /etc/goaccess/browsers.list -b /etc/goaccess/podcast.list $GOACLA; done; fi &
aio.sh & #aio.sh &
index.js index.js

View File

@@ -51,11 +51,6 @@ if ! echo "$PGID" | grep -q "^[0-9]\+$"; then
sleep inf sleep inf
fi fi
if ! echo "$NIBEP" | grep -q "^[0-9]\+$"; then
echo "NIBEP needs to be a number."
sleep inf
fi
if ! echo "$GOAIWSP" | grep -q "^[0-9]\+$"; then if ! echo "$GOAIWSP" | grep -q "^[0-9]\+$"; then
echo "GOAIWSP needs to be a number." echo "GOAIWSP needs to be a number."
sleep inf sleep inf
@@ -221,6 +216,11 @@ if [ -n "$PHP82_APKS" ] && ! echo "$PHP82_APKS" | grep -q "^[a-z0-9 _-]\+$"; the
sleep inf sleep inf
fi fi
if [ -n "$PHP83_APKS" ] && ! echo "$PHP83_APKS" | grep -q "^[a-z0-9 _-]\+$"; then
echo "PHP83_APKS can consist of lower letters a-z, numbers 0-9, spaces, underscores and hyphens."
sleep inf
fi
if [ -n "$NC_AIO" ] && ! echo "$NC_AIO" | grep -q "^true$\|^false$"; then if [ -n "$NC_AIO" ] && ! echo "$NC_AIO" | grep -q "^true$\|^false$"; then
echo "NC_AIO needs to be true or false." echo "NC_AIO needs to be true or false."
@@ -263,14 +263,9 @@ if [ -s /data/etc/goaccess/geoip/GeoLite2-Country.mmdb ] && [ -s /data/etc/goacc
fi fi
if [ "$PHP81" = "true" ] || [ "$PHP82" = "true" ] || [ "$PHP83" = "true" ]; then
apk add --no-cache fcgi
# From https://github.com/nextcloud/all-in-one/pull/1377/files # From https://github.com/nextcloud/all-in-one/pull/1377/files
if [ -n "$PHP_APKS" ]; then if [ -n "$PHP_APKS" ]; then
for apk in $(echo "$PHP_APKS" | tr " " "\n"); do for apk in $(echo "$PHP_APKS" | tr " " "\n"); do
if ! echo "$apk" | grep -q "^php-.*$"; then if ! echo "$apk" | grep -q "^php-.*$"; then
echo "$apk is a non allowed value." echo "$apk is a non allowed value."
echo "It needs to start with \"php-\"." echo "It needs to start with \"php-\"."
@@ -282,10 +277,8 @@ if [ "$PHP81" = "true" ] || [ "$PHP82" = "true" ] || [ "$PHP83" = "true" ]; then
if ! apk add --no-cache "$apk" > /dev/null 2>&1; then if ! apk add --no-cache "$apk" > /dev/null 2>&1; then
echo "The apk \"$apk\" was not installed!" echo "The apk \"$apk\" was not installed!"
fi fi
done done
fi fi
fi
if [ "$PHP81" = "true" ]; then if [ "$PHP81" = "true" ]; then
@@ -294,7 +287,6 @@ if [ "$PHP81" = "true" ]; then
# From https://github.com/nextcloud/all-in-one/pull/1377/files # From https://github.com/nextcloud/all-in-one/pull/1377/files
if [ -n "$PHP81_APKS" ]; then if [ -n "$PHP81_APKS" ]; then
for apk in $(echo "$PHP81_APKS" | tr " " "\n"); do for apk in $(echo "$PHP81_APKS" | tr " " "\n"); do
if ! echo "$apk" | grep -q "^php81-.*$"; then if ! echo "$apk" | grep -q "^php81-.*$"; then
echo "$apk is a non allowed value." echo "$apk is a non allowed value."
echo "It needs to start with \"php81-\"." echo "It needs to start with \"php81-\"."
@@ -306,7 +298,6 @@ if [ "$PHP81" = "true" ]; then
if ! apk add --no-cache "$apk" > /dev/null 2>&1; then if ! apk add --no-cache "$apk" > /dev/null 2>&1; then
echo "The apk \"$apk\" was not installed!" echo "The apk \"$apk\" was not installed!"
fi fi
done done
fi fi
@@ -327,7 +318,6 @@ if [ "$PHP82" = "true" ]; then
# From https://github.com/nextcloud/all-in-one/pull/1377/files # From https://github.com/nextcloud/all-in-one/pull/1377/files
if [ -n "$PHP82_APKS" ]; then if [ -n "$PHP82_APKS" ]; then
for apk in $(echo "$PHP82_APKS" | tr " " "\n"); do for apk in $(echo "$PHP82_APKS" | tr " " "\n"); do
if ! echo "$apk" | grep -q "^php82-.*$"; then if ! echo "$apk" | grep -q "^php82-.*$"; then
echo "$apk is a non allowed value." echo "$apk is a non allowed value."
echo "It needs to start with \"php82-\"." echo "It needs to start with \"php82-\"."
@@ -339,7 +329,6 @@ if [ "$PHP82" = "true" ]; then
if ! apk add --no-cache "$apk" > /dev/null 2>&1; then if ! apk add --no-cache "$apk" > /dev/null 2>&1; then
echo "The apk \"$apk\" was not installed!" echo "The apk \"$apk\" was not installed!"
fi fi
done done
fi fi
@@ -353,14 +342,9 @@ elif [ "$FULLCLEAN" = "true" ]; then
rm -vrf /data/php/82 rm -vrf /data/php/82
fi fi
if [ "$PHP83" = "true" ]; then
apk add --no-cache php83-fpm
# From https://github.com/nextcloud/all-in-one/pull/1377/files # From https://github.com/nextcloud/all-in-one/pull/1377/files
if [ -n "$PHP83_APKS" ]; then if [ -n "$PHP83_APKS" ]; then
for apk in $(echo "$PHP83_APKS" | tr " " "\n"); do for apk in $(echo "$PHP83_APKS" | tr " " "\n"); do
if ! echo "$apk" | grep -q "^php83-.*$"; then if ! echo "$apk" | grep -q "^php83-.*$"; then
echo "$apk is a non allowed value." echo "$apk is a non allowed value."
echo "It needs to start with \"php83-\"." echo "It needs to start with \"php83-\"."
@@ -372,20 +356,14 @@ if [ "$PHP83" = "true" ]; then
if ! apk add --no-cache "$apk" > /dev/null 2>&1; then if ! apk add --no-cache "$apk" > /dev/null 2>&1; then
echo "The apk \"$apk\" was not installed!" echo "The apk \"$apk\" was not installed!"
fi fi
done done
fi fi
mkdir -vp /data/php mkdir -vp /data/php
cp -varnT /etc/php83 /data/php/83 cp -varnT /etc/php83 /data/php/83
sed -i "s|listen =.*|listen = /run/php83.sock|" /data/php/83/php-fpm.d/www.conf sed -i "s|listen =.*|listen = /run/php83.sock|" /data/php/83/php-fpm.d/www.conf
sed -i "s|;error_log =.*|error_log = /proc/self/fd/2|g" /data/php/83/php-fpm.conf sed -i "s|;error_log =.*|error_log = /proc/self/fd/2|g" /data/php/83/php-fpm.conf
sed -i "s|include=.*|include=/data/php/83/php-fpm.d/*.conf|g" /data/php/83/php-fpm.conf sed -i "s|include=.*|include=/data/php/83/php-fpm.d/*.conf|g" /data/php/83/php-fpm.conf
elif [ "$FULLCLEAN" = "true" ]; then
rm -vrf /data/php/83
fi
if [ "$LOGROTATE" = "true" ]; then if [ "$LOGROTATE" = "true" ]; then
apk add --no-cache logrotate apk add --no-cache logrotate
sed -i "s|rotate [0-9]\+|rotate $LOGROTATIONS|g" /etc/logrotate sed -i "s|rotate [0-9]\+|rotate $LOGROTATIONS|g" /etc/logrotate

View File

@@ -6,7 +6,7 @@ ssl_stapling_verify on;
ssl_session_timeout 1d; ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off; ssl_session_tickets off;
ssl_dhparam /etc/tls/dhparam; ssl_dhparam /etc/dhparam;
# intermediate configuration. tweak to your needs. # intermediate configuration. tweak to your needs.
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;

View File

@@ -1,36 +0,0 @@
server {
http3 off;
listen 81 ssl default_server;
listen [::]:81 ssl default_server;
server_name _;
include conf.d/include/brotli.conf;
include conf.d/include/force-tls.conf;
include conf.d/include/tls-ciphers.conf;
include conf.d/include/block-exploits.conf;
modsecurity on;
modsecurity_rules_file /usr/local/nginx/conf/conf.d/include/modsecurity.conf;
#ssl_certificate ;
#ssl_certificate_key ;
#ssl_trusted_certificate ;
location /api {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
include conf.d/include/proxy-location.conf;
rewrite ^/api(/.*)$ $1 break;
proxy_pass http://127.0.0.1:48693;
}
location / {
root /html/frontend;
if ($request_uri ~ ^/(.*)\.html$) {
return 302 /$1;
}
try_files $uri $uri.html $uri/ /index.html;
}
}

View File

@@ -0,0 +1,27 @@
server {
http3 off;
listen 81 ssl default_server;
listen [::]:81 ssl default_server;
server_name _;
include conf.d/include/brotli.conf;
include conf.d/include/force-tls.conf;
include conf.d/include/tls-ciphers.conf;
include conf.d/include/block-exploits.conf;
#ssl_certificate ;
#ssl_certificate_key ;
#ssl_trusted_certificate ;
location / {
alias /html/app/public/;
location ~ [^/]\.php(/|$) {
fastcgi_pass php83;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
}
}
}

37
src/composer.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "zoeyvid/npmplus",
"description": "WebUI for NPMplus, which manages nginx and acme.sh",
"type": "project",
"require": {
"phpmailer/phpmailer": "6.9.1",
"chillerlan/php-qrcode": "5.0.2"
},
"license": "AGPL-3.0",
"version": "v0.0.1-alpha",
"authors": [
{
"name": "Zoey",
"email": "zoey@z0ey.de",
"homepage": "https://z0ey.de"
},
{
"name": "David",
"email": "david@davidcraft.de",
"homepage": "https://davidcraft.de"
},
{
"name": "ZoeyVid",
"email": "zoeyvid@zvcdn.de",
"homepage": "https://zoeyvid.de"
}
],
"minimum-stability": "alpha",
"support": {
"email": "zoey@z0ey.de",
"issues": "https://github.com/ZoeyVid/booking/issues",
"forum": "https://github.com/ZoeyVid/booking/discussions",
"wiki": "https://github.com/ZoeyVid/booking",
"source": "https://github.com/ZoeyVid/booking",
"docs": "https://github.com/ZoeyVid/booking"
}
}

259
src/composer.lock generated Normal file
View File

@@ -0,0 +1,259 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0e5e06a8da6fe6041233a057fffa913a",
"packages": [
{
"name": "chillerlan/php-qrcode",
"version": "5.0.2",
"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"
},
"type": "library",
"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"
}
],
"time": "2024-02-27T14:37:26+00:00"
},
{
"name": "chillerlan/php-settings-container",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "8f93648fac8e6bacac8e00a8d325eba4950295e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/8f93648fac8e6bacac8e00a8d325eba4950295e6",
"reference": "8f93648fac8e6bacac8e00a8d325eba4950295e6",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.1"
},
"require-dev": {
"phan/phan": "^5.4",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"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"
}
],
"time": "2024-03-02T20:07:15+00:00"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.9.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/039de174cd9c17a8389754d3b877a2ed22743e18",
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1"
},
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"time": "2023-11-25T22:23:28+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "alpha",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View File

@@ -0,0 +1,2 @@
<?php
echo '{"status":"ok"}';

25
src/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 ComposerAutoloaderInit0e5e06a8da6fe6041233a057fffa913a::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
src/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

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);
}
}

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,164 @@
# 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.
### 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,51 @@
{
"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": {
"phan/phan": "^5.4",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.9"
},
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\SettingsTest\\": "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,240 @@
<?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, ReflectionClass, ReflectionProperty;
use function array_keys, get_object_vars, is_object, json_decode,
json_encode, method_exists, property_exists, serialize, unserialize;
use const JSON_THROW_ON_ERROR;
abstract class SettingsContainerAbstract implements SettingsContainerInterface{
/**
* SettingsContainerAbstract constructor.
*/
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{
return json_encode($this, ($jsonOptions ?? 0));
}
/**
* @inheritdoc
*/
public function fromJSON(string $json):static{
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
return $this->fromIterable($data);
}
/**
* @inheritdoc
*/
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()
*/
public function __unserialize(array $data):void{
foreach($data as $key => $value){
$this->{$key} = $value;
}
}
}

View File

@@ -0,0 +1,78 @@
<?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.
*/
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.
*/
public function fromIterable(iterable $properties):static;
/**
* Returns a JSON representation of the settings object
* @see \json_encode()
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*/
public function toJSON(int|null $jsonOptions = null):string;
/**
* Sets properties from a given JSON string
*
* @throws \Exception
* @throws \JsonException
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*/
public function fromJSON(string $json):static;
}

579
src/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);
}
}

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;
}
}

View File

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

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