recreate project
This commit is contained in:
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
service_name: travis-ci
|
||||
coverage_clover: build/logs/clover.xml
|
||||
json_path: build/logs/coveralls-upload.json
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
vendor
|
||||
build
|
||||
docs/_build/
|
||||
docs/warnings.log
|
||||
docs/xml/
|
||||
docs/html/
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
---
|
||||
dist: trusty
|
||||
sudo: required
|
||||
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 7.0
|
||||
- 7.1
|
||||
- 7.2
|
||||
- 7.3
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: nightly
|
||||
|
||||
install:
|
||||
- composer install
|
||||
|
||||
before_script:
|
||||
- bash -c 'if [[ $TRAVIS_PHP_VERSION == hhvm* ]]; then echo "hhvm.php7.all = 1" | sudo tee -a /etc/hhvm/php.ini; fi'
|
||||
- mkdir -p build/logs/
|
||||
|
||||
script:
|
||||
# Check code style
|
||||
- php vendor/bin/phpcs --standard=psr2 src/ -n
|
||||
# Unit tests
|
||||
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
|
||||
# Example scripts must work too
|
||||
- mkdir -p tmp && (cd tmp && find ../example -name '*.php' -print0 | xargs -n 1 -0 sh -c 'echo $0; php $0 || exit 255')
|
||||
|
||||
after_success:
|
||||
# Upload coverage statistics to coveralls service after test
|
||||
- wget -c -nc https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar
|
||||
- php coveralls.phar -v
|
||||
...
|
||||
Vendored
+504
@@ -0,0 +1,504 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
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 this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "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
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY 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
|
||||
LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey 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 library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random
|
||||
Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
# gfx-php - The pure PHP graphics library
|
||||
|
||||
[](https://travis-ci.org/mike42/gfx-php) [](https://packagist.org/packages/mike42/gfx-php)
|
||||
[](https://packagist.org/packages/mike42/gfx-php)
|
||||
[](https://packagist.org/packages/mike42/gfx-php) [](https://coveralls.io/github/mike42/gfx-php?branch=master)
|
||||
|
||||
This library implements input, output and processing of raster images in pure PHP, so that image
|
||||
processing extensions (Gd, Imagick) are not required.
|
||||
|
||||
This allows developers to eliminate some portability issues from their applications.
|
||||
|
||||
### Features
|
||||
|
||||
- Format support includes PNG, GIF, BMP and the Netpbm formats (See docs: [File formats](https://gfx-php.readthedocs.io/en/latest/user/formats.html)).
|
||||
- Support for scaling, cropping, format conversion and colorspace transformations (See docs: [Image operations](https://gfx-php.readthedocs.io/en/latest/user/operations.html)).
|
||||
- Pure PHP: This library does not require Gd, ImageMagick or GraphicsMagick extensions.
|
||||
|
||||
## Quick start
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 7.0 or newer.
|
||||
- `zlib` extension, for reading PNG files.
|
||||
|
||||
### Installation
|
||||
|
||||
Install `gfx-php` with composer:
|
||||
|
||||
```bash
|
||||
composer install mike42/gfx-php
|
||||
```
|
||||
|
||||
### Basic usage
|
||||
|
||||
The basic usage is like this:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Mike42\GfxPhp\Image;
|
||||
$img = Image::fromFile("colorwheel256.png");
|
||||
$img -> write("test.gif");
|
||||
```
|
||||
|
||||
### Further reading
|
||||
|
||||
- Read of the documentation at [gfx-php.readthedocs.io](https://gfx-php.readthedocs.io/)
|
||||
- See the `examples/` sub-folder for snippets.
|
||||
|
||||
## Contributing
|
||||
|
||||
This project is open to all kinds of contributions, including suggestions, documentation fixes, examples, formats and image processing algorithms.
|
||||
|
||||
Some ideas for improvement listed in [the issue tracker](https://travis-ci.org/mike42/gfx-php). Code contributions must be releasable under the LGPLv3 or later.
|
||||
|
||||
### Scope
|
||||
|
||||
As a small project, we can't do everything. In particular, `gfx-php` is not likely to ever perform non-raster operations:
|
||||
|
||||
- vector image formats (PDF, SVG, EPS, etc).
|
||||
- anything involving vector fonts
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
This repository uses test files from other projects:
|
||||
|
||||
- [BMP Suite](http://entropymine.com/jason/bmpsuite/) by Jason Summers.
|
||||
- [PyGIF](https://github.com/robert-ancell/pygif) test suite by Robert Ancell.
|
||||
- [pngsuite](http://www.schaik.com/pngsuite/) by Willem van Schaik.
|
||||
|
||||
## Similar projects
|
||||
|
||||
- [Imagine](https://github.com/avalanche123/Imagine), which wraps available libraries.
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "mike42/gfx-php",
|
||||
"description": "The pure PHP graphics library",
|
||||
"homepage": "https://github.com/mike42/gfx-php",
|
||||
"type": "library",
|
||||
"license": "LGPL-2.1-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Billington",
|
||||
"email": "michael.billington@gmail.com"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.0.0"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mike42\\": "src/Mike42"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^6.5",
|
||||
"squizlabs/php_codesniffer": "^3.3.1",
|
||||
"phpbench/phpbench": "@dev"
|
||||
}
|
||||
}
|
||||
+2601
File diff suppressed because it is too large
Load Diff
Vendored
+2494
File diff suppressed because it is too large
Load Diff
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = gfx-php
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@@ -0,0 +1,4 @@
|
||||
{%- extends "sphinx_rtd_theme/breadcrumbs.html" %}
|
||||
|
||||
{% block breadcrumbs_aside %}
|
||||
{% endblock %}
|
||||
Vendored
+173
@@ -0,0 +1,173 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'gfx-php'
|
||||
copyright = '2018, Michael Billington'
|
||||
author = 'Michael Billington'
|
||||
|
||||
# The short X.Y version
|
||||
version = ''
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = ''
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinxcontrib.phpdomain"
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path .
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'gfx-phpdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'gfx-php.tex', 'gfx-php Documentation',
|
||||
'Michael Billington', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'gfx-php', 'gfx-php Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'gfx-php', 'gfx-php Documentation',
|
||||
author, 'gfx-php', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# PHP Syntax
|
||||
from sphinx.highlighting import lexers
|
||||
from pygments.lexers.web import PhpLexer
|
||||
lexers["php"] = PhpLexer(startinline=True, linenos=1)
|
||||
lexers["php-annotations"] = PhpLexer(startinline=True, linenos=1)
|
||||
|
||||
# Set domain
|
||||
primary_domain = "php"
|
||||
|
||||
# Regenerate API docs via doxygen + doxyphp2sphinx
|
||||
import subprocess, os
|
||||
read_the_docs_build = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
if read_the_docs_build:
|
||||
subprocess.call(['doxygen', 'Doxyfile'])
|
||||
subprocess.call(['doxyphp2sphinx', 'Mike42::GfxPhp'])
|
||||
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
The pure PHP graphics library
|
||||
=====================================
|
||||
|
||||
This library implements input, output and processing of raster images in pure
|
||||
PHP, so that image processing PHP extensions are not required.
|
||||
|
||||
This allows developers to eliminate some portability issues from their applications.
|
||||
|
||||
The basic usage is like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Mike42\GfxPhp\Image;
|
||||
$img = Image::fromFile("colorwheel256.png");
|
||||
$img -> write("test.gif");
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Format support includes PNG, GIF, BMP and the Netpbm formats.
|
||||
- Support for scaling, cropping, format conversion and colorspace transformations.
|
||||
- Pure PHP: This library does not require Gd, ImageMagick or GraphicsMagick extensions.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install gfx-php by running:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
composer install mike42/gfx-php
|
||||
|
||||
Contribute
|
||||
----------
|
||||
|
||||
- Issue Tracker: https://github.com/mike42/gfx-php/issues
|
||||
- Source Code: https://github.com/mike42/gfx-php
|
||||
|
||||
Navigation
|
||||
==========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: User Documentation
|
||||
|
||||
user/formats.rst
|
||||
user/imagetypes.rst
|
||||
user/operations.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Project Information
|
||||
|
||||
project/license.rst
|
||||
project/contributing.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: API Documentation
|
||||
|
||||
Classes <api.rst>
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
Contributor guide
|
||||
=================
|
||||
|
||||
This project is open source, and is built on a voluntary basis by developers like you. You can help to improve it by contributing code & documentation to the repository, or by interacting with other developers and users on the issue tracker.
|
||||
|
||||
Where to ask a question
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you need to seek clarification, then you are welcome to post questions about the library to the `gfx-php issue tracker`_. They will be tagged ``question``, and closed once there is an answer.
|
||||
|
||||
If your question is incomplete or not specific to this project, then it will be tagged ``invalid`` and closed with a short explanation. We do this to keep the issue tracker focussed, on-topic and actionable by project contributors.
|
||||
|
||||
The StackOverflow help page `How do I ask a good question?`_ contains advice about writing complete questions.
|
||||
|
||||
.. _`How do I ask a good question?`: https://stackoverflow.com/help/how-to-ask
|
||||
|
||||
How to report a bug
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
We track bugs as GitHub issues. If something does not work as documented, then you are welcome to make a bug report on the `gfx-php issue tracker`_
|
||||
|
||||
When posting a bug:
|
||||
|
||||
- Check for open issues first. If the same issue has already been reported, then you should post any additional information to the existing thread.
|
||||
- Describe what you are trying to do, and how the actual behaviour of the library differs from what you expected.
|
||||
- Include a self-contained code snippet that demonstrates the issue, as PHP code formatted in a `code block`_
|
||||
- Try to use images from the repository to demonstrate the problem. If you need to use a specific example image, then attach it to the issue in a ``.zip`` file.
|
||||
|
||||
Bug reports will stay open as long as they are actionable. Generally, this means that they can be replicated on the current stable release, and there is some expectation that the issue is solvable.
|
||||
|
||||
The title and tags on your bug may be edited so that it can be distinguished from other bugs.
|
||||
|
||||
.. _`code block`: https://help.github.com/articles/creating-and-highlighting-code-blocks/
|
||||
.. _`gfx-php issue tracker`: https://github.com/mike42/gfx-php/issues
|
||||
|
||||
Feature requests
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
You can also make suggestions for new features on the `gfx-php issue tracker`_. These are tagged ``enhancement``.
|
||||
|
||||
Please keep the scope and resources of the project in mind when making suggestions.
|
||||
|
||||
Development process
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The project is hosted online on the services below:
|
||||
|
||||
:Code:
|
||||
https://github.com/mike42/gfx-php
|
||||
:Continuous integration:
|
||||
https://travis-ci.org/mike42/gfx-php
|
||||
:Code coverage reporting:
|
||||
https://coveralls.io/github/mike42/gfx-php
|
||||
:Documentation:
|
||||
https://gfx-php.readthedocs.io/
|
||||
:Package manager:
|
||||
https://packagist.org/packages/mike42/gfx-php
|
||||
|
||||
For a change to be accepted, it will first need to meet some basic technical criteria, such as passing existing unit tests, a style check, and not breaking any of the examples.
|
||||
|
||||
Secondly, it will need to pass a human review, to confirm that it improves the overall product. You are encouraged to submit changes which address one open issue, so that this review can be as constructive as possible.
|
||||
|
||||
Commands to use locally
|
||||
-----------------------
|
||||
|
||||
To make code changes, fork the repository on GitHub, and set up
|
||||
your local copy with composer.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
composer install
|
||||
|
||||
To run unit tests, execute:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
php vendor/bin/phpunit --coverage-text
|
||||
|
||||
To test all examples:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir -p tmp && (cd tmp && find ../example -name '*.php' -print0 | xargs -n 1 -0 sh -c 'echo $0; php $0 || exit 255')
|
||||
|
||||
To run a style check and fix formatting issues:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
php vendor/bin/phpcs --standard=psr2 src/ -n
|
||||
php vendor/bin/phpcbf --standard=psr2 src/ -n
|
||||
|
||||
Submitting changes
|
||||
------------------
|
||||
|
||||
Changes should be submitted as a GitHub pull request to the ``master`` branch.
|
||||
|
||||
Licensing considerations
|
||||
------------------------
|
||||
|
||||
You are not required to assign copyright for contributions to this project, but
|
||||
you do need ensure that your changes are suitable for release under the project's
|
||||
`copyleft` license.
|
||||
|
||||
If you hold the copyright to the submitted code, then indicate this in your pull request.
|
||||
|
||||
If you are thinking of including code which you do not hold the copyright to,
|
||||
please post relevant details to the issue tracker first. Only works which are
|
||||
licensed under an LGPL-2.1-compatible license may be combined with `gfx-php` code.
|
||||
|
||||
Release process
|
||||
---------------
|
||||
|
||||
Git tags are automatically reflected as releases in packagist.
|
||||
|
||||
Release numbers approximately follow semantic versioning, and do not follow a particular schedule.
|
||||
|
||||
Updates are not typically made to old releases.
|
||||
@@ -0,0 +1,4 @@
|
||||
License
|
||||
=======
|
||||
|
||||
.. literalinclude:: ../../LICENSE
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
Sphinx==1.7.4
|
||||
sphinx-rtd-theme==0.3.0
|
||||
sphinxcontrib-phpdomain==0.4.1
|
||||
doxyphp2sphinx>=1.0.1
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
File formats
|
||||
============
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Input formats
|
||||
-------------
|
||||
|
||||
Files are read from a file or URL by using the :meth:`Image::fromFile()` function:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
$tux = Image::fromFile("tux.png")
|
||||
|
||||
If the your image is not being read from a file, then :meth:`Image::fromBlob()` can load it from a binary string:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
$tuxStr = "...";
|
||||
$tux = Image::fromBlob($tuxStr, "tux.png");
|
||||
|
||||
In either case, the input format is determined using the file's `magic number`_.
|
||||
|
||||
.. _magic number: https://en.wikipedia.org/wiki/Magic_number_(programming)
|
||||
|
||||
PNG
|
||||
^^^
|
||||
|
||||
The PNG codec is used where the input has the ``png`` file extension.
|
||||
|
||||
All valid PNG files can be read, including:
|
||||
|
||||
- RGB or RGBA images
|
||||
- Indexed images
|
||||
- Monochrome images, from 1 to 16 bits per pixel
|
||||
- Interlaced images
|
||||
|
||||
This library currently has limited support for transparency, and will discard any alpha channel from a PNG file when it is loaded.
|
||||
|
||||
GIF
|
||||
^^^
|
||||
|
||||
The GIF codec is used where the input has the ``gif`` file extension. Any well-formed GIF file can be read, but there are some limitations:
|
||||
|
||||
- If a GIF file contains multiple images, then only the first one will be loaded
|
||||
- If a transparent color is present, then this will be mixed to white
|
||||
|
||||
A GIF image will always be loaded into an instance of :class:`IndexedRasterImage`, which makes palette information available.
|
||||
|
||||
BMP
|
||||
^^^
|
||||
|
||||
The BMP codec is used where the input has the ``bmp`` or ``dib`` file extensions. Most uncompressed or run-length encoded bitmap files can be read by this library.
|
||||
|
||||
The returned object will be an instance of instance of :class:`IndexedRasterImage` if the color depth is 8 or lower.
|
||||
|
||||
24-bit, 16-bit and 32-bit bitmap images are also supported, and will be loaded into an instance of :class:`RgbRasterImage`. If an alpha channel is used, then it will be mixed to white.
|
||||
|
||||
Netpbm Formats
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The Netpbm formats are a series of uncompressed bitmap formats, which can represent most types of image. These formats can be read by ``gfx-php``:
|
||||
|
||||
:PNM: This is a file extension only. Files carrying ``.pnm`` extension can carry any of the below formats.
|
||||
:PPM: This is a color raster format. A PPM file is identified by the P6 magic number, and will be loaded into an instance of :class:`RgbRasterImage`.
|
||||
:PGM: This is a monochrome raster format. A PGM file is identified by the P5 magic number, and will be loaded instance of :class:`GrayscaleRasterImage`.
|
||||
:PBM: This is a 1-bit bitmap format. A PBM file is identified by the P4 header, and loaded into an instance of :class:`BlackAndWhiteRasterImage`.
|
||||
|
||||
Each of these formats has both a binary and text encoding. ``gfx-php`` only supports the binary encodings at this stage.
|
||||
|
||||
WBMP
|
||||
^^^
|
||||
|
||||
The WBMP codec is used where the input has the ``wbmp`` file extension. A WBMP image will always be loaded into a :class:`BlackAndWhiteRasterImage` object.
|
||||
|
||||
Output formats
|
||||
--------------
|
||||
|
||||
When you write a :class:`RasterImage` to a file, you need to specify a filename. The extension on this file is used to determine the desired output format.
|
||||
|
||||
There is currently no mechanism to write a file directly to a string.
|
||||
|
||||
PNG
|
||||
^^^
|
||||
|
||||
The PNG format is selected by using the ``png`` file extension when you call :func:`RasterImage::write()`.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$tux -> write("tux.png");
|
||||
|
||||
This library will currently output PNG files as RGB data. If you write to PNG from an instance of :class:`RgbRasterImage`, then no conversion has to be done, so the output is significantly faster.
|
||||
|
||||
GIF
|
||||
^^^
|
||||
|
||||
The GIF format is selected by using the ``gif`` file extension.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$tux -> write("tux.gif");
|
||||
|
||||
This format is limited to using a 256-color palette.
|
||||
|
||||
- If your image is not an `IndexedRasterImage`, then it is indexed when you write.
|
||||
- If the image uses more than 256 colors, then it will be converted to an 8-bit RGB representation (3 bits red, 3 bits green, 2 bits blue), which introduces some distortions.
|
||||
|
||||
When you are creating GIF images, then you can avoid these conversions by using a :class:`IndexedRasterImage` with a palette of fewer than 256 colors.
|
||||
|
||||
There is no encoder for multi-image GIF files at this stage.
|
||||
|
||||
BMP
|
||||
^^^
|
||||
|
||||
The BMP format is selected by using the ``bmp`` file extension.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$tux -> write("tux.bmp");
|
||||
|
||||
This library will currently output BMP files using an uncompressed 24-bit RGB representation of the image.
|
||||
|
||||
WBMP
|
||||
^^^
|
||||
|
||||
The WBMP format is selected by using the ``wbmp`` file extension.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$tux -> write("tux.wbmp");
|
||||
|
||||
The image will be converted to a 1-bit monochrome representation, which is the only type of image supported by WBMP.
|
||||
|
||||
Netpbm Formats
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The Netpbm formats can be used for output. Each format is identified by their respective file extension:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$tux -> write("tux.ppm");
|
||||
$tux -> write("tux.pgm";
|
||||
$tux -> write("tux.pbm");
|
||||
|
||||
Since each of these formats has a different raster data representation, you should be aware that
|
||||
|
||||
:PPM: For this output format, the file is converted to a :class:`RgbRasterImage` and typically written with a 24 bit color depth. In some cases, a 48 bit color depth will be used.
|
||||
:PGM: The file is converted to a :class:`GrayscaleRasterImage` and written with a depth of 8 or 16 bits per pixel.
|
||||
:PPM: The file is converted to a :class:`BlackAndWhiteRasterImage` and written with 1 bit per pixel.
|
||||
|
||||
If you want to avoid these conversions, then you should use the ``pnm`` extension to write your files. Since files with this extension can hold any of the above formats, the output encoder will avoid converting the raster data where possible.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$tux -> write("tux.pnm");
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
Image types
|
||||
===========
|
||||
|
||||
Every raster image in ``gfx-php`` implements the :class:`RasterImage` interface.
|
||||
|
||||
There are several classes that implement this interface, which handle different representations of
|
||||
the image data.
|
||||
|
||||
:class:`RgbRasterImage`
|
||||
Holds RGB data.
|
||||
:class:`GrayscaleRasterImage`
|
||||
Holds monochrome data.
|
||||
:class:`BlackAndWhiteRasterImage`
|
||||
Holds 1-bit raster data.
|
||||
:class:`IndexedRasterImage`
|
||||
Holds an image and associated palette.
|
||||
|
||||
Creating an image
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Each of these classes has a static method which can be used to create an image of that type.
|
||||
|
||||
These only require a ``width`` and ``height``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
|
||||
$image = BlackAndWhiteRasterImage::create(50, 100);
|
||||
|
||||
Converting between image types
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can convert between image types. This is similar to performing a `color-space conversion` in an image editor.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
$image = Image::load("tux.ppm");
|
||||
$image -> toBlackAndWhite();
|
||||
|
||||
The methods to use are:
|
||||
|
||||
- :meth:`RasterImage::toBlackAndWhite()`
|
||||
- :meth:`RasterImage::toGrayscale()`
|
||||
- :meth:`RasterImage::toIndexed()`
|
||||
- :meth:`RasterImage::toRgb()`
|
||||
|
||||
Each of these returns an image of the requested type. They work by instantiating a new image, then copying across the data as accurately as possible. As a result, the original image is unmodified.
|
||||
|
||||
Implicit conversions
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Some file formats only accept specific types of raster data, so the :meth:`RasterImage::write()` method will need to convert it. For example, this ``.pbm`` will be limited to 2 colors, which is achieved by using :meth:`RasterImage:toBlackAndWhite` in the background:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
$wheel = Image::load("colorwheel.ppm");
|
||||
$wheel -> write("wheel.pbm");
|
||||
|
||||
Since converting the color space creates a new image, the image stored in ``$wheel`` is unmodified.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
Image operations
|
||||
================
|
||||
|
||||
You can use this library to perform basic raster operations, such as:
|
||||
|
||||
- Crop
|
||||
- Scale / resize
|
||||
- Color-space conversions
|
||||
|
||||
See the ``example/`` sub-folder `on GitHub`_ for usage.
|
||||
|
||||
.. _`on GitHub`: https://github.com/mike42/gfx-php/tree/master/example
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require_once(__DIR__ . "/../vendor/autoload.php");
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
// Inputs
|
||||
$outFile = "out.pbm";
|
||||
$font = Image::fromFile(dirname(__FILE__). "/resources/5x7hex.pbm");
|
||||
$codePoint = str_split("0A2F");
|
||||
$charWidth = 5;
|
||||
$charHeight = 7;
|
||||
|
||||
// Create small image for each character
|
||||
$chars = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
|
||||
$subImages = [];
|
||||
for ($i = 0; $i < count($codePoint); $i++) {
|
||||
$id = array_search($codePoint[$i], $chars);
|
||||
if ($id === false) {
|
||||
throw new Exception("Don't know how to encode " . $codePoint[$i]);
|
||||
}
|
||||
$subImages[] = $font -> subImage($id * $charWidth, 0, $charWidth, $charHeight);
|
||||
}
|
||||
|
||||
// Place four images in a box
|
||||
$out = Image::create(18, 17, Image::IMAGE_BLACK_WHITE);
|
||||
$out -> rect(0, 0, 18, 17);
|
||||
$out -> rect(3, 0, 12, 17, true, 0, 0);
|
||||
$out -> compose($subImages[0], 0, 0, 4, 1, $charWidth, $charHeight);
|
||||
$out -> compose($subImages[1], 0, 0, 10, 1, $charWidth, $charHeight);
|
||||
$out -> compose($subImages[2], 0, 0, 4, 9, $charWidth, $charHeight);
|
||||
$out -> compose($subImages[3], 0, 0, 10, 9, $charWidth, $charHeight);
|
||||
|
||||
# Print output for debugging ;)
|
||||
echo $out -> toString();
|
||||
$out -> write($outFile);
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
require_once(__DIR__ . "/../vendor/autoload.php");
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
// Write colorwheel.ppm out as each supported format
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/colorwheel.ppm");
|
||||
$img -> write("colorwheel.bmp");
|
||||
$img -> write("colorwheel.gif");
|
||||
$img -> write("colorwheel.pbm");
|
||||
$img -> write("colorwheel.pgm");
|
||||
$img -> write("colorwheel.png");
|
||||
$img -> write("colorwheel.ppm");
|
||||
$img -> write("colorwheel.wbmp");
|
||||
|
||||
// Write gradient.pgm out as each supported format
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/gradient.pgm");
|
||||
$img -> write("gradient.bmp");
|
||||
$img -> write("gradient.gif");
|
||||
$img -> write("gradient.pbm");
|
||||
$img -> write("gradient.pgm");
|
||||
$img -> write("gradient.png");
|
||||
$img -> write("gradient.ppm");
|
||||
$img -> write("gradient.wbmp");
|
||||
|
||||
// Write 5x7hex.pbm out as each supported format
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/5x7hex.pbm");
|
||||
$img -> write("font.bmp");
|
||||
$img -> write("font.gif");
|
||||
$img -> write("font.pbm");
|
||||
$img -> write("font.pgm");
|
||||
$img -> write("font.png");
|
||||
$img -> write("font.ppm");
|
||||
$img -> write("font.wbmp");
|
||||
|
||||
// Write abc.png out as each supported format
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/abc.png");
|
||||
$img -> write("abc.bmp");
|
||||
$img -> write("abc.gif");
|
||||
$img -> write("abc.pbm");
|
||||
$img -> write("abc.pgm");
|
||||
$img -> write("abc.png");
|
||||
$img -> write("abc.ppm");
|
||||
$img -> write("abc.wbmp");
|
||||
|
||||
// Write bricks.wbmp out as each supported format
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/bricks.wbmp");
|
||||
$img -> write("bricks.bmp");
|
||||
$img -> write("bricks.gif");
|
||||
$img -> write("bricks.pbm");
|
||||
$img -> write("bricks.pgm");
|
||||
$img -> write("bricks.png");
|
||||
$img -> write("bricks.ppm");
|
||||
$img -> write("bricks.wbmp");
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require_once(__DIR__ . "/../vendor/autoload.php");
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
// Write original back
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/5x7hex.pbm");
|
||||
$img -> write("font-original.pbm");
|
||||
|
||||
// Scale a black and white image
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/5x7hex.pbm");
|
||||
$img2 = $img -> scale(40, 4);
|
||||
$img2 -> write("font-small.pbm");
|
||||
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/5x7hex.pbm");
|
||||
$img3 = $img -> scale(160, 14);
|
||||
$img3 -> write("font-large.pbm");
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require_once(__DIR__ . "/../vendor/autoload.php");
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
// Write original back
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/gradient.pgm");
|
||||
$img -> write("gradient-original.pgm");
|
||||
|
||||
// Scale a gray image
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/gradient.pgm");
|
||||
$img2 = $img -> scale(20, 20);
|
||||
$img2 -> write("gradient-small.pgm");
|
||||
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/gradient.pgm");
|
||||
$img3 = $img -> scale(60, 60);
|
||||
$img3 -> write("gradient-large.pgm");
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require_once(__DIR__ . "/../vendor/autoload.php");
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
// Write original back
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/colorwheel.ppm");
|
||||
$img -> write("colorwheel-original.ppm");
|
||||
|
||||
// Scale a color image
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/colorwheel.ppm");
|
||||
$img2 = $img -> scale(20, 20);
|
||||
$img2 -> write("colorwheel-small.ppm");
|
||||
|
||||
$img = Image::fromFile(dirname(__FILE__). "/resources/colorwheel.ppm");
|
||||
$img3 = $img -> scale(60, 60);
|
||||
$img3 -> write("colorwheel-large.ppm");
|
||||
@@ -0,0 +1,5 @@
|
||||
P4
|
||||
80 7
|
||||
a9ÉyÞg™ÇsÞ“)B”¥(J‘)B”¥(J‘=ÇsÂg½ÈKœ‘ !
|
||||
D�¥(J‘ !
|
||||
D�¥(Jc½ÁsÄg%ÇsÐ
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 794 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"php_disable_ini": true,
|
||||
"bootstrap": "vendor/autoload.php",
|
||||
"path": "test/benchmark",
|
||||
"php_config": {
|
||||
"extension": [ "json.so" ]
|
||||
},
|
||||
"outputs": {
|
||||
"benchmarks_file": {
|
||||
"extends": "html",
|
||||
"file": "benchmarks.html",
|
||||
"title": "gfx-php benchmarks"
|
||||
}
|
||||
},
|
||||
"reports": {
|
||||
"my_report": {
|
||||
"extends": "aggregate"
|
||||
}
|
||||
},
|
||||
"time_unit": "microseconds"
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
<phpunit bootstrap="test/bootstrap.php"
|
||||
timeoutForSmallTests="1"
|
||||
timeoutForMediumTests="10"
|
||||
timeoutForLargeTests="60"
|
||||
verbose="true"
|
||||
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="unit">
|
||||
<directory>test/unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="integration">
|
||||
<directory>test/integration</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
use Mike42\GfxPhp\Codec\ImageCodec;
|
||||
|
||||
abstract class AbstractRasterImage implements RasterImage
|
||||
{
|
||||
/**
|
||||
* Produce a rectangle with the given properties.
|
||||
*
|
||||
* @param int $fill
|
||||
*/
|
||||
public function rect($startX, $startY, $width, $height, $filled = false, $outline = 1, $fill = 1)
|
||||
{
|
||||
$this -> horizontalLine($startY, $startX, $startX + $width - 1, $outline);
|
||||
$this -> horizontalLine($startY + $height - 1, $startX, $startX + $width - 1, $outline);
|
||||
$this -> verticalLine($startX, $startY, $startY + $height - 1, $outline);
|
||||
$this -> verticalLine($startX + $width - 1, $startY, $startY + $height - 1, $outline);
|
||||
if ($filled) {
|
||||
// Fill center of the rectangle
|
||||
for ($y = $startY + 1; $y < $startY + $height - 1; $y++) {
|
||||
for ($x = $startX + 1; $x < $startX + $width - 1; $x++) {
|
||||
$this -> setPixel($x, $y, $fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function horizontalLine($y, $startX, $endX, $outline)
|
||||
{
|
||||
for ($x = $startX; $x <= $endX; $x++) {
|
||||
$this -> setPixel($x, $y, $outline);
|
||||
}
|
||||
}
|
||||
|
||||
protected function verticalLine($x, $startY, $endY, $outline)
|
||||
{
|
||||
for ($y = $startY; $y <= $endY; $y++) {
|
||||
$this -> setPixel($x, $y, $outline);
|
||||
}
|
||||
}
|
||||
|
||||
public function write(string $filename)
|
||||
{
|
||||
// Use file extension to decide output codec
|
||||
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
if ($ext === null) {
|
||||
throw new \Exception("Cannot write '$filename': No file extension.");
|
||||
}
|
||||
$encoder = ImageCodec::getInstance() -> getEncoderForFormat($ext);
|
||||
$blob = $encoder -> encode($this, $ext);
|
||||
file_put_contents($filename, $blob);
|
||||
}
|
||||
|
||||
protected function createCanvas(int $width, int $height) : RasterImage
|
||||
{
|
||||
return $this::create($width, $height);
|
||||
}
|
||||
|
||||
public function scale(int $width, int $height) : RasterImage
|
||||
{
|
||||
$img = $this -> createCanvas($width, $height);
|
||||
$thisWidth = $this -> getWidth();
|
||||
$thisHeight = $this -> getHeight();
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
$srcX = intdiv($x * $thisWidth, $width);
|
||||
$srcY = intdiv($y * $thisHeight, $height);
|
||||
$srcColor = $this -> getPixel($srcX, $srcY);
|
||||
$destColor = $this -> mapColor($srcColor, $img);
|
||||
$img -> setPixel($x, $y, $destColor);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function subImage(int $startX, int $startY, int $width, int $height) : RasterImage
|
||||
{
|
||||
$ret = $this::create($width, $height);
|
||||
$ret -> compose($this, $startX, $startY, 0, 0, $width, $height);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function compose(RasterImage $source, int $startX, int $startY, int $destStartX, int $destStartY, int $width, int $height)
|
||||
{
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
$srcY = $y + $startY;
|
||||
$destY = $y + $destStartY;
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
$srcX = $x + $startX;
|
||||
$destX = $x + $destStartX;
|
||||
$this -> setPixel($destX, $destY, $source -> getPixel($srcX, $srcY));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
/**
|
||||
* Small implementation of basic raster operations on PBM files to support
|
||||
* creation of placeholder glyphs
|
||||
*/
|
||||
class BlackAndWhiteRasterImage extends AbstractRasterImage
|
||||
{
|
||||
protected $width;
|
||||
|
||||
protected $bytesPerRow;
|
||||
|
||||
protected $height;
|
||||
|
||||
protected $data;
|
||||
|
||||
public function invert()
|
||||
{
|
||||
array_walk($this -> data, 'self::invertByte');
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
array_walk($this -> data, 'self::clearByte');
|
||||
}
|
||||
|
||||
protected static function invertByte(int &$item, $key)
|
||||
{
|
||||
$item = ~ $item;
|
||||
}
|
||||
|
||||
protected static function clearByte(int &$item, $key)
|
||||
{
|
||||
$item = 0;
|
||||
}
|
||||
|
||||
public function getWidth() : int
|
||||
{
|
||||
return $this -> width;
|
||||
}
|
||||
|
||||
public function getHeight() : int
|
||||
{
|
||||
return $this -> height;
|
||||
}
|
||||
|
||||
|
||||
public function setPixel(int $x, int $y, int $value)
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return;
|
||||
}
|
||||
$byte = $y * $this -> bytesPerRow + intdiv($x, 8);
|
||||
$bit = $x % 8;
|
||||
if ($value === 0) {
|
||||
// Clear
|
||||
$this -> data[$byte] &= ~(1 << (7 - $bit));
|
||||
} else {
|
||||
// Set
|
||||
$this -> data[$byte] |= (1 << (7 - $bit));
|
||||
}
|
||||
}
|
||||
|
||||
public function getPixel(int $x, int $y) : int
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return 0;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return 0;
|
||||
}
|
||||
$byte = $y * $this -> bytesPerRow + intdiv($x, 8);
|
||||
$bit = $x % 8;
|
||||
return ($this -> data[$byte] >> (7 - $bit)) & 0x01;
|
||||
}
|
||||
|
||||
protected function __construct($width, $height, array $data)
|
||||
{
|
||||
$this -> width = $width;
|
||||
$this -> height = $height;
|
||||
$this -> data = $data;
|
||||
$this -> bytesPerRow = intdiv($width + 7, 8);
|
||||
}
|
||||
|
||||
public static function create($width, $height, array $data = null) : BlackAndWhiteRasterImage
|
||||
{
|
||||
$bytesPerRow = intdiv($width + 7, 8);
|
||||
$expectedBytes = $bytesPerRow * $height;
|
||||
if ($data === null) {
|
||||
$data = array_values(array_fill(0, $expectedBytes, 0));
|
||||
}
|
||||
return new BlackAndWhiteRasterImage($width, $height, $data);
|
||||
}
|
||||
|
||||
public function toString()
|
||||
{
|
||||
$out = "";
|
||||
for ($y = 0; $y < $this -> getHeight(); $y += 2) {
|
||||
for ($x = 0; $x < $this -> getWidth(); $x++) {
|
||||
$upper = $this -> getPixel($x, $y) == 1;
|
||||
$lower = $this -> getPixel($x, $y + 1) == 1;
|
||||
if ($upper && $lower) {
|
||||
$char = "█";
|
||||
} else if ($upper) {
|
||||
$char = "▀";
|
||||
} else if ($lower) {
|
||||
$char = "▄";
|
||||
} else {
|
||||
$char = " ";
|
||||
}
|
||||
$out .= $char;
|
||||
}
|
||||
$out .= "\n";
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function getRasterData(): string
|
||||
{
|
||||
return pack("C*", ... $this -> data);
|
||||
}
|
||||
|
||||
public function mapColor(int $srcColor, RasterImage $destImage)
|
||||
{
|
||||
if ($destImage instanceof BlackAndWhiteRasterImage) {
|
||||
return $srcColor;
|
||||
}
|
||||
throw new \Exception("Cannot map colors");
|
||||
}
|
||||
|
||||
|
||||
public function toRgb() : RgbRasterImage
|
||||
{
|
||||
$img = RgbRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> getPixel($x, $y);
|
||||
$img -> setPixel($x, $y, $original == 0 ? 16777215 : 0);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toGrayscale() : GrayscaleRasterImage
|
||||
{
|
||||
$img = GrayscaleRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> getPixel($x, $y);
|
||||
$img -> setPixel($x, $y, $original == 0 ? 255 : 0);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toBlackAndWhite() : BlackAndWhiteRasterImage
|
||||
{
|
||||
return clone $this;
|
||||
}
|
||||
|
||||
public function toIndexed(): IndexedRasterImage
|
||||
{
|
||||
$width = $this -> width;
|
||||
$height = $this -> height;
|
||||
$pixelData = array_fill(0, $width * $height, 0);
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
$pixelData[$y * $width + $x] = $this -> getPixel($x, $y);
|
||||
}
|
||||
}
|
||||
$colorTable = PaletteGenerator::blackAndWhitePalette();
|
||||
return IndexedRasterImage::create($width, $height, $pixelData, $colorTable, 255);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
class BmpBitfieldDecoder
|
||||
{
|
||||
private $bitfields;
|
||||
|
||||
public function __construct(BmpColorBitfield $bitfields)
|
||||
{
|
||||
$this -> bitfields = $bitfields;
|
||||
}
|
||||
|
||||
public function read16bit(array $inpBytes) : array
|
||||
{
|
||||
$red = $this -> bitfields -> getRed();
|
||||
$green = $this -> bitfields -> getGreen();
|
||||
$blue = $this -> bitfields -> getBlue();
|
||||
$alpha = $this -> bitfields -> getAlpha();
|
||||
$hasAlpha = $alpha -> getMaxValue() > 0;
|
||||
// Fill output array to 1.5 times the size of the input array
|
||||
$pixelCount = intdiv(count($inpBytes), 2);
|
||||
$outpBytes = array_fill(0, $pixelCount * 3, 0);
|
||||
for ($i = 0; $i < $pixelCount; $i++) {
|
||||
// Extract little-endian color code in 16 bit space
|
||||
$inpColor = ($inpBytes[$i * 2 + 1] << 8) + ($inpBytes[$i * 2]);
|
||||
$outpBytes[$i * 3] = $red -> getNormalisedValue($inpColor);
|
||||
$outpBytes[$i * 3 + 1] = $green -> getNormalisedValue($inpColor);
|
||||
$outpBytes[$i * 3 + 2] = $blue -> getNormalisedValue($inpColor);
|
||||
$alphaLevel = $alpha -> getNormalisedValue($inpColor);
|
||||
if ($hasAlpha && $alphaLevel < 255) {
|
||||
// Mix alpha to white if necessary
|
||||
$outpBytes[$i * 3] = $this -> mixChannel(255, $outpBytes[$i * 3], $alphaLevel);
|
||||
$outpBytes[$i * 3 + 1] = $this -> mixChannel(255, $outpBytes[$i * 3 + 1], $alphaLevel);
|
||||
$outpBytes[$i * 3 + 2] = $this -> mixChannel(255, $outpBytes[$i * 3 + 2], $alphaLevel);
|
||||
}
|
||||
}
|
||||
return $outpBytes;
|
||||
}
|
||||
|
||||
public function read32bit(array $inpBytes) : array
|
||||
{
|
||||
$red = $this -> bitfields -> getRed();
|
||||
$green = $this -> bitfields -> getGreen();
|
||||
$blue = $this -> bitfields -> getBlue();
|
||||
$alpha = $this -> bitfields -> getAlpha();
|
||||
$hasAlpha = $alpha -> getMaxValue() > 0;
|
||||
// Fill output array to 0.75 times the size of the input array
|
||||
$pixelCount = intdiv(count($inpBytes), 4);
|
||||
$outpBytes = array_fill(0, $pixelCount * 3, 0);
|
||||
for ($i = 0; $i < $pixelCount; $i++) {
|
||||
// Extract little-endian color code in 32 bit space
|
||||
$inpColor = ($inpBytes[$i * 4 + 3] << 24) + ($inpBytes[$i * 4 + 2] << 16) + ($inpBytes[$i * 4 + 1] << 8) + ($inpBytes[$i * 4]);
|
||||
$outpBytes[$i * 3] = $red -> getNormalisedValue($inpColor);
|
||||
$outpBytes[$i * 3 + 1] = $green -> getNormalisedValue($inpColor);
|
||||
$outpBytes[$i * 3 + 2] = $blue -> getNormalisedValue($inpColor);
|
||||
$alphaLevel = $alpha -> getNormalisedValue($inpColor);
|
||||
if ($hasAlpha && $alphaLevel < 255) {
|
||||
// Mix alpha to white if necessary
|
||||
$outpBytes[$i * 3] = $this -> mixChannel(255, $outpBytes[$i * 3], $alphaLevel);
|
||||
$outpBytes[$i * 3 + 1] = $this -> mixChannel(255, $outpBytes[$i * 3 + 1], $alphaLevel);
|
||||
$outpBytes[$i * 3 + 2] = $this -> mixChannel(255, $outpBytes[$i * 3 + 2], $alphaLevel);
|
||||
}
|
||||
}
|
||||
return $outpBytes;
|
||||
}
|
||||
|
||||
public function mixChannel(int $value1, int $value2, int $amount)
|
||||
{
|
||||
$amountMultiplier = $amount / 255.0;
|
||||
$result = (($value1 / 255.0) * $amountMultiplier) + (($value2 / 255.0) * (1.0 - $amountMultiplier));
|
||||
return (int)($result * 255.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
/**
|
||||
* Hold color mask information
|
||||
*/
|
||||
class BmpColorBitfield
|
||||
{
|
||||
private $red;
|
||||
private $green;
|
||||
private $blue;
|
||||
private $alpha;
|
||||
|
||||
public function __construct(BmpColorMask $red, BmpColorMask $green, BmpColorMask $blue, BmpColorMask $alpha)
|
||||
{
|
||||
// Reject overlapping chanel masks, probably indicating corrupt files.
|
||||
$tmp = 0;
|
||||
foreach ([$red, $green, $blue, $alpha] as $channel) {
|
||||
$mask = $channel->getMask();
|
||||
if (($tmp & $mask) != 0x00) {
|
||||
throw new \Exception("Color channel masks must not overlap");
|
||||
}
|
||||
$tmp |= $mask;
|
||||
}
|
||||
$this->red = $red;
|
||||
$this->green = $green;
|
||||
$this->blue = $blue;
|
||||
$this->alpha = $alpha;
|
||||
}
|
||||
|
||||
public function getRed()
|
||||
{
|
||||
return $this->red;
|
||||
}
|
||||
|
||||
public function getGreen()
|
||||
{
|
||||
return $this->green;
|
||||
}
|
||||
|
||||
public function getBlue()
|
||||
{
|
||||
return $this->blue;
|
||||
}
|
||||
|
||||
public function getAlpha()
|
||||
{
|
||||
return $this->alpha;
|
||||
}
|
||||
|
||||
public static function fromRgba(int $r, int $g, int $b, int $a)
|
||||
{
|
||||
return new BmpColorBitfield(new BmpColorMask($r), new BmpColorMask($g), new BmpColorMask($b), new BmpColorMask($a));
|
||||
}
|
||||
|
||||
public static function from16bitDefaults()
|
||||
{
|
||||
// If not specified, we use XRRRRRGG GGGBBBBB
|
||||
return self::fromRgba(0x7c00, 0x03e0, 0x001f, 0x0000);
|
||||
}
|
||||
|
||||
public static function from32bitDefaults()
|
||||
{
|
||||
// If not specified, we use XXXXXXXX RRRRRRRR GGGGGGGG BBBBBBBB
|
||||
return self::fromRgba(0x00ff0000, 0x0000ff00, 0x000000ff, 0x00000000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
class BmpColorMask
|
||||
{
|
||||
private $mask;
|
||||
private $len;
|
||||
private $offset;
|
||||
|
||||
public function __construct(int $mask)
|
||||
{
|
||||
$this->mask = $mask;
|
||||
$offset = 0;
|
||||
$len = 0;
|
||||
if ($mask != 0x00) {
|
||||
// Count 0's on the right of the mask
|
||||
while ((($mask >> $offset) & 0x01) == 0x00) {
|
||||
$offset++;
|
||||
}
|
||||
// Count length until nothing is left
|
||||
while ((($mask >> ($offset + $len)) & 0x01) == 0x01) {
|
||||
$len++;
|
||||
}
|
||||
// Report issues if there are other 1's, indicating a non-contiguous mask
|
||||
if (($mask >> ($offset + $len)) !== 0x00) {
|
||||
throw new \RuntimeException("Bad mask value, should be contiguous");
|
||||
}
|
||||
}
|
||||
$this->offset = $offset;
|
||||
$this->len = $len;
|
||||
}
|
||||
|
||||
public function getLen()
|
||||
{
|
||||
return $this->len;
|
||||
}
|
||||
|
||||
public function getOffset()
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
public function getMask()
|
||||
{
|
||||
return $this->mask;
|
||||
}
|
||||
|
||||
public function getValue(int $input)
|
||||
{
|
||||
// Read value 0 to max value
|
||||
return ($input & $this -> mask) >> $this -> offset;
|
||||
}
|
||||
|
||||
public function getMaxValue()
|
||||
{
|
||||
// Max value for size of this channel
|
||||
return (2 ** $this->len)- 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask out a value for this channel, and return its value in a 0-255 range
|
||||
*/
|
||||
public function getNormalisedValue(int $input)
|
||||
{
|
||||
// Get raw value, range depends on mask length
|
||||
$rawValue = ($input & ($this -> mask)) >> ($this -> offset);
|
||||
// Out of 255
|
||||
$normalisedValue = $rawValue;
|
||||
if ($this -> len < 8) {
|
||||
$normalisedValue = $rawValue << (8 - ($this->len));
|
||||
} else if ($this -> len > 8) {
|
||||
$normalisedValue = $rawValue >> (($this->len) - 8);
|
||||
}
|
||||
// Correct scaling up
|
||||
$repeat = ($normalisedValue >> ($this -> len));
|
||||
while ($repeat > 0) {
|
||||
$normalisedValue |= $repeat;
|
||||
$repeat >>= ($this -> len);
|
||||
}
|
||||
return $normalisedValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
use Exception;
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
use Mike42\GfxPhp\Codec\Png\PngImage;
|
||||
use Mike42\GfxPhp\IndexedRasterImage;
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
|
||||
class BmpFile
|
||||
{
|
||||
const BMP_SIGNATURE = "BM";
|
||||
|
||||
private $fileHeader;
|
||||
private $infoHeader;
|
||||
private $palette;
|
||||
private $uncompressedData;
|
||||
|
||||
public function __construct(BmpFileHeader $fileHeader, BmpInfoHeader $infoHeader, array $data, array $palette)
|
||||
{
|
||||
$this -> fileHeader = $fileHeader;
|
||||
$this -> infoHeader = $infoHeader;
|
||||
$this -> uncompressedData = $data;
|
||||
$this -> palette = $palette;
|
||||
}
|
||||
|
||||
public static function fromBinary(DataInputStream $data) : BmpFile
|
||||
{
|
||||
// Read two different headers
|
||||
$fileHeader = BmpFileHeader::fromBinary($data);
|
||||
$infoHeader = BmpInfoHeader::fromBinary($data);
|
||||
if ($infoHeader -> bpp != 0 &&
|
||||
$infoHeader -> bpp != 1 &&
|
||||
$infoHeader -> bpp != 2 &&
|
||||
$infoHeader -> bpp != 4 &&
|
||||
$infoHeader -> bpp != 8 &&
|
||||
$infoHeader -> bpp != 16 &&
|
||||
$infoHeader -> bpp != 24 &&
|
||||
$infoHeader -> bpp != 32) {
|
||||
throw new Exception("Bit depth " . $infoHeader -> bpp . " not valid.");
|
||||
} else if ($infoHeader -> bpp === 0) {
|
||||
// Fail early to give a clearer error: bit depth 0 is used for embedding PNG/JPEG in a bitmap
|
||||
throw new Exception("Bit depth " . $infoHeader -> bpp . " not supported.");
|
||||
}
|
||||
// See how many colors we expect. 2^n colors in table for bpp <= 8, 0 for higher color depths
|
||||
$colorCount = $infoHeader -> bpp <= 8 ? 2 ** $infoHeader -> bpp : 0;
|
||||
$colorTable = [];
|
||||
if (self::isOs21XBitmap($fileHeader, $infoHeader, $colorCount)) {
|
||||
// This type of image may only be 1, 4, 8 or 24 bit
|
||||
if ($infoHeader -> bpp != 1 &&
|
||||
$infoHeader -> bpp != 4 &&
|
||||
$infoHeader -> bpp != 8 &&
|
||||
$infoHeader -> bpp != 24) {
|
||||
throw new Exception("Bit depth " . $infoHeader->bpp . " not valid for OS/2 1.x bitmap.");
|
||||
}
|
||||
$calculatedTableSize = intdiv($fileHeader -> offset - (BmpInfoHeader::OS21XBITMAPHEADER_SIZE + BmpFileHeader::FILE_HEADER_SIZE), 3);
|
||||
if ($calculatedTableSize < $colorCount) {
|
||||
// Downsize the palette based on observed offset: only non-standard files do this.
|
||||
$colorCount = $calculatedTableSize;
|
||||
}
|
||||
// OS/2 1.x bitmaps use 3-bytes per color
|
||||
for ($i = 0; $i < $colorCount; $i++) {
|
||||
$entryData = $data->read(3);
|
||||
$color = unpack("C*", $entryData);
|
||||
$colorTable[] = [$color[3], $color[2], $color[1]];
|
||||
}
|
||||
// In the case of 1bpp or small palettes, it is possible that we are not aligned to a multiple of 4 bytes now.
|
||||
} else {
|
||||
if ($infoHeader -> colors > 0) {
|
||||
// .. override color count when specified
|
||||
$colorCount = $infoHeader -> colors;
|
||||
}
|
||||
for ($i = 0; $i < $colorCount; $i++) {
|
||||
$entryData = $data->read(4);
|
||||
$color = unpack("C*", $entryData);
|
||||
$colorTable[] = [$color[3], $color[2], $color[1]];
|
||||
}
|
||||
// May need to skip here if header shows pixel data later than we expect
|
||||
$calculatedOffset = $colorCount * 4 + $infoHeader -> headerSize + BmpFileHeader::FILE_HEADER_SIZE;
|
||||
if ($calculatedOffset > $fileHeader -> offset) {
|
||||
// Apparently we have read into the image data already, better to fail now before we run out of data later in the file
|
||||
throw new Exception("Header extends into image data. File is likely to be corrupt.");
|
||||
} else if ($calculatedOffset < $fileHeader -> offset) {
|
||||
// Skip up to the data.
|
||||
$data -> advance($fileHeader -> offset - $calculatedOffset);
|
||||
}
|
||||
}
|
||||
if ($infoHeader -> headerSize == BmpInfoHeader::OS22XBITMAPHEADER_FULL_SIZE || $infoHeader -> headerSize == BmpInfoHeader::OS22XBITMAPHEADER_MIN_SIZE) {
|
||||
// Some compression modes in OS/2 V2 bitmaps use the same numeric ID' as unrelated Windows BMP compression modes, but are not supported.
|
||||
if ($infoHeader -> compression != BmpInfoHeader::B1_RGB &&
|
||||
$infoHeader -> compression != BmpInfoHeader::B1_RLE4 &&
|
||||
$infoHeader -> compression != BmpInfoHeader::B1_RLE8) {
|
||||
throw new Exception("Compression method not implemented for OS/2 V2 bitmaps");
|
||||
}
|
||||
}
|
||||
// Determine compressed & uncompressed size
|
||||
$topDown = false;
|
||||
$height = $infoHeader -> height;
|
||||
if ($infoHeader -> height < 0 && $infoHeader -> compression == BmpInfoHeader::B1_RGB) {
|
||||
// negative heights are valid for uncompressed images, and indicate reversed row order
|
||||
$height = -$infoHeader -> height;
|
||||
$topDown = true;
|
||||
}
|
||||
if ($infoHeader -> width > 65535 || $height > 65535 || $infoHeader -> width < 0 || $height < 0) {
|
||||
// Limit height to prevent insane allocations during decompression if file is corrupt
|
||||
throw new Exception("Image size " . $infoHeader -> width . "x" . $height . " is outside the supported range.");
|
||||
}
|
||||
$rowSizeBytes = intdiv(($infoHeader -> bpp * $infoHeader -> width + 31), 32) * 4;
|
||||
$uncompressedImgSizeBytes = $rowSizeBytes * $height;
|
||||
if ($infoHeader -> compression == BmpInfoHeader::B1_RGB) {
|
||||
$compressedImgSizeBytes = $uncompressedImgSizeBytes;
|
||||
} else {
|
||||
$compressedImgSizeBytes = $infoHeader -> compressedSize;
|
||||
}
|
||||
$compressedImgData = $data -> read($compressedImgSizeBytes);
|
||||
// De-compress if necessary
|
||||
switch ($infoHeader -> compression) {
|
||||
case BmpInfoHeader::B1_RGB:
|
||||
case BmpInfoHeader::B1_BITFILEDS:
|
||||
case BmpInfoHeader::B1_ALPHABITFIELDS:
|
||||
$uncompressedImgData = $compressedImgData;
|
||||
break;
|
||||
case BmpInfoHeader::B1_RLE8:
|
||||
if ($infoHeader -> bpp !== 8) {
|
||||
throw new Exception("RLE8 compression only valid for 8-bit images");
|
||||
}
|
||||
$decoder = new Rle8Decoder();
|
||||
$uncompressedImgData = $decoder -> decode($compressedImgData, $infoHeader -> width, $height);
|
||||
$actualSize = strlen($uncompressedImgData);
|
||||
if ($uncompressedImgSizeBytes !== $actualSize) {
|
||||
throw new Exception("RLE8 decode failed. Expected $uncompressedImgSizeBytes bytes uncompressed, got $actualSize");
|
||||
}
|
||||
break;
|
||||
case BmpInfoHeader::B1_RLE4:
|
||||
if ($infoHeader -> bpp !== 4) {
|
||||
throw new Exception("RLE4 compression only valid for 4-bit images");
|
||||
}
|
||||
$decoder = new Rle4Decoder();
|
||||
$uncompressedImgData = $decoder -> decode($compressedImgData, $infoHeader -> width, $height);
|
||||
$actualSize = strlen($uncompressedImgData);
|
||||
if ($uncompressedImgSizeBytes !== $actualSize) {
|
||||
throw new Exception("RLE4 decode failed. Expected $uncompressedImgSizeBytes bytes uncompressed, got $actualSize");
|
||||
}
|
||||
break;
|
||||
case BmpInfoHeader::B1_JPEG:
|
||||
case BmpInfoHeader::B1_PNG:
|
||||
case BmpInfoHeader::B1_CMYK:
|
||||
case BmpInfoHeader::B1_CMYKRLE8:
|
||||
case BmpInfoHeader::B1_CMYKRLE4:
|
||||
throw new Exception("Compression method not implemented");
|
||||
default:
|
||||
throw new Exception("Bad compression method");
|
||||
}
|
||||
// Account for padding, row order
|
||||
$paddedLines = str_split($uncompressedImgData, $rowSizeBytes);
|
||||
$dataLines = [];
|
||||
$rowDataBytes = intdiv($infoHeader -> bpp * $infoHeader -> width + 7, 8); // Excludes padding bytes
|
||||
for ($i = count($paddedLines) - 1; $i >= 0; $i--) { // Iterate lines backwards
|
||||
$dataLines[] = substr($paddedLines[$i], 0, $rowDataBytes);
|
||||
}
|
||||
if ($topDown) {
|
||||
// Top-down storage (not as common), reverse them again.
|
||||
$dataLines = array_reverse($dataLines);
|
||||
}
|
||||
$uncompressedImgData = implode("", $dataLines);
|
||||
// Account for RGB vs BGR in file format
|
||||
if ($infoHeader -> bpp == 24) {
|
||||
$pixels = str_split($uncompressedImgData, 3);
|
||||
array_walk($pixels, ["\\Mike42\\GfxPhp\\Codec\\Bmp\\BmpFile", "transformRevString"]);
|
||||
$uncompressedImgData = implode("", $pixels);
|
||||
}
|
||||
// Convert to array of numbers 0-255.
|
||||
$dataArray = array_values(unpack("C*", $uncompressedImgData));
|
||||
if ($infoHeader -> profileSize > 0) {
|
||||
// Skip color profile if present after the image
|
||||
$imgEnd = $compressedImgSizeBytes + $fileHeader -> offset - BmpFileHeader::FILE_HEADER_SIZE;
|
||||
$profileStart = $infoHeader -> profileData;
|
||||
if ($profileStart >= $imgEnd) { // Profile may be before image data, in which case it's already been skipped
|
||||
$padding = $profileStart - $imgEnd;
|
||||
$data -> read($infoHeader -> profileSize + $padding);
|
||||
}
|
||||
}
|
||||
if (!$data -> isEof()) {
|
||||
throw new Exception("BMP image has unexpected trailing data");
|
||||
}
|
||||
return new BmpFile($fileHeader, $infoHeader, $dataArray, $colorTable);
|
||||
}
|
||||
|
||||
private static function isOs21XBitmap(BmpFileHeader $fileHeader, BmpInfoHeader $infoHeader, int $colorCount)
|
||||
{
|
||||
// OS/2 1.x bitmaps use 24 bits per entry in the color palette, rather than 32, but share the same 12-byte
|
||||
// header as original Windows bitmaps. If the header size, color count and offset to the bitmap data are
|
||||
// consistent with 24-bit color table, then this function returns true.
|
||||
if ($infoHeader -> headerSize !== BmpInfoHeader::OS21XBITMAPHEADER_SIZE) {
|
||||
// Wrong header size
|
||||
return false;
|
||||
}
|
||||
if ($fileHeader -> offset > $colorCount * 3 + BmpInfoHeader::OS21XBITMAPHEADER_SIZE + BmpFileHeader::FILE_HEADER_SIZE) {
|
||||
// Data starts later than we expect
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toRasterImage() : RasterImage
|
||||
{
|
||||
$height = abs($this -> infoHeader -> height);
|
||||
$width = $this -> infoHeader -> width;
|
||||
if ($this -> infoHeader -> bpp == 1) {
|
||||
$expandedData = PngImage::expandBytes1Bpp($this -> uncompressedData, $width);
|
||||
return IndexedRasterImage::create($width, $height, $expandedData, $this -> palette);
|
||||
} else if ($this -> infoHeader -> bpp == 2) {
|
||||
$expandedData = PngImage::expandBytes2Bpp($this -> uncompressedData, $width);
|
||||
return IndexedRasterImage::create($this->infoHeader -> width, $height, $expandedData, $this -> palette);
|
||||
} else if ($this -> infoHeader -> bpp == 4) {
|
||||
$expandedData = PngImage::expandBytes4Bpp($this -> uncompressedData, $width);
|
||||
return IndexedRasterImage::create($width, $height, $expandedData, $this -> palette);
|
||||
} else if ($this -> infoHeader -> bpp == 8) {
|
||||
return IndexedRasterImage::create($width, $height, $this -> uncompressedData, $this -> palette);
|
||||
} else if ($this -> infoHeader -> bpp == 16) {
|
||||
$masks = BmpColorBitfield::from16bitDefaults();
|
||||
if ($this -> infoHeader -> redMask !== 0 || $this -> infoHeader -> greenMask !== 0 || $this -> infoHeader -> blueMask || $this -> infoHeader -> alphaMask !== 0) {
|
||||
// Any non-default values?
|
||||
$masks = BmpColorBitfield::fromRgba($this -> infoHeader -> redMask, $this -> infoHeader -> greenMask, $this -> infoHeader -> blueMask, $this -> infoHeader -> alphaMask);
|
||||
}
|
||||
// Map 16-bit raster data to 24-bit
|
||||
$decoder = new BmpBitfieldDecoder($masks);
|
||||
$expandedData = $decoder -> read16bit($this -> uncompressedData);
|
||||
return RgbRasterImage::create($width, $height, $expandedData);
|
||||
} else if ($this -> infoHeader -> bpp == 24) {
|
||||
return RgbRasterImage::create($this->infoHeader->width, $this->infoHeader->height, $this->uncompressedData);
|
||||
} else if ($this -> infoHeader -> bpp == 32) {
|
||||
$masks = BmpColorBitfield::from32bitDefaults();
|
||||
if ($this -> infoHeader -> redMask !== 0 || $this -> infoHeader -> greenMask !== 0 || $this -> infoHeader -> blueMask || $this -> infoHeader -> alphaMask !== 0) {
|
||||
// Any non-default values?
|
||||
$masks = BmpColorBitfield::fromRgba($this -> infoHeader -> redMask, $this -> infoHeader -> greenMask, $this -> infoHeader -> blueMask, $this -> infoHeader -> alphaMask);
|
||||
}
|
||||
// Map 32-bit raster data to 24-bit
|
||||
$decoder = new BmpBitfieldDecoder($masks);
|
||||
$expandedData = $decoder -> read32bit($this -> uncompressedData);
|
||||
return RgbRasterImage::create($width, $height, $expandedData);
|
||||
}
|
||||
throw new Exception("Unknown bit depth " . $this -> infoHeader -> bpp);
|
||||
}
|
||||
|
||||
public static function transformRevString(&$item, $key)
|
||||
{
|
||||
// Convert RGB to BGR
|
||||
$item = strrev($item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
use Exception;
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class BmpFileHeader
|
||||
{
|
||||
const FILE_HEADER_SIZE = 14;
|
||||
|
||||
public $offset;
|
||||
public $size;
|
||||
|
||||
public function __construct(string $fileType, int $size, int $offset)
|
||||
{
|
||||
$this -> fileType = $fileType;
|
||||
$this -> size = $size;
|
||||
$this -> offset = $offset;
|
||||
}
|
||||
|
||||
public static function fromBinary(DataInputStream $data) : BmpFileHeader
|
||||
{
|
||||
$fileType = $data->read(2);
|
||||
if (array_search($fileType, ["BM", "BA", "CI", "CP", "IC", "PT", "OS"]) === false) {
|
||||
throw new Exception("Not a bitmap image");
|
||||
}
|
||||
$fileHeaderData = $data->read(12);
|
||||
$fields = unpack("Vsize/vreserved1/vreserved2/Voffset", $fileHeaderData);
|
||||
return new BmpFileHeader($fileType, $fields['size'], $fields['offset']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
use Exception;
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class BmpInfoHeader
|
||||
{
|
||||
const BITMAPCOREHEADER_SIZE = 12;
|
||||
const OS21XBITMAPHEADER_SIZE = 12;
|
||||
const OS22XBITMAPHEADER_MIN_SIZE = 16;
|
||||
const OS22XBITMAPHEADER_FULL_SIZE = 64;
|
||||
const BITMAPINFOHEADER_SIZE = 40;
|
||||
const BITMAPV2INFOHEADER_SIZE = 52;
|
||||
const BITMAPV3INFOHEADER_SIZE = 56;
|
||||
const BITMAPV4HEADER_SIZE = 108;
|
||||
const BITMAPV5HEADER_SIZE = 124;
|
||||
|
||||
const B1_RGB = 0;
|
||||
const B1_RLE8 = 1;
|
||||
const B1_RLE4 = 2;
|
||||
const B1_BITFILEDS = 3;
|
||||
const B1_JPEG = 4;
|
||||
const B1_PNG = 5;
|
||||
const B1_ALPHABITFIELDS = 6;
|
||||
const B1_CMYK = 11;
|
||||
const B1_CMYKRLE8 = 12;
|
||||
const B1_CMYKRLE4 = 13;
|
||||
|
||||
public $bpp;
|
||||
public $colors;
|
||||
public $compressedSize;
|
||||
public $compression;
|
||||
public $headerSize;
|
||||
public $height;
|
||||
public $horizontalRes;
|
||||
public $importantColors;
|
||||
public $planes;
|
||||
public $verticalRes;
|
||||
public $width;
|
||||
public $redMask;
|
||||
public $greenMask;
|
||||
public $blueMask;
|
||||
public $alphaMask;
|
||||
public $csType;
|
||||
public $endpoint;
|
||||
public $gamma;
|
||||
public $intent;
|
||||
public $profileData;
|
||||
public $profileSize;
|
||||
|
||||
public function __construct(
|
||||
int $headerSize,
|
||||
int $width,
|
||||
int $height,
|
||||
int $planes,
|
||||
int $bpp,
|
||||
int $compression = 0,
|
||||
int $compressedSize = 0,
|
||||
int $horizontalRes = 0,
|
||||
int $verticalRes = 0,
|
||||
int $colors = 0,
|
||||
int $importantColors = 0,
|
||||
int $redMask = 0,
|
||||
int $greenMask = 0,
|
||||
int $blueMask = 0,
|
||||
int $alphaMask = 0,
|
||||
int $csType = 0,
|
||||
array $endpoint = [],
|
||||
array $gamma = [],
|
||||
int $intent = 0,
|
||||
int $profileData = 0,
|
||||
int $profileSize = 0
|
||||
) {
|
||||
$this -> headerSize = $headerSize;
|
||||
$this -> width = $width;
|
||||
// Not possible to read signed little-endian 32-bit long with unpack(), but height may be negative.
|
||||
$this -> height = ($height >= (2**31)) ? ($height - (2**32)) : $height;
|
||||
$this -> planes = $planes;
|
||||
$this -> bpp = $bpp;
|
||||
$this -> compression = $compression;
|
||||
$this -> compressedSize = $compressedSize;
|
||||
$this -> horizontalRes = $horizontalRes;
|
||||
$this -> verticalRes = $verticalRes;
|
||||
$this -> colors = $colors;
|
||||
$this -> importantColors = $importantColors;
|
||||
$this -> redMask = $redMask;
|
||||
$this -> greenMask = $greenMask;
|
||||
$this -> blueMask = $blueMask;
|
||||
$this -> alphaMask = $alphaMask;
|
||||
$this -> csType = $csType;
|
||||
$this -> endpoint = $endpoint;
|
||||
$this -> gamma = $gamma;
|
||||
$this -> intent = $intent;
|
||||
$this -> profileData = $profileData;
|
||||
$this -> profileSize = $profileSize;
|
||||
}
|
||||
|
||||
public static function fromBinary(DataInputStream $data) : BmpInfoHeader
|
||||
{
|
||||
$infoHeaderSizeData = $data -> read(4);
|
||||
$infoHeaderSize = unpack("V", $infoHeaderSizeData)[1];
|
||||
switch ($infoHeaderSize) {
|
||||
case self::BITMAPCOREHEADER_SIZE:
|
||||
return self::readCoreHeader($data);
|
||||
case self::OS22XBITMAPHEADER_MIN_SIZE:
|
||||
case self::OS22XBITMAPHEADER_FULL_SIZE:
|
||||
// OS/2 v2 bitmap header is technically variable-length, only 16 and 64 are used in practice.
|
||||
return self::readOs22xBitmapHeader($infoHeaderSize, $data);
|
||||
case self::BITMAPINFOHEADER_SIZE:
|
||||
return self::readBitmapInfoHeader($data);
|
||||
case self::BITMAPV2INFOHEADER_SIZE:
|
||||
return self::readBitmapV2InfoHeader($data);
|
||||
case self::BITMAPV3INFOHEADER_SIZE:
|
||||
return self::readBitmapV3InfoHeader($data);
|
||||
case self::BITMAPV4HEADER_SIZE:
|
||||
return self::readBitmapV4Header($data);
|
||||
case self::BITMAPV5HEADER_SIZE:
|
||||
return self::readBitmapV5Header($data);
|
||||
default:
|
||||
throw new Exception("Info header size " . $infoHeaderSize . " is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private static function readCoreHeader(DataInputStream $data) : BmpInfoHeader
|
||||
{
|
||||
$infoData = $data -> read(self::BITMAPCOREHEADER_SIZE - 4);
|
||||
$fields = unpack("vwidth/vheight/vplanes/vbpp", $infoData);
|
||||
return new BmpInfoHeader(
|
||||
self::BITMAPCOREHEADER_SIZE,
|
||||
$fields['width'],
|
||||
$fields['height'],
|
||||
$fields['planes'],
|
||||
$fields['bpp']
|
||||
);
|
||||
}
|
||||
|
||||
private static function getInfoFields(DataInputStream $data) : array
|
||||
{
|
||||
$infoData = $data -> read(self::BITMAPINFOHEADER_SIZE - 4);
|
||||
return unpack("Vwidth/Vheight/vplanes/vbpp/Vcompression/VcompressedSize/VhorizontalRes/VverticalRes/Vcolors/VimportantColors", $infoData);
|
||||
}
|
||||
|
||||
private static function getV2fields(DataInputStream $data) : array
|
||||
{
|
||||
$infoData = $data -> read(self::BITMAPV2INFOHEADER_SIZE - self::BITMAPINFOHEADER_SIZE);
|
||||
return unpack("VredMask/VgreenMask/VblueMask", $infoData);
|
||||
}
|
||||
|
||||
private static function getV3fields(DataInputStream $data) : array
|
||||
{
|
||||
$infoData = $data -> read(self::BITMAPV3INFOHEADER_SIZE - self::BITMAPV2INFOHEADER_SIZE);
|
||||
return unpack("ValphaMask", $infoData);
|
||||
}
|
||||
|
||||
private static function getV4fields(DataInputStream $data) : array
|
||||
{
|
||||
// color space
|
||||
$csTypeData = $data -> read(4);
|
||||
$csType = unpack("VcsType", $csTypeData);
|
||||
// endpoint
|
||||
$csEndpoints = [];
|
||||
foreach (['red', 'green', 'blue'] as $channel) {
|
||||
$channelData = $data -> read(12);
|
||||
$csEndpoints[$channel] = unpack('Vx/Vy/Vz', $channelData);
|
||||
}
|
||||
// gamma
|
||||
$gammaData = $data -> read(12);
|
||||
$gamma = unpack("Vred/Vgreen/Vblue", $gammaData);
|
||||
return [
|
||||
'csType' => $csType['csType'],
|
||||
'endpoint' => $csEndpoints,
|
||||
'gamma' => $gamma
|
||||
];
|
||||
}
|
||||
|
||||
private static function getV5fields(DataInputStream $data) : array
|
||||
{
|
||||
$infoData = $data -> read(self::BITMAPV5HEADER_SIZE - self::BITMAPV4HEADER_SIZE);
|
||||
return unpack("Vintent/VprofileData/VprofileSize/Vreserved", $infoData);
|
||||
}
|
||||
|
||||
private static function readBitmapInfoHeader(DataInputStream $data) : BmpInfoHeader
|
||||
{
|
||||
$extraBytes = 0;
|
||||
$infoFields = self::getInfoFields($data);
|
||||
// Quirk- A BITMAPINFOHEADER specifying B1_BITFIELDS has 12 bytes of masks after it.
|
||||
// In later versions, this information is part of the header itself, and is read unconditionally.
|
||||
$redMask = 0;
|
||||
$greenMask = 0;
|
||||
$blueMask = 0;
|
||||
$alphaMask = 0;
|
||||
if ($infoFields['compression'] === self::B1_BITFILEDS || $infoFields['compression'] === self::B1_ALPHABITFIELDS) {
|
||||
// Quirk!
|
||||
$rgbMaskFields = self::getV2fields($data);
|
||||
$redMask = $rgbMaskFields['redMask'];
|
||||
$greenMask = $rgbMaskFields['greenMask'];
|
||||
$blueMask = $rgbMaskFields['blueMask'];
|
||||
$extraBytes += 12;
|
||||
}
|
||||
if ($infoFields['compression'] === self::B1_ALPHABITFIELDS) {
|
||||
// we might or might not need to read a 4-byte alpha mask too, depending on the compression type.
|
||||
$alphaMaskFields = self::getV3fields($data);
|
||||
$alphaMask = $alphaMaskFields['alphaMask'];
|
||||
$extraBytes += 4;
|
||||
}
|
||||
return new BmpInfoHeader(
|
||||
self::BITMAPINFOHEADER_SIZE + $extraBytes, // Count any extra bytes as part of the header
|
||||
$infoFields['width'],
|
||||
$infoFields['height'],
|
||||
$infoFields['planes'],
|
||||
$infoFields['bpp'],
|
||||
$infoFields['compression'],
|
||||
$infoFields['compressedSize'],
|
||||
$infoFields['horizontalRes'],
|
||||
$infoFields['verticalRes'],
|
||||
$infoFields['colors'],
|
||||
$infoFields['importantColors'],
|
||||
$redMask,
|
||||
$greenMask,
|
||||
$blueMask,
|
||||
$alphaMask
|
||||
);
|
||||
}
|
||||
|
||||
private static function readBitmapV2InfoHeader(DataInputStream $data) : BmpInfoHeader
|
||||
{
|
||||
$infoFields = self::getInfoFields($data);
|
||||
$v2fields = self::getV2fields($data);
|
||||
return new BmpInfoHeader(
|
||||
self::BITMAPV2INFOHEADER_SIZE,
|
||||
$infoFields['width'],
|
||||
$infoFields['height'],
|
||||
$infoFields['planes'],
|
||||
$infoFields['bpp'],
|
||||
$infoFields['compression'],
|
||||
$infoFields['compressedSize'],
|
||||
$infoFields['horizontalRes'],
|
||||
$infoFields['verticalRes'],
|
||||
$infoFields['colors'],
|
||||
$infoFields['importantColors'],
|
||||
$v2fields['redMask'],
|
||||
$v2fields['greenMask'],
|
||||
$v2fields['blueMask']
|
||||
);
|
||||
}
|
||||
|
||||
private static function readBitmapV3InfoHeader(DataInputStream $data) : BmpInfoHeader
|
||||
{
|
||||
$infoFields = self::getInfoFields($data);
|
||||
$v2fields = self::getV2fields($data);
|
||||
$v3fields = self::getV3fields($data);
|
||||
return new BmpInfoHeader(
|
||||
self::BITMAPV3INFOHEADER_SIZE,
|
||||
$infoFields['width'],
|
||||
$infoFields['height'],
|
||||
$infoFields['planes'],
|
||||
$infoFields['bpp'],
|
||||
$infoFields['compression'],
|
||||
$infoFields['compressedSize'],
|
||||
$infoFields['horizontalRes'],
|
||||
$infoFields['verticalRes'],
|
||||
$infoFields['colors'],
|
||||
$infoFields['importantColors'],
|
||||
$v2fields['redMask'],
|
||||
$v2fields['greenMask'],
|
||||
$v2fields['blueMask'],
|
||||
$v3fields['alphaMask']
|
||||
);
|
||||
}
|
||||
|
||||
private static function readBitmapV4Header(DataInputStream $data) : BmpInfoHeader
|
||||
{
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv4header
|
||||
$infoFields = self::getInfoFields($data);
|
||||
$v2fields = self::getV2fields($data);
|
||||
$v3fields = self::getV3fields($data);
|
||||
$v4fields = self::getV4fields($data);
|
||||
return new BmpInfoHeader(
|
||||
self::BITMAPV4HEADER_SIZE,
|
||||
$infoFields['width'],
|
||||
$infoFields['height'],
|
||||
$infoFields['planes'],
|
||||
$infoFields['bpp'],
|
||||
$infoFields['compression'],
|
||||
$infoFields['compressedSize'],
|
||||
$infoFields['horizontalRes'],
|
||||
$infoFields['verticalRes'],
|
||||
$infoFields['colors'],
|
||||
$infoFields['importantColors'],
|
||||
$v2fields['redMask'],
|
||||
$v2fields['greenMask'],
|
||||
$v2fields['blueMask'],
|
||||
$v3fields['alphaMask'],
|
||||
$v4fields['csType'],
|
||||
$v4fields['endpoint'],
|
||||
$v4fields['gamma']
|
||||
);
|
||||
}
|
||||
|
||||
private static function readBitmapV5Header(DataInputStream $data) : BmpInfoHeader
|
||||
{
|
||||
// Structure documented @ https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
|
||||
$infoFields = self::getInfoFields($data);
|
||||
$v2fields = self::getV2fields($data);
|
||||
$v3fields = self::getV3fields($data);
|
||||
$v4fields = self::getV4fields($data);
|
||||
$v5fields = self::getV5fields($data);
|
||||
return new BmpInfoHeader(
|
||||
self::BITMAPV5HEADER_SIZE,
|
||||
$infoFields['width'],
|
||||
$infoFields['height'],
|
||||
$infoFields['planes'],
|
||||
$infoFields['bpp'],
|
||||
$infoFields['compression'],
|
||||
$infoFields['compressedSize'],
|
||||
$infoFields['horizontalRes'],
|
||||
$infoFields['verticalRes'],
|
||||
$infoFields['colors'],
|
||||
$infoFields['importantColors'],
|
||||
$v2fields['redMask'],
|
||||
$v2fields['greenMask'],
|
||||
$v2fields['blueMask'],
|
||||
$v3fields['alphaMask'],
|
||||
$v4fields['csType'],
|
||||
$v4fields['endpoint'],
|
||||
$v4fields['gamma'],
|
||||
$v5fields['intent'],
|
||||
$v5fields['profileData'],
|
||||
$v5fields['profileSize']
|
||||
);
|
||||
}
|
||||
|
||||
private static function readOs22xBitmapHeader(int $size, DataInputStream $data)
|
||||
{
|
||||
$coreData = $data -> read(self::OS22XBITMAPHEADER_MIN_SIZE - 4);
|
||||
$coreFields = unpack("Vwidth/Vheight/vplanes/vbpp", $coreData);
|
||||
if ($size == self::OS22XBITMAPHEADER_MIN_SIZE) {
|
||||
return new BmpInfoHeader(
|
||||
self::OS22XBITMAPHEADER_MIN_SIZE,
|
||||
$coreFields['width'],
|
||||
$coreFields['height'],
|
||||
$coreFields['planes'],
|
||||
$coreFields['bpp']
|
||||
);
|
||||
}
|
||||
// Read up to the full header size
|
||||
$extraData = $data -> read(self::OS22XBITMAPHEADER_FULL_SIZE - self::OS22XBITMAPHEADER_MIN_SIZE);
|
||||
$extraFields = unpack("Vcompression/VcompressedSize/VhorizontalRes/VverticalRes/Vcolors/VimportantColors/vunits/vreserved/vrecording/vrendering/Vsize1/Vsize2/VcolorEncoding/Videntifier", $extraData);
|
||||
return new BmpInfoHeader(
|
||||
self::OS22XBITMAPHEADER_FULL_SIZE,
|
||||
$coreFields['width'],
|
||||
$coreFields['height'],
|
||||
$coreFields['planes'],
|
||||
$coreFields['bpp'],
|
||||
$extraFields['compression'],
|
||||
$extraFields['compressedSize'],
|
||||
$extraFields['horizontalRes'],
|
||||
$extraFields['verticalRes'],
|
||||
$extraFields['colors'],
|
||||
$extraFields['importantColors']
|
||||
); // Other fields are ignored.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
use Exception;
|
||||
|
||||
/*
|
||||
* Similar to RLE8 decoder, but works on 4-bytes per pixel.
|
||||
*/
|
||||
class Rle4Decoder
|
||||
{
|
||||
const RLE_ESCAPE = 0;
|
||||
const RLE_END_LINE = 0;
|
||||
const RLE_END_BITMAP = 1;
|
||||
const RLE_JUMP = 2;
|
||||
|
||||
public function decode(string $compressedImgData, int $width, int $height) : string
|
||||
{
|
||||
// Read into numeric input.
|
||||
$inpNum = array_values(unpack("C*", $compressedImgData));
|
||||
$outpNum = $this -> decodeNumbers($inpNum, $width, $height);
|
||||
// Back to string
|
||||
$outStringArr = [];
|
||||
$rowWidth = intdiv((4 * $width + 31), 32) * 4; // Padding 4-bit data to a 4-byte boundary.
|
||||
foreach ($outpNum as $rowOrig) {
|
||||
// Combine two numbers 0-16 into one numeric value 0-256
|
||||
$row = array_fill(0, $rowWidth, 0);
|
||||
$chunks = array_chunk($rowOrig, 2);
|
||||
for ($i = 0; $i < count($chunks); $i++) {
|
||||
$chunk = $chunks[$i];
|
||||
if (count($chunk) == 2) {
|
||||
$row[$i] = ($chunk[0] << 4) + $chunk[1];
|
||||
} else {
|
||||
$row[$i] = $chunk[0];
|
||||
}
|
||||
}
|
||||
$outStringArr[] = pack("C*", ...$row);
|
||||
}
|
||||
return implode($outStringArr);
|
||||
}
|
||||
|
||||
private function decodeNumbers(array $inpNum, int $width, int $height) : array
|
||||
{
|
||||
// Initialize canvas
|
||||
$buffer = new RleCanvas($width, $height);
|
||||
// read input data, 2 bytes at a time
|
||||
$i = 0;
|
||||
$len = intdiv(count($inpNum), 2) * 2;
|
||||
while ($i < $len) {
|
||||
$firstByte = $inpNum[$i];
|
||||
$secondByte = $inpNum[$i + 1];
|
||||
$i += 2;
|
||||
if ($firstByte === self::RLE_ESCAPE) {
|
||||
if ($secondByte === self::RLE_END_LINE) {
|
||||
$buffer -> endOfLine();
|
||||
} else if ($secondByte === self::RLE_END_BITMAP) {
|
||||
$buffer -> endOfBitmap();
|
||||
} else if ($secondByte === self::RLE_JUMP) {
|
||||
// "Delta".
|
||||
if ($i + 2 > $len) { // Need 2 more bytes to find out how far to jump
|
||||
throw new Exception("Unexpected EOF");
|
||||
}
|
||||
$deltaX = $inpNum[$i];
|
||||
$deltaY = $inpNum[$i + 1];
|
||||
$i += 2;
|
||||
$buffer -> delta($deltaX, $deltaY);
|
||||
} else {
|
||||
// "Absolute run". Paste the requested number of bytes onto the canvas
|
||||
$absoluteLenPixels = $secondByte;
|
||||
$absoluteLenBytes = intdiv($absoluteLenPixels + 1, 2);
|
||||
if ($i + $absoluteLenBytes > $len) {
|
||||
throw new Exception("Unexpected EOF");
|
||||
}
|
||||
$bytesToPaste = array_slice($inpNum, $i, $absoluteLenBytes);
|
||||
$valuesToPaste = array_fill(0, $absoluteLenPixels, 0);
|
||||
for ($j = 0; $j < $absoluteLenPixels; $j++) {
|
||||
$sourceVal = $bytesToPaste[intdiv($j, 2)];
|
||||
$valuesToPaste[$j] = ($j % 2 == 0) ? ($sourceVal >> 4) : ($sourceVal & 0x0F);
|
||||
}
|
||||
$i += $absoluteLenBytes;
|
||||
if ($absoluteLenBytes % 2 != 0) {
|
||||
// skip a padding byte too
|
||||
$i++;
|
||||
}
|
||||
$buffer -> absolute($valuesToPaste);
|
||||
}
|
||||
} else {
|
||||
// Alternate between first and second index when repeating
|
||||
$col1 = ($secondByte >> 4);
|
||||
$col2 = ($secondByte & 0x0F);
|
||||
for ($j = 0; $j < $firstByte; $j++) {
|
||||
$buffer -> set($j % 2 == 0 ? $col1 : $col2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $buffer -> getContents();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Rle8Decoder
|
||||
{
|
||||
const RLE_ESCAPE = 0;
|
||||
const RLE_END_LINE = 0;
|
||||
const RLE_END_BITMAP = 1;
|
||||
const RLE_JUMP = 2;
|
||||
|
||||
public function decode(string $compressedImgData, int $width, int $height) : string
|
||||
{
|
||||
// Read into numeric input.
|
||||
$inpNum = array_values(unpack("C*", $compressedImgData));
|
||||
$outpNum = $this -> decodeNumbers($inpNum, $width, $height);
|
||||
// Back to string
|
||||
$outStringArr = [];
|
||||
$rowWidth = intdiv((8 * $width + 31), 32) * 4; // Padding to 4-byte boundary: Can this be simplified?
|
||||
$padding = str_repeat("\0", $rowWidth - $width);
|
||||
foreach ($outpNum as $row) {
|
||||
$outStringArr[] = pack("C*", ...$row);
|
||||
}
|
||||
return implode($padding, $outStringArr) . $padding;
|
||||
}
|
||||
|
||||
public function decodeNumbers(array $inpNum, int $width, int $height) : array
|
||||
{
|
||||
// Initialize canvas
|
||||
$buffer = new RleCanvas($width, $height);
|
||||
// read input data, 2 bytes at a time
|
||||
$i = 0;
|
||||
$len = intdiv(count($inpNum), 2) * 2;
|
||||
while ($i < $len) {
|
||||
$firstByte = $inpNum[$i];
|
||||
$secondByte = $inpNum[$i + 1];
|
||||
$i += 2;
|
||||
if ($firstByte === self::RLE_ESCAPE) {
|
||||
if ($secondByte === self::RLE_END_LINE) {
|
||||
$buffer -> endOfLine();
|
||||
} else if ($secondByte === self::RLE_END_BITMAP) {
|
||||
$buffer -> endOfBitmap();
|
||||
} else if ($secondByte === self::RLE_JUMP) {
|
||||
// "Delta".
|
||||
if ($i + 2 > $len) { // Need 2 more bytes to find out how far to jump
|
||||
throw new Exception("Unexpected EOF");
|
||||
}
|
||||
$deltaX = $inpNum[$i];
|
||||
$deltaY = $inpNum[$i + 1];
|
||||
$i += 2;
|
||||
$buffer -> delta($deltaX, $deltaY);
|
||||
} else {
|
||||
// "Absolute run". Paste the requested number of bytes onto the canvas
|
||||
$absoluteLen = $secondByte;
|
||||
if ($i + $absoluteLen > $len) {
|
||||
throw new Exception("Unexpected EOF");
|
||||
}
|
||||
$bytesToPaste = array_slice($inpNum, $i, $absoluteLen);
|
||||
$i += $absoluteLen;
|
||||
if ($absoluteLen % 2 != 0) {
|
||||
// skip a padding byte too
|
||||
$i++;
|
||||
}
|
||||
$buffer -> absolute($bytesToPaste);
|
||||
}
|
||||
} else {
|
||||
$buffer -> repeat($secondByte, $firstByte);
|
||||
}
|
||||
}
|
||||
return $buffer -> getContents();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Bmp;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* 2D canvas for writing RLE-encoded BMP data into. A cursor is moved based on the commands issued, and an Exception
|
||||
* is thrown if the data goes outside the canvas boundary, or if the data continues after endOfBitmap is called.
|
||||
*/
|
||||
class RleCanvas
|
||||
{
|
||||
private $buffer;
|
||||
private $cursorX;
|
||||
private $cursorY;
|
||||
private $width;
|
||||
private $height;
|
||||
private $complete;
|
||||
|
||||
public function __construct(int $width, int $height)
|
||||
{
|
||||
$this -> cursorX = 0;
|
||||
$this -> cursorY = 0;
|
||||
$this -> width = $width;
|
||||
$this -> height = $height;
|
||||
$this -> complete = false;
|
||||
// Initialise empty space
|
||||
$tmp = [];
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
$tmp[] = array_fill(0, $width, 0);
|
||||
}
|
||||
$this->buffer = $tmp;
|
||||
}
|
||||
|
||||
public function delta(int $deltaX, int $deltaY)
|
||||
{
|
||||
$this -> cursorX += $deltaX;
|
||||
$this -> cursorY += $deltaY;
|
||||
}
|
||||
|
||||
public function endOfLine()
|
||||
{
|
||||
$this -> cursorY++;
|
||||
$this -> cursorX = 0;
|
||||
}
|
||||
|
||||
public function endOfBitmap()
|
||||
{
|
||||
$this -> complete = true;
|
||||
}
|
||||
|
||||
public function set(int $val)
|
||||
{
|
||||
// Range check when we attempt to write the pixel.
|
||||
if ($this -> cursorY < 0 || $this -> cursorY >= $this -> height) {
|
||||
throw new Exception("Bitmap compressed data exceeds image boundary; file is not valid. Y-overflow");
|
||||
}
|
||||
if ($this -> cursorX < 0 || $this -> cursorX >= $this -> width) {
|
||||
throw new Exception("Bitmap compressed data exceeds image boundary; file is not valid. X-overflow");
|
||||
}
|
||||
if ($this -> complete) {
|
||||
throw new Exception("Bitmap compressed data continued after end-of-bitmap code was found; file is not valid.");
|
||||
}
|
||||
// Write the pixel
|
||||
$this -> buffer[$this -> cursorY][$this -> cursorX] = $val;
|
||||
$this -> cursorX++;
|
||||
}
|
||||
|
||||
public function absolute(array $values)
|
||||
{
|
||||
for ($i = 0; $i < count($values); $i++) {
|
||||
$this -> set($values[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
public function repeat(int $val, int $times)
|
||||
{
|
||||
for ($j = 0; $j < $times; $j++) {
|
||||
$this -> set($val);
|
||||
}
|
||||
}
|
||||
|
||||
public function getContents()
|
||||
{
|
||||
return $this -> buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Bmp\BmpFile;
|
||||
use Mike42\GfxPhp\Codec\Common\DataBlobInputStream;
|
||||
use Mike42\GfxPhp\Codec\Gif\GifDataStream;
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
|
||||
class BmpCodec implements ImageEncoder, ImageDecoder
|
||||
{
|
||||
protected static $instance = null;
|
||||
const INFO_HEADER_SIZE = 40;
|
||||
const FILE_HEADER_SIZE = 14;
|
||||
|
||||
public function encode(RasterImage $image, string $format): string
|
||||
{
|
||||
if (!($image instanceof RgbRasterImage)) {
|
||||
// Convert if necessary
|
||||
$image = $image -> toRgb();
|
||||
}
|
||||
$width = $image -> getWidth();
|
||||
$height = $image -> getHeight();
|
||||
$infoHeader = pack(
|
||||
"V3v2V6",
|
||||
self::INFO_HEADER_SIZE,
|
||||
$width, // Width
|
||||
$height, // Height
|
||||
1, // Planes
|
||||
24, // bpp
|
||||
0, // Compression (none)
|
||||
0, // Image size compressed
|
||||
1, // Horizontal res
|
||||
1, // Vertical res
|
||||
0, // Number of colors
|
||||
0 // Number of important colors
|
||||
);
|
||||
$colorTable = "";
|
||||
// Transform RGB ordering to BGR ordering
|
||||
$pixels = str_split($image -> getRasterData(), 3);
|
||||
array_walk($pixels, [$this, "transformRevString"]);
|
||||
$rasterData = implode("", $pixels);
|
||||
// Transform top-down unpadded lines to bottom-up padded lines
|
||||
$originalWidth = $width * 3;
|
||||
$paddingLength = (4 - ($originalWidth & 3)) & 3;
|
||||
$padding = str_repeat("\x00", $paddingLength);
|
||||
$lines = str_split($rasterData, $originalWidth);
|
||||
$lines = array_reverse($lines, false);
|
||||
// Uncompressed 24 bit BMP file
|
||||
$pixelData = implode($padding, $lines) . $padding;
|
||||
// Return bitmap & header
|
||||
$fileSize = strlen($pixelData) + strlen($colorTable) + self::INFO_HEADER_SIZE + self::FILE_HEADER_SIZE;
|
||||
$header = pack(
|
||||
"C2Vv2V",
|
||||
0x42, // 'BM' magic number
|
||||
0x4d,
|
||||
$fileSize, // file size
|
||||
0, // Reserved
|
||||
0, // Reserved
|
||||
self::INFO_HEADER_SIZE + self::FILE_HEADER_SIZE // Offset
|
||||
);
|
||||
return $header . $infoHeader . $colorTable . $pixelData;
|
||||
}
|
||||
|
||||
protected function transformRevString(&$item, $key)
|
||||
{
|
||||
// Convert RGB to BGR
|
||||
$item = strrev($item);
|
||||
}
|
||||
|
||||
public function getEncodeFormats(): array
|
||||
{
|
||||
return ["bmp", "dib"];
|
||||
}
|
||||
|
||||
public function decode(string $blob): RasterImage
|
||||
{
|
||||
$data = DataBlobInputStream::fromBlob($blob);
|
||||
$bmp = BmpFile::fromBinary($data);
|
||||
return $bmp -> toRasterImage();
|
||||
}
|
||||
|
||||
public function identify(string $blob): string
|
||||
{
|
||||
if (substr($blob, 0, 2) == BmpFile::BMP_SIGNATURE) {
|
||||
return "bmp";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public function getDecodeFormats(): array
|
||||
{
|
||||
return ["bmp", "dib"];
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new BmpCodec();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Common;
|
||||
|
||||
class DataBlobInputStream implements DataInputStream
|
||||
{
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$this -> data = $data;
|
||||
$this -> offset = 0;
|
||||
}
|
||||
|
||||
public function read(int $bytes)
|
||||
{
|
||||
$chunk = $this -> peek($bytes);
|
||||
$this -> advance($bytes);
|
||||
return $chunk;
|
||||
}
|
||||
|
||||
public function advance(int $bytes)
|
||||
{
|
||||
$this -> offset += $bytes;
|
||||
}
|
||||
|
||||
public function peek(int $bytes)
|
||||
{
|
||||
$chunk = substr($this -> data, $this -> offset, $bytes);
|
||||
if ($chunk === false) {
|
||||
throw new \Exception("End of file reached, cannot retrieve more data.");
|
||||
}
|
||||
$read = strlen($chunk);
|
||||
if ($read !== $bytes) {
|
||||
throw new \Exception("Unexpected end of file, needed $bytes but read $read");
|
||||
}
|
||||
return $chunk;
|
||||
}
|
||||
|
||||
public function isEof()
|
||||
{
|
||||
return $this -> offset >= strlen($this -> data);
|
||||
}
|
||||
|
||||
public static function fromBlob(string $blob)
|
||||
{
|
||||
return new DataBlobInputStream($blob);
|
||||
}
|
||||
|
||||
public static function fromFilename(string $filename)
|
||||
{
|
||||
$blob = file_get_contents($filename);
|
||||
if ($blob === false) {
|
||||
throw new \Exception($filename);
|
||||
}
|
||||
return self::fromBlob($blob);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Common;
|
||||
|
||||
interface DataInputStream
|
||||
{
|
||||
public function read(int $bytes);
|
||||
public function isEof();
|
||||
public function peek(int $bytes);
|
||||
public function advance(int $bytes);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifApplicationExt
|
||||
{
|
||||
private $appIdentifer;
|
||||
private $appAuthCode;
|
||||
private $data;
|
||||
|
||||
public function __construct(string $appIdentifer, string $appAuthCode, array $data)
|
||||
{
|
||||
$this->appIdentifer = $appIdentifer;
|
||||
$this->appAuthCode = $appAuthCode;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getAppIdentifer(): string
|
||||
{
|
||||
return $this->appIdentifer;
|
||||
}
|
||||
|
||||
public function getAppAuthCode(): string
|
||||
{
|
||||
return $this->appAuthCode;
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in) : GifApplicationExt
|
||||
{
|
||||
$extIntroducer = $in->read(1);
|
||||
$extLabel = $in->read(1);
|
||||
if ($extIntroducer != GifData::GIF_EXTENSION || $extLabel != GifData::GIF_EXTENSION_APPLICATION) {
|
||||
throw new \Exception("Not a GIF application extension block");
|
||||
}
|
||||
$lenData = $in->read(1);
|
||||
$len = unpack("C", $lenData)[1];
|
||||
if ($len != 11) {
|
||||
throw new \Exception("Incorrect size on application extension block");
|
||||
}
|
||||
$appIdentifier = $in->read(8);
|
||||
$appAuthCode = $in->read(3);
|
||||
$data = GifData::readDataSubBlocks($in);
|
||||
return new GifApplicationExt($appIdentifier, $appAuthCode, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
class GifColorTable
|
||||
{
|
||||
private $palette;
|
||||
|
||||
public function __construct(array $palette)
|
||||
{
|
||||
$this -> palette = $palette;
|
||||
}
|
||||
|
||||
public static function fromBin(\Mike42\GfxPhp\Codec\Common\DataInputStream $in, int $globalColorTableSize)
|
||||
{
|
||||
$tableData = $in -> read($globalColorTableSize * 3);
|
||||
$paletteArr = array_values(unpack("C*", $tableData));
|
||||
$palette = array_chunk($paletteArr, 3);
|
||||
return new GifColorTable($palette);
|
||||
}
|
||||
|
||||
public function getPalette(): array
|
||||
{
|
||||
return $this->palette;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifCommentExt
|
||||
{
|
||||
private $data;
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this -> data = $data;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in)
|
||||
{
|
||||
$extIntroducer = $in->read(1);
|
||||
$extLabel = $in->read(1);
|
||||
if ($extIntroducer != GifData::GIF_EXTENSION || $extLabel != GifData::GIF_EXTENSION_COMMENT) {
|
||||
throw new \Exception("Not a GIF comment extension block");
|
||||
}
|
||||
$data = GifData::readDataSubBlocks($in);
|
||||
return new GifCommentExt($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifData
|
||||
{
|
||||
const GIF_IMAGE_SEPARATOR="\x2C";
|
||||
const GIF_EXTENSION="\x21";
|
||||
const GIF_EXTENSION_GRAPHIC_CONTROL="\xF9";
|
||||
const GIF_EXTENSION_PLAINTEXT="\x01";
|
||||
const GIF_EXTENSION_APPLICATION="\xFF";
|
||||
const GIF_EXTENSION_COMMENT="\xFE";
|
||||
|
||||
private $graphicsBlock;
|
||||
private $specialPurposeBlock;
|
||||
private $unrecognisedBlock;
|
||||
|
||||
public function __construct(GifGraphicsBlock $graphicsBlock = null, GifSpecialPurposeBlock $specialPurposeBlock = null, GifUnknownExt $unrecognisedBlock = null)
|
||||
{
|
||||
$this->graphicsBlock = $graphicsBlock;
|
||||
$this->specialPurposeBlock = $specialPurposeBlock;
|
||||
$this->unrecognisedBlock = $unrecognisedBlock;
|
||||
}
|
||||
|
||||
public function getUnrecognisedBlock()
|
||||
{
|
||||
return $this->unrecognisedBlock;
|
||||
}
|
||||
|
||||
public function getSpecialPurposeBlock()
|
||||
{
|
||||
return $this->specialPurposeBlock;
|
||||
}
|
||||
|
||||
public function getGraphicsBlock()
|
||||
{
|
||||
return $this->graphicsBlock;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in) : GifData
|
||||
{
|
||||
$peek = $in -> peek(2);
|
||||
$blockId = $peek[0];
|
||||
$extensionId = $peek[1];
|
||||
if ($blockId == GifData::GIF_EXTENSION) {
|
||||
// Special-purpose blocks
|
||||
if ($extensionId == GifData::GIF_EXTENSION_APPLICATION) {
|
||||
$applicationExt = GifApplicationExt::fromBin($in);
|
||||
$specialPurposeBlock = new GifSpecialPurposeBlock($applicationExt, null);
|
||||
return new GifData(null, $specialPurposeBlock, null);
|
||||
} else if ($extensionId == GifData::GIF_EXTENSION_COMMENT) {
|
||||
$commentExt = GifCommentExt::fromBin($in);
|
||||
$specialPurposeBlock = new GifSpecialPurposeBlock(null, $commentExt);
|
||||
return new GifData(null, $specialPurposeBlock, null);
|
||||
}
|
||||
}
|
||||
// Unknown extension blocks
|
||||
if ($blockId == GifData::GIF_EXTENSION && $extensionId != GifData::GIF_EXTENSION_GRAPHIC_CONTROL && $extensionId != GifData::GIF_EXTENSION_PLAINTEXT) {
|
||||
$unrecognisedBlock = GifUnknownExt::fromBin($in);
|
||||
return new GifData(null, null, $unrecognisedBlock);
|
||||
}
|
||||
$graphicsBlock = GifGraphicsBlock::fromBin($in);
|
||||
return new GifData($graphicsBlock, null, null);
|
||||
}
|
||||
|
||||
public static function readDataSubBlocks(DataInputStream $in) : array
|
||||
{
|
||||
$blocks = [];
|
||||
while ($in -> peek(1) != "\x00") {
|
||||
$blockSizeData = $in -> read(1);
|
||||
$blockSize = unpack("C", $blockSizeData)[1];
|
||||
$blocks[] = $in -> read($blockSize);
|
||||
}
|
||||
$in -> read(1); // Discard terminating byte
|
||||
return $blocks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
use Mike42\GfxPhp\IndexedRasterImage;
|
||||
use Mike42\GfxPhp\Util\LzwCompression;
|
||||
|
||||
class GifDataStream
|
||||
{
|
||||
const GIF87_SIGNATURE="GIF87a";
|
||||
const GIF89_SIGNATURE="GIF89a";
|
||||
const GIF_TRAILER="\x3B";
|
||||
|
||||
private $header;
|
||||
private $logicalScreen;
|
||||
private $data;
|
||||
private $trailer;
|
||||
|
||||
private function __construct(string $header, GifLogicalScreen $logicalScreen, array $data, string $trailer)
|
||||
{
|
||||
$this -> header = $header;
|
||||
$this -> logicalScreen = $logicalScreen;
|
||||
$this -> data = $data;
|
||||
$this -> trailer = $trailer;
|
||||
}
|
||||
|
||||
public static function fromBinary(DataInputStream $data) : GifDataStream
|
||||
{
|
||||
// Check header
|
||||
$header = $data -> read(6);
|
||||
if ($header != GifDataStream::GIF87_SIGNATURE && $header != GifDataStream::GIF89_SIGNATURE) {
|
||||
throw new \Exception("Bad GIF header");
|
||||
}
|
||||
$logicalScreen = GifLogicalScreen::fromBin($data);
|
||||
$imageData = [];
|
||||
while ($data -> peek(1) != GifDataStream::GIF_TRAILER) {
|
||||
$imageData[] = GifData::fromBin($data);
|
||||
}
|
||||
$trailer = $data -> read(1); // Discard trailer byte
|
||||
if (!$data -> isEof()) {
|
||||
throw new \Exception("The GIF file is corrupt; data found after the GIF trailer");
|
||||
}
|
||||
return new GifDataStream($header, $logicalScreen, $imageData, $trailer);
|
||||
}
|
||||
|
||||
public function toRasterImage(int $imageIndex = 0) : IndexedRasterImage
|
||||
{
|
||||
// Extract an image from the GIF
|
||||
$currentIndex = 0;
|
||||
foreach ($this -> data as $dataBlock) {
|
||||
if ($dataBlock -> getGraphicsBlock() !== null && $dataBlock -> getGraphicsBlock() -> getTableBasedImage() != null) {
|
||||
// This is a raster image
|
||||
if ($currentIndex == $imageIndex) {
|
||||
return GifDataStream::extractImage($this -> logicalScreen, $dataBlock -> getGraphicsBlock() -> getTableBasedImage(), $dataBlock -> getGraphicsBlock() -> getGraphicControlExt());
|
||||
}
|
||||
$currentIndex++;
|
||||
}
|
||||
}
|
||||
throw new \Exception("Could not find image #$imageIndex in GIF file");
|
||||
}
|
||||
|
||||
private static function extractImage(GifLogicalScreen $logicalScreen, GifTableBasedImage $tableBasedImage, GifGraphicControlExt $graphicControlExt = null) : IndexedRasterImage
|
||||
{
|
||||
|
||||
$width = $tableBasedImage->getImageDescriptor()->getWidth();
|
||||
$height = $tableBasedImage->getImageDescriptor()->getHeight();
|
||||
$colorTable = $tableBasedImage->getLocalColorTable() == null ? $logicalScreen->getGlobalColorTable() : $tableBasedImage->getLocalColorTable();
|
||||
if ($colorTable == null) {
|
||||
throw new \Exception("GIF contains no color table for the image. Loading this type of file is not supported.");
|
||||
}
|
||||
if ($width == 0 || $height == 0) {
|
||||
throw new \Exception("GIF contains no pixels. Loading this type of file is not supported.");
|
||||
}
|
||||
// De-compress the actual image data
|
||||
$compressedData = join($tableBasedImage->getDataSubBlocks());
|
||||
$decompressedData = LzwCompression::decompress($compressedData, $tableBasedImage->getLzqMinSize());
|
||||
// Discard extra bytes here, since IndexedRasterImage will reject it
|
||||
$actualLen = strlen($decompressedData);
|
||||
$expectedLen = $width * $height;
|
||||
if ($actualLen > $expectedLen) {
|
||||
$decompressedData = substr($decompressedData, 0, $expectedLen);
|
||||
} else if ($actualLen < $expectedLen) {
|
||||
throw new \Exception("GIF corrupt or truncated. Expexted $expectedLen pixels for $width x $height image, but only $actualLen pixels were encoded.");
|
||||
}
|
||||
if ($tableBasedImage -> getImageDescriptor() -> isInterlaced()) {
|
||||
$decompressedData = self::deinterlace($width, $decompressedData);
|
||||
}
|
||||
// Array of ints for IndexedRasterImage
|
||||
$dataArr = array_values(unpack("C*", $decompressedData));
|
||||
$image = IndexedRasterImage::create($width, $height, $dataArr, $colorTable -> getPalette());
|
||||
// Lastly, transparency handling
|
||||
if ($graphicControlExt != null && $graphicControlExt -> hasTransparentColor()) {
|
||||
$image -> setTransparentColor($graphicControlExt -> getTransparentColorIndex());
|
||||
}
|
||||
return $image;
|
||||
}
|
||||
|
||||
private static function deinterlace(int $width, string $data) : string
|
||||
{
|
||||
// Four-pass GIF de-interlace. Reads input in order.
|
||||
$old = str_split($data, $width);
|
||||
$height = count($old);
|
||||
$new = array_fill(0, $height, "");
|
||||
$j = 0;
|
||||
for ($i = 0; $i < $height; $i += 8) {
|
||||
$new[$i] = $old[$j];
|
||||
$j++;
|
||||
}
|
||||
for ($i = 4; $i < $height; $i += 8) {
|
||||
$new[$i] = $old[$j];
|
||||
$j++;
|
||||
}
|
||||
for ($i = 2; $i < $height; $i += 4) {
|
||||
$new[$i] = $old[$j];
|
||||
$j++;
|
||||
}
|
||||
for ($i = 1; $i < $height; $i += 2) {
|
||||
$new[$i] = $old[$j];
|
||||
$j++;
|
||||
}
|
||||
return join($new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifGraphicControlExt
|
||||
{
|
||||
private $disposalMethod;
|
||||
private $hasUserInputFlag;
|
||||
private $hasTransparentColor;
|
||||
private $delayTime;
|
||||
private $transparentColorIndex;
|
||||
|
||||
public function __construct(int $disposalMethod, bool $hasUserInputFlag, bool $hasTransparentColor, int $delayTime, int $transparentColorIndex)
|
||||
{
|
||||
$this->disposalMethod = $disposalMethod;
|
||||
$this->hasUserInputFlag = $hasUserInputFlag;
|
||||
$this->hasTransparentColor = $hasTransparentColor;
|
||||
$this->delayTime = $delayTime;
|
||||
$this->transparentColorIndex = $transparentColorIndex;
|
||||
}
|
||||
|
||||
public function getDisposalMethod(): int
|
||||
{
|
||||
return $this->disposalMethod;
|
||||
}
|
||||
|
||||
public function hasUserInputFlag(): bool
|
||||
{
|
||||
return $this->hasUserInputFlag;
|
||||
}
|
||||
|
||||
public function hasTransparentColor(): bool
|
||||
{
|
||||
return $this->hasTransparentColor;
|
||||
}
|
||||
|
||||
public function getDelayTime(): int
|
||||
{
|
||||
return $this->delayTime;
|
||||
}
|
||||
|
||||
public function getTransparentColorIndex(): int
|
||||
{
|
||||
return $this->transparentColorIndex;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in) : GifGraphicControlExt
|
||||
{
|
||||
$extIntroducer = $in->read(1);
|
||||
$extLabel = $in->read(1);
|
||||
if ($extIntroducer != GifData::GIF_EXTENSION || $extLabel != GifData::GIF_EXTENSION_GRAPHIC_CONTROL) {
|
||||
throw new \Exception("Not a GIF application extension block");
|
||||
}
|
||||
$lenData = $in->read(1);
|
||||
$len = unpack("C", $lenData)[1];
|
||||
if ($len != 4) {
|
||||
throw new \Exception("Incorrect size on application extension block");
|
||||
}
|
||||
$packedFieldData = $in -> read(1);
|
||||
$packedFields = unpack("C", $packedFieldData)[1]; // Note 3 most-significant bits are reserved
|
||||
$disposalMethod = ($packedFields >> 2) & 0x07;
|
||||
$hasUserInputFlag = (($packedFields >> 1) & 0x01) == 1;
|
||||
$hasTransparentColor = ($packedFields & 0x01) == 1;
|
||||
$delayTimeData = $in -> read(2);
|
||||
$delayTime = unpack("v", $delayTimeData)[1];
|
||||
$transparentColorIndexData = $in -> read(1);
|
||||
$transparentColorIndex = unpack("C", $transparentColorIndexData)[1];
|
||||
$end = $in -> read(1);
|
||||
if ($end != "\x00") {
|
||||
throw new \Exception("GIF graphic control block not correctly terminated");
|
||||
}
|
||||
return new GifGraphicControlExt($disposalMethod, $hasUserInputFlag, $hasTransparentColor, $delayTime, $transparentColorIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifGraphicsBlock
|
||||
{
|
||||
private $graphicControlExt;
|
||||
private $tableBasedImage;
|
||||
private $plaintextExt;
|
||||
|
||||
public function __construct(GifGraphicControlExt $graphicControlExt = null, GifTableBasedImage $tableBasedImage = null, GifPlaintextExt $plaintextExt = null)
|
||||
{
|
||||
$this->graphicControlExt = $graphicControlExt;
|
||||
$this->tableBasedImage = $tableBasedImage;
|
||||
$this->plaintextExt = $plaintextExt;
|
||||
}
|
||||
|
||||
public function getGraphicControlExt()
|
||||
{
|
||||
return $this->graphicControlExt;
|
||||
}
|
||||
|
||||
public function getTableBasedImage()
|
||||
{
|
||||
return $this->tableBasedImage;
|
||||
}
|
||||
|
||||
public function getPlaintextExt()
|
||||
{
|
||||
return $this->plaintextExt;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in) : GifGraphicsBlock
|
||||
{
|
||||
$peek = $in -> peek(2);
|
||||
$blockId = $peek[0];
|
||||
$extensionId = $peek[1];
|
||||
// Could have a graphic control extension before it
|
||||
$graphicControlExtension = null;
|
||||
if ($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_GRAPHIC_CONTROL) {
|
||||
// Optional graphic control extension
|
||||
$graphicControlExtension = GifGraphicControlExt::fromBin($in);
|
||||
// Re-populate for next block
|
||||
$peek = $in -> peek(2);
|
||||
$blockId = $peek[0];
|
||||
$extensionId = $peek[1];
|
||||
}
|
||||
while ($blockId == GifData::GIF_EXTENSION && $extensionId != GifData::GIF_EXTENSION_PLAINTEXT) {
|
||||
// We would need a slight re-structure to record these blocks correctly.
|
||||
if ($extensionId == GifData::GIF_EXTENSION_APPLICATION) {
|
||||
// ImageMagick drops an 'application' block here, which we can discard (gfx-php does not use it at this stage)
|
||||
GifApplicationExt::fromBin($in);
|
||||
} else if ($extensionId == GifData::GIF_EXTENSION_COMMENT) {
|
||||
// Also GIMP places a 'comment' block here.
|
||||
GifCommentExt::fromBin($in);
|
||||
} else {
|
||||
// And who knows what else we will have to skip. This should cover it anyway.
|
||||
GifUnknownExt::fromBin($in);
|
||||
}
|
||||
$peek = $in -> peek(2);
|
||||
$blockId = $peek[0];
|
||||
$extensionId = $peek[1];
|
||||
}
|
||||
if ($blockId == GifData::GIF_EXTENSION && $extensionId == GifData::GIF_EXTENSION_PLAINTEXT) {
|
||||
// Plain text
|
||||
$plaintextExtension = GifPlaintextExt::fromBin($in);
|
||||
return new GifGraphicsBlock($graphicControlExtension, null, $plaintextExtension);
|
||||
} else if ($blockId == GifData::GIF_IMAGE_SEPARATOR) {
|
||||
// Table-based image
|
||||
$tableBasedImage = GifTableBasedImage::fromBin($in);
|
||||
return new GifGraphicsBlock($graphicControlExtension, $tableBasedImage, null);
|
||||
}
|
||||
throw new \Exception("Could not recognise a graphics or extension block; GIF file is corrupt");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifImageDescriptor
|
||||
{
|
||||
|
||||
private $left;
|
||||
private $top;
|
||||
private $width;
|
||||
private $height;
|
||||
private $hasLocalColorTable;
|
||||
private $isInterlaced;
|
||||
private $hasSortedLocalColorTable;
|
||||
private $localColorTableSize;
|
||||
|
||||
public function __construct(int $left, int $top, int $width, int $height, bool $hasLocalColorTable, bool $isInterlaced, bool $hasSortedLocalColorTable, int $localColorTableSize)
|
||||
{
|
||||
|
||||
$this->left = $left;
|
||||
$this->top = $top;
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->hasLocalColorTable = $hasLocalColorTable;
|
||||
$this->isInterlaced = $isInterlaced;
|
||||
$this->hasSortedLocalColorTable = $hasSortedLocalColorTable;
|
||||
$this->localColorTableSize = $localColorTableSize;
|
||||
}
|
||||
|
||||
public function getLeft(): int
|
||||
{
|
||||
return $this->left;
|
||||
}
|
||||
|
||||
public function getTop(): int
|
||||
{
|
||||
return $this->top;
|
||||
}
|
||||
|
||||
public function getWidth(): int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function getHeight(): int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
public function hasLocalColorTable(): bool
|
||||
{
|
||||
return $this->hasLocalColorTable;
|
||||
}
|
||||
|
||||
public function isInterlaced(): bool
|
||||
{
|
||||
return $this->isInterlaced;
|
||||
}
|
||||
|
||||
public function hasSortedLocalColorTable(): bool
|
||||
{
|
||||
return $this->hasSortedLocalColorTable;
|
||||
}
|
||||
|
||||
public function getLocalColorTableSize(): int
|
||||
{
|
||||
return $this->localColorTableSize;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in): GifImageDescriptor
|
||||
{
|
||||
$imageSep = $in->read(1);
|
||||
if ($imageSep != GifData::GIF_IMAGE_SEPARATOR) {
|
||||
throw new \Exception("Not a GIF image descriptor block");
|
||||
}
|
||||
$sizeData = $in -> read(8);
|
||||
$size = unpack("v4", $sizeData);
|
||||
$left = $size[1];
|
||||
$top = $size[2];
|
||||
$width = $size[3];
|
||||
$height = $size[4];
|
||||
$packedFieldData = $in->read(1);
|
||||
$packedFields = unpack("C", $packedFieldData)[1];
|
||||
$hasLocalColorTable = ($packedFields >> 7) == 1;
|
||||
$isInterlaced = (($packedFields >> 6) & 0x01) == 1;
|
||||
$hasSortedLocalColorTable = (($packedFields >> 5) & 0x01) == 1;
|
||||
// 2 bits are reserved here and not parsed
|
||||
$localColorTableSize = $packedFields & 0x07;
|
||||
return new GifImageDescriptor($left, $top, $width, $height, $hasLocalColorTable, $isInterlaced, $hasSortedLocalColorTable, $localColorTableSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifLogicalScreen
|
||||
{
|
||||
|
||||
private $logicalScreenDescriptor;
|
||||
private $globalColorTable;
|
||||
|
||||
public function __construct(GifLogicalScreenDescriptor $logicalScreenDescriptor, GifColorTable $globalColorTable = null)
|
||||
{
|
||||
$this->logicalScreenDescriptor = $logicalScreenDescriptor;
|
||||
$this->globalColorTable = $globalColorTable;
|
||||
}
|
||||
|
||||
public function getLogicalScreenDescriptor(): GifLogicalScreenDescriptor
|
||||
{
|
||||
return $this->logicalScreenDescriptor;
|
||||
}
|
||||
|
||||
public function getGlobalColorTable(): GifColorTable
|
||||
{
|
||||
return $this->globalColorTable;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $data): GifLogicalScreen
|
||||
{
|
||||
$logicalScreenDescriptor = GifLogicalScreenDescriptor::fromBin($data);
|
||||
$globalColorTable = null;
|
||||
if ($logicalScreenDescriptor->hasGlobalColorTable()) {
|
||||
$globalColorTableSize = 1 << ($logicalScreenDescriptor->getGlobalColorTableSize() + 1);
|
||||
$globalColorTable = GifColorTable::fromBin($data, $globalColorTableSize);
|
||||
}
|
||||
return new GifLogicalScreen($logicalScreenDescriptor, $globalColorTable);
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifLogicalScreenDescriptor
|
||||
{
|
||||
private $width;
|
||||
private $height;
|
||||
private $hasGlobalColorTable;
|
||||
private $colorResolution;
|
||||
private $hasSortedGlobalColorTable;
|
||||
private $globalColorTableSize;
|
||||
private $backgroundColorIndex;
|
||||
private $pixelAspectRatio;
|
||||
|
||||
public function __construct(int $width, int $height, bool $hasGlobalColorTable, int $colorResolution, bool $hasSortedGlobalColorTable, int $globalColorTableSize, int $backgroundColorIndex, int $pixelAspectRatio)
|
||||
{
|
||||
$this -> width = $width;
|
||||
$this -> height = $height;
|
||||
$this -> hasGlobalColorTable = $hasGlobalColorTable;
|
||||
$this -> colorResolution = $colorResolution;
|
||||
$this -> hasSortedGlobalColorTable = $hasSortedGlobalColorTable;
|
||||
$this -> globalColorTableSize = $globalColorTableSize;
|
||||
$this -> backgroundColorIndex = $backgroundColorIndex;
|
||||
$this -> pixelAspectRatio = $pixelAspectRatio;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in) : GifLogicalScreenDescriptor
|
||||
{
|
||||
$sizeData = $in -> read(4);
|
||||
$size = unpack("v2", $sizeData);
|
||||
$width = $size[1];
|
||||
$height = $size[2];
|
||||
$packedFieldData = $in -> read(1);
|
||||
$packedFields = unpack("C", $packedFieldData)[1];
|
||||
$hasGlobalColorTable = ($packedFields >> 7) == 1;
|
||||
$colorResolution = ($packedFields >> 4) & 0x0F;
|
||||
$hasSortedGlobalColorTable = (($packedFields >> 3) & 0x01) == 1;
|
||||
$globalColorTableSize = $packedFields & 0x07;
|
||||
// Everything else
|
||||
$otherFieldData = $in -> read(2);
|
||||
$otherFields = unpack("C2", $otherFieldData);
|
||||
$pixelAspectRatio = $otherFields[1];
|
||||
$backgroundColorIndex = $otherFields[2];
|
||||
return new GifLogicalScreenDescriptor($width, $height, $hasGlobalColorTable, $colorResolution, $hasSortedGlobalColorTable, $globalColorTableSize, $backgroundColorIndex, $pixelAspectRatio);
|
||||
}
|
||||
|
||||
public function getHeight(): int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
public function hasGlobalColorTable(): bool
|
||||
{
|
||||
return $this->hasGlobalColorTable;
|
||||
}
|
||||
|
||||
public function getColorResolution(): int
|
||||
{
|
||||
return $this->colorResolution;
|
||||
}
|
||||
|
||||
public function hasSortedGlobalColorTabled(): bool
|
||||
{
|
||||
return $this->hasSortedGlobalColorTable;
|
||||
}
|
||||
|
||||
public function getGlobalColorTableSize(): int
|
||||
{
|
||||
return $this->globalColorTableSize;
|
||||
}
|
||||
|
||||
public function getBackgroundColorIndex(): int
|
||||
{
|
||||
return $this->backgroundColorIndex;
|
||||
}
|
||||
|
||||
public function getPixelAspectRatio(): int
|
||||
{
|
||||
return $this->pixelAspectRatio;
|
||||
}
|
||||
|
||||
public function getWidth(): int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifPlaintextExt
|
||||
{
|
||||
|
||||
private $header;
|
||||
|
||||
public function getHeader(): string
|
||||
{
|
||||
return $this->header;
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
private $data;
|
||||
|
||||
public function __construct(string $header, array $data)
|
||||
{
|
||||
$this -> header = $header;
|
||||
$this -> data = $data;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in) : GifPlaintextExt
|
||||
{
|
||||
$introducer = $in->read(1);
|
||||
$label = $in->read(1);
|
||||
if ($introducer != GifData::GIF_EXTENSION || $label != GifData::GIF_EXTENSION_PLAINTEXT) {
|
||||
throw new \Exception("Not a GIF plaintext block");
|
||||
}
|
||||
$lenData = $in->read(1);
|
||||
$len = unpack("C", $lenData)[1];
|
||||
if ($len != 12) {
|
||||
throw new \Exception("Incorrect size on plain text block");
|
||||
}
|
||||
// these 12 bytes have meaning, but we are more interested in correctly skipping past this info rather than parsing it, since the feature is quite obscure.
|
||||
$header = $in -> read($len);
|
||||
$data = GifData::readDataSubBlocks($in);
|
||||
return new GifPlaintextExt($header, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
class GifSpecialPurposeBlock
|
||||
{
|
||||
|
||||
private $applicationExt;
|
||||
private $commentExt;
|
||||
|
||||
public function __construct(GifApplicationExt $applicationExt = null, GifCommentExt $commentExt = null)
|
||||
{
|
||||
$this->applicationExt = $applicationExt;
|
||||
$this->commentExt = $commentExt;
|
||||
}
|
||||
|
||||
public function getApplicationExt()
|
||||
{
|
||||
return $this->applicationExt;
|
||||
}
|
||||
|
||||
public function getCommentExt()
|
||||
{
|
||||
return $this->commentExt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifTableBasedImage
|
||||
{
|
||||
private $imageDescriptor;
|
||||
private $lzqMinSize;
|
||||
private $dataSubBlocks;
|
||||
private $localColorTable;
|
||||
|
||||
public function getImageDescriptor(): GifImageDescriptor
|
||||
{
|
||||
return $this->imageDescriptor;
|
||||
}
|
||||
|
||||
public function getLzqMinSize(): int
|
||||
{
|
||||
return $this->lzqMinSize;
|
||||
}
|
||||
|
||||
public function getDataSubBlocks(): array
|
||||
{
|
||||
return $this->dataSubBlocks;
|
||||
}
|
||||
|
||||
public function getLocalColorTable()
|
||||
{
|
||||
return $this->localColorTable;
|
||||
}
|
||||
|
||||
public function __construct(GifImageDescriptor $imageDescriptor, int $lzqMinSize, array $dataSubBlocks, GifColorTable $localColorTable = null)
|
||||
{
|
||||
|
||||
$this->imageDescriptor = $imageDescriptor;
|
||||
$this->lzqMinSize = $lzqMinSize;
|
||||
$this->dataSubBlocks = $dataSubBlocks;
|
||||
$this->localColorTable = $localColorTable;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in) : GifTableBasedImage
|
||||
{
|
||||
$imageDescriptor = GifImageDescriptor::fromBin($in);
|
||||
$localColorTable = null;
|
||||
if ($imageDescriptor->hasLocalColorTable()) {
|
||||
$localColorTableSize = 1 << ($imageDescriptor->getLocalColorTableSize() + 1);
|
||||
$localColorTable = GifColorTable::fromBin($in, $localColorTableSize);
|
||||
}
|
||||
$lzwMinSizeData = $in -> read(1);
|
||||
$lzwMinSize = unpack("C", $lzwMinSizeData)[1];
|
||||
$dataSubBlocks = GifData::readDataSubBlocks($in);
|
||||
return new GifTableBasedImage($imageDescriptor, $lzwMinSize, $dataSubBlocks, $localColorTable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Mike42\GfxPhp\Codec\Gif;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class GifUnknownExt
|
||||
{
|
||||
private $label;
|
||||
private $header;
|
||||
private $data;
|
||||
|
||||
public function __construct(string $label, string $header, array $data)
|
||||
{
|
||||
|
||||
$this->label = $label;
|
||||
$this->header = $header;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getHeader(): string
|
||||
{
|
||||
return $this->header;
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in)
|
||||
{
|
||||
$introducer = $in->read(1);
|
||||
if ($introducer != GifData::GIF_EXTENSION) {
|
||||
throw new \Exception("Not a GIF extension block");
|
||||
}
|
||||
$label = $in->read(1);
|
||||
$lenData = $in->read(1);
|
||||
$len = unpack("C", $lenData)[1];
|
||||
$header = $in -> read($len);
|
||||
$data = GifData::readDataSubBlocks($in);
|
||||
return new GifUnknownExt($label, $header, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataBlobInputStream;
|
||||
use Mike42\GfxPhp\Codec\Gif\GifDataStream;
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
use Mike42\GfxPhp\Util\LzwCompression;
|
||||
use Mike42\GfxPhp\IndexedRasterImage;
|
||||
|
||||
class GifCodec implements ImageEncoder, ImageDecoder
|
||||
{
|
||||
protected static $instance = null;
|
||||
|
||||
public function encode(RasterImage $image, string $format): string
|
||||
{
|
||||
if (!($image instanceof IndexedRasterImage)) {
|
||||
// Convert if necessary
|
||||
$image = $image -> toIndexed();
|
||||
}
|
||||
$palette = $image -> getPalette();
|
||||
if ($image -> getMaxVal() > 256) {
|
||||
// Color count is too large, clone the image and drop the color depth
|
||||
$image = $image -> toIndexed();
|
||||
$image -> setMaxVal(255);
|
||||
$palette = $image -> getPalette();
|
||||
}
|
||||
// GIF signature
|
||||
$signature = pack("c6", 0x47, 0x49, 0x46, 0x38, 0x39, 0x61);
|
||||
// Header chunk
|
||||
$width = $image -> getWidth();
|
||||
$height = $image -> getHeight();
|
||||
$header = pack('v2c3', $width, $height, 0xF7, 0, 0);
|
||||
// Color table of grayscale
|
||||
$colorTable = [];
|
||||
$paletteSize = count($palette);
|
||||
for ($i = 0; $i < $paletteSize; $i++) {
|
||||
$entry = $palette[$i];
|
||||
$colorTable[] = $entry[0];
|
||||
$colorTable[] = $entry[1];
|
||||
$colorTable[] = $entry[2];
|
||||
}
|
||||
// Padding to 256 color entries
|
||||
for ($i = $paletteSize; $i < 256; $i++) {
|
||||
$colorTable[] = 0;
|
||||
$colorTable[] = 0;
|
||||
$colorTable[] = 0;
|
||||
}
|
||||
|
||||
$gct = pack("C*", ... $colorTable);
|
||||
// Transparent color for graphic control
|
||||
$transparentColorFlag = 0x00;
|
||||
$transparentColor = 0;
|
||||
if ($image -> getTransparentColor() >= 0) {
|
||||
$transparentColor = $image -> getTransparentColor() & 0xFF;
|
||||
$transparentColorFlag = 0x01;
|
||||
}
|
||||
// Graphic control
|
||||
// TODO one of these flags does not do what you think it does.
|
||||
$gce = pack("C4vC2", 0x21, 0xF9, 0x04, 0x00 | $transparentColorFlag, 0x00, $transparentColor, 0x00);
|
||||
// Image
|
||||
$imageDescriptor = pack('Cv4C', 0x2C, 0, 0, $width, $height, 0);
|
||||
$raster = $image -> getRasterData();
|
||||
$compressedData = LzwCompression::compress($raster, 0x08);
|
||||
// Field testing the corresponding decoder, which we don't use anywhere else.
|
||||
// It's a good start for GIF decoding if we can at least read our own LZW back.
|
||||
$decompressedData = LzwCompression::decompress($compressedData, 0x08);
|
||||
if ($raster !== $decompressedData) {
|
||||
throw new \Exception("Failed to read back the generated LZW data.");
|
||||
}
|
||||
$slices = str_split($compressedData, 255);
|
||||
$imageData = chr(0x08);
|
||||
foreach ($slices as $slice) {
|
||||
$imageData .= chr(strlen($slice)) . $slice;
|
||||
}
|
||||
$imageData .= chr(0);
|
||||
$terminator = pack("C", 0x3B);
|
||||
return $signature . $header . $gct . $gce . $imageDescriptor . $imageData . $terminator;
|
||||
}
|
||||
|
||||
public function getDecodeFormats(): array
|
||||
{
|
||||
return ["gif"];
|
||||
}
|
||||
|
||||
public function identify(string $blob): string
|
||||
{
|
||||
if (substr($blob, 0, 6) == GifDataStream::GIF87_SIGNATURE) {
|
||||
return "gif";
|
||||
}
|
||||
if (substr($blob, 0, 6) == GifDataStream::GIF89_SIGNATURE) {
|
||||
return "gif";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public function decode(string $blob): RasterImage
|
||||
{
|
||||
$data = DataBlobInputStream::fromBlob($blob);
|
||||
$gif = GifDataStream::fromBinary($data);
|
||||
return $gif -> toRasterImage();
|
||||
}
|
||||
|
||||
public function getEncodeFormats(): array
|
||||
{
|
||||
return ["gif"];
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new GifCodec();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
class ImageCodec
|
||||
{
|
||||
protected static $instance = null;
|
||||
|
||||
protected $encoders;
|
||||
|
||||
protected $decoders;
|
||||
|
||||
public function __construct(array $encoders, array $decoders)
|
||||
{
|
||||
$this -> encoders = $encoders;
|
||||
$this -> decoders = $decoders;
|
||||
}
|
||||
|
||||
public function identify(string $blob) : string
|
||||
{
|
||||
foreach ($this -> decoders as $decoder) {
|
||||
$identity = $decoder -> identify($blob);
|
||||
if ($identity !== '') {
|
||||
return $identity;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getDecoderForFormat(string $format) : ImageDecoder
|
||||
{
|
||||
$format = strtolower($format);
|
||||
foreach ($this -> decoders as $decoder) {
|
||||
if (array_search($format, $decoder -> getDecodeFormats(), true) !== false) {
|
||||
return $decoder;
|
||||
}
|
||||
}
|
||||
throw new \Exception("No decoder for format $format");
|
||||
}
|
||||
|
||||
public function getEncoderForFormat(string $format) : ImageEncoder
|
||||
{
|
||||
$format = strtolower($format);
|
||||
foreach ($this -> encoders as $encoder) {
|
||||
if (array_search($format, $encoder -> getEncodeFormats(), true) !== false) {
|
||||
return $encoder;
|
||||
}
|
||||
}
|
||||
throw new \Exception("No encoder for format '$format'");
|
||||
}
|
||||
|
||||
public static function getInstance() : ImageCodec
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$encoders = [
|
||||
PnmCodec::getInstance(),
|
||||
BmpCodec::getInstance(),
|
||||
PngCodec::getInstance(),
|
||||
GifCodec::getInstance(),
|
||||
WbmpCodec::getInstance()
|
||||
];
|
||||
$decoders = [
|
||||
PngCodec::getInstance(),
|
||||
GifCodec::getInstance(),
|
||||
BmpCodec::getInstance(),
|
||||
PnmCodec::getInstance(),
|
||||
WbmpCodec::getInstance()
|
||||
];
|
||||
self::$instance = new ImageCodec($encoders, $decoders);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
|
||||
interface ImageDecoder
|
||||
{
|
||||
public function getDecodeFormats() : array;
|
||||
|
||||
public function identify(string $blob) : string;
|
||||
|
||||
public function decode(string $blob) : RasterImage;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
|
||||
interface ImageEncoder
|
||||
{
|
||||
public function getEncodeFormats() : array;
|
||||
|
||||
public function encode(RasterImage $image, string $format) : string;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Png;
|
||||
|
||||
class FilterDecoder
|
||||
{
|
||||
/*
|
||||
* Unfilter entire image, or a pass of an interlaced image.
|
||||
*/
|
||||
public function unfilterImage(string $binData, int $scanlineBytes, int $channels, int $bitDepth)
|
||||
{
|
||||
// Extract filtered data
|
||||
$scanlinesWithFiltering = str_split($binData, $scanlineBytes + 1);
|
||||
$filterType = [];
|
||||
$filteredData = [];
|
||||
foreach ($scanlinesWithFiltering as $scanline) {
|
||||
$filterType[] = ord($scanline[0]);
|
||||
$filteredData[] = array_values(unpack("C*", substr($scanline, 1)));
|
||||
}
|
||||
|
||||
// Transform back to raw data
|
||||
$rawData = [];
|
||||
$bytesPerPixel = intdiv($bitDepth + 7, 8) * $channels;
|
||||
$prior = array_fill(0, $scanlineBytes, 0);
|
||||
foreach ($filteredData as $key => $currentFiltered) {
|
||||
$current = $this -> unfilterScanline($currentFiltered, $prior, $filterType[$key], $bytesPerPixel);
|
||||
$imgScanlineData[] = $current;
|
||||
$prior = $current;
|
||||
}
|
||||
return array_merge(...$imgScanlineData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfilter an individual scanline
|
||||
*/
|
||||
public function unfilterScanline(array $currentFiltered, array $prior, int $filterType, int $bpp)
|
||||
{
|
||||
$lw = count($currentFiltered);
|
||||
if ($filterType === 0) {
|
||||
// None
|
||||
return $currentFiltered;
|
||||
} elseif ($filterType === 1) {
|
||||
$ret = array_fill(0, $lw, 128);
|
||||
for ($i = 0; $i < $lw; $i++) {
|
||||
$rawLeft = ($i < $bpp ? 0 : $ret[$i-$bpp]);
|
||||
$subX = $currentFiltered[$i];
|
||||
$ret[$i] = ($subX + $rawLeft) % 256;
|
||||
}
|
||||
return $ret;
|
||||
} elseif ($filterType === 2) {
|
||||
$ret = array_fill(0, $lw, 0);
|
||||
for ($i = 0; $i < $lw; $i++) {
|
||||
$ret[$i] = ($currentFiltered[$i] + $prior[$i]) % 256;
|
||||
}
|
||||
return $ret;
|
||||
} elseif ($filterType === 3) {
|
||||
$ret = array_fill(0, $lw, 0);
|
||||
for ($i = 0; $i < $lw; $i++) {
|
||||
$prevX = $i < $bpp ? 0 : $ret[$i-$bpp];
|
||||
$priorX = $prior[$i];
|
||||
$avgX = intdiv($prevX + $priorX, 2);
|
||||
$prediction = $currentFiltered[$i] - $avgX;
|
||||
$ret[$i] = ($avgX + $currentFiltered[$i]) % 256;
|
||||
}
|
||||
return $ret;
|
||||
} elseif ($filterType === 4) {
|
||||
$ret = array_fill(0, $lw, 0);
|
||||
for ($i = 0; $i < $lw; $i++) {
|
||||
$upperLeft = $i < $bpp ? 0 : $prior[$i-$bpp];
|
||||
$left = $i < $bpp ? 0 : $ret[$i-$bpp];
|
||||
$upper = $prior[$i];
|
||||
$ret[$i] = ($this -> paethPredictor($left, $upper, $upperLeft) + $currentFiltered[$i]) % 256;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
throw new \Exception("Filter type $filterType not valid");
|
||||
}
|
||||
|
||||
private function paethPredictor(int $a, int $b, int $c)
|
||||
{
|
||||
// Nearest-neighbor, based on pseudocode from the PNG spec.
|
||||
$p = $a + $b - $c;
|
||||
$pa = abs($p - $a);
|
||||
$pb = abs($p - $b);
|
||||
$pc = abs($p - $c);
|
||||
if ($pa <= $pb && $pa <= $pc) {
|
||||
return $a;
|
||||
} else if ($pb <= $pc) {
|
||||
return $b;
|
||||
}
|
||||
return $c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Png;
|
||||
|
||||
class InterlaceDecoder
|
||||
{
|
||||
private $filterDecoder;
|
||||
|
||||
public function __construct(FilterDecoder $filterDecoder)
|
||||
{
|
||||
$this -> filterDecoder = $filterDecoder;
|
||||
}
|
||||
|
||||
public function decode(PngHeader $header, string $binData)
|
||||
{
|
||||
if ($header -> getInterlace() === PngHeader::INTERLACE_NONE) {
|
||||
// No interlacing!
|
||||
return $this -> decodeNoInterlace($header, $binData);
|
||||
} else if ($header -> getInterlace() === PngHeader::INTERLACE_ADAM7) {
|
||||
return $this -> decodeAdam7Interlace($header, $binData);
|
||||
} else {
|
||||
throw new \Exception("Unknown interlace type");
|
||||
}
|
||||
return $imageData;
|
||||
}
|
||||
|
||||
private function decodeAdam7Interlace(PngHeader $header, string $binData)
|
||||
{
|
||||
$bitDepth = $header -> getBitDepth();
|
||||
$width = $header -> getWidth();
|
||||
$height = $header -> getHeight();
|
||||
$channels = $header -> getChannels();
|
||||
$scanlineBytes = intdiv($width * $bitDepth + 7, 8) * $channels;
|
||||
// ADAM7 interlace.
|
||||
// Params for laying out pixels in each pass
|
||||
// (startX, stepX, startY, stepY)
|
||||
$passParams = [
|
||||
[0, 8, 0, 8],
|
||||
[4, 8, 0, 8],
|
||||
[0, 4, 4, 8],
|
||||
[2, 4, 0, 4],
|
||||
[0, 2, 2, 4],
|
||||
[1, 2, 0, 2],
|
||||
[0, 1, 1, 2]
|
||||
];
|
||||
// Calculate width and height of each of the seven
|
||||
// sub-images.
|
||||
$passes = [
|
||||
[
|
||||
"width" => intdiv($width + 7, 8),
|
||||
"height" => intdiv($height + 7, 8)
|
||||
],
|
||||
[
|
||||
"width" => intdiv($width + 3, 8),
|
||||
"height" => intdiv($height + 7, 8)
|
||||
],
|
||||
[
|
||||
"width" => intdiv($width + 3, 4),
|
||||
"height" => intdiv($height + 3, 8)
|
||||
],
|
||||
[
|
||||
"width" => intdiv($width + 1, 4),
|
||||
"height" => intdiv($height + 3, 4)
|
||||
],
|
||||
[
|
||||
"width" => intdiv($width + 1, 2),
|
||||
"height" => intdiv($height + 1, 4)
|
||||
],
|
||||
[
|
||||
"width" => intdiv($width, 2),
|
||||
"height" => intdiv($height + 1, 2)
|
||||
],
|
||||
[
|
||||
"width" => $width,
|
||||
"height" => intdiv($height, 2)
|
||||
],
|
||||
];
|
||||
// Extract and unfilter each pass
|
||||
$position = 0;
|
||||
$imageData = array_fill(0, $scanlineBytes * $height, 0);
|
||||
foreach ($passes as $passId => $pass) {
|
||||
$passWidth = $pass['width'];
|
||||
$passHeight = $pass['height'];
|
||||
if ($passWidth == 0 || $passHeight == 0) {
|
||||
// No data in this scanline, proceed.
|
||||
continue;
|
||||
}
|
||||
$passScanlineWidth = intdiv($passWidth * $bitDepth + 7, 8) * $channels;
|
||||
$len = ($passScanlineWidth + 1) * $passHeight;
|
||||
// Extract and de-filter scanlines in this pass.
|
||||
$passScanlines = [];
|
||||
$passUnfiltered = substr($binData, $position, $len);
|
||||
if ($passUnfiltered === false || strlen($passUnfiltered) !== $len) {
|
||||
throw new \Exception("Incomplete image detected.");
|
||||
}
|
||||
$passData = $this -> filterDecoder -> unfilterImage($passUnfiltered, $passScanlineWidth, $channels, $bitDepth);
|
||||
$position += $len;
|
||||
// Paste this pass data over the original image.
|
||||
$startX = $passParams[$passId][0];
|
||||
$stepX = $passParams[$passId][1];
|
||||
$startY = $passParams[$passId][2];
|
||||
$stepY = $passParams[$passId][3];
|
||||
if (($bitDepth * $channels) % 8 == 0) {
|
||||
// Simple case: the pixels fill bytes and never cross byte boundaries.
|
||||
$pixelBytes = intdiv($bitDepth + 1, 8) * $channels;
|
||||
for ($srcY = 0; $srcY < $passHeight; $srcY++) {
|
||||
for ($srcX = 0; $srcX < $passWidth; $srcX++) {
|
||||
$destX = $startX + $stepX * $srcX;
|
||||
$destY = $startY + $stepY * $srcY;
|
||||
for ($i = 0; $i < $pixelBytes; $i++) {
|
||||
// Map byte within pixel (eg. RGBA pixel can be 4 bytes).
|
||||
$srcByte = $srcY * $passWidth * $pixelBytes + $srcX * $pixelBytes + $i;
|
||||
$destByte = $destY * $width * $pixelBytes + $destX * $pixelBytes + $i;
|
||||
$imageData[$destByte] = $passData[$srcByte];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// More complex case: The pixels are 1, 2, or 4 bits wide
|
||||
$pixelBits = $bitDepth * $channels;
|
||||
for ($srcY = 0; $srcY < $passHeight; $srcY++) {
|
||||
for ($srcX = 0; $srcX < $passWidth; $srcX++) {
|
||||
$destX = $startX + $stepX * $srcX;
|
||||
$destY = $startY + $stepY * $srcY;
|
||||
$srcBit = $srcY * $passScanlineWidth * 8 + $srcX * $pixelBits;
|
||||
$destBit = $destY * $scanlineBytes * 8 + $destX * $pixelBits;
|
||||
$srcByte = intdiv($srcBit, 8);
|
||||
$destByte = intdiv($destBit, 8);
|
||||
$srcOffset = $srcBit % 8;
|
||||
$destOffset = $destBit % 8;
|
||||
$srcVal = (($passData[$srcByte] << $srcOffset) & 0xFF) >> (8 - $pixelBits);
|
||||
$destVal = ($srcVal << (8 - $pixelBits - $destOffset));
|
||||
// Logical OR the relevant bits in
|
||||
$imageData[$destByte] |= $destVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $imageData;
|
||||
}
|
||||
|
||||
private function decodeNoInterlace(PngHeader $header, string $binData)
|
||||
{
|
||||
$bitDepth = $header -> getBitDepth();
|
||||
$width = $header -> getWidth();
|
||||
$height = $header -> getHeight();
|
||||
$channels = $header -> getChannels();
|
||||
$scanlineBytes = intdiv($width * $bitDepth + 7, 8) * $channels;
|
||||
return $this -> filterDecoder -> unfilterImage(
|
||||
$binData,
|
||||
$scanlineBytes,
|
||||
$channels,
|
||||
$bitDepth
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Png;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class PngChunk
|
||||
{
|
||||
private $type;
|
||||
private $data;
|
||||
private $crc;
|
||||
|
||||
public function __construct(string $type, string $data)
|
||||
{
|
||||
$this -> type = $type;
|
||||
$this -> data = $data;
|
||||
// Always compute CRC based on the data we have.
|
||||
// If this is being read from a chunk's binary, then
|
||||
// this will be compared, if not, it will be written.
|
||||
$this -> crc = crc32($type . $data);
|
||||
}
|
||||
|
||||
public function toBin()
|
||||
{
|
||||
$len = strlen($this -> data);
|
||||
$lenData = pack("N", $len);
|
||||
$bodyData = $this -> type . $this -> data;
|
||||
$crcData = pack("N", $this -> crc);
|
||||
return $lenData . $bodyData . $crcData;
|
||||
}
|
||||
|
||||
public function getCrc()
|
||||
{
|
||||
return $this -> crc;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return $this -> type;
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this -> data;
|
||||
}
|
||||
|
||||
public static function isValidChunkName(string $name)
|
||||
{
|
||||
if (array_search($name, ["IHDR", "IDAT", "PLTE", "IEND"], true) !== false) {
|
||||
// Critical chunks defined as of PNG 1.2
|
||||
return true;
|
||||
} elseif (preg_match("/[a-z][a-zA-Z]{3}/", $name)) {
|
||||
// Ancillary chunks
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function fromBin(DataInputStream $in)
|
||||
{
|
||||
if ($in -> isEof()) {
|
||||
return null;
|
||||
}
|
||||
$lenData = $in -> read(4);
|
||||
$len = unpack("N", $lenData)[1];
|
||||
$type = $in -> read(4);
|
||||
if (!self::isValidChunkName($type)) {
|
||||
// In case this is not a real chunk, we don't want
|
||||
// to use random binary data in error messages later.
|
||||
throw new \Exception("Bad chunk name");
|
||||
}
|
||||
$data = $in -> read($len);
|
||||
$crcData = $in -> read(4);
|
||||
$crc = unpack("N", $crcData)[1];
|
||||
$chunk = new PngChunk($type, $data);
|
||||
if ($crc != $chunk -> getCrc()) {
|
||||
// Refuse to return chunk with bad checksum
|
||||
throw new \Exception("CRC did not match on $type chunk");
|
||||
}
|
||||
return $chunk;
|
||||
}
|
||||
|
||||
public function toString()
|
||||
{
|
||||
return $this -> type . " chunk";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Png;
|
||||
|
||||
use Mike42\GfxPhp\Codec\Png\PngHeader;
|
||||
|
||||
class PngHeader
|
||||
{
|
||||
const HEADER_SIZE = 13;
|
||||
const COLOR_TYPE_MONOCHROME = 0;
|
||||
const COLOR_TYPE_RGB = 2;
|
||||
const COLOR_TYPE_INDEXED = 3;
|
||||
const COLOR_TYPE_MONOCHROME_ALPHA = 4;
|
||||
const COLOR_TYPE_RGBA = 6;
|
||||
|
||||
const COMPRESSION_DEFLATE = 0;
|
||||
|
||||
const INTERLACE_NONE = 0;
|
||||
const INTERLACE_ADAM7 = 1;
|
||||
|
||||
private $width;
|
||||
private $height;
|
||||
private $bitDepth;
|
||||
private $colorType;
|
||||
private $compression;
|
||||
private $filter;
|
||||
private $interlace;
|
||||
|
||||
public function __construct(int $width, int $height, int $bitDepth, int $colorType, int $compression, int $filter, int $interlace)
|
||||
{
|
||||
// Image dimensions
|
||||
if ($width < 1 || $width > 2147483647 ||
|
||||
$height < 1 || $height > 2147483647) {
|
||||
throw new \Exception("Invalid image dimensions");
|
||||
}
|
||||
$this -> width = $width;
|
||||
$this -> height = $height;
|
||||
// Color type & bit depth - Only some combinations of bit depth and colorType are valid.
|
||||
// I'm sure you could abbreviate this code, but it's written for comparison with the PNG standard.
|
||||
if ($colorType === 0 && ($bitDepth === 1 || $bitDepth === 2 || $bitDepth === 4 || $bitDepth === 8 || $bitDepth === 16)) {
|
||||
$this -> bitDepth = $bitDepth;
|
||||
$this -> colorType = $colorType;
|
||||
} else if ($colorType === 2 && ($bitDepth === 8 || $bitDepth === 16)) {
|
||||
$this -> bitDepth = $bitDepth;
|
||||
$this -> colorType = $colorType;
|
||||
} else if ($colorType === 3 && ($bitDepth === 1 || $bitDepth === 2 || $bitDepth === 4 || $bitDepth === 8)) {
|
||||
$this -> bitDepth = $bitDepth;
|
||||
$this -> colorType = $colorType;
|
||||
} else if ($colorType === 4 && ($bitDepth === 8 || $bitDepth === 16)) {
|
||||
$this -> bitDepth = $bitDepth;
|
||||
$this -> colorType = $colorType;
|
||||
} else if ($colorType === 6 && ($bitDepth === 8 || $bitDepth === 16)) {
|
||||
$this -> bitDepth = $bitDepth;
|
||||
$this -> colorType = $colorType;
|
||||
} else {
|
||||
throw new \Exception("Invalid color type / bit depth combination.");
|
||||
}
|
||||
// Compression
|
||||
if ($compression != PngHeader::COMPRESSION_DEFLATE) {
|
||||
throw new \Exception("Compression type not supported");
|
||||
}
|
||||
$this -> compression = $compression;
|
||||
// Filter type set
|
||||
if ($filter != 0) {
|
||||
throw new \Exception("Filter type set not supported");
|
||||
}
|
||||
$this -> filter = $filter;
|
||||
// Interlace method
|
||||
if ($interlace != PngHeader::INTERLACE_NONE &&
|
||||
$interlace != PngHeader::INTERLACE_ADAM7) {
|
||||
throw new \Exception("Interlace method not supported");
|
||||
}
|
||||
$this -> interlace = $interlace;
|
||||
}
|
||||
|
||||
public static function fromChunk(PngChunk $chunk)
|
||||
{
|
||||
$chunkData = $chunk -> getData();
|
||||
$chunkLen = strlen($chunkData);
|
||||
if ($chunkLen !== PngHeader::HEADER_SIZE) {
|
||||
throw new \Exception("Header must be " . PngHeader::HEADER_SIZE . " bytes, but got $chunkLen bytes.");
|
||||
}
|
||||
// Unpack binary
|
||||
$dataItems = unpack("Nwidth/Nheight/CbitDepth/CcolorType/Ccompression/Cfilter/Cinterlace", $chunkData);
|
||||
// Construct
|
||||
return new PngHeader($dataItems['width'], $dataItems['height'], $dataItems['bitDepth'], $dataItems['colorType'], $dataItems['compression'], $dataItems['filter'], $dataItems['interlace']);
|
||||
}
|
||||
|
||||
public function toString()
|
||||
{
|
||||
return "Image dimensions " . $this -> width . " x " . $this -> height .
|
||||
", bitDepth " . $this -> bitDepth .
|
||||
", colorType " . $this -> colorType .
|
||||
", compression " . $this -> compression .
|
||||
", filter " . $this -> filter .
|
||||
", interlace " . $this -> interlace;
|
||||
}
|
||||
|
||||
public function allowsPalette()
|
||||
{
|
||||
return $this -> requiresPalette() ||
|
||||
$this -> colorType === PngHeader::COLOR_TYPE_RGB ||
|
||||
$this -> colorType === PngHeader::COLOR_TYPE_RGBA;
|
||||
}
|
||||
|
||||
public function requiresPalette()
|
||||
{
|
||||
return $this -> colorType === PngHeader::COLOR_TYPE_INDEXED;
|
||||
}
|
||||
|
||||
public function getWidth()
|
||||
{
|
||||
return $this -> width;
|
||||
}
|
||||
|
||||
public function getHeight()
|
||||
{
|
||||
return $this -> height;
|
||||
}
|
||||
|
||||
public function getBitDepth()
|
||||
{
|
||||
return $this -> bitDepth;
|
||||
}
|
||||
|
||||
public function getColorType()
|
||||
{
|
||||
return $this -> colorType;
|
||||
}
|
||||
|
||||
public function getCompresssion()
|
||||
{
|
||||
return $this -> compresssion;
|
||||
}
|
||||
|
||||
public function getChannels()
|
||||
{
|
||||
// Return number of channels
|
||||
$channelLookup = [
|
||||
PngHeader::COLOR_TYPE_MONOCHROME => 1,
|
||||
PngHeader::COLOR_TYPE_RGB => 3,
|
||||
PngHeader::COLOR_TYPE_INDEXED => 1,
|
||||
PngHeader::COLOR_TYPE_MONOCHROME_ALPHA => 2,
|
||||
PngHeader::COLOR_TYPE_RGBA => 4,
|
||||
];
|
||||
return $channelLookup[$this -> getColorType()];
|
||||
}
|
||||
|
||||
public function getFilter()
|
||||
{
|
||||
return $this -> filter;
|
||||
}
|
||||
|
||||
public function getInterlace()
|
||||
{
|
||||
return $this -> interlace;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec\Png;
|
||||
|
||||
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
|
||||
use Mike42\GfxPhp\GrayscaleRasterImage;
|
||||
use Mike42\GfxPhp\IndexedRasterImage;
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
use Mike42\GfxPhp\Codec\Common\DataInputStream;
|
||||
|
||||
class PngImage
|
||||
{
|
||||
const PNG_SIGNATURE="\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
|
||||
|
||||
private $header;
|
||||
private $imageData;
|
||||
private $chunkPalette;
|
||||
|
||||
private function __construct(PngHeader $header, array $imageData, PngChunk $chunkPalette = null)
|
||||
{
|
||||
$this -> header = $header;
|
||||
$this -> imageData = $imageData;
|
||||
$this -> chunkPalette = $chunkPalette;
|
||||
}
|
||||
|
||||
public static function fromBinary(DataInputStream $data) : PngImage
|
||||
{
|
||||
// Check signature
|
||||
$signature = $data -> read(8);
|
||||
if ($signature != PngImage::PNG_SIGNATURE) {
|
||||
throw new \Exception("Bad PNG signature");
|
||||
}
|
||||
|
||||
// Iterate chunks
|
||||
$chunk_header = PngChunk::fromBin($data);
|
||||
$header = PngHeader::fromChunk($chunk_header);
|
||||
if ($chunk_header == null || $chunk_header -> getType() !== "IHDR") {
|
||||
throw new \Exception("File does not begin with IHDR chunk");
|
||||
}
|
||||
$chunk_palette = null;
|
||||
$chunk_data = [];
|
||||
$chunk_end = null;
|
||||
|
||||
while (( $chunk = PngChunk::fromBin($data) ) !== null) {
|
||||
if ($chunk -> getType() === "IEND") {
|
||||
$chunk_end = $chunk;
|
||||
break;
|
||||
}
|
||||
if ($chunk -> getType() === "PLTE") {
|
||||
if (!$header -> allowsPalette()) {
|
||||
throw new \Exception("Palette not allowed for this image type");
|
||||
} else if ($chunk_palette !== null) {
|
||||
throw new \Exception("Multiple palette entries");
|
||||
} else if (count($chunk_data) > 0) {
|
||||
throw new \Exception("Palette must be issued before first data chunk");
|
||||
}
|
||||
$paletteLen = strlen($chunk -> getData());
|
||||
if ($paletteLen === 0 || $paletteLen > (256 * 3) || $paletteLen % 3 !== 0) {
|
||||
throw new \Exception("Palette length is invalid");
|
||||
}
|
||||
$chunk_palette = $chunk;
|
||||
}
|
||||
if ($chunk -> getType() === "IDAT") {
|
||||
$chunk_data[] = $chunk;
|
||||
}
|
||||
}
|
||||
|
||||
if ($header -> requiresPalette() && $chunk_palette === null) {
|
||||
throw new \Exception("Missing palette, required for this image type");
|
||||
}
|
||||
|
||||
if (count($chunk_data) === 0) {
|
||||
throw new \Exception("No data received");
|
||||
}
|
||||
|
||||
if ($chunk_end === null) {
|
||||
throw new \Exception("File does not end with IEND chunk");
|
||||
}
|
||||
|
||||
if (!$data -> isEof()) {
|
||||
throw new \Exception("Data extends past end of file");
|
||||
}
|
||||
|
||||
// Extract, join and decompress chunks
|
||||
$imageDataCompressed = '';
|
||||
foreach ($chunk_data as $chunk) {
|
||||
$imageDataCompressed .= $chunk -> getData();
|
||||
}
|
||||
// TODO maximum decoded data size can be determined from image size and bit depth
|
||||
$binData = zlib_decode($imageDataCompressed);
|
||||
if ($binData === false) {
|
||||
throw new \Exception("DEFLATE decompression failed");
|
||||
}
|
||||
|
||||
// Turn into array of scan-lines based on filtering
|
||||
$filterDecoder = new FilterDecoder();
|
||||
$interlaceDecoder = new InterlaceDecoder($filterDecoder);
|
||||
$imageData = $interlaceDecoder -> decode($header, $binData);
|
||||
return new PngImage($header, $imageData, $chunk_palette);
|
||||
}
|
||||
|
||||
public function toRasterImage() : RasterImage
|
||||
{
|
||||
// Further processing depends on image type
|
||||
$bitDepth = $this -> header -> getBitDepth();
|
||||
$width = $this -> header -> getWidth();
|
||||
$height = $this -> header -> getHeight();
|
||||
$channels = $this -> header -> getChannels();
|
||||
$imageData = $this -> imageData;
|
||||
switch ($this -> header -> getColorType()) {
|
||||
case PngHeader::COLOR_TYPE_MONOCHROME:
|
||||
switch ($bitDepth) {
|
||||
case 1:
|
||||
$im = BlackAndWhiteRasterImage::create($width, $height, $imageData);
|
||||
$im -> invert(); // Difference in meaning for set/unset pixels.
|
||||
break;
|
||||
case 2:
|
||||
// Re-interpret data with lower depth (2 bits per sample);
|
||||
$expandedData = self::expandBytes2Bpp($imageData, $width);
|
||||
$im = GrayscaleRasterImage::create($width, $height, $expandedData, 0x03);
|
||||
break;
|
||||
case 4:
|
||||
// Re-interpret data with lower depth (4 bits per sample);
|
||||
$expandedData = self::expandBytes4Bpp($imageData, $width);
|
||||
$im = GrayscaleRasterImage::create($width, $height, $expandedData, 0x0F);
|
||||
break;
|
||||
case 8:
|
||||
$im = GrayscaleRasterImage::create($width, $height, $imageData);
|
||||
break;
|
||||
case 16:
|
||||
// Re-interpret data with higher depth.
|
||||
$combinedData = self::combineBytes16Bpp($imageData);
|
||||
$im = GrayscaleRasterImage::create($width, $height, $combinedData, 65535);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("COLOR_TYPE_MONOCHROME at bit depth $bitDepth not supported");
|
||||
}
|
||||
break;
|
||||
case PngHeader::COLOR_TYPE_RGB:
|
||||
switch ($bitDepth) {
|
||||
case 8:
|
||||
$im = RgbRasterImage::create($width, $height, $imageData);
|
||||
break;
|
||||
case 16:
|
||||
// Re-interpret data with higher depth.
|
||||
$combinedData = self::combineBytes16Bpp($imageData);
|
||||
$im = RgbRasterImage::create($width, $height, $combinedData, 0xFFFF);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("COLOR_TYPE_RGB at bit depth $bitDepth not supported");
|
||||
}
|
||||
break;
|
||||
case PngHeader::COLOR_TYPE_INDEXED:
|
||||
switch ($bitDepth) {
|
||||
case 1:
|
||||
$imageData = self::expandBytes1Bpp($imageData, $width);
|
||||
break;
|
||||
case 2:
|
||||
$imageData = self::expandBytes2Bpp($imageData, $width);
|
||||
break;
|
||||
case 4:
|
||||
$imageData = self::expandBytes4Bpp($imageData, $width);
|
||||
break;
|
||||
case 8:
|
||||
// Data is all good.
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("COLOR_TYPE_INDEXED at bit depth $bitDepth not supported");
|
||||
}
|
||||
$paletteArr = array_values(unpack("C*", $this -> chunkPalette -> getData()));
|
||||
$palette = array_chunk($paletteArr, 3);
|
||||
$im = IndexedRasterImage::create($width, $height, $imageData, $palette, 0xFF);
|
||||
break;
|
||||
case PngHeader::COLOR_TYPE_MONOCHROME_ALPHA:
|
||||
// Mix out Alpha and load as Grayscale.
|
||||
switch ($bitDepth) {
|
||||
case 8:
|
||||
$mixedData = $this -> alphaMix($imageData, 2);
|
||||
$im = GrayscaleRasterImage::create($width, $height, $mixedData, 0xFF);
|
||||
break;
|
||||
case 16:
|
||||
$mixedData = $this -> alphaMix(self::combineBytes16Bpp($imageData), 2);
|
||||
$im = GrayscaleRasterImage::create($width, $height, $mixedData, 0xFFFF);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("COLOR_TYPE_MONOCHROME_ALPHA at bit depth $bitDepth not supported");
|
||||
}
|
||||
break;
|
||||
case PngHeader::COLOR_TYPE_RGBA:
|
||||
// Mix out Alpha and load as RGB.
|
||||
switch ($bitDepth) {
|
||||
case 8:
|
||||
$mixedData = $this -> alphaMix($imageData, 4);
|
||||
$im = RgbRasterImage::create($width, $height, $mixedData, 0xFF);
|
||||
break;
|
||||
case 16:
|
||||
$mixedData = $this -> alphaMix(self::combineBytes16Bpp($imageData), 4);
|
||||
$im = RgbRasterImage::create($width, $height, $mixedData, 0xFFFF);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("COLOR_TYPE_RGBA at bit depth $bitDepth not supported");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("Unsupported image type");
|
||||
}
|
||||
return $im;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes 8-bit samples, and produces eight times as many 1-bit samples,
|
||||
* dropping padding bits along the way.
|
||||
*/
|
||||
public static function expandBytes1Bpp(array $in, int $width)
|
||||
{
|
||||
$res = [];
|
||||
$scanlineBytes = intdiv($width + 7, 8);
|
||||
$scanlines = array_chunk($in, $scanlineBytes);
|
||||
foreach ($scanlines as $line) {
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
$srcByte = intdiv($x, 8);
|
||||
$part = $x % 8;
|
||||
$shift = 7 - $part;
|
||||
$res[] = ($line[$srcByte] >> $shift) & 0x01;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes 8-bit samples, and produces four times as many 2-bit samples,
|
||||
* dropping padding bits along the way.
|
||||
*/
|
||||
public static function expandBytes2Bpp(array $in, int $width)
|
||||
{
|
||||
$res = [];
|
||||
$scanlineBytes = intdiv($width + 3, 4);
|
||||
$scanlines = array_chunk($in, $scanlineBytes);
|
||||
foreach ($scanlines as $line) {
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
$srcByte = intdiv($x, 4);
|
||||
$part = $x % 4;
|
||||
$shift = 6 - (2 * $part);
|
||||
$res[] = ($line[$srcByte] >> $shift) & 0x03;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes 8-bit samples, and produces twice as many 4-bit samples,
|
||||
* dropping padding bits along the way.
|
||||
*/
|
||||
public static function expandBytes4Bpp(array $in, int $width)
|
||||
{
|
||||
$scanlineBytes = intdiv($width + 1, 2);
|
||||
$scanlines = array_chunk($in, $scanlineBytes);
|
||||
$res = [];
|
||||
foreach ($scanlines as $line) {
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
$srcByte = intdiv($x, 2);
|
||||
$part = $x % 2;
|
||||
$shift = 4 - (4 * $part);
|
||||
$res[] = ($line[$srcByte] >> $shift) & 0x0F;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes 8-bit samples, and produces half as many 16-bit samples.
|
||||
*/
|
||||
public static function combineBytes16Bpp(array $in)
|
||||
{
|
||||
$data = array_values(unpack("n*", pack("C*", ...$in)));
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* We'll use this to mix with a background color.
|
||||
*/
|
||||
private function alphaMix(array $data, $chunkSize)
|
||||
{
|
||||
// Will need to change to "alphaMixPixel" to [$this, "alphaMixPixel"] once we are in a class.
|
||||
$noAlphaPixels = array_map([$this, "alphaMixPixel"], array_chunk($data, $chunkSize, false));
|
||||
return array_merge(...$noAlphaPixels);
|
||||
}
|
||||
|
||||
private function alphaMixPixel(array $channels)
|
||||
{
|
||||
// Mix alpha to white
|
||||
$maxLevel = 2 ** $this -> header -> getBitDepth() - 1;
|
||||
$backGround = $maxLevel;
|
||||
$alpha = array_pop($channels) / $maxLevel;
|
||||
foreach ($channels as $id => $channel) {
|
||||
$pixels[$id] = ($maxLevel - ($maxLevel - $channel) * $alpha);
|
||||
}
|
||||
return $pixels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
use Mike42\GfxPhp\Codec\Common\DataBlobInputStream;
|
||||
use Mike42\GfxPhp\Codec\Png\PngImage;
|
||||
|
||||
class PngCodec implements ImageEncoder, ImageDecoder
|
||||
{
|
||||
protected static $instance = null;
|
||||
|
||||
public function encode(RasterImage $image, string $format): string
|
||||
{
|
||||
if (!($image instanceof RgbRasterImage)) {
|
||||
// Convert if necessary
|
||||
$image = $image -> toRgb();
|
||||
}
|
||||
return $this -> encodeRgb($image);
|
||||
}
|
||||
|
||||
public function identify(string $blob): string
|
||||
{
|
||||
if (substr($blob, 0, 8) == PngImage::PNG_SIGNATURE) {
|
||||
return "png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public function decode(string $blob): RasterImage
|
||||
{
|
||||
$data = DataBlobInputStream::fromBlob($blob);
|
||||
$png = PngImage::fromBinary($data);
|
||||
return $png -> toRasterImage();
|
||||
}
|
||||
|
||||
public function encodeRgb(RgbRasterImage $image)
|
||||
{
|
||||
// PNG signature
|
||||
$signature = PngImage::PNG_SIGNATURE;
|
||||
// Header chunk
|
||||
$width = $image -> getWidth();
|
||||
$height = $image -> getHeight();
|
||||
$ihdr = $this -> chunk('IHDR', pack('N2C5', $width, $height, 8, 2, 0, 0, 0));
|
||||
// Prepend '0' filter method to each scanline
|
||||
$scanLines = str_split($image -> getRasterData(), $width * 3);
|
||||
$pngRasterData = chr(0) . implode(chr(0), $scanLines);
|
||||
$idat = $this -> chunk('IDAT', zlib_encode($pngRasterData, ZLIB_ENCODING_DEFLATE));
|
||||
// End chunk
|
||||
$iend = $this -> chunk('IEND');
|
||||
return $signature . $ihdr . $idat . $iend;
|
||||
}
|
||||
|
||||
protected function chunk(string $type, string $data = '')
|
||||
{
|
||||
$len = strlen($data);
|
||||
$lenData = pack("N", $len);
|
||||
$bodyData = $type . $data;
|
||||
$crcData = pack("N", crc32($bodyData));
|
||||
return $lenData . $bodyData . $crcData;
|
||||
}
|
||||
|
||||
public function getEncodeFormats(): array
|
||||
{
|
||||
return ["png"];
|
||||
}
|
||||
|
||||
public function getDecodeFormats(): array
|
||||
{
|
||||
return ["png"];
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new PngCodec();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
|
||||
use Mike42\GfxPhp\GrayscaleRasterImage;
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
use Exception;
|
||||
|
||||
class PnmCodec implements ImageDecoder, ImageEncoder
|
||||
{
|
||||
protected static $instance = null;
|
||||
|
||||
public function identify(string $blob): string
|
||||
{
|
||||
$pnmMagic = substr($blob, 0, 2);
|
||||
if ($pnmMagic == "P1" || $pnmMagic == "P4") {
|
||||
// Portable BitMap
|
||||
return "pbm";
|
||||
} else if ($pnmMagic == "P2" || $pnmMagic == "P5") {
|
||||
// Portable GrayMap
|
||||
return "pgm";
|
||||
} else if ($pnmMagic == "P3" || $pnmMagic == "P6") {
|
||||
// Portable PixMap
|
||||
return "ppm";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public function decode(string $blob): RasterImage
|
||||
{
|
||||
// Read header line
|
||||
$im_hdr_line = substr($blob, 0, 3);
|
||||
if ($im_hdr_line !== "P4\n" &&
|
||||
$im_hdr_line !== "P5\n" &&
|
||||
$im_hdr_line !== "P6\n") {
|
||||
throw new Exception("Format not supported. Expected PNM bitmap.");
|
||||
}
|
||||
$pnmMagicNumber = substr($im_hdr_line, 0, 2);
|
||||
// Skip comments
|
||||
$line_end = self::skipComments($blob, 2);
|
||||
// Read image size
|
||||
$next_line_end = strpos($blob, "\n", $line_end + 1);
|
||||
if ($next_line_end === false) {
|
||||
throw new Exception("Unexpected end of file, probably corrupt.");
|
||||
}
|
||||
$size_line = substr($blob, $line_end + 1, ($next_line_end - $line_end) - 1);
|
||||
$sizes = explode(" ", $size_line);
|
||||
if (count($sizes) != 2 || !is_numeric($sizes[0]) || !is_numeric($sizes[1])) {
|
||||
throw new Exception("Image size is bogus, file probably corrupt.");
|
||||
}
|
||||
$width = $sizes[0];
|
||||
$height = $sizes[1];
|
||||
$line_end = $next_line_end;
|
||||
// Extract data and return differently based on each magic number.
|
||||
switch ($pnmMagicNumber) {
|
||||
case "P4":
|
||||
$bytesPerRow = intdiv($width + 7, 8);
|
||||
$expectedBytes = $bytesPerRow * $height;
|
||||
$data = substr($blob, $line_end + 1);
|
||||
$actualBytes = strlen($data);
|
||||
if ($expectedBytes != $actualBytes) {
|
||||
throw new Exception("Expected $expectedBytes data, but got $actualBytes, file probably corrupt.");
|
||||
}
|
||||
$dataUnpacked = unpack("C*", $data);
|
||||
$dataValues = array_values($dataUnpacked);
|
||||
return BlackAndWhiteRasterImage::create($width, $height, $dataValues);
|
||||
case "P5":
|
||||
// Determine color depth
|
||||
$line_end = self::skipComments($blob, $line_end);
|
||||
$next_line_end = strpos($blob, "\n", $line_end + 1);
|
||||
$maxValLine = substr($blob, $line_end + 1, ($next_line_end - $line_end) - 1);
|
||||
$maxVal = (int)$maxValLine;
|
||||
$depth = $maxVal > 255 ? 2 : 1;
|
||||
$line_end = $next_line_end;
|
||||
// Extract data
|
||||
$expectedBytes = $width * $height * $depth;
|
||||
$data = substr($blob, $line_end + 1);
|
||||
$actualBytes = strlen($data);
|
||||
if ($expectedBytes != $actualBytes) {
|
||||
throw new Exception("Expected $expectedBytes data, but got $actualBytes, file probably corrupt.");
|
||||
}
|
||||
if ($depth == 2) {
|
||||
$dataUnpacked = unpack("n*", $data);
|
||||
} else {
|
||||
$dataUnpacked = unpack("C*", $data);
|
||||
}
|
||||
$dataValues = array_values($dataUnpacked);
|
||||
return GrayscaleRasterImage::create($width, $height, $dataValues, $maxVal);
|
||||
case "P6":
|
||||
$line_end = self::skipComments($blob, $line_end);
|
||||
$next_line_end = strpos($blob, "\n", $line_end + 1);
|
||||
$maxValLine = substr($blob, $line_end + 1, ($next_line_end - $line_end) - 1);
|
||||
$maxVal = (int)$maxValLine;
|
||||
$depth = $maxVal > 255 ? 2 : 1;
|
||||
$line_end = $next_line_end;
|
||||
$expectedBytes = $width * $height * $depth * 3;
|
||||
$data = substr($blob, $line_end + 1);
|
||||
$actualBytes = strlen($data);
|
||||
if ($expectedBytes != $actualBytes) {
|
||||
throw new Exception("Expected $expectedBytes data, but got $actualBytes, file probably corrupt.");
|
||||
}
|
||||
if ($depth == 2) {
|
||||
$dataUnpacked = unpack("n*", $data);
|
||||
} else {
|
||||
$dataUnpacked = unpack("C*", $data);
|
||||
}
|
||||
$dataValues = array_values($dataUnpacked);
|
||||
return RgbRasterImage::create($width, $height, $dataValues, $maxVal);
|
||||
}
|
||||
// TODO handle formats in a way that lets us remove this fallthrough.
|
||||
throw new Exception("Format not supported.");
|
||||
}
|
||||
|
||||
public function getDecodeFormats(): array
|
||||
{
|
||||
return ["ppm", "pgm", "pbm"];
|
||||
}
|
||||
|
||||
protected static function skipComments(string $im_data, int $line_end) : int
|
||||
{
|
||||
while ($line_end !== false && substr($im_data, $line_end + 1, 1) == "#") {
|
||||
$line_end = strpos($im_data, "\n", $line_end + 1);
|
||||
}
|
||||
if ($line_end === false) {
|
||||
throw new Exception("Unexpected end of file, probably corrupt.");
|
||||
}
|
||||
return $line_end;
|
||||
}
|
||||
|
||||
public function encode(RasterImage $image, string $format): string
|
||||
{
|
||||
if ($format === "pnm") {
|
||||
// PNM extension can hold PBM, PGM or PPM.
|
||||
// Auto-select based on type of image
|
||||
if ($image instanceof BlackAndWhiteRasterImage) {
|
||||
$format = "pbm";
|
||||
} else if ($image instanceof GrayscaleRasterImage) {
|
||||
$format = "pgm";
|
||||
} else {
|
||||
$format = "ppm";
|
||||
}
|
||||
}
|
||||
// Encode based on extension
|
||||
switch ($format) {
|
||||
case "pbm":
|
||||
if (!($image instanceof BlackAndWhiteRasterImage)) {
|
||||
// Convert if necessary
|
||||
$image = $image -> toBlackAndWhite();
|
||||
}
|
||||
$dimensions = $image -> getWidth() . " " . $image -> getHeight();
|
||||
$data = $image -> getRasterData();
|
||||
$contents = "P4\n$dimensions\n$data";
|
||||
return $contents;
|
||||
case "pgm":
|
||||
if (!($image instanceof GrayscaleRasterImage)) {
|
||||
// Convert if necessary
|
||||
$image = $image -> toGrayscale();
|
||||
}
|
||||
$dimensions = $image -> getWidth() . " " . $image -> getHeight();
|
||||
$maxVal = $image -> getMaxVal();
|
||||
$data = $image -> getRasterData();
|
||||
$contents = "P5\n$dimensions\n$maxVal\n$data";
|
||||
return $contents;
|
||||
case "ppm":
|
||||
if (!($image instanceof RgbRasterImage)) {
|
||||
// Convert if necessary
|
||||
$image = $image -> toRgb();
|
||||
}
|
||||
$dimensions = $image -> getWidth() . " " . $image -> getHeight();
|
||||
$maxVal = $image -> getMaxVal();
|
||||
$data = $image -> getRasterData();
|
||||
$contents = "P6\n$dimensions\n$maxVal\n$data";
|
||||
return $contents;
|
||||
}
|
||||
throw new Exception("Unsupported image type: $format");
|
||||
}
|
||||
|
||||
public function getEncodeFormats(): array
|
||||
{
|
||||
return ["ppm", "pgm", "pbm"];
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new PnmCodec();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Codec;
|
||||
|
||||
use Exception;
|
||||
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
|
||||
use Mike42\GfxPhp\Codec\Common\DataBlobInputStream;
|
||||
use Mike42\GfxPhp\Codec\ImageDecoder;
|
||||
use Mike42\GfxPhp\Codec\ImageEncoder;
|
||||
use Mike42\GfxPhp\RasterImage;
|
||||
|
||||
class WbmpCodec implements ImageDecoder, ImageEncoder
|
||||
{
|
||||
protected static $instance = null;
|
||||
|
||||
public function identify(string $blob): string
|
||||
{
|
||||
$wbmpMagic = substr($blob, 0, 2);
|
||||
if ($wbmpMagic == "\x00\x00") {
|
||||
// Wireless Application Protocol Bitmap
|
||||
return "wbmp";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public function decode(string $blob): RasterImage
|
||||
{
|
||||
$data = DataBlobInputStream::fromBlob($blob);
|
||||
$header = $data -> read(2);
|
||||
if ($header != "\x00\x00") {
|
||||
throw new Exception("Not a WBMP file");
|
||||
}
|
||||
$width = $this -> readInt($data);
|
||||
$height = $this -> readInt($data);
|
||||
$bytesPerRow = intdiv($width + 7, 8);
|
||||
$expectedBytes = $bytesPerRow * $height;
|
||||
$binaryData = $data -> read($expectedBytes);
|
||||
$dataUnpacked = unpack("C*", $binaryData);
|
||||
$dataValues = array_values($dataUnpacked);
|
||||
// 1 for white, 0 for black (opposite)
|
||||
$image = BlackAndWhiteRasterImage::create($width, $height, $dataValues);
|
||||
$image -> invert();
|
||||
return $image;
|
||||
}
|
||||
|
||||
public function readInt(DataBlobInputStream $data) : int
|
||||
{
|
||||
$i = 0;
|
||||
$ret = 0;
|
||||
do {
|
||||
$byte = ord($data -> read(1));
|
||||
$ret = ($ret << 7) | ($byte & 0x7F);
|
||||
$continuation = $byte >> 7 == 1;
|
||||
$i++;
|
||||
} while ($continuation && $i < 4); // Limit to 4 bytes to avoid overflow.
|
||||
if ($continuation) {
|
||||
throw new Exception("WBMP image size too large, file may be corrupt");
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function writeInt(int $val) : string
|
||||
{
|
||||
$i = 0;
|
||||
$ret = chr($val & 0x7F);
|
||||
$val >>= 7;
|
||||
while ($val > 0 && $i < 3) {
|
||||
$byteVal = ($val & 0x7F) | 0x80;
|
||||
$ret = chr($byteVal) . $ret;
|
||||
$val >>= 7;
|
||||
$i++;
|
||||
}
|
||||
if ($val > 0) {
|
||||
throw new Exception("WBMP image size too large.");
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getDecodeFormats(): array
|
||||
{
|
||||
return ["wbmp"];
|
||||
}
|
||||
|
||||
public function encode(RasterImage $image, string $format): string
|
||||
{
|
||||
$image = $image = $image -> toBlackAndWhite();
|
||||
$image -> invert();
|
||||
return "\x00\x00" . $this -> writeInt($image -> getWidth()) . $this -> writeInt($image -> getHeight()) . $image -> getRasterData();
|
||||
}
|
||||
|
||||
public function getEncodeFormats(): array
|
||||
{
|
||||
return ["wbmp"];
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new WbmpCodec();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
class GrayscaleRasterImage extends AbstractRasterImage
|
||||
{
|
||||
protected $width;
|
||||
|
||||
protected $height;
|
||||
|
||||
protected $data;
|
||||
|
||||
protected $maxVal;
|
||||
|
||||
public function getWidth() : int
|
||||
{
|
||||
return $this -> width;
|
||||
}
|
||||
|
||||
public function getHeight() : int
|
||||
{
|
||||
return $this -> height;
|
||||
}
|
||||
|
||||
public function setPixel(int $x, int $y, int $value)
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return;
|
||||
}
|
||||
// Cut off at max and min
|
||||
if ($value < 0) {
|
||||
$value = 0;
|
||||
} else if ($value > $this -> maxVal) {
|
||||
$value = $this -> maxVal;
|
||||
}
|
||||
$byte = $y * $this -> width + $x;
|
||||
$this -> data[$byte] = $value;
|
||||
}
|
||||
|
||||
public function getPixel(int $x, int $y) : int
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return 0;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return 0;
|
||||
}
|
||||
$byte = $y * $this -> width + $x;
|
||||
return $this -> data[$byte];
|
||||
}
|
||||
|
||||
protected function __construct($width, $height, array $data, int $maxVal)
|
||||
{
|
||||
$this -> width = $width;
|
||||
$this -> height = $height;
|
||||
$this -> data = $data;
|
||||
$this -> maxVal = $maxVal;
|
||||
}
|
||||
|
||||
public function getMaxVal()
|
||||
{
|
||||
return $this -> maxVal;
|
||||
}
|
||||
|
||||
public static function create($width, $height, array $data = null, $maxVal = 255) : GrayscaleRasterImage
|
||||
{
|
||||
$expectedBytes = $width * $height;
|
||||
if ($data === null) {
|
||||
$data = array_values(array_fill(0, $expectedBytes, $maxVal));
|
||||
}
|
||||
return new GrayscaleRasterImage($width, $height, $data, $maxVal);
|
||||
}
|
||||
|
||||
public function getRasterData(): string
|
||||
{
|
||||
if ($this -> maxVal > 255) {
|
||||
return pack("n*", ... $this -> data);
|
||||
}
|
||||
return pack("C*", ... $this -> data);
|
||||
}
|
||||
|
||||
public function mapColor(int $srcColor, RasterImage $destImage)
|
||||
{
|
||||
if ($destImage instanceof GrayscaleRasterImage) {
|
||||
if ($destImage -> maxVal == $this -> maxVal) {
|
||||
return $srcColor;
|
||||
}
|
||||
$destVal = intdiv($srcColor * $destImage -> maxVal, $this -> maxVal);
|
||||
return $destVal;
|
||||
}
|
||||
throw new \Exception("Cannot map colors");
|
||||
}
|
||||
|
||||
public function toRgb() : RgbRasterImage
|
||||
{
|
||||
$img = RgbRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$lightness = intdiv($this -> getPixel($x, $y) * 255, $this -> maxVal);
|
||||
$rgb = ($lightness << 16) | ($lightness << 8) | ($lightness);
|
||||
$img -> setPixel($x, $y, $rgb);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toGrayscale() : GrayscaleRasterImage
|
||||
{
|
||||
return clone $this;
|
||||
}
|
||||
|
||||
public function toBlackAndWhite() : BlackAndWhiteRasterImage
|
||||
{
|
||||
$img = BlackAndWhiteRasterImage::create($this -> width, $this -> height);
|
||||
$threshold = intdiv($this -> maxVal, 2);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> getPixel($x, $y);
|
||||
$img -> setPixel($x, $y, $original > $threshold ? 0 : 1);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toIndexed(): IndexedRasterImage
|
||||
{
|
||||
if ($this -> maxVal > 255) {
|
||||
// Making use of how scale() uses default values to make a new canvas, which has the
|
||||
// side-effect of creating an 8-bit image.
|
||||
return $this -> scale($this -> width, $this -> height) -> toIndexed();
|
||||
}
|
||||
$data = $this -> data;
|
||||
$colorTable = PaletteGenerator::monochromePalette();
|
||||
return IndexedRasterImage::create($this -> width, $this -> height, $data, $colorTable, 255);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
use Mike42\GfxPhp\Codec\ImageCodec;
|
||||
|
||||
class Image
|
||||
{
|
||||
// Color depths
|
||||
const IMAGE_BLACK_WHITE = 1;
|
||||
const IMAGE_GRAY = 2;
|
||||
const IMAGE_RGB = 3;
|
||||
const IMAGE_RGBA = 4;
|
||||
|
||||
protected static $codecs = null;
|
||||
|
||||
public static function fromFile(string $filename) : RasterImage
|
||||
{
|
||||
// Attempt to catch the cause of any errors
|
||||
self::clearLastError();
|
||||
$blob = @file_get_contents($filename);
|
||||
if ($blob === false) {
|
||||
$error = self::getLastErrorOrDefault("Check that the file exists and can be read.");
|
||||
throw new \Exception("Could not retrieve image data from '$filename'. $error");
|
||||
}
|
||||
return self::fromBlob($blob, $filename);
|
||||
}
|
||||
|
||||
public static function fromBlob(string $blob, string $filename = null) : RasterImage
|
||||
{
|
||||
if (self::$codecs === null) {
|
||||
self::$codecs = ImageCodec::getInstance();
|
||||
}
|
||||
$format = self::$codecs -> identify($blob);
|
||||
if ($format === null) {
|
||||
throw new \Exception("Unknown format for image '$filename'.");
|
||||
}
|
||||
$decoder = self::$codecs ->getDecoderForFormat($format);
|
||||
if ($decoder === null) {
|
||||
throw new \Exception("Format $format not supported, reading '$filename'.");
|
||||
}
|
||||
return $decoder -> decode($blob);
|
||||
}
|
||||
|
||||
public static function create(int $width, int $height, int $impl = self::IMAGE_BLACK_WHITE)
|
||||
{
|
||||
return BlackAndWhiteRasterImage::create($width, $height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call error_clear_last() if it exists. This is dependent on which PHP runtime is used.
|
||||
*/
|
||||
private static function clearLastError()
|
||||
{
|
||||
if (function_exists('error_clear_last')) {
|
||||
error_clear_last();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the message from error_get_last() if possible. This is very useful for debugging, but it will not
|
||||
* always exist or return anything useful.
|
||||
*/
|
||||
private static function getLastErrorOrDefault(string $default)
|
||||
{
|
||||
if (function_exists('error_clear_last')) {
|
||||
$e = error_get_last();
|
||||
if (isset($e) && isset($e['message']) && $e['message'] != "") {
|
||||
return $e['message'];
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
class IndexedRasterImage extends AbstractRasterImage
|
||||
{
|
||||
protected $width;
|
||||
|
||||
protected $height;
|
||||
|
||||
protected $data;
|
||||
|
||||
protected $maxVal;
|
||||
|
||||
protected $palette;
|
||||
|
||||
protected $transparentColor = -1;
|
||||
|
||||
protected function __construct($width, $height, array $data, int $maxVal, array $palette)
|
||||
{
|
||||
$this -> width = $width;
|
||||
$this -> height = $height;
|
||||
$this -> data = $data;
|
||||
$this -> palette = $palette;
|
||||
$this -> maxVal = $maxVal;
|
||||
}
|
||||
|
||||
public function getPalette()
|
||||
{
|
||||
return $this -> palette;
|
||||
}
|
||||
|
||||
public function getRasterData(): string
|
||||
{
|
||||
|
||||
if ($this -> maxVal > 255) {
|
||||
return pack("n*", ... $this -> data);
|
||||
}
|
||||
return pack("C*", ... $this -> data);
|
||||
}
|
||||
|
||||
public function getHeight(): int
|
||||
{
|
||||
return $this -> height;
|
||||
}
|
||||
|
||||
public function getMaxVal()
|
||||
{
|
||||
return $this -> maxVal;
|
||||
}
|
||||
|
||||
public function setPixel(int $x, int $y, int $value)
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return;
|
||||
}
|
||||
// Use 0 if $value is out of range
|
||||
if ($value < 0 || $value > $this -> maxVal) {
|
||||
$value = 0;
|
||||
}
|
||||
$byte = $y * $this -> width + $x;
|
||||
$this -> data[$byte] = $value;
|
||||
}
|
||||
|
||||
public function toRgb(): RgbRasterImage
|
||||
{
|
||||
$img = RgbRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> indexToRgb($this -> getPixel($x, $y));
|
||||
$val = $img -> rgbToInt($original[0], $original[1], $original[2]);
|
||||
$img -> setPixel($x, $y, $val);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toBlackAndWhite() : BlackAndWhiteRasterImage
|
||||
{
|
||||
$img = BlackAndWhiteRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> indexToRgb($this -> getPixel($x, $y));
|
||||
$lightness = intdiv($original[0] + $original[1] + $original[2], 3);
|
||||
$img -> setPixel($x, $y, $lightness > 128 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toGrayscale(): GrayscaleRasterImage
|
||||
{
|
||||
$img = GrayscaleRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> indexToRgb($this -> getPixel($x, $y));
|
||||
$lightness = intdiv($original[0] + $original[1] + $original[2], 3);
|
||||
$img -> setPixel($x, $y, $lightness);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function getPixel(int $x, int $y) : int
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return 0;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return 0;
|
||||
}
|
||||
$byte = $y * $this -> width + $x;
|
||||
return $this -> data[$byte];
|
||||
}
|
||||
|
||||
public function getWidth(): int
|
||||
{
|
||||
return $this -> width;
|
||||
}
|
||||
|
||||
public function toIndexed(): IndexedRasterImage
|
||||
{
|
||||
return clone $this;
|
||||
}
|
||||
|
||||
public function indexToRgb(int $index)
|
||||
{
|
||||
if ($index === $this -> transparentColor) {
|
||||
// White
|
||||
return [255, 255, 255];
|
||||
}
|
||||
if ($index >= 0 && $index < count($this -> palette)) {
|
||||
// Defined index
|
||||
return $this -> palette[$index];
|
||||
}
|
||||
// Black
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
public function rgbToIndex(array $rgb)
|
||||
{
|
||||
$ret = array_search($rgb, $this -> palette, true);
|
||||
if ($ret !== false) {
|
||||
// Index of defined color
|
||||
return $ret;
|
||||
}
|
||||
// First color.
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getTransparentColor() : int
|
||||
{
|
||||
return $this -> transparentColor;
|
||||
}
|
||||
|
||||
public function setTransparentColor(int $color)
|
||||
{
|
||||
$this -> transparentColor = $color;
|
||||
}
|
||||
|
||||
public function setPalette(array $palette)
|
||||
{
|
||||
$palette = self::validatePalette($palette);
|
||||
// Build map of old palette colors to new ones
|
||||
$map = [];
|
||||
foreach ($this -> palette as $id => $color) {
|
||||
$map[$id] = self::closestColorId($color, $palette);
|
||||
}
|
||||
// Replace existing data values using the map
|
||||
foreach ($this -> data as $k => $v) {
|
||||
$this -> data[$k] = $map[$v];
|
||||
}
|
||||
// Swap palette in the background
|
||||
$this -> palette = $palette;
|
||||
}
|
||||
|
||||
protected static function closestColorId(array $color, array $palette)
|
||||
{
|
||||
$closest = 0;
|
||||
$distance = (256 ** 2) * 3;
|
||||
foreach ($palette as $id => $compare) {
|
||||
$compareDist = (($color[0] - $compare[0]) ** 2) +
|
||||
(($color[1] - $compare[1]) ** 2) +
|
||||
(($color[2] - $compare[2]) ** 2);
|
||||
if ($compareDist < $distance) {
|
||||
$closest = $id;
|
||||
$distance = $compareDist;
|
||||
}
|
||||
}
|
||||
return $closest;
|
||||
}
|
||||
|
||||
public function setMaxVal(int $maxVal)
|
||||
{
|
||||
if ($maxVal >= count($this -> palette) - 1) {
|
||||
// No need to adjust palette
|
||||
$this -> maxVal = $maxVal;
|
||||
return;
|
||||
}
|
||||
// TODO construct suitably-sized palette using quantization
|
||||
if ($maxVal >= 255) {
|
||||
$this -> maxVal = $maxVal;
|
||||
$this -> setPalette(PaletteGenerator::colorPalette());
|
||||
return;
|
||||
} else if ($maxVal >= 1) {
|
||||
$this -> maxVal = $maxVal;
|
||||
$this -> setPalette(PaletteGenerator::blackAndWhitePalette());
|
||||
return;
|
||||
} else if ($maxVal >= 0) {
|
||||
$this -> maxVal = $maxVal;
|
||||
$this -> setPalette(PaletteGenerator::whitePalette());
|
||||
return;
|
||||
}
|
||||
throw new \Exception("Image must contain at least one color");
|
||||
}
|
||||
|
||||
public function allocateColor(array $color)
|
||||
{
|
||||
$idx = count($this -> palette);
|
||||
if ($idx > $this -> maxVal) {
|
||||
throw new \Exception("Palette is at its maximum size");
|
||||
}
|
||||
$this -> palette[] = $color;
|
||||
return $idx;
|
||||
}
|
||||
|
||||
public function deallocateColor(array $color)
|
||||
{
|
||||
// TODO
|
||||
throw new \Exception("Not implemented");
|
||||
}
|
||||
|
||||
protected function createCanvas(int $width, int $height) : RasterImage
|
||||
{
|
||||
return IndexedRasterImage::create($width, $height, null, $this -> getPalette(), $this -> getMaxVal());
|
||||
}
|
||||
|
||||
public function mapColor(int $srcColor, RasterImage $destImage)
|
||||
{
|
||||
if ($destImage instanceof IndexedRasterImage) {
|
||||
return $srcColor;
|
||||
}
|
||||
throw new \Exception("Cannot map colors");
|
||||
}
|
||||
|
||||
public static function create(int $width, int $height, array $data = null, array $palette = [], int $maxVal = 255)
|
||||
{
|
||||
$expectedSize = $width * $height;
|
||||
if ($data == null) {
|
||||
// Empty image, white background
|
||||
if (count($palette) == 0) {
|
||||
$palette = PaletteGenerator::whitePalette(); // White
|
||||
}
|
||||
$data = array_fill(0, $expectedSize, 0);
|
||||
}
|
||||
// Validation
|
||||
$actualSize = count($data);
|
||||
if ($actualSize !== $expectedSize) {
|
||||
throw new \Exception("Expected $expectedSize pixels for $width x $height image, but got $actualSize.");
|
||||
}
|
||||
// Normalise palette data
|
||||
$newPalette = self::validatePalette($palette);
|
||||
// Validate that we can render this data with this palette
|
||||
$highestPaletteValue = count($palette) - 1;
|
||||
$highestPixel = max($data);
|
||||
if ($highestPixel > $highestPaletteValue) {
|
||||
throw new \Exception("Expected all image values to be <= the palette size ($highestPaletteValue), but the highest is $highestPixel.");
|
||||
}
|
||||
$highestPixel = max($data);
|
||||
if ($highestPixel > $highestPaletteValue) {
|
||||
throw new \Exception("Image data cannot be rendered with this palette. The palette contains values up to $highestPaletteValue, but image values go up to $highestPixel.");
|
||||
}
|
||||
return new IndexedRasterImage($width, $height, $data, $maxVal, $palette);
|
||||
}
|
||||
|
||||
protected static function validatePalette($palette)
|
||||
{
|
||||
// So that we know that we aren't missing any keys, palette should be array, not map.
|
||||
// Palette entries must be array of three values up to 255 for R, G, B.
|
||||
// It's slightly easier to just convert the structure than to check all of this
|
||||
$newPalette = [];
|
||||
foreach ($palette as $color) {
|
||||
if (!is_array($color) || count($color) !== 3) {
|
||||
throw new \Exception("Bad palette data: Need three values per entry.");
|
||||
}
|
||||
// Eradicate keys and non-numeric values
|
||||
$color = array_values($color);
|
||||
$color = [(int)$color[0], (int)$color[1], (int)$color[2]];
|
||||
// Gets written to image formats with 8-bits for each value
|
||||
if (max($color) > 255) {
|
||||
throw new \Exception("Bad palette data: Entries cannot exceed 255.");
|
||||
}
|
||||
$newPalette[] = $color;
|
||||
}
|
||||
return $newPalette;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
class PaletteGenerator
|
||||
{
|
||||
public static function monochromePalette()
|
||||
{
|
||||
// 256 even levels of grey
|
||||
$colorTable = [];
|
||||
for ($i = 0; $i < 256; $i++) {
|
||||
$colorTable[] = [$i, $i, $i];
|
||||
}
|
||||
return $colorTable;
|
||||
}
|
||||
|
||||
public static function blackAndWhitePalette()
|
||||
{
|
||||
// 2 color levels
|
||||
return [[255, 255, 255], [0, 0, 0]];
|
||||
}
|
||||
|
||||
public static function colorPalette()
|
||||
{
|
||||
// Three bits of red, three bits of green, two bits of blue.
|
||||
$colorTable = [];
|
||||
for ($i = 0; $i < 256; $i++) {
|
||||
// Unscaled 8, 8, 4 level values
|
||||
$r = ($i >> 5) & 0x07;
|
||||
$g = ($i >> 2) & 0x07;
|
||||
$b = $i & 0x03;
|
||||
|
||||
// Scaled 256-level values
|
||||
$rScaled = (int)round($r * (255 / 7));
|
||||
$gScaled = (int)round($g * (255 / 7));
|
||||
$bScaled = (int)round($b * (255 / 3));
|
||||
|
||||
$colorTable[] = [$rScaled, $gScaled, $bScaled];
|
||||
}
|
||||
return $colorTable;
|
||||
}
|
||||
|
||||
public static function whitePalette()
|
||||
{
|
||||
return [[255, 255, 255]];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
// This file is part of mike42/gfx-php, which is released under LGPL-2.1-or-later.
|
||||
// See file LICENSE or go to https://github.com/mike42/gfx-php for full license details
|
||||
|
||||
/**
|
||||
* Top-level namespace for Mike42\GfxPhp, containing the classes and
|
||||
* interfaces that a provide an entry-point into the library.
|
||||
*/
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
/**
|
||||
* Generic interface to raster images.
|
||||
*/
|
||||
interface RasterImage
|
||||
{
|
||||
/**
|
||||
* Get the width of the image in pixels.
|
||||
*
|
||||
* @return int The width of the image in pixels.
|
||||
*/
|
||||
public function getWidth(): int;
|
||||
|
||||
/**
|
||||
* Get the height of the image in pixels.
|
||||
*
|
||||
* @return int The height of the image in pixels.
|
||||
*/
|
||||
public function getHeight(): int;
|
||||
|
||||
/**
|
||||
* Get a binary string representing the underlying image data. The formatting of this data is implementation-dependent.
|
||||
* @return string A binary string representation of the raster data for this image.
|
||||
*/
|
||||
public function getRasterData(): string;
|
||||
|
||||
/**
|
||||
* Produce a new RasterImage based on this one. The new image will be scaled to the requested dimensions via resampling.
|
||||
*
|
||||
* @param int $width The width of the returned image.
|
||||
* @param int $height The height of the returned image.
|
||||
* @return RasterImage A scaled version of the image.
|
||||
*/
|
||||
public function scale(int $width, int $height): RasterImage;
|
||||
|
||||
/**
|
||||
* Write the image to a file. The output format is determined by the file extension.
|
||||
*
|
||||
* @param string $filename Filename to write to.
|
||||
*/
|
||||
public function write(string $filename);
|
||||
|
||||
/**
|
||||
* Produce a copy of this RasterImage in the RGB colorspace.
|
||||
*
|
||||
* @return RgbRasterImage An RGB version of the image.
|
||||
*/
|
||||
public function toRgb() : RgbRasterImage;
|
||||
|
||||
/**
|
||||
* Get the value of a given pixel. The meaning of the integer value of this pixel is implementation-dependent.
|
||||
*
|
||||
* @param int $x X co-ordinate
|
||||
* @param int $y Y co-ordinate
|
||||
* @return int The value of the pixel at ($x, $y).
|
||||
*/
|
||||
public function getPixel(int $x, int $y) : int;
|
||||
|
||||
/**
|
||||
* Set the value of a given pixel.
|
||||
* @param int $x X co-ordinate
|
||||
* @param int $y Y co-ordinate
|
||||
* @param int $value Value to set
|
||||
*/
|
||||
public function setPixel(int $x, int $y, int $value);
|
||||
|
||||
/**
|
||||
* Produce a copy of this RasterImage in a monochrome colorspace.
|
||||
*
|
||||
* @return GrayscaleRasterImage A monochrome version of the image.
|
||||
*/
|
||||
public function toGrayscale() : GrayscaleRasterImage;
|
||||
|
||||
/**
|
||||
* Produce a copy of this RasterImage in a pure black-and-white colorspace.
|
||||
* @return BlackAndWhiteRasterImage a black and white version of the image.
|
||||
*/
|
||||
public function toBlackAndWhite() : BlackAndWhiteRasterImage;
|
||||
|
||||
/**
|
||||
* Produce a copy of this RasterImage as an indexed image with an associated palette of unique colors.
|
||||
*
|
||||
* @return IndexedRasterImage An paletted version of the image.
|
||||
*/
|
||||
public function toIndexed() : IndexedRasterImage;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp;
|
||||
|
||||
class RgbRasterImage extends AbstractRasterImage
|
||||
{
|
||||
protected $width;
|
||||
|
||||
protected $height;
|
||||
|
||||
protected $data;
|
||||
|
||||
protected $maxVal;
|
||||
|
||||
public function getWidth() : int
|
||||
{
|
||||
return $this -> width;
|
||||
}
|
||||
|
||||
public function getHeight() : int
|
||||
{
|
||||
return $this -> height;
|
||||
}
|
||||
|
||||
public function getRasterData(): string
|
||||
{
|
||||
if ($this -> maxVal > 255) {
|
||||
return pack("n*", ... $this -> data);
|
||||
}
|
||||
return pack("C*", ... $this -> data);
|
||||
}
|
||||
|
||||
public function getMaxVal()
|
||||
{
|
||||
return $this -> maxVal;
|
||||
}
|
||||
|
||||
public function getPixel(int $x, int $y) : int
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return 0;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return 0;
|
||||
}
|
||||
$byte = ($y * $this -> width + $x) * 3;
|
||||
return self::rgbToInt($this -> data[$byte], $this -> data[$byte + 1], $this -> data[$byte + 2], $this -> maxVal);
|
||||
}
|
||||
|
||||
public function indexToRgb(int $val)
|
||||
{
|
||||
return self::intToRgb($val);
|
||||
}
|
||||
|
||||
public function rgbToIndex(array $val)
|
||||
{
|
||||
return self::rgbToInt($val[0], $val[1], $val[2]);
|
||||
}
|
||||
|
||||
public static function rgbToInt(int $r, int $g, int $b)
|
||||
{
|
||||
return ($r << 16) | ($g << 8) | $b;
|
||||
}
|
||||
|
||||
public static function intToRgb($in)
|
||||
{
|
||||
return [
|
||||
($in >> 16) & 0xFF,
|
||||
($in >> 8) & 0xFF,
|
||||
($in) & 0xFF
|
||||
];
|
||||
}
|
||||
|
||||
public function setPixel(int $x, int $y, int $value)
|
||||
{
|
||||
if ($x < 0 || $x >= $this -> width) {
|
||||
return;
|
||||
}
|
||||
if ($y < 0 || $y >= $this -> height) {
|
||||
return;
|
||||
}
|
||||
$rgb = self::intToRgb($value);
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$value = $rgb[$i] & 0xFF;
|
||||
$byte = ($y * $this -> width + $x) * 3;
|
||||
$this -> data[$byte + $i] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function mapColor(int $srcColor, RasterImage $destImage)
|
||||
{
|
||||
if ($destImage instanceof RgbRasterImage) {
|
||||
return $srcColor;
|
||||
}
|
||||
throw new \Exception("Cannot map colors");
|
||||
}
|
||||
|
||||
protected function __construct($width, $height, array $data, int $maxVal)
|
||||
{
|
||||
$this -> width = $width;
|
||||
$this -> height = $height;
|
||||
$this -> data = $data;
|
||||
$this -> maxVal = $maxVal;
|
||||
}
|
||||
|
||||
public static function create($width, $height, array $data = null, $maxVal = 255) : RgbRasterImage
|
||||
{
|
||||
$expectedBytes = $width * $height * 3;
|
||||
if ($data === null) {
|
||||
$data = array_values(array_fill(0, $expectedBytes, $maxVal));
|
||||
}
|
||||
if ($maxVal > 255) {
|
||||
array_walk($data, array('self', 'convertDepth'), [$maxVal, 255]);
|
||||
$maxVal = 255;
|
||||
}
|
||||
return new RgbRasterImage($width, $height, $data, $maxVal);
|
||||
}
|
||||
|
||||
public static function convertDepth(&$item, $key, array $data)
|
||||
{
|
||||
$maxVal = $data[0];
|
||||
$newMaxVal = $data[1];
|
||||
$item = intdiv($item * $newMaxVal, $maxVal);
|
||||
}
|
||||
|
||||
public function toRgb() : RgbRasterImage
|
||||
{
|
||||
return clone $this;
|
||||
}
|
||||
|
||||
public function toGrayscale() : GrayscaleRasterImage
|
||||
{
|
||||
$img = GrayscaleRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> intToRgb($this -> getPixel($x, $y));
|
||||
$lightness = intdiv($original[0] + $original[1] + $original[2], 3);
|
||||
$img -> setPixel($x, $y, $lightness);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toBlackAndWhite() : BlackAndWhiteRasterImage
|
||||
{
|
||||
$img = BlackAndWhiteRasterImage::create($this -> width, $this -> height);
|
||||
for ($y = 0; $y < $this -> height; $y++) {
|
||||
for ($x = 0; $x < $this -> width; $x++) {
|
||||
$original = $this -> intToRgb($this -> getPixel($x, $y));
|
||||
$lightness = intdiv($original[0] + $original[1] + $original[2], 3);
|
||||
$img -> setPixel($x, $y, $lightness > 128 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
public function toIndexed(): IndexedRasterImage
|
||||
{
|
||||
// NB: It might be possible to speed this up with array_fill_keys and array_replace.
|
||||
// Each pixel as a numeric value
|
||||
$pixels = array_map([$this, "rgbToIndex"], array_chunk($this -> data, 3, false));
|
||||
// List of unique colors
|
||||
$colorValues = array_values(array_unique($pixels, SORT_NUMERIC));
|
||||
$paletteSize = count($colorValues);
|
||||
// Use 24-bit color number as key, palette index as value
|
||||
$lookup = array_flip($colorValues);
|
||||
// Replace palette values w/ expanded [r, g, b] values for use in IndexedRasterImage
|
||||
for ($i = 0; $i < $paletteSize; $i++) {
|
||||
$colorValues[$i] = $this -> intToRgb($colorValues[$i]);
|
||||
}
|
||||
// Replace pixel values with color ID's
|
||||
$imageSize = count($pixels);
|
||||
for ($i = 0; $i < $imageSize; $i++) {
|
||||
$pixels[$i] = $lookup[$pixels[$i]];
|
||||
}
|
||||
// Max value round-off to 256 colors if possible, otherwise leave unlimited
|
||||
$maxVal = $paletteSize > 256 ? 16777215 : 255;
|
||||
return IndexedRasterImage::create($this -> width, $this -> height, $pixels, $colorValues, $maxVal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Util;
|
||||
|
||||
abstract class AbstractLzwDictionary
|
||||
{
|
||||
const MAX_SIZE = 4096;
|
||||
|
||||
protected $minCodeSize;
|
||||
protected $clearCode;
|
||||
protected $eodCode;
|
||||
protected $size;
|
||||
|
||||
/**
|
||||
* @return number
|
||||
*/
|
||||
public function getClearCode()
|
||||
{
|
||||
return $this->clearCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return number
|
||||
*/
|
||||
public function getEodCode()
|
||||
{
|
||||
return $this->eodCode;
|
||||
}
|
||||
|
||||
public function __construct(int $minCodeSize)
|
||||
{
|
||||
$this -> minCodeSize = $minCodeSize;
|
||||
$this -> clear();
|
||||
}
|
||||
|
||||
public function getSize()
|
||||
{
|
||||
return $this -> size;
|
||||
}
|
||||
|
||||
abstract public function clear();
|
||||
|
||||
abstract public function add(string $entry);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* Compression and decompression of binary data in the LZW format, as
|
||||
* used in GIF.
|
||||
*
|
||||
* This is implemented based on these decriptions:
|
||||
*
|
||||
* - http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp
|
||||
* - https://www2.cs.duke.edu/csed/curious/compression/lzw.html
|
||||
* - https://www.cs.cmu.edu/~cil/lzw.and.gif.txt
|
||||
* - https://rosettacode.org/wiki/LZW_compression
|
||||
*/
|
||||
namespace Mike42\GfxPhp\Util;
|
||||
|
||||
/**
|
||||
* Utility classes to decode or encode entire strings.
|
||||
*/
|
||||
class LzwCompression
|
||||
{
|
||||
public static function compress(string $inp, int $minCodeSize)
|
||||
{
|
||||
$bits = $minCodeSize + 1;
|
||||
$dict = new LzwEncodeDictionary($minCodeSize);
|
||||
$outp = new LzwEncodeBuffer();
|
||||
$outp -> add($dict -> getClearCode(), $bits);
|
||||
$word = "";
|
||||
for ($i = 0; $i < strlen($inp); $i++) {
|
||||
$ch = $word . $inp[$i];
|
||||
if ($dict -> contains($ch)) {
|
||||
$word = $ch;
|
||||
} else {
|
||||
$outp -> add($dict -> get($word), $bits);
|
||||
$dict -> add($ch);
|
||||
if ($dict -> getSize() > (2 ** $bits)) {
|
||||
// expand bit size
|
||||
$bits++;
|
||||
}
|
||||
$word = $inp[$i];
|
||||
}
|
||||
// Separately to parsing the input stream, periodically reset to keep under 12 bits
|
||||
if ($dict -> getSize() >= AbstractLzwDictionary::MAX_SIZE) {
|
||||
if ($word !== "") {
|
||||
$outp -> add($dict -> get($word), $bits);
|
||||
}
|
||||
$outp -> add($dict -> getClearCode(), $bits);
|
||||
$bits = $minCodeSize + 1;
|
||||
$dict -> clear();
|
||||
$word = "";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($word !== "") {
|
||||
$outp -> add($dict -> get($word), $bits);
|
||||
}
|
||||
$outp -> add($dict -> getEodCode(), $bits);
|
||||
return $outp -> asString();
|
||||
}
|
||||
|
||||
public static function decompress(string $inp, int $minCodeSize)
|
||||
{
|
||||
$bits = $minCodeSize + 1;
|
||||
$dict = new LzwDecodeDictionary($minCodeSize);
|
||||
$buffer = new LzwDecodeBuffer($inp);
|
||||
$outp = "";
|
||||
// Output first code
|
||||
$prevcode = $buffer -> read($bits);
|
||||
while ($prevcode == $dict -> getClearCode()) {
|
||||
// Skip any 'clear' codes at the start
|
||||
$prevcode = $buffer -> read($bits);
|
||||
}
|
||||
if ($prevcode === false || $prevcode >= $dict -> getSize()) {
|
||||
throw new \Exception("Not LZW data");
|
||||
}
|
||||
$prevcodeStr = $dict -> get($prevcode);
|
||||
$outp .= $prevcodeStr;
|
||||
while (($code = $buffer -> read($bits)) !== false) {
|
||||
// Decode
|
||||
if ($code > $dict -> getSize()) {
|
||||
throw new \Exception("Bad LZW, code too large");
|
||||
}
|
||||
|
||||
if ($code === $dict -> getClearCode()) {
|
||||
// got a clear code
|
||||
$bits = $minCodeSize + 1;
|
||||
$prevcodeStr = "";
|
||||
$dict -> clear();
|
||||
// Manually do an iteration with no dictionary modification.
|
||||
// This involves reading ahead 1 character, same as what we do above the
|
||||
// 'while' loop. There is probably some way to combine these
|
||||
$prevcode = $buffer -> read($bits);
|
||||
while ($prevcode == $dict -> getClearCode()) {
|
||||
// In case of consecutive clear codes
|
||||
$prevcode = $buffer -> read($bits);
|
||||
}
|
||||
if ($prevcode === false || $prevcode === $dict -> getEodCode()) {
|
||||
// EOF
|
||||
break;
|
||||
}
|
||||
if ($prevcode >= $dict -> getSize()) {
|
||||
throw new \Exception("Bad LZW, code too large after reset");
|
||||
}
|
||||
$prevcodeStr = $dict -> get($prevcode);
|
||||
$outp .= $prevcodeStr;
|
||||
continue;
|
||||
} else if ($code === $dict -> getEodCode()) {
|
||||
// End the stream
|
||||
break;
|
||||
}
|
||||
|
||||
if ($dict -> contains($code)) {
|
||||
$codeStr = $dict -> get($code);
|
||||
} else {
|
||||
$codeStr = $prevcodeStr . $prevcodeStr[0];
|
||||
}
|
||||
$outp .= $codeStr;
|
||||
$ch = $codeStr[0];
|
||||
if ($dict -> getSize() < 4096) { // Don't modify dictionary after a certain point
|
||||
$dict -> add($prevcodeStr . $ch);
|
||||
}
|
||||
// assign current code to prev code
|
||||
$prevcode = $code;
|
||||
$prevcodeStr = $codeStr;
|
||||
if ($dict -> getSize() >= (2 ** $bits)) {
|
||||
if (($bits + 1) > 12) {
|
||||
// Don't exceed 12 bits. If table gets max'd out at
|
||||
// 4096, the following RESET or END code remains 12-bit encoded.
|
||||
continue;
|
||||
} else {
|
||||
// Expand
|
||||
$bits++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $outp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mike42\GfxPhp\Util;
|
||||
|
||||
/**
|
||||
* Treat incoming string as a stream of bits
|
||||
*/
|
||||
class LzwDecodeBuffer
|
||||
{
|
||||
protected $contents;
|
||||
protected $ptr;
|
||||
protected $len;
|
||||
const MASK = [0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF];
|
||||
|
||||
public function __construct(string $contents)
|
||||
{
|
||||
$this -> contents = array_values(unpack("C*", strrev($contents)));
|
||||
$this -> ptr = count($this -> contents) * 8 - 1;
|
||||
//print_r($this -> contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $readBits Number of bits to read
|
||||
*/
|
||||
public function read(int $readBits)
|
||||
{
|
||||
$num = 0;
|
||||
$firstBit = $this -> ptr - $readBits + 1;
|
||||
$lastBit = $this -> ptr;
|
||||
if ($firstBit < 0) {
|
||||
return false;
|
||||
}
|
||||
$val = 0;
|
||||
for ($i = $firstBit; $i <= $lastBit; $i++) {
|
||||
$bit = $this -> readBit($i);
|
||||
$val = ($val << 1) | $bit;
|
||||
}
|
||||
$this -> ptr -= $readBits;
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function readBit(int $i)
|
||||
{
|
||||
$byte = intdiv($i, 8);
|
||||
$bit = $i % 8;
|
||||
$rightIgnore = 7 - $bit;
|
||||
return (($this -> contents[$byte]) >> $rightIgnore) & 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Util;
|
||||
|
||||
class LzwDecodeDictionary extends AbstractLzwDictionary
|
||||
{
|
||||
|
||||
protected $decodeDict;
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$count = 2 << ($this -> minCodeSize - 1);
|
||||
$this -> decodeDict = range(chr(0), chr($count - 1));
|
||||
$this -> clearCode = $count;
|
||||
$count++;
|
||||
$this -> eodCode = $count;
|
||||
$count++;
|
||||
$this -> size = $count;
|
||||
}
|
||||
|
||||
public function get(int $code)
|
||||
{
|
||||
if (!$this -> contains($code)) {
|
||||
throw new \Exception("LZW decode error; was asked to retrieve code $code but dict is only 0-" . ($this -> size - 1) . ".");
|
||||
}
|
||||
return $this -> decodeDict[$code];
|
||||
}
|
||||
|
||||
public function contains(int $code)
|
||||
{
|
||||
return $code < $this -> size;
|
||||
}
|
||||
|
||||
public function add(string $entry)
|
||||
{
|
||||
if ($this -> size == AbstractLzwDictionary::MAX_SIZE) {
|
||||
throw new \Exception("LZW code table overflow");
|
||||
}
|
||||
$this -> decodeDict[$this -> size] = $entry;
|
||||
$this -> size++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Util;
|
||||
|
||||
/*
|
||||
* Write variable-width output in the GIF layout
|
||||
*/
|
||||
class LzwEncodeBuffer
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this -> textBuffer = "";
|
||||
$this -> bitBuffer = 0;
|
||||
$this -> bitPos = 0;
|
||||
}
|
||||
|
||||
public function add(int $code, int $bits)
|
||||
{
|
||||
$mask = [];
|
||||
$byte = 0;
|
||||
for ($i = 0; $i <= 8; $i++) {
|
||||
$mask[] = $byte;
|
||||
$byte = ($byte << 1) | 1;
|
||||
}
|
||||
while ($bits > 0) {
|
||||
// Determine number of bits to write to this byte
|
||||
$capacity = (8 - $this -> bitPos);
|
||||
$writeBits = min($capacity, $bits);
|
||||
// Extract bits to use, shift and OR them on
|
||||
$maskedByte = ($code & $mask[$writeBits]);
|
||||
$shiftDir = ($this -> bitPos);
|
||||
$theBits = $maskedByte << $shiftDir;
|
||||
$this -> bitBuffer = $this -> bitBuffer | $theBits;
|
||||
// Truncate lower bits of code and repeat
|
||||
$code = $code >> $writeBits;
|
||||
$bits = $bits - $writeBits;
|
||||
$this -> bitPos += $writeBits;
|
||||
if ($this -> bitPos >= 8) {
|
||||
$this -> bitPos = 0;
|
||||
$this -> textBuffer .= chr($this -> bitBuffer);
|
||||
$this -> bitBuffer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function asString()
|
||||
{
|
||||
if ($this -> bitPos !== 0) {
|
||||
// Flush
|
||||
$this -> textBuffer .= chr($this -> bitBuffer);
|
||||
$this -> bitPos = 0;
|
||||
}
|
||||
return $this -> textBuffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace Mike42\GfxPhp\Util;
|
||||
|
||||
class LzwEncodeDictionary extends AbstractLzwDictionary
|
||||
{
|
||||
protected $encodeDict;
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$count = 2 << ($this -> minCodeSize - 1);
|
||||
$this -> encodeDict = array_flip(range(chr(0), chr($count - 1)));
|
||||
$this -> clearCode = $count;
|
||||
$count++;
|
||||
$this -> eodCode = $count;
|
||||
$count++;
|
||||
$this -> size = $count;
|
||||
}
|
||||
|
||||
public function get(string $code)
|
||||
{
|
||||
if (!$this -> contains($code)) {
|
||||
throw new \Exception("LZW encode error; code sequence not in dictionary.");
|
||||
}
|
||||
return $this -> encodeDict[$code];
|
||||
}
|
||||
|
||||
public function contains(string $code)
|
||||
{
|
||||
return isset($this -> encodeDict[$code]);
|
||||
}
|
||||
|
||||
public function add(string $entry)
|
||||
{
|
||||
if ($this -> size == self::MAX_SIZE) {
|
||||
throw new \Exception("LZW code table overflow");
|
||||
}
|
||||
$this -> encodeDict[$entry] = $this -> size;
|
||||
$this -> size++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"init"})
|
||||
* @Revs(1000)
|
||||
*/
|
||||
class GifDecodeBenchmark {
|
||||
|
||||
private static $blob;
|
||||
|
||||
public function init()
|
||||
{
|
||||
self::$blob = file_get_contents(__DIR__ . "/../../../resources/pygif/high-color.gif");
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function gifDecode()
|
||||
{
|
||||
Image::fromBlob(self::$blob, "foo.gif");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\Util\LzwCompression;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"init"})
|
||||
* @Revs(100)
|
||||
*/
|
||||
class LzwDecodeBenchmark
|
||||
{
|
||||
private $blank;
|
||||
private $random;
|
||||
private $repeating;
|
||||
|
||||
public function init()
|
||||
{
|
||||
$this -> blank = LzwCompression::compress(str_repeat("\0", 65536), 0x08);
|
||||
$this -> random = LzwCompression::compress($this -> random_str(65536), 0x08);
|
||||
$this -> repeating = LzwCompression::compress(str_repeat($this -> random_str(256), 256), 0x08);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function lzwDecodeBlank() {
|
||||
LzwCompression::decompress($this -> blank, 0x08);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function lzwDecodeRandom() {
|
||||
LzwCompression::decompress($this -> random, 0x08);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function lzwDecodeRepeating() {
|
||||
LzwCompression::decompress($this -> repeating, 0x08);
|
||||
}
|
||||
|
||||
/** Random string, not cryptographically strong */
|
||||
public static function random_str(int $len)
|
||||
{
|
||||
$str = str_repeat("\0", $len);
|
||||
for($i = 0 ; $i < $len; $i++) {
|
||||
$str[$i] = chr(rand(0, 255));
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\Util\LzwCompression;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"init"})
|
||||
* @Revs(10)
|
||||
*/
|
||||
class LzwEncodeBenchmark
|
||||
{
|
||||
private $blank;
|
||||
private $random;
|
||||
private $repeating;
|
||||
|
||||
public function init()
|
||||
{
|
||||
$this -> blank = str_repeat("\0", 65536);
|
||||
$this -> random = $this -> random_str(65536);
|
||||
$this -> repeating = str_repeat($this -> random_str(256), 256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function lzwEncodeBlank() {
|
||||
LzwCompression::compress($this -> blank, 0x08);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function lzwEncodeRandom() {
|
||||
LzwCompression::compress($this -> random, 0x08);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function lzwEncodeRepeating() {
|
||||
LzwCompression::compress($this -> repeating, 0x08);
|
||||
}
|
||||
|
||||
/** Random string, not cryptographically strong */
|
||||
public static function random_str(int $len)
|
||||
{
|
||||
$str = str_repeat("\0", $len);
|
||||
for($i = 0 ; $i < $len; $i++) {
|
||||
$str[$i] = chr(rand(0, 255));
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"init"})
|
||||
* @Revs(1000)
|
||||
*/
|
||||
class PngDecodeBenchmark {
|
||||
|
||||
private static $blob;
|
||||
|
||||
public function init()
|
||||
{
|
||||
self::$blob = file_get_contents(__DIR__ . "/../../../resources/pngsuite/z09n2c08.png");
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function pngDecode()
|
||||
{
|
||||
Image::fromBlob(self::$blob, "foo.png");
|
||||
}
|
||||
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"init"})
|
||||
* @Revs(1000)
|
||||
*/
|
||||
class ConvertRasterImageBenchmark
|
||||
{
|
||||
private static $bw100;
|
||||
private static $gray100;
|
||||
private static $indexed100;
|
||||
private static $rgb100;
|
||||
|
||||
public function init()
|
||||
{
|
||||
self::$rgb100 = Image::fromFile(__DIR__ . "/../../resources/pngsuite/z09n2c08.png");
|
||||
self::$bw100 = self::$rgb100 -> toBlackAndWhite();
|
||||
self::$indexed100 = self::$rgb100 -> toIndexed();
|
||||
self::$gray100 = self::$rgb100 -> toGrayscale();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertBwToBw()
|
||||
{
|
||||
self::$bw100->toBlackAndWhite();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertBwToGray()
|
||||
{
|
||||
self::$bw100->toGrayscale();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertBwToIndexed()
|
||||
{
|
||||
self::$bw100->toIndexed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertBwToRgb()
|
||||
{
|
||||
self::$bw100->toRgb();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertGrayToBw()
|
||||
{
|
||||
self::$gray100->toBlackAndWhite();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertGrayToGray()
|
||||
{
|
||||
self::$gray100->toGrayscale();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertGrayToIndexed()
|
||||
{
|
||||
self::$gray100->toIndexed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertGrayToRgb()
|
||||
{
|
||||
self::$gray100->toRgb();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertIndexedToBw()
|
||||
{
|
||||
self::$indexed100->toBlackAndWhite();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertIndexedToGray()
|
||||
{
|
||||
self::$indexed100->toGrayscale();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertIndexedToIndexed()
|
||||
{
|
||||
self::$indexed100->toIndexed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertIndexedToRgb()
|
||||
{
|
||||
self::$indexed100->toRgb();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertRgbToBw()
|
||||
{
|
||||
self::$rgb100->toBlackAndWhite();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertRgbToGray()
|
||||
{
|
||||
self::$rgb100->toGrayscale();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertRgbToIndexed()
|
||||
{
|
||||
self::$rgb100->toIndexed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function convertRgbToRgb()
|
||||
{
|
||||
self::$rgb100->toRgb();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
|
||||
use Mike42\GfxPhp\GrayscaleRasterImage;
|
||||
use Mike42\GfxPhp\IndexedRasterImage;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"init"})
|
||||
* @Revs(10000)
|
||||
*/
|
||||
class ScaleDownBenchmark
|
||||
{
|
||||
private static $bw100;
|
||||
private static $rgb100;
|
||||
private static $indexed100;
|
||||
private static $gray100;
|
||||
|
||||
public function init()
|
||||
{
|
||||
self::$bw100 = BlackAndWhiteRasterImage::create(100, 100);
|
||||
self::$rgb100 = RgbRasterImage::create(100, 100);
|
||||
self::$indexed100 = IndexedRasterImage::create(100, 100);
|
||||
self::$gray100 = GrayscaleRasterImage::create(100, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleDownBw()
|
||||
{
|
||||
self::$bw100->scale(10, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleDownGrayscale()
|
||||
{
|
||||
self::$gray100->scale(10, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleDownIndexed()
|
||||
{
|
||||
self::$indexed100->scale(10, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleDownRgb()
|
||||
{
|
||||
self::$rgb100->scale(10, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
|
||||
use Mike42\GfxPhp\GrayscaleRasterImage;
|
||||
use Mike42\GfxPhp\IndexedRasterImage;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"init"})
|
||||
* @Revs(10)
|
||||
*/
|
||||
class ScaleUpBenchmark
|
||||
{
|
||||
private static $bw10;
|
||||
private static $rgb10;
|
||||
private static $indexed10;
|
||||
private static $gray10;
|
||||
|
||||
public function init()
|
||||
{
|
||||
self::$bw10 = BlackAndWhiteRasterImage::create(10, 10);
|
||||
self::$rgb10 = RgbRasterImage::create(10, 10);
|
||||
self::$indexed10 = IndexedRasterImage::create(10, 10);
|
||||
self::$gray10 = GrayscaleRasterImage::create(10, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleUpBw()
|
||||
{
|
||||
self::$bw10->scale(100, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleUpGrayscale()
|
||||
{
|
||||
self::$gray10->scale(100, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleUpIndexed()
|
||||
{
|
||||
self::$indexed10->scale(100, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Subject
|
||||
*/
|
||||
public function scaleUpRgb()
|
||||
{
|
||||
self::$rgb10->scale(100, 100);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
$composer_autoload = __DIR__ . "/../vendor/autoload.php";
|
||||
require_once($composer_autoload);
|
||||
|
||||
@@ -0,0 +1,618 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Simple check that files in the BMP Suite can be read, and have the expected dimensions.
|
||||
*/
|
||||
class BmpsuiteTest extends TestCase
|
||||
{
|
||||
function loadImage(string $filename)
|
||||
{
|
||||
return Image::fromFile(__DIR__ . "/../resources/bmpsuite/$filename");
|
||||
}
|
||||
|
||||
function test_badbitcount()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badbitcount.bmp");
|
||||
}
|
||||
|
||||
function test_badbitssize()
|
||||
{
|
||||
$img = $this -> loadImage("b/badbitssize.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_baddens1()
|
||||
{
|
||||
$img = $this -> loadImage("b/baddens1.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_baddens2()
|
||||
{
|
||||
$img = $this -> loadImage("b/baddens2.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_badfilesize()
|
||||
{
|
||||
$img = $this -> loadImage("b/badfilesize.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_badheadersize()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badheadersize.bmp");
|
||||
}
|
||||
|
||||
function test_badpalettesize()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badpalettesize.bmp");
|
||||
}
|
||||
|
||||
function test_badplanes()
|
||||
{
|
||||
$img = $this -> loadImage("b/badplanes.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_badrle()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badrle.bmp");
|
||||
}
|
||||
|
||||
function test_badrle4()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badrle4.bmp");
|
||||
}
|
||||
|
||||
function test_badrle4bis()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badrle4bis.bmp");
|
||||
}
|
||||
|
||||
function test_badrle4ter()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badrle4ter.bmp");
|
||||
}
|
||||
|
||||
function test_badrlebis()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badrlebis.bmp");
|
||||
}
|
||||
|
||||
function test_badrleter()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badrleter.bmp");
|
||||
}
|
||||
|
||||
function test_badwidth()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/badwidth.bmp");
|
||||
}
|
||||
|
||||
function test_pal8badindex()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/pal8badindex.bmp");
|
||||
}
|
||||
|
||||
function test_reallybig()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/reallybig.bmp");
|
||||
}
|
||||
|
||||
function test_rgb16_880()
|
||||
{
|
||||
$img = $this -> loadImage("b/rgb16-880.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rletopdown()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/rletopdown.bmp");
|
||||
}
|
||||
|
||||
function test_shortfile()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("b/shortfile.bmp");
|
||||
}
|
||||
|
||||
function test_pal1()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal1.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal1bg()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal1bg.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal1wb()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal1wb.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal4()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal4.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal4gs()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal4gs.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal4rle()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal4rle.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8_0()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8-0.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8gs()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8gs.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8nonsquare()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8nonsquare.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(32, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8os2.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8rle()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8rle.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8topdown()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8topdown.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8v4()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8v4.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8v5()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8v5.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8w124()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8w124.bmp");
|
||||
$this -> assertEquals(124, $img -> getWidth());
|
||||
$this -> assertEquals(61, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8w125()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8w125.bmp");
|
||||
$this -> assertEquals(125, $img -> getWidth());
|
||||
$this -> assertEquals(62, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8w126()
|
||||
{
|
||||
$img = $this -> loadImage("g/pal8w126.bmp");
|
||||
$this -> assertEquals(126, $img -> getWidth());
|
||||
$this -> assertEquals(63, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb16_565()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb16-565.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb16_565pal()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb16-565pal.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb16()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb16.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb16bfdef()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb16bfdef.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb24.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24pal()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb24pal.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb32.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32bf()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb32bf.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32bfdef()
|
||||
{
|
||||
$img = $this -> loadImage("g/rgb32bfdef.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal1huff()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("q/pal1huff.bmp");
|
||||
}
|
||||
|
||||
function test_pal1p1()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal1p1.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal2()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal2.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal2color()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal2color.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal4rlecut()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal4rlecut.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal4rletrns()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal4rletrns.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8offs()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8offs.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2_hs()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8os2-hs.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2_sz()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8os2-sz.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2sp()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8os2sp.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2v2_16()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8os2v2-16.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2v2_40sz()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8os2v2-40sz.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2v2_sz()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8os2v2-sz.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8os2v2()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8os2v2.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8oversizepal()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8oversizepal.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8rlecut()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8rlecut.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_pal8rletrns()
|
||||
{
|
||||
$img = $this -> loadImage("q/pal8rletrns.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb16_231()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb16-231.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb16_3103()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb16-3103.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb16faketrns()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb16faketrns.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24jpeg()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("q/rgb24jpeg.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24largepal()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb24largepal.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24lprof()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb24lprof.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24png()
|
||||
{
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("q/rgb24png.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24prof()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb24prof.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb24prof2()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb24prof2.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32_111110()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb32-111110.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32_7187()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb32-7187.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32_xbgr()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb32-xbgr.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32fakealpha()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb32fakealpha.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgb32h52()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgb32h52.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba16_1924()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba16-1924.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba16_4444()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba16-4444.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba16_5551()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba16-5551.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba32_1010102()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba32-1010102.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba32_61754()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba32-61754.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba32_81284()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba32-81284.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba32()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba32.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba32abf()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba32abf.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_rgba32h56()
|
||||
{
|
||||
$img = $this -> loadImage("q/rgba32h56.bmp");
|
||||
$this -> assertEquals(127, $img -> getWidth());
|
||||
$this -> assertEquals(64, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_ba_bm()
|
||||
{
|
||||
// Different container format, not recognised as bitmap at all
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("x/ba-bm.bmp");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
use Mike42\GfxPhp\Image;
|
||||
use Mike42\GfxPhp\RgbRasterImage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
|
||||
use Mike42\GfxPhp\GrayscaleRasterImage;
|
||||
use Mike42\GfxPhp\IndexedRasterImage;
|
||||
|
||||
/**
|
||||
* Simple check that files in the PyGIF test suite can be read, and have the expected dimensions.
|
||||
*/
|
||||
class GifsuiteTest extends TestCase
|
||||
{
|
||||
function loadImage(string $filename) {
|
||||
return Image::fromFile(__DIR__ . "/../resources/pygif/$filename");
|
||||
}
|
||||
|
||||
function test_255_codes() {
|
||||
$img = $this -> loadImage("255-codes.gif");
|
||||
$this -> assertEquals(100, $img -> getWidth());
|
||||
$this -> assertEquals(100, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_4095_codes_clear() {
|
||||
$img = $this -> loadImage("4095-codes-clear.gif");
|
||||
$this -> assertEquals(100, $img -> getWidth());
|
||||
$this -> assertEquals(100, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_4095_codes() {
|
||||
$img = $this -> loadImage("4095-codes.gif");
|
||||
$this -> assertEquals(100, $img -> getWidth());
|
||||
$this -> assertEquals(100, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_all_blues() {
|
||||
$img = $this -> loadImage("all-blues.gif");
|
||||
$this -> assertEquals(16, $img -> getWidth());
|
||||
$this -> assertEquals(16, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_all_greens() {
|
||||
$img = $this -> loadImage("all-greens.gif");
|
||||
$this -> assertEquals(16, $img -> getWidth());
|
||||
$this -> assertEquals(16, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_all_reds() {
|
||||
$img = $this -> loadImage("all-reds.gif");
|
||||
$this -> assertEquals(16, $img -> getWidth());
|
||||
$this -> assertEquals(16, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_animation() {
|
||||
$img = $this -> loadImage("animation.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_animation_multi_image_explicit_zero_delay() {
|
||||
$img = $this -> loadImage("animation-multi-image-explicit-zero-delay.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_animation_multi_image() {
|
||||
$img = $this -> loadImage("animation-multi-image.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_animation_no_delays() {
|
||||
$img = $this -> loadImage("animation-no-delays.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_animation_speed() {
|
||||
$img = $this -> loadImage("animation-speed.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_animation_zero_delays() {
|
||||
$img = $this -> loadImage("animation-zero-delays.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_comment() {
|
||||
$img = $this -> loadImage("comment.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth1() {
|
||||
$img = $this -> loadImage("depth1.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth2() {
|
||||
$img = $this -> loadImage("depth2.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth3() {
|
||||
$img = $this -> loadImage("depth3.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth4() {
|
||||
$img = $this -> loadImage("depth4.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth5() {
|
||||
$img = $this -> loadImage("depth5.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth6() {
|
||||
$img = $this -> loadImage("depth6.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth7() {
|
||||
$img = $this -> loadImage("depth7.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_depth8() {
|
||||
$img = $this -> loadImage("depth8.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_disabled_transparent() {
|
||||
$img = $this -> loadImage("disabled-transparent.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_dispose_keep() {
|
||||
$img = $this -> loadImage("dispose-keep.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_dispose_none() {
|
||||
$img = $this -> loadImage("dispose-none.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_dispose_restore_background() {
|
||||
$img = $this -> loadImage("dispose-restore-background.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_dispose_restore_previous() {
|
||||
$img = $this -> loadImage("dispose-restore-previous.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_double_clears() {
|
||||
$img = $this -> loadImage("double-clears.gif");
|
||||
$this -> assertEquals(8, $img -> getWidth());
|
||||
$this -> assertEquals(8, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_extra_data() {
|
||||
$img = $this -> loadImage("extra-data.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_extra_pixels() {
|
||||
$img = $this -> loadImage("extra-pixels.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_four_colors() {
|
||||
$img = $this -> loadImage("four-colors.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_gif87a_animation() {
|
||||
$img = $this -> loadImage("gif87a-animation.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_gif87a() {
|
||||
$img = $this -> loadImage("gif87a.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_high_color() {
|
||||
$img = $this -> loadImage("high-color.gif");
|
||||
$this -> assertEquals(16, $img -> getWidth());
|
||||
$this -> assertEquals(16, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_icc_color_profile_empty() {
|
||||
$img = $this -> loadImage("icc-color-profile-empty.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_icc_color_profile() {
|
||||
$img = $this -> loadImage("icc-color-profile.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_image_inside_bg() {
|
||||
$img = $this -> loadImage("image-inside-bg.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_image_outside_bg() {
|
||||
$img = $this -> loadImage("image-outside-bg.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_image_overlap_bg() {
|
||||
$img = $this -> loadImage("image-overlap-bg.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_images_combine() {
|
||||
$img = $this -> loadImage("images-combine.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_images_overlap() {
|
||||
$img = $this -> loadImage("images-overlap.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_image_zero_height() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("image-zero-height.gif");
|
||||
}
|
||||
|
||||
function test_image_zero_size() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("image-zero-size.gif");
|
||||
}
|
||||
|
||||
function test_image_zero_width() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("image-zero-width.gif");
|
||||
}
|
||||
|
||||
function test_interlace() {
|
||||
$img = $this -> loadImage("interlace.gif");
|
||||
$this -> assertEquals(16, $img -> getWidth());
|
||||
$this -> assertEquals(16, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_invalid_ascii_comment() {
|
||||
$img = $this -> loadImage("invalid-ascii-comment.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_invalid_background() {
|
||||
$img = $this -> loadImage("invalid-background.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_invalid_code() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("invalid-code.gif");
|
||||
}
|
||||
|
||||
function test_invalid_colors() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("invalid-colors.gif");
|
||||
}
|
||||
|
||||
function test_invalid_transparent() {
|
||||
$img = $this -> loadImage("invalid-transparent.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_invalid_utf8_comment() {
|
||||
$img = $this -> loadImage("invalid-utf8-comment.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_large_codes() {
|
||||
$img = $this -> loadImage("large-codes.gif");
|
||||
$this -> assertEquals(100, $img -> getWidth());
|
||||
$this -> assertEquals(100, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_large_comment() {
|
||||
$img = $this -> loadImage("large-comment.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_local_color_table() {
|
||||
$img = $this -> loadImage("local-color-table.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_loop_animexts() {
|
||||
$img = $this -> loadImage("loop-animexts.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_loop_buffer() {
|
||||
$img = $this -> loadImage("loop-buffer.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_loop_buffer_max() {
|
||||
$img = $this -> loadImage("loop-buffer_max.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_loop_infinite() {
|
||||
$img = $this -> loadImage("loop-infinite.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_loop_max() {
|
||||
$img = $this -> loadImage("loop-max.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_loop_once() {
|
||||
$img = $this -> loadImage("loop-once.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_many_clears() {
|
||||
$img = $this -> loadImage("many-clears.gif");
|
||||
$this -> assertEquals(8, $img -> getWidth());
|
||||
$this -> assertEquals(8, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_max_codes() {
|
||||
$img = $this -> loadImage("max-codes.gif");
|
||||
$this -> assertEquals(100, $img -> getWidth());
|
||||
$this -> assertEquals(100, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_max_height() {
|
||||
$img = $this -> loadImage("max-height.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(65535, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_max_size() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("max-size.gif");
|
||||
}
|
||||
|
||||
function test_max_width() {
|
||||
$img = $this -> loadImage("max-width.gif");
|
||||
$this -> assertEquals(65535, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_missing_pixels() {
|
||||
$img = $this -> loadImage("missing-pixels.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_no_clear_and_eoi() {
|
||||
$img = $this -> loadImage("no-clear-and-eoi.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_no_clear() {
|
||||
$img = $this -> loadImage("no-clear.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_no_data() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("no-data.gif");
|
||||
}
|
||||
|
||||
function test_no_eoi() {
|
||||
$img = $this -> loadImage("no-eoi.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_no_global_color_table() {
|
||||
$img = $this -> loadImage("no-global-color-table.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_nul_application_extension() {
|
||||
$img = $this -> loadImage("nul-application-extension.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_nul_comment() {
|
||||
$img = $this -> loadImage("nul-comment.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_plain_text() {
|
||||
$img = $this -> loadImage("plain-text.gif");
|
||||
$this -> assertEquals(40, $img -> getWidth());
|
||||
$this -> assertEquals(8, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_transparent() {
|
||||
$img = $this -> loadImage("transparent.gif");
|
||||
$this -> assertEquals(2, $img -> getWidth());
|
||||
$this -> assertEquals(2, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_unknown_application_extension() {
|
||||
$img = $this -> loadImage("unknown-application-extension.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_unknown_extension() {
|
||||
$img = $this -> loadImage("unknown-extension.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_xmp_data_empty() {
|
||||
$img = $this -> loadImage("xmp-data-empty.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_xmp_data() {
|
||||
$img = $this -> loadImage("xmp-data.gif");
|
||||
$this -> assertEquals(1, $img -> getWidth());
|
||||
$this -> assertEquals(1, $img -> getHeight());
|
||||
}
|
||||
|
||||
function test_zero_height() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("zero-height.gif");
|
||||
}
|
||||
|
||||
function test_zero_size() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("zero-size.gif");
|
||||
}
|
||||
|
||||
function test_zero_width() {
|
||||
$this -> expectException(Exception::class);
|
||||
$img = $this -> loadImage("zero-width.gif");
|
||||
}
|
||||
}
|
||||
|
||||
+1216
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use Mike42\GfxPhp\Image;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SmallFileTest extends TestCase
|
||||
{
|
||||
function loadImage(string $filename) {
|
||||
$img = Image::fromFile(__DIR__ . "/../resources/small-files/$filename");
|
||||
return $img -> toBlackAndWhite();
|
||||
}
|
||||
|
||||
function testWhite() {
|
||||
$expected = " \n";
|
||||
$width = 1;
|
||||
$height = 1;
|
||||
foreach(['canvas_white.gif', 'canvas_white.png'] as $fn) { // Also jpg and bmp
|
||||
$img = $this -> loadImage($fn);
|
||||
$this -> assertEquals($expected, $img -> toString());
|
||||
$this -> assertEquals($height, $img -> getHeight());
|
||||
$this -> assertEquals($width, $img -> getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
function testBlack() {
|
||||
$expected = "▀\n";
|
||||
$width = 1;
|
||||
$height = 1;
|
||||
foreach(['canvas_black.gif', 'canvas_black.png'] as $fn) { // Also jpg and bmp
|
||||
$img = $this -> loadImage($fn);
|
||||
$this -> assertEquals($expected, $img -> toString());
|
||||
$this -> assertEquals($height, $img -> getHeight());
|
||||
$this -> assertEquals($width, $img -> getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
function testBlackWhite() {
|
||||
$expected = "▀▀\n";
|
||||
$width = 2;
|
||||
$height = 2;
|
||||
foreach(['black_white.gif', 'black_white.png'] as $fn) { // Also jpg and bmp
|
||||
$img = $this -> loadImage($fn);
|
||||
$this -> assertEquals($expected, $img -> toString());
|
||||
$this -> assertEquals($height, $img -> getHeight());
|
||||
$this -> assertEquals($width, $img -> getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
function testBlackTransparent() {
|
||||
$expected = "▀▀\n";
|
||||
$width = 2;
|
||||
$height = 2;
|
||||
foreach(['black_transparent.gif', 'black_transparent.png'] as $fn) { // Also jpg and bmp
|
||||
$img = $this -> loadImage($fn);
|
||||
$this -> assertEquals($expected, $img -> toString());
|
||||
$this -> assertEquals($height, $img -> getHeight());
|
||||
$this -> assertEquals($width, $img -> getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
function testBlackWhiteTall() {
|
||||
$expected = "██\n" .
|
||||
"██\n" .
|
||||
"██\n" .
|
||||
"██\n" .
|
||||
" \n" .
|
||||
" \n" .
|
||||
" \n" .
|
||||
" \n";
|
||||
$width = 2;
|
||||
$height = 16;
|
||||
$img = $this -> loadImage('black_white_tall.png');
|
||||
$this -> assertEquals($expected, $img -> toString());
|
||||
$this -> assertEquals($height, $img -> getHeight());
|
||||
$this -> assertEquals($width, $img -> getWidth());
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// Read an image file,
|
||||
require_once("../../vendor/autoload.php");
|
||||
use Mike42\GfxPhp\Image;
|
||||
|
||||
if(!isset($argv[1])) {
|
||||
die("Usage " . $argv[0] . " IMAGE\n");
|
||||
}
|
||||
$filename = $argv[1];
|
||||
$outp = basename($filename, '.png');
|
||||
echo "Opening file $filename\n";
|
||||
$img = Image::fromFile($filename);
|
||||
$img -> write('out/' . $outp . ".ppm");
|
||||
echo $img -> toBlackAndWhite() -> toString() . "\n";
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
|
||||
TEST_OK=0
|
||||
TEST_FAIL=0
|
||||
|
||||
function test_end {
|
||||
echo ""
|
||||
echo ""
|
||||
printf "%-40s" "$1 test failures found"
|
||||
if [ $1 -gt 0 ]; then
|
||||
log_fail
|
||||
else
|
||||
log_ok
|
||||
fi
|
||||
return "$1"
|
||||
}
|
||||
|
||||
function test_start {
|
||||
TEST_OK=0
|
||||
TEST_FAIL=0
|
||||
}
|
||||
|
||||
function log_ok {
|
||||
echo -e "[ \033[0;32mPASS\033[0m ]"
|
||||
}
|
||||
|
||||
function log_fail {
|
||||
echo -e "[ \033[0;31mFAIL\033[0m ]"
|
||||
}
|
||||
|
||||
function expect_failure {
|
||||
temp_file=$(mktemp)
|
||||
printf "%-40s" "$1"
|
||||
shift
|
||||
$@ &> log.txt
|
||||
if [ $? -ne 0 ]; then
|
||||
log_ok
|
||||
TEST_OK=$((TEST_OK+1))
|
||||
else
|
||||
log_fail
|
||||
#cat ${temp_file}
|
||||
TEST_FAIL=$((TEST_FAIL+1))
|
||||
fi
|
||||
rm ${temp_file}
|
||||
}
|
||||
|
||||
function expect_success {
|
||||
temp_file=$(mktemp)
|
||||
printf "%-40s" "$1"
|
||||
shift
|
||||
$@ &> ${temp_file}
|
||||
if [ $? -ne 0 ]; then
|
||||
log_fail
|
||||
#cat ${temp_file}
|
||||
TEST_FAIL=$((TEST_FAIL+1))
|
||||
else
|
||||
log_ok
|
||||
TEST_OK=$((TEST_OK+1))
|
||||
fi
|
||||
rm ${temp_file}
|
||||
}
|
||||
|
||||
function test_commands {
|
||||
PNGSUITE="../resources/pngsuite/"
|
||||
for i in $(find $PNGSUITE -type f -name '*.png' | sort | grep -v pngsuite/x); do
|
||||
echo $0 expect_success "$(basename $i)" php read.php "$i"
|
||||
done
|
||||
for i in $(find $PNGSUITE -type f -name '*.png' | sort | grep pngsuite/x); do
|
||||
echo $0 expect_failure "$(basename $i)" php read.php "$i"&
|
||||
done
|
||||
}
|
||||
|
||||
set -u -o pipefail
|
||||
if [ $# -eq 0 ]; then
|
||||
|
||||
mkdir -p out/
|
||||
test_start
|
||||
test_commands | parallel --no-notice
|
||||
test_end $?
|
||||
else
|
||||
subcmd=$1
|
||||
shift
|
||||
test_start
|
||||
if [ $subcmd == "expect_success" ]; then
|
||||
expect_success $@
|
||||
elif [ $subcmd == "expect_failure" ]; then
|
||||
expect_failure $@
|
||||
fi
|
||||
exit $TEST_FAIL
|
||||
fi
|
||||
@@ -0,0 +1,16 @@
|
||||
# BMP Suite
|
||||
|
||||
The images in this directory from BMP Suite 2.5 by Jason Summers, with
|
||||
contributions by Matthieu Darbois.
|
||||
|
||||
The utility which generated the test images is available at the following URL:
|
||||
|
||||
- http://entropymine.com/jason/bmpsuite/
|
||||
|
||||
According to the `readme.txt` which ships with BMP Suite, the image files
|
||||
themselves are in the public domain.
|
||||
|
||||
> Image files generated by this program are not covered by this license, and are
|
||||
in the public domain.
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user