Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+302
View File
@@ -0,0 +1,302 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/).
## [Unreleased]
### Fixed
* Corrupted CFF index data
there was a subtle bug in cff index implementation that resulted in
a data corruption. in certain circumstances some items didn't get
properly encoded. this happened when items were not previously accessed.
this resulted, for instance, in missing glyphs. but only sometimes
because indexes might've still contain data that shouldn't've been
there. in combination with incorrect encoding (see further) this
resulted in some glyphs still being rendered, sometimes even correctly.
along with the fix a rather large api change landed. this resulted in
quite a big diff.
Alexander Mankuta
* Incorrect CFF encoding in subsets
TTFunk used to reuse encoding from the original font. This mapping was
incorrect for subset fonts which used not just a subset of glyphs but
also a different encoding.
A separate issue was that some fonts have empty CFF encoding. This
incorrect mapping resulted in encoding that mapped all codes to glyph 0.
This had impact on Prawn in particular. PDF spec explicitly says that
CFF encoding is not to be used in OpenType fonts. `cmap` table should
directly index charstrings in the CFF table. Despite this PDF renderers
still use CFF encoding to retrieve glyphs. So TTFunk has to discard the
original CFF encoding and supply its own.
Alexander Mankuta
* `maxp` table
The table is now correctly parsed and encoded for both TrueType and CFF-based
OpenType fonts.
Cameron Dutro, Alexander Mankuta
* Files are closed sooner
Files were garbage collected but could stay open for longer than necessary.
Jon Burgess
* Long date time in the `head` table
The `created` and `modified` fields we parsed and encoded with incorrect
endiannes. Additionally helper methods were added to convert these fields to
and from Ruby `Time`.
Jens Kutilek, Peter Goldstein
* Removed execution permissions on non-executable files
Keenan Brock
### Changes
* Minimum Ruby is 2.7
Alexander Mankuta
* Performance improvement in subsets construction
Thomas Leitner
* CI improvememnts
Peter Goldstein
## 1.7.0
### Changes
* Allow gem installation on Ruby 3.0
Pavel Lobashov
* Allow TTC files to be read from IO object
Tom de Grunt
## [1.6.2.1]
### Fixed
* 1.6.2 gem conains local debuging code. This is the same commit but without
local changes.
Alexander Mankuta
## [1.6.2]
### Fixed
* Reverted to pre 1.6 maxp table serialization.
Cameron Dutro
## [1.6.1]
### Fixed
* Fixed maxp table encoding
Cameron Dutro
## [1.6.0]
### Added
* OpenType fonts support
* Added support for CFF-flavored fonts (also known as CID-keyed or OpenType fonts)
* Added support for the VORG and DSIG tables
* Improved charset encoding support
* Improved font metrics calculations in the head, maxp, hhea, hmtx, and os/2 tables
* Subsetted fonts verified with Font-Validator, fontlint, and Mac OS's Font Book
Cameron Dutro
* Ruby 2.6 support
Alexander Mankuta
* JRuby 9.2 support
Alexander Mankuta
### Removed
* Dropped Ruby 2.1 & 2.2 support
Alexander Mankuta
* Removed JRuby 9.1 support
Alexander Mankuta
### Fixed
* Sort name table entries when generating subset font
Matjaz Gregoric
* Map the 0xFFFF char code to glyph 0 in cmap format 4
Matjaz Gregoric
* Order tables by tag when generating font subset
Matjaz Gregoric
* Fix typo in TTFunk::Subset::Unicode#includes?
Matjaz Gregoric
* Fixe calculation of search_range for font subsets
Matjaz Gregoric
* Fixed instance variable @offset and @length not initialized
Katsuya HIDAKA
* Code style fixes
Katsuya HIDAKA, Matjaz Gregoric, Alexander Mankuta
## [1.5.1]
### Fixed
* loca table corruption during subsetting. The loca table serialization code
didn't properly detect suitable table format.
* Fixed checksum calculation for empty tables.
## [1.5.0] - 2017-02-13
### Added
* Support for reading TTF fonts from TTC files
### Changed
* Subset font naming is consistent now and depends on content
## [1.4.0] - 2014-09-21
### Added
* sbix table support
## [1.3.0] - 2014-09-10
### Removed
* Post table version 2.5
## [1.2.2] - 2014-08-29
### Fixed
* Ignore unsupported cmap table versions
## [1.2.1] - 2014-08-28
### Fixed
* Added missing Pathname require
## [1.2.0] - 2014-06-23
### Added
* Rubocop checks
* Ability to parse IO objects
### Changed
* Improved preferred family name selection
## [1.1.1] - 2014-02-24
### Changed
* Clarified licensing
### Removed
* comicsans.ttf
## [1.1.0] - 2014-01-21
### Added
* Improved Unicode astral planes support
* Support for cmap table formats 6, 10, 12
* RSpec-based specs
### Fixed
* Subsetting in JRuby
## [1.0.3] - 2011-10-11
### Added
* Authorship information
## 1.0.2 - 2011-08-08
### Fixed
* Ruby 1.9.2 segmentation fault on Enumerable#zip(range)
## 1.0.0 - 2011-04-02 [YANKED]
Initial release as a standalone gem
[Unreleased]: https://github.com/prawnpdf/ttfunk/compare/1.7.0...HEAD
[1.7.0]: https://github.com/prawnpdf/ttfunk/compare/1.6.2.1...1.7.0
[1.6.2.1]: https://github.com/prawnpdf/ttfunk/compare/1.6.2...1.6.2.1
[1.6.2]: https://github.com/prawnpdf/ttfunk/compare/1.6.1...1.6.2
[1.6.1]: https://github.com/prawnpdf/ttfunk/compare/1.6.0...1.6.1
[1.6.0]: https://github.com/prawnpdf/ttfunk/compare/1.5.1...1.6.0
[1.5.1]: https://github.com/prawnpdf/ttfunk/compare/1.5.0...1.5.1
[1.5.0]: https://github.com/prawnpdf/ttfunk/compare/1.4.0...1.5.0
[1.4.0]: https://github.com/prawnpdf/ttfunk/compare/1.3.0...1.4.0
[1.3.0]: https://github.com/prawnpdf/ttfunk/compare/1.2.2...1.3.0
[1.2.2]: https://github.com/prawnpdf/ttfunk/compare/1.2.1...1.2.2
[1.2.1]: https://github.com/prawnpdf/ttfunk/compare/1.2.0...1.2.1
[1.2.0]: https://github.com/prawnpdf/ttfunk/compare/1.1.1...1.2.0
[1.1.1]: https://github.com/prawnpdf/ttfunk/compare/1.1.0...1.1.1
[1.1.0]: https://github.com/prawnpdf/ttfunk/compare/1.0.3...1.1.0
[1.0.3]: https://github.com/prawnpdf/ttfunk/compare/1.0.2...1.0.3
+2
View File
@@ -0,0 +1,2 @@
ttfunk may be used under Matz's original licensing terms for Ruby,
or GPLv2 or GPLv3. See LICENSE for Matz's terms, or GPLv2 and GPLv3 files.
+340
View File
@@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 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.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
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 Program or any portion
of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
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 Program, 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 Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) 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; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, 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 executable. However, as a
special exception, the source code 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.
If distribution of executable or 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 counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program 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.
5. 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 Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program 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 to
this License.
7. 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 Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program 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 Program.
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.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program 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.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies 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 Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, 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
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. 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 PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
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 program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+56
View File
@@ -0,0 +1,56 @@
ttfunk is copyrighted free software produced by Gregory Brown along with
community contributions. See git log for authorship information.
Licensing terms follow:
You can redistribute ttfunk and/or modify it under either the terms of the GPLv2
or GPLv3 (see GPLv2 and GPLv3 files), or the conditions below:
1. You may make and give away verbatim copies of the source form of the
software without restriction, provided that you duplicate all of the
original copyright notices and associated disclaimers.
2. You may modify your copy of the software in any way, provided that
you do at least ONE of the following:
a) place your modifications in the Public Domain or otherwise
make them Freely Available, such as by posting said
modifications to Usenet or an equivalent medium, or by allowing
the author to include your modifications in the software.
b) use the modified software only within your corporation or
organization.
c) rename any non-standard executables so the names do not conflict
with standard executables, which must also be provided.
d) make other distribution arrangements with the author.
3. You may distribute the software in object code or executable
form, provided that you do at least ONE of the following:
a) distribute the executables and library files of the software,
together with instructions (in the manual page or equivalent)
on where to get the original distribution.
b) accompany the distribution with the machine-readable source of
the software.
c) give non-standard executables non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
4. You may modify and include the part of the software into any other
software (possibly commercial).
5. The scripts and library files supplied as input to or produced as
output from the software do not automatically fall under the
copyright of the software, but belong to whomever generated them,
and may be sold commercially, and may be aggregated with this
software.
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.
+48
View File
@@ -0,0 +1,48 @@
# TTFunk
![Maintained: yes](https://img.shields.io/badge/maintained-yes-brightgreen.svg)
![CI status](https://github.com/prawnpdf/ttfunk/workflows/CI/badge.svg)
TTFunk is a TrueType and OpenType font library written in pure ruby. It supports
both parsing and encoding of fonts. Also provides limited font subsetting.
## Installation
The recommended installation method is via Rubygems.
```shell
gem install ttfunk
```
## Usage
Basic usage:
```ruby
require 'ttfunk'
file = TTFunk::File.open("some/path/myfont.ttf")
puts "name : #{file.name.font_name.join(', ')}"
puts "ascent : #{file.ascent}"
puts "descent : #{file.descent}"
```
For more detailed examples, explore the examples directory.
## Licensing
Matz's terms for Ruby, GPLv2, or GPLv3. See LICENSE for details.
## Authorship
This project is maintained by the same folks who run the Prawn PDF project.
Here's the [full list](https://github.com/prawnpdf/ttfunk/contributors) of
Github users who have at least one patch accepted to TTFunk.
## Community support
TTFunk is maintained as a dependency of Prawn, the ruby PDF generation library.
Any questions or feedback should be sent to the [Prawn
Diccussions](https://github.com/orgs/prawnpdf/discussions) group.
+332
View File
@@ -0,0 +1,332 @@
# frozen_string_literal: true
require 'stringio'
require 'pathname'
require_relative 'ttfunk/aggregate'
require_relative 'ttfunk/directory'
require_relative 'ttfunk/resource_file'
require_relative 'ttfunk/collection'
require_relative 'ttfunk/ttf_encoder'
require_relative 'ttfunk/encoded_string'
require_relative 'ttfunk/placeholder'
require_relative 'ttfunk/otf_encoder'
require_relative 'ttfunk/sci_form'
require_relative 'ttfunk/bit_field'
require_relative 'ttfunk/bin_utils'
require_relative 'ttfunk/sub_table'
require_relative 'ttfunk/min'
require_relative 'ttfunk/max'
require_relative 'ttfunk/sum'
require_relative 'ttfunk/one_based_array'
# TTFunk is a TrueType and OpenType font library written in pure ruby. It
# supports both parsing and encoding of fonts. Also provides limited font
# subsetting.
#
# It supports a veriety of SFNT-based formats:
# * TrueType fonts (ttf)
# * OpenType fonts (otf), with both TrueType and CFF glyph outlines
# * DFont resources (dfont)
# * TrueType Collections (ttc)
#
# While not all TrueType and OpenType tables are implemented the most common
# ones are.
module TTFunk
# TTFunk-specific exceptions
class Error < StandardError; end
# File represents an individual font. It can represents both TrueType and
# OpenType fonts.
class File
# Raw content of the font.
# @return [String]
attr_reader :contents
# Font tables directory.
# @return [TTFunk::Directory]
attr_reader :directory
# Open font file
#
# @overload open(io)
# @param io [IO] IO to read font content from. IO position and binmode
# might change.
# @return [TTFunk::File]
# @overload open(path)
# @param path [String, Pathname] Path to file to read the font from.
# @return [TTFunk::File]
def self.open(io_or_path)
new(verify_and_read(io_or_path))
end
# Load a font from a resource file.
#
# @param file [String, Pathname] Path to the resource file.
# @param which [Integer, String] index or name of the font to load
# @return [TTFunk::File]]
def self.from_dfont(file, which = 0)
new(ResourceFile.open(file) { |dfont| dfont['sfnt', which] })
end
# Load a font from a TrueType collection.
#
# @overload from_ttc(io, which = 0)
# @param file [IO] IO to read the collection from.
# @param which [Integer] index of the font to load
# @return [TTFunk::File]
# @overload from_ttc(file_path, which = 0)
# @param file_path [String, Pathname] Path to the resource file.
# @param which [Integer] index of the font to load
# @return [TTFunk::File]
def self.from_ttc(file, which = 0)
Collection.open(file) { |ttc| ttc[which] }
end
# Turn a path or IO into an IO convenient for TTFunk. The resulting IO is
# going to be in bin mode and its position set to the beginning.
#
# @overload verify_and_open(io)
# @param io [IO] IO to prepare. Its position and binmode might
# change.
# @return [io]
# @overload verify_and_open(path)
# @param path [String, Pathname] path of the file to turn into an IO.
# @return [IO] newly opened IO for the path
# @deprecated This method might retain open files for longer than necessary.
# @see .verify_and_read
def self.verify_and_open(io_or_path)
# File or IO
if io_or_path.respond_to?(:rewind)
io = io_or_path
# Rewind if the object we're passed is an IO, so that multiple embeds of
# the same IO object will work
io.rewind
# read the file as binary so the size is calculated correctly
# guard binmode because some objects acting io-like don't implement it
io.binmode if io.respond_to?(:binmode)
return io
end
# String or Pathname
io_or_path = Pathname.new(io_or_path)
raise ArgumentError, "#{io_or_path} not found" unless io_or_path.file?
io_or_path.open('rb')
end
# Read contents of a path or IO.
#
# @overload verify_and_read(io)
# @param io [IO] IO to read from. Its position and binmode might
# change. IO is read from the beginning regardless of its initial
# position.
# @return [String]
# @overload verify_and_read(path)
# @param path [String, Pathname] path of the file to read.
# @return [String]
def self.verify_and_read(io_or_path)
# File or IO
if io_or_path.respond_to?(:rewind)
io = io_or_path
# Rewind if the object we're passed is an IO, so that multiple embeds of
# the same IO object will work
io.rewind
# read the file as binary so the size is calculated correctly
# guard binmode because some objects acting io-like don't implement it
io.binmode if io.respond_to?(:binmode)
return io.read
end
# String or Pathname
io_or_path = Pathname.new(io_or_path)
raise ArgumentError, "#{io_or_path} not found" unless io_or_path.file?
io_or_path.binread
end
# @param contents [String] binary string containg the font data
# @param offset [Integer] offset at which the font data starts
def initialize(contents, offset = 0)
@contents = StringIO.new(contents)
@directory = Directory.new(@contents, offset)
end
# Glyphs ascent as defined for in the font.
#
# @return [Integer]
def ascent
@ascent ||= (os2.exists? && os2.ascent && os2.ascent.nonzero?) ||
horizontal_header.ascent
end
# Glyphs descent as defined in the font.
#
# @return [Integer]
def descent
@descent ||= (os2.exists? && os2.descent && os2.descent.nonzero?) ||
horizontal_header.descent
end
# Line gap as defined in the font.
#
# @return [Integer]
def line_gap
@line_gap ||= (os2.exists? && os2.line_gap && os2.line_gap.nonzero?) ||
horizontal_header.line_gap
end
# Glyps bounding box as defined in the font.
#
# @return [Array(Integer, Integer, Integer, Integer)]
def bbox
[header.x_min, header.y_min, header.x_max, header.y_max]
end
# Font directory entry for the table with the provided tag.
#
# @param tag [String] table tab
# @return [Hash, nil]
def directory_info(tag)
directory.tables[tag.to_s]
end
# Font Header (`head`) table
#
# @return [TTFunk::Table::Head, nil]
def header
@header ||= TTFunk::Table::Head.new(self)
end
# Character to Glyph Index Mapping (`cmap`) table
#
# @return [TTFunk::Tbale::Cmap, nil]
def cmap
@cmap ||= TTFunk::Table::Cmap.new(self)
end
# Horizontal Header (`hhea`) table
#
# @return [TTFunk::Table::Hhea, nil]
def horizontal_header
@horizontal_header ||= TTFunk::Table::Hhea.new(self)
end
# Horizontal Metrics (`hmtx`) table
#
# @return [TTFunk::Table::Hmtx, nil]
def horizontal_metrics
@horizontal_metrics ||= TTFunk::Table::Hmtx.new(self)
end
# Maximum Profile (`maxp`) table
#
# @return [TTFunk::Table::Maxp, nil]
def maximum_profile
@maximum_profile ||= TTFunk::Table::Maxp.new(self)
end
# Kerning (`kern`) table
#
# @return [TTFunk::Table::Kern, nil]
def kerning
@kerning ||= TTFunk::Table::Kern.new(self)
end
# Naming (`name`) table
#
# @return [TTFunk::Table::Name, nil]
def name
@name ||= TTFunk::Table::Name.new(self)
end
# OS/2 and Windows Metrics (`OS/2`) table
#
# @return [TTFunk::Table:OS2, nil]
def os2
@os2 ||= TTFunk::Table::OS2.new(self)
end
# PostScript (`post`) table
#
# @return [TTFunk::Table::Post, nil]
def postscript
@postscript ||= TTFunk::Table::Post.new(self)
end
# Index to Location (`loca`) table
#
# @return [TTFunk::Table::Loca, nil]
def glyph_locations
@glyph_locations ||= TTFunk::Table::Loca.new(self)
end
# Glyph Data (`glyf`) table
#
# @return [TTFunk::Table::Glyf, nil]
def glyph_outlines
@glyph_outlines ||= TTFunk::Table::Glyf.new(self)
end
# Standard Bitmap Graphics (`sbix`) table
#
# @return [TTFunk::Table::Sbix, nil]
def sbix
@sbix ||= TTFunk::Table::Sbix.new(self)
end
# Compact Font Format (`CFF `) table
#
# @return [Table::Table::Cff, nil]
def cff
@cff ||= TTFunk::Table::Cff.new(self)
end
# Vertical Origin (`VORG`) table
#
# @return [TTFunk::Table::Vorg, nil]
def vertical_origins
@vertical_origins ||=
if directory.tables.include?(TTFunk::Table::Vorg::TAG)
TTFunk::Table::Vorg.new(self)
end
end
# Digital Signature (`DSIG`) table
#
# @return [TTFunk::Table::Dsig, nil]
def digital_signature
@digital_signature ||=
if directory.tables.include?(TTFunk::Table::Dsig::TAG)
TTFunk::Table::Dsig.new(self)
end
end
# Find glyph by its index.
#
# @return [TTFunk::Table::Cff::Charstring] if it's a CFF-based OpenType font
# @return [TTFunk::Table::Glyf::Simple, TTFunk::Table::Glyf::Compound]
# if it's a TrueType font
def find_glyph(glyph_id)
if cff.exists?
cff.top_index[0].charstrings_index[glyph_id].glyph
else
glyph_outlines.for(glyph_id)
end
end
end
end
require_relative 'ttfunk/table/cff'
require_relative 'ttfunk/table/cmap'
require_relative 'ttfunk/table/dsig'
require_relative 'ttfunk/table/glyf'
require_relative 'ttfunk/table/head'
require_relative 'ttfunk/table/hhea'
require_relative 'ttfunk/table/hmtx'
require_relative 'ttfunk/table/kern'
require_relative 'ttfunk/table/loca'
require_relative 'ttfunk/table/maxp'
require_relative 'ttfunk/table/name'
require_relative 'ttfunk/table/os2'
require_relative 'ttfunk/table/post'
require_relative 'ttfunk/table/sbix'
require_relative 'ttfunk/table/vorg'
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module TTFunk
# Base class for different aggregate values and accumulators.
#
# @see TTFunk::Min
# @see TTFunk::Max
# @see TTFunk::Sum
class Aggregate
private
def coerce(other)
if other.respond_to?(:value_or)
other.value_or(0)
else
other
end
end
end
end
@@ -0,0 +1,66 @@
# frozen_string_literal: true
module TTFunk # rubocop: disable Style/Documentation # false positive
# Bit crunching utility methods.
module BinUtils
# Turn a bunch of small integers into one big integer. Assumes big-endian.
#
# @param arr [Array<Integer>]
# @param bit_width [Integer] bit width of the elements
# @return [Integer]
def stitch_int(arr, bit_width:)
value = 0
arr.each_with_index do |element, index|
value |= element << (bit_width * index)
end
value
end
# Slice a big integer into a bunch of small integers. Assumes big-endian.
#
# @param value [Integer]
# @param bit_width [Integer] bit width of the elements
# @param slice_count [Integer] number of elements to slice into. This is
# needed for cases where top bits are zero.
# @return [Array<Integer>]
def slice_int(value, bit_width:, slice_count:)
mask = (2**bit_width) - 1
Array.new(slice_count) do |i|
(value >> (bit_width * i)) & mask
end
end
# Two's compliment to an integer.
#
# @param num [Integer]
# @param bit_width [Integer] number width
# @return [Integer]
def twos_comp_to_int(num, bit_width:)
if num >> (bit_width - 1) == 1
# we want all ones
mask = (2**bit_width) - 1
# find 2's complement, i.e. flip bits (xor with mask) and add 1
-((num ^ mask) + 1)
else
num
end
end
# Turns a (sorted) sequence of values into a series of two-element arrays
# where the first element is the start and the second is the length.
#
# @param values [Array<Integer>]
# @return [Array<Array(Integer, Integer)>]
def rangify(values)
values
.slice_when { |a, b| b - a > 1 }
.map { |span| [span.first, span.length - 1] }
end
end
BinUtils.extend(BinUtils)
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
module TTFunk
# Bitfield represents a series of bits that can individually be toggled.
class BitField
# Serialized value.
# @return [Integer]
attr_reader :value
# @param value [Integer] initial value
def initialize(value = 0)
@value = value
end
# Set bit on.
#
# @param pos [Integer] bit position
# @return [void]
def on(pos)
@value |= 2**pos
end
# If bit on?
#
# @param pos [Integer]
# @return [Boolean]
def on?(pos)
(value & (2**pos)).positive?
end
# Set bit off.
#
# @param pos [Integer]
# @return [void]
def off(pos)
@value &= (2**Math.log2(value).ceil) - (2**pos) - 1
end
# Is bit off?
#
# @param pos [Integer]
# @return [Boolean]
def off?(pos)
!on?(pos)
end
# Get a duplicate of this bit field.
#
# @return [BitField]
def dup
self.class.new(value)
end
end
end
@@ -0,0 +1,71 @@
# frozen_string_literal: true
module TTFunk
# TrueType font collection. Usually a file with `.ttc` extension.
class Collection
include Enumerable
# Load a TrueType collection.
#
# @overload open(io)
# @param io [IO] IO to read the collection from.
# @yieldparam collection [TTFunk::Collection]
# @return [any] whatever the block returns
# @overload open(file_path)
# @param file_path [String, Pathname] Path to the font collection file.
# @yieldparam collection [TTFunk::Collection]
# @return [any] whatever the block returns
def self.open(path)
if path.respond_to?(:read)
result = yield(new(path))
path.rewind
result
else
::File.open(path, 'rb') do |io|
yield(new(io))
end
end
end
# @param io [IO(#read & #rewind)]
# @raise [ArgumentError] if `io` doesn't start with a ttc tag
def initialize(io)
tag = io.read(4)
raise ArgumentError, 'not a TTC file' unless tag == 'ttcf'
_major, _minor = io.read(4).unpack('n*')
count = io.read(4).unpack1('N')
@offsets = io.read(count * 4).unpack('N*')
io.rewind
@contents = io.read
@cache = []
end
# Number of fonts in this collection.
#
# @return [Integer]
def count
@offsets.length
end
# Iterate over fonts in the collection.
#
# @yieldparam font [TTFunk::File]
# @return [self]
def each
count.times do |index|
yield(self[index])
end
self
end
# Get font by index.
#
# @param index [Integer]
# @return [TTFunk::File]
def [](index)
@cache[index] ||= TTFunk::File.new(@contents, @offsets[index])
end
end
end
@@ -0,0 +1,34 @@
# frozen_string_literal: true
module TTFunk
# SFNT table directory.
class Directory
# Table descriptors
# @return [Hash{String => Hash}]
attr_reader :tables
# Scaler type
# @return [Integer]
attr_reader :scaler_type
def initialize(io, offset = 0)
io.seek(offset)
# https://www.microsoft.com/typography/otspec/otff.htm#offsetTable
# We're ignoring searchRange, entrySelector, and rangeShift here, but
# skipping past them to get to the table information.
@scaler_type, table_count = io.read(12).unpack('Nn')
@tables = {}
table_count.times do
tag, checksum, offset, length = io.read(16).unpack('a4N*')
@tables[tag] = {
tag: tag,
checksum: checksum,
offset: offset,
length: length,
}
end
end
end
end
@@ -0,0 +1,151 @@
# frozen_string_literal: true
require 'stringio'
require_relative 'placeholder'
module TTFunk
# Risen when the final encoded string was requested but there were some
# unresolved placeholders in it.
class UnresolvedPlaceholderError < StandardError
end
# Risen when a placeholder is added to an Encoded String but it already
# contains a placeholder with the same name.
class DuplicatePlaceholderError < StandardError
end
# Encoded string takes care of placeholders in binary strings. Placeholders
# are used when bytes need to be placed in the stream before their value is
# known.
#
# @api private
class EncodedString
# @yieldparam [self]
def initialize
yield(self) if block_given?
end
# Append to string.
#
# @param obj [String, Placeholder, EncodedString]
# @return [self]
def <<(obj)
case obj
when String
io << obj
when Placeholder
add_placeholder(obj)
io << ("\0" * obj.length)
when self.class
# adjust placeholders to be relative to the entire encoded string
obj.placeholders.each_pair do |_, placeholder|
add_placeholder(placeholder.dup, placeholder.position + io.length)
end
io << obj.unresolved_string
end
self
end
# Append multiple objects.
#
# @param objs [Array<String, Placeholder, EncodedString>]
# @return [self]
def concat(*objs)
objs.each do |obj|
self << obj
end
self
end
# Append padding to align string to the specified word width.
#
# @param width [Integer]
# @return [self]
def align!(width = 4)
if (length % width).positive?
self << ("\0" * (width - (length % width)))
end
self
end
# Length of this string.
#
# @return [Integer]
def length
io.length
end
# Raw string.
#
# @return [String]
# @raise [UnresolvedPlaceholderError] if there are any unresolved
# placeholders left.
def string
unless placeholders.empty?
raise UnresolvedPlaceholderError,
"string contains #{placeholders.size} unresolved placeholder(s)"
end
io.string
end
# Raw bytes.
#
# @return [Array<Integer>]
# @raise [UnresolvedPlaceholderError] if there are any unresolved
# placeholders left.
def bytes
string.bytes
end
# Unresolved raw string.
#
# @return [String]
def unresolved_string
io.string
end
# Resolve placeholder.
#
# @param name [Symbol]
# @param value [String]
# @return [void]
def resolve_placeholder(name, value)
last_pos = io.pos
if (placeholder = placeholders[name])
io.seek(placeholder.position)
io.write(value[0..placeholder.length])
placeholders.delete(name)
end
ensure
io.seek(last_pos)
end
# Plaholders
#
# @return [Hash{Symbol => Plaholder}]
def placeholders
@placeholders ||= {}
end
private
def add_placeholder(new_placeholder, pos = io.pos)
if placeholders.include?(new_placeholder.name)
raise DuplicatePlaceholderError,
"placeholder #{new_placeholder.name} already exists"
end
new_placeholder.position = pos
placeholders[new_placeholder.name] = new_placeholder
end
def io
@io ||= StringIO.new(''.b).binmode
end
end
end
+40
View File
@@ -0,0 +1,40 @@
# frozen_string_literal: true
module TTFunk
# Maximum aggregate. Its value can only become greater.
class Max < Aggregate
# Value
#
# @return [Comparable, nil]
attr_reader :value
# @param init_value [Comparable] initial value
def initialize(init_value = nil)
super()
@value = init_value
end
# Push a value. It will become the new value if it's greater than the
# current value (or if there was no value).
#
# @param new_value [Comparable]
# @return [void]
def <<(new_value)
new_value = coerce(new_value)
if value.nil? || new_value > value
@value = new_value
end
end
# Get the stored value or default.
#
# @param default [any]
# @return [any]
def value_or(default)
return default if value.nil?
value
end
end
end
+40
View File
@@ -0,0 +1,40 @@
# frozen_string_literal: true
module TTFunk
# Minimum aggregate. Its value can only become lower.
class Min < Aggregate
# Value
#
# @return [Comparable, nil]
attr_reader :value
# @param init_value [Comparable] initial value
def initialize(init_value = nil)
super()
@value = init_value
end
# Push a value. It will become the new value if it's lower than the current
# value (or if there was no value).
#
# @param new_value [Comparable]
# @return [void]
def <<(new_value)
new_value = coerce(new_value)
if value.nil? || new_value < value
@value = new_value
end
end
# Get the stored value or default.
#
# @param default [any]
# @return [any]
def value_or(default)
return default if value.nil?
value
end
end
end
@@ -0,0 +1,56 @@
# frozen_string_literal: true
module TTFunk
# Array with indexing starting at 1.
class OneBasedArray
include Enumerable
# @overload initialize(size)
# @param size [Integer] number of entries in this array
# @overload initialize(entries)
# @param entries [Array] an array to take entries from
def initialize(size = 0)
@entries = Array.new(size)
end
# Get element by index.
#
# @param idx [Integer]
# @return [any, nil]
# @raise IndexError if index is 0
def [](idx)
if idx.zero?
raise IndexError,
"index #{idx} was outside the bounds of the array"
end
entries[idx - 1]
end
# Number of elements in this array.
#
# @return [Integer]
def size
entries.size
end
# Convert to native array.
#
# @return [Array]
def to_ary
entries
end
# Iterate over elements.
#
# @yieldparam element [any]
# @return [void]
def each(&block)
entries.each(&block)
end
private
attr_reader :entries
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module TTFunk
# Encodes a CFF-based OpenType font subset to its binary representation.
class OTFEncoder < TTFEncoder
# Optimal table order according to OpenType specification.
OPTIMAL_TABLE_ORDER = ['head', 'hhea', 'maxp', 'OS/2', 'name', 'cmap', 'post', 'CFF '].freeze
private
# CFF fonts don't maintain a glyf table, all glyph information is stored
# in the charstrings index. Return an empty hash here to indicate a glyf
# table should not be encoded.
def glyf_table
@glyf_table ||= {}
end
# Since CFF fonts don't maintain a glyf table, they also don't maintain
# a loca table. Return an empty hash here to indicate a loca table
# shouldn't be encoded.
def loca_table
@loca_table ||= {}
end
def base_table
@base_table ||= TTFunk::Table::Simple.new(original, 'BASE').raw
end
def cff_table
@cff_table ||= original.cff.encode(subset)
end
def vorg_table
@vorg_table ||= TTFunk::Table::Vorg.encode(original.vertical_origins)
end
def tables
@tables ||= super.merge(
'BASE' => base_table,
'VORG' => vorg_table,
'CFF ' => cff_table,
).compact
end
def optimal_table_order
# DSIG is always last
OPTIMAL_TABLE_ORDER +
(tables.keys - ['DSIG'] - OPTIMAL_TABLE_ORDER) +
['DSIG']
end
end
end
@@ -0,0 +1,27 @@
# frozen_string_literal: true
module TTFunk
# Encoded String placeholder.
#
# @api private
class Placeholder
# Placeholder position in the cintaining Encoded String
# @return [Integer]
attr_accessor :position
# Planceholder name
# @return [Symbol]
attr_reader :name
# Length of the placeholder
# @return [Integer]
attr_reader :length
# @param name [Symbol]
# @param length [Integer]
def initialize(name, length: 1)
@name = name
@length = length
end
end
end
@@ -0,0 +1,49 @@
# frozen_string_literal: true
module TTFunk
# Helper methods to read form file content.
# @api rpivate
module Reader
private
def io
@file.contents
end
def read(bytes, format)
io.read(bytes).unpack(format)
end
def read_signed(count)
read(count * 2, 'n*').map { |i| to_signed(i) }
end
def to_signed(number)
number >= 0x8000 ? -((number ^ 0xFFFF) + 1) : number
end
def parse_from(position)
saved = io.pos
io.pos = position
result = yield(position)
io.pos = saved
result
end
# For debugging purposes
def hexdump(string)
bytes = string.unpack('C*')
bytes.each_with_index do |c, i|
printf('%02X', c)
if ((i + 1) % 16).zero?
puts
elsif ((i + 1) % 8).zero?
print(' ')
else
print(' ')
end
end
puts if (bytes.length % 16) != 0
end
end
end
@@ -0,0 +1,111 @@
# frozen_string_literal: true
module TTFunk
# Data-fork suitcases resource file
class ResourceFile
# Resource map
#
# @return [Hash]
attr_reader :map
# Open a resource file
#
# @param path [String, Pathname]
# @yieldparam resource_file [TTFunk::ResourceFile]
# @return [any] result of the block
def self.open(path)
::File.open(path, 'rb') { |io|
file = new(io)
yield(file)
}
end
# @param io [IO]
def initialize(io)
@io = io
data_offset, map_offset, map_length = @io.read(16).unpack('NNx4N')
@map = {}
# skip header copy, next map handle, file reference, and attrs
@io.pos = map_offset + 24
type_list_offset, name_list_offset = @io.read(4).unpack('n*')
type_list_offset += map_offset
name_list_offset += map_offset
@io.pos = type_list_offset
max_index = @io.read(2).unpack1('n')
0.upto(max_index) do
type, max_type_index, ref_list_offset = @io.read(8).unpack('A4nn')
@map[type] = { list: [], named: {} }
parse_from(type_list_offset + ref_list_offset) do
0.upto(max_type_index) do
id, name_ofs, attr = @io.read(5).unpack('nnC')
data_ofs = @io.read(3)
data_ofs = data_offset + [0, data_ofs].pack('CA*').unpack1('N')
handle = @io.read(4).unpack1('N')
entry = {
id: id,
attributes: attr,
offset: data_ofs,
handle: handle,
}
if name_list_offset + name_ofs < map_offset + map_length
parse_from(name_ofs + name_list_offset) do
len = @io.read(1).unpack1('C')
entry[:name] = @io.read(len)
end
end
@map[type][:list] << entry
@map[type][:named][entry[:name]] = entry if entry[:name]
end
end
end
end
# Get resource
#
# @overload [](type, index = 0)
# @param type [String]
# @param inxed [Integer]
# @return [String]
# @overload [](type, name)
# @param type [String]
# @param name [String]
# @return [String]
def [](type, index = 0)
if @map[type]
collection = index.is_a?(Integer) ? :list : :named
if @map[type][collection][index]
parse_from(@map[type][collection][index][:offset]) do
length = @io.read(4).unpack1('N')
return @io.read(length)
end
end
end
end
# Get resource names
#
# @param type [String]
# @return [Array<String>]
def resources_for(type)
((@map[type] && @map[type][:named]) || {}).keys
end
private
def parse_from(offset)
saved = @io.pos
@io.pos = offset
yield
ensure
@io.pos = saved
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
module TTFunk
# Scientific number representation
class SciForm
# Significand
# @return [Float, Integer]
attr_reader :significand
# Exponent
# @return [Float, Integer]
attr_reader :exponent
# @param significand [Float, Integer]
# @param exponent [Float, Integer]
def initialize(significand, exponent = 0)
@significand = significand
@exponent = exponent
end
# Convert to Float.
#
# @return [Float]
def to_f
significand * (10**exponent)
end
# Check equality to another number.
#
# @param other [Float, SciForm]
# @return [Boolean]
def ==(other)
case other
when Float
other == to_f # rubocop: disable Lint/FloatComparison
when self.class
other.significand == significand &&
other.exponent == exponent
else
false
end
end
alias eql? ==
end
end
@@ -0,0 +1,63 @@
# frozen_string_literal: true
require_relative 'reader'
module TTFunk
# SFNT sub-table
class SubTable
# A read past sub-table end was attempted.
class EOTError < StandardError
end
include Reader
# File or IO this sub-table is in.
# @return [IO]
attr_reader :file
# This sub-table's offset from the file beginning.
# @return [Integer]
attr_reader :table_offset
# This sub-table's length in byes.
# @return [Integer, nil]
attr_reader :length
# @param file [IO]
# @param offset [Integer]
# @param length [Integer]
def initialize(file, offset, length = nil)
@file = file
@table_offset = offset
@length = length
parse_from(@table_offset) { parse! }
end
# End of sub-table?
#
# @return [Boolean]
def eot?
# if length isn't set yet there's no way to know if we're at the end of
# the sub-table or not
return false unless length
io.pos > table_offset + length
end
# Read a series of values.
#
# @overload read(bytes, format)
# @param bytes [Integer] number of bytes to read.
# @param format [String] format to parse the bytes.
# @return [Array]
# @raise [EOTError]
# @see # Ruby Packed data
def read(*args)
if eot?
raise EOTError, 'attempted to read past the end of the table'
end
super
end
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
require_relative 'subset/unicode'
require_relative 'subset/unicode_8bit'
require_relative 'subset/mac_roman'
require_relative 'subset/windows_1252'
module TTFunk
# Namespace for different types of subsets.
module Subset
# Create a subset for the font using the specified encoding.
#
# @param original [TTFunk::File]
# @param encoding [:unicode, :unicode_8bit, :mac_roman, :windows_1252]
# @raise [NotImplementedError] for unsupported encodings
# @return [TTFunk::Subset::Unicode, TTFunk::Subset::Unicode8Bit,
# TTFunk::Subset::MacRoman, TTFunk::Subset::Windows1252]
def self.for(original, encoding)
case encoding.to_sym
when :unicode then Unicode.new(original)
when :unicode_8bit then Unicode8Bit.new(original)
when :mac_roman then MacRoman.new(original)
when :windows_1252 then Windows1252.new(original) # rubocop: disable Naming/VariableNumber
else raise NotImplementedError, "encoding #{encoding} is not supported"
end
end
end
end
@@ -0,0 +1,161 @@
# frozen_string_literal: true
require_relative '../table/cmap'
require_relative '../table/glyf'
require_relative '../table/head'
require_relative '../table/hhea'
require_relative '../table/hmtx'
require_relative '../table/kern'
require_relative '../table/loca'
require_relative '../table/maxp'
require_relative '../table/name'
require_relative '../table/post'
require_relative '../table/simple'
module TTFunk
module Subset
# Base subset.
#
# @api private
class Base
# Microsoft Platform ID
MICROSOFT_PLATFORM_ID = 3
# Symbol Encoding ID for Microsoft Platform
MS_SYMBOL_ENCODING_ID = 0
# Original font
#
# @return [TTFunk::File]
attr_reader :original
# @param original [TTFunk::File]
def initialize(original)
@original = original
end
# Is this Unicode-based subset?
#
# @return [Boolean]
def unicode?
false
end
# Does this subset use Microsoft Symbolic encoding?
#
# @return [Boolean]
def microsoft_symbol?
new_cmap_table[:platform_id] == MICROSOFT_PLATFORM_ID &&
new_cmap_table[:encoding_id] == MS_SYMBOL_ENCODING_ID
end
# Get a mapping from this subset to Unicode.
#
# @return [Hash{Integer => Integer}]
def to_unicode_map
{}
end
# Encode this subset into a binary font representation.
#
# @param options [Hash]
# @return [String]
def encode(options = {})
encoder_klass.new(original, self, options).encode
end
# Encoder class for this subset.
#
# @return [TTFunk::TTFEncoder, TTFunk::OTFEncoder]
def encoder_klass
original.cff.exists? ? OTFEncoder : TTFEncoder
end
# Get the first Unicode cmap from the original font.
#
# @return [TTFunk::Table::Cmap::Subtable]
def unicode_cmap
@unicode_cmap ||= @original.cmap.unicode.first
end
# Get glyphs in this subset.
#
# @return [Hash{Integer => TTFunk::Table::Glyf::Simple,
# TTFunk::Table::Glyf::Compound}] if original is a TrueType font
# @return [Hash{Integer => TTFunk::Table::Cff::Charstring] if original is
# a CFF-based OpenType font
def glyphs
@glyphs ||= collect_glyphs(original_glyph_ids)
end
# Get glyphs by their IDs in the original font.
#
# @param glyph_ids [Array<Integer>]
# @return [Hash{Integer => TTFunk::Table::Glyf::Simple,
# TTFunk::Table::Glyf::Compound>] if original is a TrueType font
# @return [Hash{Integer => TTFunk::Table::Cff::Charstring}] if original is
# a CFF-based OpenType font
def collect_glyphs(glyph_ids)
collected =
glyph_ids.each_with_object({}) do |id, h|
h[id] = glyph_for(id)
end
additional_ids = collected.values
.select { |g| g && g.compound? }
.map(&:glyph_ids)
.flatten
collected.update(collect_glyphs(additional_ids)) if additional_ids.any?
collected
end
# Glyph ID mapping from the original font to this subset.
#
# @return [Hash{Integer => Integer}]
def old_to_new_glyph
@old_to_new_glyph ||=
begin
charmap = new_cmap_table[:charmap]
old_to_new =
charmap.each_with_object(0 => 0) do |(_, ids), map|
map[ids[:old]] = ids[:new]
end
next_glyph_id = new_cmap_table[:max_glyph_id]
glyphs.each_key do |old_id|
unless old_to_new.key?(old_id)
old_to_new[old_id] = next_glyph_id
next_glyph_id += 1
end
end
old_to_new
end
end
# Glyph ID mapping from this subset to the original font.
#
# @return [Hash{Integer => Integer}]
def new_to_old_glyph
@new_to_old_glyph ||= old_to_new_glyph.invert
end
private
def glyph_for(glyph_id)
if original.cff.exists?
original
.cff
.top_index[0]
.charstrings_index[glyph_id]
.glyph
else
original.glyph_outlines.for(glyph_id)
end
end
end
end
end
@@ -0,0 +1,133 @@
# frozen_string_literal: true
require 'set'
require_relative 'base'
module TTFunk
module Subset
# A subset that uses standard code page encoding.
class CodePage < Base
class << self
# Get a mapping from an encoding to Unicode
#
# @param encoding [Encoding, String, Symbol]
# @return [Hash{Integer => Integer}]
def unicode_mapping_for(encoding)
mapping_cache[encoding] ||=
(0..255).each_with_object({}) do |c, ret|
codepoint =
c.chr(encoding)
.encode(Encoding::UTF_8, undef: :replace, replace: '')
.codepoints
.first
ret[c] = codepoint if codepoint
end
end
private
def mapping_cache
@mapping_cache ||= {}
end
end
# Code page used in this subset.
# This is used for proper `OS/2` table encoding.
# @return [Integer]
attr_reader :code_page
# Encoding used in this subset.
# @return [Encoding, String, Symbol]
attr_reader :encoding
# @param original [TTFunk::File]
# @param code_page [Integer]
# @param encoding [Encoding, String, Symbol]
def initialize(original, code_page, encoding)
super(original)
@code_page = code_page
@encoding = encoding
@subset = Array.new(256)
@from_unicode_cache = {}
use(space_char_code)
end
# Get a mapping from this subset to Unicode.
#
# @return [Hash]
def to_unicode_map
self.class.unicode_mapping_for(encoding)
.select { |codepoint, _unicode| @subset[codepoint] }
end
# Add a character to subset.
#
# @param character [Integer] Unicode codepoint
# @return [void]
def use(character)
@subset[from_unicode(character)] = character
end
# Can this subset include the character? This depends on the encoding used
# in this subset.
#
# @param character [Integer] Unicode codepoint
# @return [Boolean]
def covers?(character)
!from_unicode(character).nil?
end
# Does this subset actually has the character?
#
# @param character [Integer] Unicode codepoint
# @return [Boolean]
def includes?(character)
code = from_unicode(character)
code && @subset[code]
end
# Get character code for Unicode codepoint.
#
# @param character [Integer] Unicode codepoint
# @return [Integer, nil]
def from_unicode(character)
@from_unicode_cache[character] ||= (+'' << character).encode!(encoding).ord
rescue Encoding::UndefinedConversionError
nil
end
# Get `cmap` table for this subset.
#
# @return [TTFunk::Table::Cmap]
def new_cmap_table
@new_cmap_table ||=
begin
mapping = {}
@subset.each_with_index do |unicode, roman|
mapping[roman] = unicode_cmap[unicode]
end
TTFunk::Table::Cmap.encode(mapping, :mac_roman)
end
end
# Get the list of Glyph IDs from the original font that are in this
# subset.
#
# @return [Array<Integer>]
def original_glyph_ids
([0] + @subset.map { |unicode| unicode && unicode_cmap[unicode] })
.compact.uniq.sort
end
# Get a chacter code for Space in this subset
#
# @return [Integer, nil]
def space_char_code
@space_char_code ||= from_unicode(Unicode::SPACE_CHAR)
end
end
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'set'
require_relative 'code_page'
module TTFunk
module Subset
# Mac Roman subset. It uses code page 10,000 and Mac OS Roman encoding.
class MacRoman < CodePage
# @param original [TTFunk::File]
def initialize(original)
super(original, 10_000, Encoding::MACROMAN)
end
end
end
end
@@ -0,0 +1,90 @@
# frozen_string_literal: true
require 'set'
require_relative 'base'
module TTFunk
module Subset
# Unicode-based subset.
class Unicode < Base
# Space character code
SPACE_CHAR = 0x20
# @param original [TTFunk::File]
def initialize(original)
super
@subset = Set.new
use(SPACE_CHAR)
end
# Is this a Unicode-based subset?
#
# @return [true]
def unicode?
true
end
# Get a mapping from this subset to Unicode.
#
# @return [Hash{Integer => Integer}]
def to_unicode_map
@subset.each_with_object({}) { |code, map| map[code] = code }
end
# Add a character to subset.
#
# @param character [Integer] Unicode codepoint
# @return [void]
def use(character)
@subset << character
end
# Can this subset include the character?
#
# @param _character [Integer] Unicode codepoint
# @return [true]
def covers?(_character)
true
end
# Does this subset actually has the character?
#
# @param character [Integer] Unicode codepoint
# @return [Boolean]
def includes?(character)
@subset.include?(character)
end
# Get character code for Unicode codepoint.
#
# @param character [Integer] Unicode codepoint
# @return [Integer]
def from_unicode(character)
character
end
# Get `cmap` table for this subset.
#
# @return [TTFunk::Table::Cmap]
def new_cmap_table
@new_cmap_table ||=
begin
mapping =
@subset.each_with_object({}) do |code, map|
map[code] = unicode_cmap[code]
end
TTFunk::Table::Cmap.encode(mapping, :unicode)
end
end
# Get the list of Glyph IDs from the original font that are in this
# subset.
#
# @return [Array<Integer>]
def original_glyph_ids
([0] + @subset.map { |code| unicode_cmap[code] }).uniq.sort
end
end
end
end
@@ -0,0 +1,99 @@
# frozen_string_literal: true
require 'set'
require_relative 'base'
module TTFunk
module Subset
# An 8-bit Unicode-based subset. It can include any Unicode character but
# limits number of characters so that the could be encoded by a single byte.
class Unicode8Bit < Base
# @param original [TTFunk::File]
def initialize(original)
super
@subset = { 0x20 => 0x20 }
@unicodes = { 0x20 => 0x20 }
@next = 0x21 # apparently, PDF's don't like to use chars between 0-31
end
# Is this a Unicode-based subset?
#
# @return [true]
def unicode?
true
end
# Get a mapping from this subset to Unicode.
#
# @return [Hash{Integer => Integer}]
def to_unicode_map
@subset.dup
end
# Add a character to subset.
#
# @param character [Integer] Unicode codepoint
# @return [void]
def use(character)
unless @unicodes.key?(character)
@subset[@next] = character
@unicodes[character] = @next
@next += 1
end
end
# Can this subset include the character?
#
# @param character [Integer] Unicode codepoint
# @return [Boolean]
def covers?(character)
@unicodes.key?(character) || @next < 256
end
# Does this subset actually has the character?
#
# @param character [Integer] Unicode codepoint
# @return [Boolean]
def includes?(character)
@unicodes.key?(character)
end
# Get character code for Unicode codepoint.
#
# @param character [Integer] Unicode codepoint
# @return [Integer]
def from_unicode(character)
@unicodes[character]
end
# Get `cmap` table for this subset.
#
# @return [TTFunk::Table::Cmap]
def new_cmap_table
@new_cmap_table ||=
begin
mapping =
@subset.each_with_object({}) do |(code, unicode), map|
map[code] = unicode_cmap[unicode]
map
end
# since we're mapping a subset of the unicode glyphs into an
# arbitrary 256-character space, the actual encoding we're
# using is irrelevant. We choose MacRoman because it's a 256-character
# encoding that happens to be well-supported in both TTF and
# PDF formats.
TTFunk::Table::Cmap.encode(mapping, :mac_roman)
end
end
# Get the list of Glyph IDs from the original font that are in this
# subset.
#
# @return [Array<Integer>]
def original_glyph_ids
([0] + @unicodes.keys.map { |unicode| unicode_cmap[unicode] }).uniq.sort
end
end
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'set'
require_relative 'code_page'
module TTFunk
module Subset
# Windows 1252 sbset. It uses code page 1252 and Windows-1252 encoding.
class Windows1252 < CodePage
# @param original [TTFunk::File]
def initialize(original)
super(original, 1252, Encoding::CP1252)
end
end
end
end
@@ -0,0 +1,104 @@
# frozen_string_literal: true
require_relative 'subset'
module TTFunk
# Subset collection.
#
# For many use cases a font subset can be efficiently encoded using MacRoman
# encoding. However, for full font coverage and characters that are not in
# MacRoman encoding an additional Unicode subset is used. There can be as many
# as needed Unicode subsets to fully cover glyphs provided by the original
# font. Ther resulting set of subsets all use 8-bit encoding helping to
# efficiently encode text in Prawn.
class SubsetCollection
# @param original [TTFunk::File]
def initialize(original)
@original = original
@subsets = [Subset.for(@original, :mac_roman)]
end
# Get subset by index.
#
# @param subset [Integer]
# @return [TTFunk::Subset::Unicode, TTFunk::Subset::Unicode8Bit,
# TTFunk::Subset::MacRoman, TTFunk::Subset::Windows1252]
def [](subset)
@subsets[subset]
end
# Add chracters to appropiate subsets.
#
# @param characters [Array<Integer>] should be an array of UTF-16 code
# points
# @return [void]
def use(characters)
characters.each do |char|
covered = false
i = 0
length = @subsets.length
while i < length
subset = @subsets[i]
if subset.covers?(char)
subset.use(char)
covered = true
break
end
i += 1
end
unless covered
@subsets << Subset.for(@original, :unicode_8bit)
@subsets.last.use(char)
end
end
end
# Encode characters into subset-character pairs.
#
# @param characters [Array<Integer>] should be an array of UTF-16 code
# points
# @return [Array<Array(Integer, String)>] subset chunks, where each chunk
# is another array of two elements. The first element is the subset
# number, and the second element is the string of characters to render
# with that font subset. The strings will be encoded for their subset
# font, and so may not look (in the raw) like what was passed in, but they
# will render correctly with the corresponding subset font.
def encode(characters)
return [] if characters.empty?
# TODO: probably would be more optimal to nix the #use method,
# and merge it into this one, so it can be done in a single
# pass instead of two passes.
use(characters)
parts = []
current_subset = 0
current_char = 0
char = characters[current_char]
loop do
while @subsets[current_subset].includes?(char)
char = @subsets[current_subset].from_unicode(char)
if parts.empty? || parts.last[0] != current_subset
encoded_char = char.chr
if encoded_char.respond_to?(:force_encoding)
encoded_char.force_encoding('ASCII-8BIT')
end
parts << [current_subset, encoded_char]
else
parts.last[1] << char
end
current_char += 1
return parts if current_char >= characters.length
char = characters[current_char]
end
current_subset = (current_subset + 1) % @subsets.length
end
end
end
end
+34
View File
@@ -0,0 +1,34 @@
# frozen_string_literal: true
module TTFunk
# Sum aggreaget. Is sums all pushed values.
class Sum < Aggregate
# Value
#
# @return [#+]
attr_reader :value
# @param init_value [#+] initial value
def initialize(init_value = 0)
super()
@value = init_value
end
# Push a value. It will be added to the current value.
#
# @param operand [any]
# @return [void]
def <<(operand)
@value += coerce(operand)
end
# Get the stored value or default.
#
# @param _default [any] Unused. Here for API compatibility.
# @return [any]
def value_or(_default)
# value should always be non-nil
value
end
end
end
+67
View File
@@ -0,0 +1,67 @@
# frozen_string_literal: true
require_relative 'reader'
module TTFunk
# SFNT table
class Table
include Reader
# File this table is in.
# @return [TTFunk::File]
attr_reader :file
# This table's offset from the file beginning.
# @return [Integer]
attr_reader :offset
# This table's length in byes.
# @return [Integer, nil]
attr_reader :length
# @param file [TTFunk::File]
def initialize(file)
@file = file
@offset = nil
@length = nil
info = file.directory_info(tag)
if info
@offset = info[:offset]
@length = info[:length]
parse_from(@offset) { parse! }
end
end
# Does this table exist in the file?
#
# @return [Boolean]
def exists?
!@offset.nil?
end
# Raw bytes of this table in the file.
#
# @return [String, nil]
def raw
if exists?
parse_from(offset) { io.read(length) }
end
end
# Table tag.
#
# @return [String]
def tag
self.class.name.split('::').last.downcase
end
private
def parse!
# do nothing, by default
end
end
end
@@ -0,0 +1,89 @@
# frozen_string_literal: true
module TTFunk
class Table
# Compact Font Format (`CFF `) table
class Cff < TTFunk::Table
autoload :Charset, 'ttfunk/table/cff/charset'
autoload :Charsets, 'ttfunk/table/cff/charsets'
autoload :Charstring, 'ttfunk/table/cff/charstring'
autoload :CharstringsIndex, 'ttfunk/table/cff/charstrings_index'
autoload :Dict, 'ttfunk/table/cff/dict'
autoload :Encoding, 'ttfunk/table/cff/encoding'
autoload :Encodings, 'ttfunk/table/cff/encodings'
autoload :FdSelector, 'ttfunk/table/cff/fd_selector'
autoload :FontDict, 'ttfunk/table/cff/font_dict'
autoload :FontIndex, 'ttfunk/table/cff/font_index'
autoload :Header, 'ttfunk/table/cff/header'
autoload :Index, 'ttfunk/table/cff/index'
autoload :OneBasedIndex, 'ttfunk/table/cff/one_based_index'
autoload :Path, 'ttfunk/table/cff/path'
autoload :PrivateDict, 'ttfunk/table/cff/private_dict'
autoload :SubrIndex, 'ttfunk/table/cff/subr_index'
autoload :TopDict, 'ttfunk/table/cff/top_dict'
autoload :TopIndex, 'ttfunk/table/cff/top_index'
# Table tag. The extra space is important.
TAG = 'CFF '
# Table header.
# @return [TTFunk::Table::Cff::Header]
attr_reader :header
# Name index.
# @return [TTFunk::Table::Cff::Index]
attr_reader :name_index
# Top dict index.
# @return [TTFunk::Table::Cff::TopIndex]
attr_reader :top_index
# Strings index.
# @return [TTFunk::Table::Cff::OneBasedIndex]
attr_reader :string_index
# Global subroutine index.
# @return [TTFunk::Table::Cff::SubrIndex]
attr_reader :global_subr_index
# Table tag.
# @return [String]
def tag
TAG
end
# Encode table.
#
# @param subset [TTFunk::Subset::MacRoman, TTFunk::Subset::Windows1252,
# TTFunk::Subset::Unicode, TTFunk::Subset::Unicode8Bit]
# @return [TTFunk::EncodedString]
def encode(subset)
# Make sure TopDict has an entry for encoding so it could be properly replaced
top_index[0][TopDict::OPERATORS[:encoding]] = 0
EncodedString.new do |result|
result.concat(
header.encode,
name_index.encode,
top_index.encode,
string_index.encode,
global_subr_index.encode,
)
charmap = subset.new_cmap_table[:charmap]
top_index[0].finalize(result, charmap)
end
end
private
def parse!
@header = Header.new(file, offset)
@name_index = Index.new(file, @header.table_offset + @header.length)
@top_index = TopIndex.new(file, @name_index.table_offset + @name_index.length)
@string_index = OneBasedIndex.new(file, @top_index.table_offset + @top_index.length)
@global_subr_index = SubrIndex.new(file, @string_index.table_offset + @string_index.length)
end
end
end
end
@@ -0,0 +1,291 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Charset
class Charset < TTFunk::SubTable
include Enumerable
# First glyph string. This is an implicit glyph present in all charsets.
FIRST_GLYPH_STRING = '.notdef'
# Format 0.
ARRAY_FORMAT = 0
# Format 1.
RANGE_FORMAT_8 = 1
# Format 2.
RANGE_FORMAT_16 = 2
# Predefined ISOAdobe charset ID.
ISO_ADOBE_CHARSET_ID = 0
# Predefined Expert charset ID.
EXPERT_CHARSET_ID = 1
# Predefined Expert Subset charset ID.
EXPERT_SUBSET_CHARSET_ID = 2
# Default charset ID.
DEFAULT_CHARSET_ID = ISO_ADOBE_CHARSET_ID
class << self
# Standard strings defined in the spec that do not need to be defined
# in the CFF.
#
# @return [TTFunk::OneBasedArray<String>]
def standard_strings
Charsets::STANDARD_STRINGS
end
# Strings for charset ID.
#
# @param charset_id [Integer]
# @return [TTFunk::OneBasedArray<String>]
def strings_for_charset_id(charset_id)
case charset_id
when ISO_ADOBE_CHARSET_ID
Charsets::ISO_ADOBE
when EXPERT_CHARSET_ID
Charsets::EXPERT
when EXPERT_SUBSET_CHARSET_ID
Charsets::EXPERT_SUBSET
end
end
end
# Encoded entries.
# @return [TTFunk::OneBasedArray<Integer>, Array<Range<Integer>>]
attr_reader :entries
# Length of encoded charset subtable.
# @return [Integer]
attr_reader :length
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# Encodign format.
# @return [Integer]
attr_reader :format
# Number of encoded items.
# @return [Integer]
attr_reader :items_count
# Offset or charset ID.
# @return [Integer]
attr_reader :offset_or_id
# @overload initialize(top_dict, file, offset = nil, length = nil)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
# @overload initialize(top_dict, file, charset_id)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param charset_id [Integer] 0, 1, or 2
def initialize(top_dict, file, offset_or_id = nil, length = nil)
@top_dict = top_dict
@offset_or_id = offset_or_id || DEFAULT_CHARSET_ID
if offset
super(file, offset, length)
else
@items_count = self.class.strings_for_charset_id(offset_or_id).size
end
end
# Iterate over character names.
#
# @overload each()
# @yieldparam name [String]
# @return [void]
# @overload each()
# @return [Enumerator]
def each
return to_enum(__method__) unless block_given?
# +1 adjusts for the implicit .notdef glyph
(items_count + 1).times { |i| yield(self[i]) }
end
# Get character name for glyph index.
#
# @param glyph_id [Integer]
# @return [String, nil]
def [](glyph_id)
return FIRST_GLYPH_STRING if glyph_id.zero?
find_string(sid_for(glyph_id))
end
# Charset offset in the file.
#
# @return [Integer, nil]
def offset
# Numbers from 0..2 mean charset IDs instead of offsets. IDs are
# basically pre-defined sets of characters.
#
# In the case of an offset, add the CFF table's offset since the
# charset offset is relative to the start of the CFF table. Otherwise
# return nil (no offset).
if offset_or_id > 2
offset_or_id + top_dict.cff_offset
end
end
# Encode charset.
#
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [String]
def encode(charmap)
# no offset means no charset was specified (i.e. we're supposed to
# use a predefined charset) so there's nothing to encode
return '' unless offset
sids =
charmap
.values
.reject { |mapping| mapping[:new].zero? }
.sort_by { |mapping| mapping[:new] }
.map { |mapping| sid_for(mapping[:old]) }
ranges = TTFunk::BinUtils.rangify(sids)
range_max = ranges.map(&:last).max
range_bytes =
if range_max.positive?
(Math.log2(range_max) / 8).floor + 1
else
# for cases when there are no sequences at all
Float::INFINITY
end
# calculate whether storing the charset as a series of ranges is
# more efficient (i.e. takes up less space) vs storing it as an
# array of SID values
total_range_size = (2 * ranges.size) + (range_bytes * ranges.size)
total_array_size = sids.size * element_width(:array_format)
if total_array_size <= total_range_size
([format_int(:array_format)] + sids).pack('Cn*')
else
fmt = range_bytes == 1 ? :range_format8 : :range_format16
element_fmt = element_format(fmt)
result = [format_int(fmt)].pack('C')
ranges.each { |range| result << range.pack(element_fmt) }
result
end
end
private
def sid_for(glyph_id)
return 0 if glyph_id.zero?
# rather than validating the glyph as part of one of the predefined
# charsets, just pass it through
return glyph_id unless offset
case format_sym
when :array_format
entries[glyph_id]
when :range_format8, :range_format16
entries.reduce(glyph_id) do |remaining, range|
if range.size >= remaining
break (range.first + remaining) - 1
end
remaining - range.size
end
end
end
def find_string(sid)
if offset
return self.class.standard_strings[sid] if sid <= 390
idx = sid - 390
if idx < file.cff.string_index.items_count
file.cff.string_index[idx]
end
else
self.class.strings_for_charset_id(offset_or_id)[sid]
end
end
def parse!
return unless offset
@format = read(1, 'C').first
case format_sym
when :array_format
@items_count = top_dict.charstrings_index.items_count - 1
@length = @items_count * element_width
@entries = OneBasedArray.new(read(length, 'n*'))
when :range_format8, :range_format16
# The number of ranges is not explicitly specified in the font.
# Instead, software utilizing this data simply processes ranges
# until all glyphs in the font are covered.
@items_count = 0
@entries = []
@length = 0
until @items_count >= top_dict.charstrings_index.items_count - 1
@length += 1 + element_width
sid, num_left = read(element_width, element_format)
@entries << (sid..(sid + num_left))
@items_count += num_left + 1
end
end
end
def element_width(fmt = format_sym)
{
array_format: 2, # SID
range_format8: 3, # SID + Card8
range_format16: 4, # SID + Card16
}[fmt]
end
def element_format(fmt = format_sym)
{
array_format: 'n',
range_format8: 'nC',
range_format16: 'nn',
}[fmt]
end
def format_sym
case @format
when ARRAY_FORMAT then :array_format
when RANGE_FORMAT_8 then :range_format8
when RANGE_FORMAT_16 then :range_format16
else
raise Error, "unsupported charset format '#{fmt}'"
end
end
def format_int(sym = format_sym)
{
array_format: ARRAY_FORMAT,
range_format8: RANGE_FORMAT_8,
range_format16: RANGE_FORMAT_16,
}[sym]
end
end
end
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# Predefined CFF data
module Charsets
autoload :EXPERT, 'ttfunk/table/cff/charsets/expert'
autoload :EXPERT_SUBSET, 'ttfunk/table/cff/charsets/expert_subset'
autoload :ISO_ADOBE, 'ttfunk/table/cff/charsets/iso_adobe'
autoload :STANDARD_STRINGS, 'ttfunk/table/cff/charsets/standard_strings'
end
end
end
end
@@ -0,0 +1,190 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Predefinde CFF Expert charset
EXPERT = OneBasedArray.new(
[
'space',
*[nil] * 11,
'comma',
'hyphen',
'period',
*[nil] * 11,
'colon',
'semicolon',
*[nil] * 70,
'fraction',
*[nil] * 9,
'fi',
'fl',
*[nil] * 39,
'onesuperior',
*[nil] * 4,
'onehalf',
nil,
nil,
'onequarter',
*[nil] * 4,
'threequarters',
'twosuperior',
*[nil] * 4,
'threesuperior',
*[nil] * 59,
'exclamsmall',
'Hungarumlautsmall',
'dollaroldstyle',
'dollarsuperior',
'ampersandsmall',
'Acutesmall',
'parenleftsuperior',
'parenrightsuperior',
'twodotenleader',
'onedotenleader',
'zerooldstyle',
'oneoldstyle',
'twooldstyle',
'threeoldstyle',
'fouroldstyle',
'fiveoldstyle',
'sixoldstyle',
'sevenoldstyle',
'eightoldstyle',
'nineoldstyle',
'commasuperior',
'threequartersemdash',
'periodsuperior',
'questionsmall',
'asuperior',
'bsuperior',
'centsuperior',
'dsuperior',
'esuperior',
'isuperior',
'lsuperior',
'msuperior',
'nsuperior',
'osuperior',
'rsuperior',
'ssuperior',
'tsuperior',
'ff',
'ffi',
'ffl',
'parenleftinferior',
'parenrightinferior',
'Circumflexsmall',
'hyphensuperior',
'Gravesmall',
'Asmall',
'Bsmall',
'Csmall',
'Dsmall',
'Esmall',
'Fsmall',
'Gsmall',
'Hsmall',
'Ismall',
'Jsmall',
'Ksmall',
'Lsmall',
'Msmall',
'Nsmall',
'Osmall',
'Psmall',
'Qsmall',
'Rsmall',
'Ssmall',
'Tsmall',
'Usmall',
'Vsmall',
'Wsmall',
'Xsmall',
'Ysmall',
'Zsmall',
'colonmonetary',
'onefitted',
'rupiah',
'Tildesmall',
'exclamdownsmall',
'centoldstyle',
'Lslashsmall',
'Scaronsmall',
'Zcaronsmall',
'Dieresissmall',
'Brevesmall',
'Caronsmall',
'Dotaccentsmall',
'Macronsmall',
'figuredash',
'hypheninferior',
'Ogoneksmall',
'Ringsmall',
'Cedillasmall',
'questiondownsmall',
'oneeighth',
'threeeighths',
'fiveeighths',
'seveneighths',
'onethird',
'twothirds',
'zerosuperior',
'foursuperior',
'fivesuperior',
'sixsuperior',
'sevensuperior',
'eightsuperior',
'ninesuperior',
'zeroinferior',
'oneinferior',
'twoinferior',
'threeinferior',
'fourinferior',
'fiveinferior',
'sixinferior',
'seveninferior',
'eightinferior',
'nineinferior',
'centinferior',
'dollarinferior',
'periodinferior',
'commainferior',
'Agravesmall',
'Aacutesmall',
'Acircumflexsmall',
'Atildesmall',
'Adieresissmall',
'Aringsmall',
'AEsmall',
'Ccedillasmall',
'Egravesmall',
'Eacutesmall',
'Ecircumflexsmall',
'Edieresissmall',
'Igravesmall',
'Iacutesmall',
'Icircumflexsmall',
'Idieresissmall',
'Ethsmall',
'Ntildesmall',
'Ogravesmall',
'Oacutesmall',
'Ocircumflexsmall',
'Otildesmall',
'Odieresissmall',
'OEsmall',
'Oslashsmall',
'Ugravesmall',
'Uacutesmall',
'Ucircumflexsmall',
'Udieresissmall',
'Yacutesmall',
'Thornsmall',
],
).freeze
end
end
end
end
@@ -0,0 +1,120 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Predefined CFF Expert Subset charset
EXPERT_SUBSET = OneBasedArray.new(
[
'space',
*[nil] * 11,
'comma',
'hyphen',
'period',
*[nil] * 11,
'colon',
'semicolon',
*[nil] * 70,
'fraction',
*[nil] * 9,
'fi',
'fl',
*[nil] * 39,
'onesuperior',
*[nil] * 4,
'onehalf',
nil,
nil,
'onequarter',
*[nil] * 4,
'threequarters',
'twosuperior',
*[nil] * 4,
'threesuperior',
*[nil] * 61,
'dollaroldstyle',
'dollarsuperior',
nil,
nil,
'parenleftsuperior',
'parenrightsuperior',
'twodotenleader',
'onedotenleader',
'zerooldstyle',
'oneoldstyle',
'twooldstyle',
'threeoldstyle',
'fouroldstyle',
'fiveoldstyle',
'sixoldstyle',
'sevenoldstyle',
'eightoldstyle',
'nineoldstyle',
'commasuperior',
'threequartersemdash',
'periodsuperior',
nil,
'asuperior',
'bsuperior',
'centsuperior',
'dsuperior',
'esuperior',
'isuperior',
'lsuperior',
'msuperior',
'nsuperior',
'osuperior',
'rsuperior',
'ssuperior',
'tsuperior',
'ff',
'ffi',
'ffl',
'parenleftinferior',
'parenrightinferior',
nil,
'hyphensuperior',
*[nil] * 27,
'colonmonetary',
'onefitted',
'rupiah',
nil,
nil,
'centoldstyle',
*[nil] * 8,
'figuredash',
'hypheninferior',
*[nil] * 4,
'oneeighth',
'threeeighths',
'fiveeighths',
'seveneighths',
'onethird',
'twothirds',
'zerosuperior',
'foursuperior',
'fivesuperior',
'sixsuperior',
'sevensuperior',
'eightsuperior',
'ninesuperior',
'zeroinferior',
'oneinferior',
'twoinferior',
'threeinferior',
'fourinferior',
'fiveinferior',
'sixinferior',
'seveninferior',
'eightinferior',
'nineinferior',
'centinferior',
'dollarinferior',
'periodinferior',
],
).freeze
end
end
end
end
@@ -0,0 +1,242 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Predefined CFF ISOAdobe charset
ISO_ADOBE = OneBasedArray.new(
[
'space',
'exclam',
'quotedbl',
'numbersign',
'dollar',
'percent',
'ampersand',
'quoteright',
'parenleft',
'parenright',
'asterisk',
'plus',
'comma',
'hyphen',
'period',
'slash',
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'colon',
'semicolon',
'less',
'equal',
'greater',
'question',
'at',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'bracketleft',
'backslash',
'bracketright',
'asciicircum',
'underscore',
'quoteleft',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'braceleft',
'bar',
'braceright',
'asciitilde',
'exclamdown 97 cent',
'sterling',
'fraction',
'yen',
'florin',
'section',
'currency',
'quotesingle',
'quotedblleft',
'guillemotleft',
'guilsinglleft',
'guilsinglright',
'fi',
'fl',
'endash',
'dagger',
'daggerdbl',
'periodcentered',
'paragraph',
'bullet',
'quotesinglbase',
'quotedblbase',
'quotedblright',
'guillemotright',
'ellipsis',
'perthousand',
'questiondown',
'grave',
'acute',
'circumflex',
'tilde',
'macron',
'breve',
'dotaccent',
'dieresis',
'ring',
'cedilla',
'hungarumlaut',
'ogonek',
'caron',
'emdash',
'AE',
'ordfeminine',
'Lslash',
'Oslash',
'OE',
'ordmasculine',
'ae',
'dotlessi',
'lslash',
'oslash',
'oe',
'germandbls',
'onesuperior',
'logicalnot',
'mu',
'trademark',
'Eth',
'onehalf',
'plusminus',
'Thorn',
'onequarter',
'divide',
'brokenbar',
'degree',
'thorn',
'threequarters',
'twosuperior',
'registered',
'minus',
'eth',
'multiply',
'threesuperior',
'copyright',
'Aacute',
'Acircumflex',
'Adieresis',
'Agrave',
'Aring',
'Atilde',
'Ccedilla',
'Eacute',
'Ecircumflex',
'Edieresis',
'Egrave',
'Iacute',
'Icircumflex',
'Idieresis',
'Igrave',
'Ntilde',
'Oacute',
'Ocircumflex',
'Odieresis',
'Ograve',
'Otilde',
'Scaron',
'Uacute',
'Ucircumflex',
'Udieresis',
'Ugrave',
'Yacute',
'Ydieresis',
'Zcaron',
'aacute',
'acircumflex',
'adieresis',
'agrave',
'aring',
'atilde',
'ccedilla',
'eacute',
'ecircumflex',
'edieresis',
'egrave',
'iacute',
'icircumflex',
'idieresis',
'igrave',
'ntilde',
'oacute',
'ocircumflex',
'odieresis',
'ograve',
'otilde',
'scaron',
'uacute',
'ucircumflex',
'udieresis',
'ugrave',
'yacute',
'ydieresis',
'zcaron',
],
).freeze
end
end
end
end
@@ -0,0 +1,405 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Charsets
# Standard CFF strings
STANDARD_STRINGS = OneBasedArray.new(
[
'space',
'exclam',
'quotedbl',
'numbersign',
'dollar',
'percent',
'ampersand',
'quoteright',
'parenleft',
'parenright',
'asterisk',
'plus',
'comma',
'hyphen',
'period',
'slash',
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'colon',
'semicolon',
'less',
'equal',
'greater',
'question',
'at',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'bracketleft',
'backslash',
'bracketright',
'asciicircum',
'underscore',
'quoteleft',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'braceleft',
'bar',
'braceright',
'asciitilde',
'exclamdown',
'cent',
'sterling',
'fraction',
'yen',
'florin',
'section',
'currency',
'quotesingle',
'quotedblleft',
'guillemotleft',
'guilsinglleft',
'guilsinglright',
'fi',
'fl',
'endash',
'dagger',
'daggerdbl',
'periodcentered',
'paragraph',
'bullet',
'quotesinglbase',
'quotedblbase',
'quotedblright',
'guillemotright',
'ellipsis',
'perthousand',
'questiondown',
'grave',
'acute',
'circumflex',
'tilde',
'macron',
'breve',
'dotaccent',
'dieresis',
'ring',
'cedilla',
'hungarumlaut',
'ogonek',
'caron',
'emdash',
'AE',
'ordfeminine',
'Lslash',
'Oslash',
'OE',
'ordmasculine',
'ae',
'dotlessi',
'lslash',
'oslash',
'oe',
'germandbls',
'onesuperior',
'logicalnot',
'mu',
'trademark',
'Eth',
'onehalf',
'plusminus',
'Thorn',
'onequarter',
'divide',
'brokenbar',
'degree',
'thorn',
'threequarters',
'twosuperior',
'registered',
'minus',
'eth',
'multiply',
'threesuperior',
'copyright',
'Aacute',
'Acircumflex',
'Adieresis',
'Agrave',
'Aring',
'Atilde',
'Ccedilla',
'Eacute',
'Ecircumflex',
'Edieresis',
'Egrave',
'Iacute',
'Icircumflex',
'Idieresis',
'Igrave',
'Ntilde',
'Oacute',
'Ocircumflex',
'Odieresis',
'Ograve',
'Otilde',
'Scaron',
'Uacute',
'Ucircumflex',
'Udieresis',
'Ugrave',
'Yacute',
'Ydieresis',
'Zcaron',
'aacute',
'acircumflex',
'adieresis',
'agrave',
'aring',
'atilde',
'ccedilla',
'eacute',
'ecircumflex',
'edieresis',
'egrave',
'iacute',
'icircumflex',
'idieresis',
'igrave',
'ntilde',
'oacute',
'ocircumflex',
'odieresis',
'ograve',
'otilde',
'scaron',
'uacute',
'ucircumflex',
'udieresis',
'ugrave',
'yacute',
'ydieresis',
'zcaron',
'exclamsmall',
'Hungarumlautsmall',
'dollaroldstyle',
'dollarsuperior',
'ampersandsmall',
'Acutesmall',
'parenleftsuperior',
'parenrightsuperior',
'266 ff',
'onedotenleader',
'zerooldstyle',
'oneoldstyle',
'twooldstyle',
'threeoldstyle',
'fouroldstyle',
'fiveoldstyle',
'sixoldstyle',
'sevenoldstyle',
'eightoldstyle',
'nineoldstyle',
'commasuperior',
'threequartersemdash',
'periodsuperior',
'questionsmall',
'asuperior',
'bsuperior',
'centsuperior',
'dsuperior',
'esuperior',
'isuperior',
'lsuperior',
'msuperior',
'nsuperior',
'osuperior',
'rsuperior',
'ssuperior',
'tsuperior',
'ff',
'ffi',
'ffl',
'parenleftinferior',
'parenrightinferior',
'Circumflexsmall',
'hyphensuperior',
'Gravesmall',
'Asmall',
'Bsmall',
'Csmall',
'Dsmall',
'Esmall',
'Fsmall',
'Gsmall',
'Hsmall',
'Ismall',
'Jsmall',
'Ksmall',
'Lsmall',
'Msmall',
'Nsmall',
'Osmall',
'Psmall',
'Qsmall',
'Rsmall',
'Ssmall',
'Tsmall',
'Usmall',
'Vsmall',
'Wsmall',
'Xsmall',
'Ysmall',
'Zsmall',
'colonmonetary',
'onefitted',
'rupiah',
'Tildesmall',
'exclamdownsmall',
'centoldstyle',
'Lslashsmall',
'Scaronsmall',
'Zcaronsmall',
'Dieresissmall',
'Brevesmall',
'Caronsmall',
'Dotaccentsmall',
'Macronsmall',
'figuredash',
'hypheninferior',
'Ogoneksmall',
'Ringsmall',
'Cedillasmall',
'questiondownsmall',
'oneeighth',
'threeeighths',
'fiveeighths',
'seveneighths',
'onethird',
'twothirds',
'zerosuperior',
'foursuperior',
'fivesuperior',
'sixsuperior',
'sevensuperior',
'eightsuperior',
'ninesuperior',
'zeroinferior',
'oneinferior',
'twoinferior',
'threeinferior',
'fourinferior',
'fiveinferior',
'sixinferior',
'seveninferior',
'eightinferior',
'nineinferior',
'centinferior',
'dollarinferior',
'periodinferior',
'commainferior',
'Agravesmall',
'Aacutesmall',
'Acircumflexsmall',
'Atildesmall',
'Adieresissmall',
'Aringsmall',
'AEsmall',
'Ccedillasmall',
'Egravesmall',
'Eacutesmall',
'Ecircumflexsmall',
'Edieresissmall',
'Igravesmall',
'Iacutesmall',
'Icircumflexsmall',
'Idieresissmall',
'Ethsmall',
'Ntildesmall',
'Ogravesmall',
'Oacutesmall',
'Ocircumflexsmall',
'Otildesmall',
'Odieresissmall',
'OEsmall',
'Oslashsmall',
'Ugravesmall',
'Uacutesmall',
'Ucircumflexsmall',
'Udieresissmall',
'Yacutesmall',
'Thornsmall',
'Ydieresissmall',
'001.000',
'001.001',
'001.002',
'001.003',
'Black',
'Bold',
'Book',
'Light',
'Medium',
'Regular',
'Roman',
'Semibold',
],
).freeze
end
end
end
end
@@ -0,0 +1,507 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Charstring.
class Charstring
# Type 2 charstring operators
CODE_MAP = {
1 => :hstem,
3 => :vstem,
4 => :vmoveto,
5 => :rlineto,
6 => :hlineto,
7 => :vlineto,
8 => :rrcurveto,
10 => :callsubr,
12 => :flex_select,
14 => :endchar,
18 => :hstemhm,
19 => :hintmask,
20 => :cntrmask,
21 => :rmoveto,
22 => :hmoveto,
23 => :vstemhm,
24 => :rcurveline,
25 => :rlinecurve,
26 => :vvcurveto,
27 => :hhcurveto,
28 => :shortint,
29 => :callgsubr,
30 => :vhcurveto,
31 => :hvcurveto,
}.freeze
# Type 2 Flex operators.
FLEX_CODE_MAP = {
35 => :flex,
34 => :hflex,
36 => :hflex1,
37 => :flex1,
}.freeze
# Glyph ID.
# @return [Integer]
attr_reader :glyph_id
# Encoded charstring.
# @return [String]
attr_reader :raw
# @param glyph_id [Integer]
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param font_dict [TTFunk::Table:Cff::FontDict]
# @param raw [String]
def initialize(glyph_id, top_dict, font_dict, raw)
@glyph_id = glyph_id
@top_dict = top_dict
@font_dict = font_dict
@raw = raw
default_width_x = (@font_dict || @top_dict).private_dict.default_width_x
@nominal_width_x = (@font_dict || @top_dict).private_dict.nominal_width_x
@subrs = (@font_dict || @top_dict).private_dict.subr_index
@gsubrs = @top_dict.cff.global_subr_index
@subrs_bias = @subrs.bias if @subrs
@gsubrs_bias = @gsubrs.bias if @gsubrs
@stack = []
@data = raw.bytes
@index = 0
@n_stems = 0
@have_width = false
@open = false
@width = default_width_x
@x = 0
@y = 0
end
# Get path representation of this charstring.
#
# @return [TTFunk::Table::Cff::Path]
def path
@path || begin
@path = Path.new
parse!
@path
end
end
# Get a TrueType-compatible glyph representation of this charstring.
#
# @return [TTFunk::Table::Glyf::PathBased]
def glyph
@glyph ||=
begin
horizontal_metrics = @top_dict.file.horizontal_metrics.for(glyph_id)
Glyf::PathBased.new(path, horizontal_metrics)
end
end
# Get path representation of this charstring at the specified font size.
#
# @param x [Integer, Float] new horizontal position.
# @param y [Integer, Float] new vertical position.
# @param font_size [Integer, Float] font size.
# @return [TTFunk::Table::Cff::Path]
def render(x: 0, y: 0, font_size: 72)
path.render(
x: x,
y: y,
font_size: font_size,
units_per_em: @top_dict.file.header.units_per_em,
)
end
private
def parse!
until @index >= @data.size
code = @data[@index]
@index += 1
# return from callgsubr - do nothing since we inline subrs
next if code == 11
if code >= 32 && code <= 246
@stack << (code - 139)
elsif (m = CODE_MAP[code])
__send__(m)
elsif code >= 247 && code <= 250
b0 = code
b1 = @data[@index]
@index += 1
@stack << (((b0 - 247) * 256) + b1 + 108)
elsif code >= 251 && code <= 254
b0 = code
b1 = @data[@index]
@index += 1
@stack << ((-(b0 - 251) * 256) - b1 - 108)
else
b1, b2, b3, b4 = read_bytes(4)
@stack << (((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) / 65_536)
end
end
end
def read_bytes(length)
bytes = @data[@index, length]
@index += length
bytes
end
def hstem
stem
end
def vstem
stem
end
# rubocop:disable Style/EvenOdd
def stem
# The number of stem operators on the stack is always even.
# If the value is uneven, that means a width is specified.
has_width_arg = @stack.size % 2 == 1
if has_width_arg && !@have_width
@width = @stack.shift + @nominal_width_x
end
@n_stems += @stack.length >> 1
@stack.clear
@have_width = true
end
# rubocop:enable Style/EvenOdd
def vmoveto
if @stack.size > 1 && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
@y += @stack.pop
add_contour(@x, @y)
end
def add_contour(x, y)
if @open
@path.close_path
end
@path.move_to(x, y)
@open = true
end
def rlineto
until @stack.empty?
@x += @stack.shift
@y += @stack.shift
@path.line_to(@x, @y)
end
end
def hlineto
until @stack.empty?
@x += @stack.shift
@path.line_to(@x, @y)
break if @stack.empty?
@y += @stack.shift
@path.line_to(@x, @y)
end
end
def vlineto
until @stack.empty?
@y += @stack.shift
@path.line_to(@x, @y)
break if @stack.empty?
@x += @stack.shift
@path.line_to(@x, @y)
end
end
def rrcurveto
until @stack.empty?
c1x = @x + @stack.shift
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
def callsubr
code_index = @stack.pop + @subrs_bias
subr_code = @subrs[code_index].bytes
@data[@index, 0] = subr_code
end
def flex_select
flex_code = @data[@index]
@index += 1
__send__(FLEX_CODE_MAP[flex_code])
end
def flex
c1x = @x + @stack.shift # dx1
c1y = @y + @stack.shift # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y + @stack.shift # dy3
c3x = jpx + @stack.shift # dx4
c3y = jpy + @stack.shift # dy4
c4x = c3x + @stack.shift # dx5
c4y = c3y + @stack.shift # dy5
@x = c4x + @stack.shift # dx6
@y = c4y + @stack.shift # dy6
@stack.shift # flex depth
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def hflex
c1x = @x + @stack.shift # dx1
c1y = @y # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y # dy3
c3x = jpx + stack.shift # dx4
c3y = c2y # dy4
c4x = c3x + stack.shift # dx5
c4y = @y # dy5
@x = c4x + stack.shift # dx6
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def hflex1
c1x = @x + @stack.shift # dx1
c1y = @y + @stack.shift # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y # dy3
c3x = jpx + @stack.shift # dx4
c3y = c2y # dy4
c4x = c3x + @stack.shift # dx5
c4y = c3y + @stack.shift # dy5
@x = c4x + @stack.shift # dx6
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def flex1
c1x = @x + @stack.shift # dx1
c1y = @y + @stack.shift # dy1
c2x = c1x + @stack.shift # dx2
c2y = c1y + @stack.shift # dy2
jpx = c2x + @stack.shift # dx3
jpy = c2y + @stack.shift # dy3
c3x = jpx + @stack.shift # dx4
c3y = jpy + @stack.shift # dy4
c4x = c3x + @stack.shift # dx5
c4y = c3y + @stack.shift # dy5
if (c4x - @x).abs > (c4y - @y).abs
@x = c4x + @stack.shift
else
@y = c4y + @stack.shift
end
@path.curve_to(c1x, c1y, c2x, c2y, jpx, jpy)
@path.curve_to(c3x, c3y, c4x, c4y, @x, @y)
end
def endchar
if !@stack.empty? && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
if @open
@path.close_path
@open = false
end
end
def hstemhm
stem
end
def hintmask
cntrmask
end
def cntrmask
stem
# skip past hinting data, no need to worry about it since we're not
# very concerned about rendering glyphs
read_bytes((@n_stems + 7) >> 3)
end
def rmoveto
if @stack.size > 2 && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
@y += @stack.pop
@x += @stack.pop
add_contour(@x, @y)
end
def hmoveto
if @stack.size > 1 && !@have_width
@width = @stack.shift + @nominal_width_x
@have_width = true
end
@x += @stack.pop
add_contour(@x, @y)
end
def vstemhm
stem
end
def rcurveline
while @stack.size > 2
c1x = @x + @stack.shift
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
@x += @stack.shift
@y += @stack.shift
@path.line_to(@x, @y)
end
def rlinecurve
while @stack.size > 6
@x += @stack.shift
@y += @stack.shift
@path.line_to(@x, @y)
end
c1x = @x + @stack.shift
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
# rubocop:disable Style/EvenOdd
def vvcurveto
if @stack.size % 2 == 1
@x += @stack.shift
end
until @stack.empty?
c1x = @x
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x
@y = c2y + @stack.shift
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
def hhcurveto
if @stack.size % 2 == 1
@y += @stack.shift
end
until @stack.empty?
c1x = @x + @stack.shift
c1y = @y
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
# rubocop:enable Style/EvenOdd
def shortint
b1, b2 = read_bytes(2)
@stack << (((b1 << 24) | (b2 << 16)) >> 16)
end
def callgsubr
code_index = @stack.pop + @gsubrs_bias
subr_code = @gsubrs[code_index].bytes
@data[@index, 0] = subr_code
end
def vhcurveto
until @stack.empty?
c1x = @x
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
break if @stack.empty?
c1x = @x + @stack.shift
c1y = @y
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@y = c2y + @stack.shift
@x = c2x + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
def hvcurveto
until @stack.empty?
c1x = @x + @stack.shift
c1y = @y
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@y = c2y + @stack.shift
@x = c2x + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
break if @stack.empty?
c1x = @x
c1y = @y + @stack.shift
c2x = c1x + @stack.shift
c2y = c1y + @stack.shift
@x = c2x + @stack.shift
@y = c2y + (@stack.size == 1 ? @stack.shift : 0)
@path.curve_to(c1x, c1y, c2x, c2y, @x, @y)
end
end
end
end
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Charstrings Index.
class CharstringsIndex < TTFunk::Table::Cff::Index
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# @overload initialize(top_dict, file, offset, length = nil)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, *remaining_args)
super(*remaining_args)
@top_dict = top_dict
end
private
def decode_item(index, _offset, _length)
TTFunk::Table::Cff::Charstring.new(index, top_dict, font_dict_for(index), super)
end
def encode_items(charmap)
charmap
.reject { |code, mapping| mapping[:new].zero? && !code.zero? }
.sort_by { |_code, mapping| mapping[:new] }
.map { |(_code, mapping)| items[mapping[:old]] }
end
def font_dict_for(index)
# only CID-keyed fonts contain an FD selector and font dicts
if top_dict.is_cid_font?
fd_index = top_dict.font_dict_selector[index]
top_dict.font_index[fd_index]
end
end
end
end
end
end
@@ -0,0 +1,298 @@
# frozen_string_literal: true
require 'bigdecimal'
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Dict.
class Dict < TTFunk::SubTable
# Indicates malformed operand.
class InvalidOperandError < StandardError; end
# Indicates too many operands.
class TooManyOperandsError < StandardError; end
# Single-byte operators.
OPERATOR_BZERO = (0..21).freeze
# Bytes indicating an operand.
OPERAND_BZERO = [28..30, 32..254].freeze
# Two-byte operator
WIDE_OPERATOR_BZERO = 12
# Two-byte operator adjustment. Used for encoding and decoding of wide
# operators.
WIDE_OPERATOR_ADJUSTMENT = 1200
# Maximum number of operands allowed per operator.
MAX_OPERANDS = 48
# Scientific notation operand significand validation regular
# experession.
VALID_SCI_SIGNIFICAND_RE = /\A-?(\.\d+|\d+|\d+\.\d+)\z/.freeze
# Scientific notation operand exponent validation regular experession.
VALID_SCI_EXPONENT_RE = /\A-?\d+\z/.freeze
include Enumerable
# Get dict value by operator.
#
# @param operator [Integer]
# @return [Array<Integer, TTFunk::SciForm>]
def [](operator)
@dict[operator]
end
# Add dict entry.
#
# @param operator [Integer] Entry operator. Must be in range 0..255. Wide operators must be in range 1200..1455.
# @param operands [Array<Integer, TTFunk::SciForm>]
def []=(operator, *operands)
@dict[operator] = Array(*operands)
end
# Iterate over dict entries.
#
# @yieldparam key [Integer]
# @yieldparam value [Array<Integer, TTFunk::SciForm>]
# @return [void]
def each(&block)
@dict.each(&block)
end
alias each_pair each
# Encode dict.
#
# @return [String]
def encode
sort_by(&:first)
.map { |(operator, operands)|
operands.map { |operand| encode_operand(operand) }.join +
encode_operator(operator)
}
.join
end
private
def encode_operator(operator)
if operator >= WIDE_OPERATOR_ADJUSTMENT
[
WIDE_OPERATOR_BZERO,
operator - WIDE_OPERATOR_ADJUSTMENT,
].pack('C*')
else
[operator].pack('C')
end
end
def encode_operand(operand)
case operand
when Integer
encode_integer(operand)
when Float, BigDecimal
encode_float(operand)
when SciForm
encode_sci(operand)
end
end
def encode_integer(int)
case int
when -107..107
[int + 139].pack('C')
when 108..1131
int -= 108
[(int >> 8) + 247, int & 0xFF].pack('C*')
when -1131..-108
int = -int - 108
[(int >> 8) + 251, int & 0xFF].pack('C*')
when -32_768..32_767
[28, (int >> 8) & 0xFF, int & 0xFF].pack('C*')
else
encode_integer32(int)
end
end
def encode_integer32(int)
[29, int].pack('CN')
end
def encode_float(float)
pack_decimal_nibbles(encode_significand(float))
end
def encode_sci(sci)
sig_bytes = encode_significand(sci.significand)
exp_bytes = encode_exponent(sci.exponent)
pack_decimal_nibbles(sig_bytes + exp_bytes)
end
def encode_exponent(exp)
return [] if exp.zero?
[exp.positive? ? 0xB : 0xC, *encode_significand(exp.abs)]
end
def encode_significand(sig)
sig.to_s.each_char.with_object([]) do |char, ret|
case char
when '0'..'9'
ret << Integer(char)
when '.'
ret << 0xA
when '-'
ret << 0xE
else
break ret
end
end
end
def pack_decimal_nibbles(nibbles)
bytes = [30]
nibbles.each_slice(2).each do |(high_nb, low_nb)|
# low_nb can be nil if nibbles contains an odd number of elements
low_nb ||= 0xF
bytes << ((high_nb << 4) | low_nb)
end
bytes << 0xFF if nibbles.size.even?
bytes.pack('C*')
end
def parse!
@dict = {}
operands = []
# @length must be set via the constructor
while io.pos < table_offset + length
case b_zero = read(1, 'C').first
when WIDE_OPERATOR_BZERO
operator = decode_wide_operator
@dict[operator] = operands
operands = []
when OPERATOR_BZERO
@dict[b_zero] = operands unless operands.empty?
operands = []
when *OPERAND_BZERO
operands << decode_operand(b_zero)
if operands.size > MAX_OPERANDS
raise TooManyOperandsError,
"found one too many operands at position #{io.pos} in dict at position #{table_offset}"
end
else
raise Error, "dict byte value #{b_zero} is reserved"
end
end
end
def decode_wide_operator
WIDE_OPERATOR_ADJUSTMENT + read(1, 'C').first
end
def decode_operand(b_zero)
case b_zero
when 30
decode_sci
else
decode_integer(b_zero)
end
end
def decode_sci
significand = ''.b
exponent = ''.b
loop do
current = read(1, 'C').first
break if current == 0xFF
high_nibble = current >> 4
low_nibble = current & 0x0F # 0b00001111
[high_nibble, low_nibble].each do |nibble|
case nibble
when 0..9
(exponent.empty? ? significand : exponent) << nibble.to_s
when 0xA
significand << '.'
when 0xB
# take advantage of Integer#to_i not caring about whitespace
exponent << ' '
when 0xC
exponent << '-'
when 0xE
significand << '-'
end
end
break if low_nibble == 0xF
end
validate_sci!(significand, exponent)
exponent = 0 if exponent.empty?
SciForm.new(Float(significand), Integer(exponent))
end
def validate_sci!(significand, exponent)
unless valid_significand?(significand) && valid_exponent?(exponent)
raise InvalidOperandError,
'invalid scientific notation operand with significand ' \
"'#{significand}' and exponent '#{exponent}' ending at " \
"position #{io.pos} in dict at position #{table_offset}"
end
end
def valid_significand?(significand)
!(significand.strip =~ VALID_SCI_SIGNIFICAND_RE).nil?
end
def valid_exponent?(exponent)
exponent = exponent.strip
return true if exponent.empty?
!(exponent.strip =~ VALID_SCI_EXPONENT_RE).nil?
end
def decode_integer(b_zero)
case b_zero
when 32..246
# 1 byte
b_zero - 139
when 247..250
# 2 bytes
b_one = read(1, 'C').first
((b_zero - 247) * 256) + b_one + 108
when 251..254
# 2 bytes
b_one = read(1, 'C').first
(-(b_zero - 251) * 256) - b_one - 108
when 28
# 2 bytes in number (3 total)
b_one, b_two = read(2, 'C*')
BinUtils.twos_comp_to_int((b_one << 8) | b_two, bit_width: 16)
when 29
# 4 bytes in number (5 total)
b_one, b_two, b_three, b_four = read(4, 'C*')
BinUtils.twos_comp_to_int((b_one << 24) | (b_two << 16) | (b_three << 8) | b_four, bit_width: 32)
end
end
end
end
end
end
@@ -0,0 +1,277 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Encoding.
class Encoding < TTFunk::SubTable
include Enumerable
# Predefined Standard Encoding ID.
STANDARD_ENCODING_ID = 0
# Predefined Expert Encoding ID.
EXPERT_ENCODING_ID = 1
# Default encoding ID.
DEFAULT_ENCODING_ID = STANDARD_ENCODING_ID
class << self
# Get predefined encoding by ID.
#
# @param encoding_id [Integer]
# @return [TTFunk::OneBasedArray<Integer>]
def codes_for_encoding_id(encoding_id)
case encoding_id
when STANDARD_ENCODING_ID
Encodings::STANDARD
when EXPERT_ENCODING_ID
Encodings::EXPERT
end
end
end
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# Encodign format.
# @return [Integer]
attr_reader :format
# Number of encoded items.
# @return [Integer]
attr_reader :items_count
# Offset or encoding ID.
# @return [Integer]
attr_reader :offset_or_id
# @overload initialize(top_dict, file, offset = nil, length = nil)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
# @overload initialize(top_dict, file, charset_id)
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param encoding_id [Integer] 0, 1, or 2
def initialize(top_dict, file, offset_or_id = nil, length = nil)
@top_dict = top_dict
@offset_or_id = offset_or_id || DEFAULT_ENCODING_ID
if offset
super(file, offset, length)
@supplemental = format >> 7 == 1
else
@items_count = self.class.codes_for_encoding_id(offset_or_id).size
@supplemental = false
end
end
# Iterate over character codes.
#
# @overload each()
# @yieldparam code [Integer]
# @return [void]
# @overload each()
# @return [Enumerator]
def each
return to_enum(__method__) unless block_given?
# +1 adjusts for the implicit .notdef glyph
(items_count + 1).times { |i| yield(self[i]) }
end
# Get character code for glyph index.
#
# @param glyph_id [Integer]
# @return [Integer, nil]
def [](glyph_id)
return 0 if glyph_id.zero?
return code_for(glyph_id) if offset
self.class.codes_for_encoding_id(offset_or_id)[glyph_id]
end
# Encoding offset in the file.
#
# @return [Integer, nil]
def offset
# Numbers from 0..1 mean encoding IDs instead of offsets. IDs are
# pre-defined, generic encodings that define the characters present
# in the font.
#
# In the case of an offset, add the CFF table's offset since the
# charset offset is relative to the start of the CFF table. Otherwise
# return nil (no offset).
if offset_or_id > 1
offset_or_id + top_dict.cff_offset
end
end
# Encode encoding.
#
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [String]
def encode(charmap)
# Any subset encoding is all but guaranteed to be different from the
# standard encoding so we don't even attempt to see if it matches. We
# assume it's different and just encode it anew.
return encode_supplemental(charmap) if supplemental?
codes =
charmap
.reject { |_code, mapping| mapping[:new].zero? }
.sort_by { |_code, mapping| mapping[:new] }
.map { |(code, _m)| code }
ranges = TTFunk::BinUtils.rangify(codes)
# calculate whether storing the charset as a series of ranges is
# more efficient (i.e. takes up less space) vs storing it as an
# array of SID values
total_range_size = (2 * ranges.size) +
(element_width(:range_format) * ranges.size)
total_array_size = codes.size * element_width(:array_format)
if total_array_size <= total_range_size
([format_int(:array_format), codes.size] + codes).pack('C*')
else
element_fmt = element_format(:range_format)
result = [format_int(:range_format), ranges.size].pack('CC')
ranges.each { |range| result << range.pack(element_fmt) }
result
end
end
# Is this a supplemental encoding?
#
# @return [Boolean]
def supplemental?
# high-order bit set to 1 indicates supplemental encoding
@supplemental
end
private
def encode_supplemental(charmap)
new_entries =
charmap
.reject { |_code, mapping| mapping[:new].zero? }
.transform_values { |mapping| mapping[:new] }
result = [format_int(:supplemental), new_entries.size].pack('CC')
fmt = element_format(:supplemental)
new_entries.each do |code, new_gid|
result << [code, new_gid].pack(fmt)
end
result
end
def code_for(glyph_id)
return 0 if glyph_id.zero?
# rather than validating the glyph as part of one of the predefined
# encodings, just pass it through
return glyph_id unless offset
case format_sym
when :array_format, :supplemental
@entries[glyph_id]
when :range_format
remaining = glyph_id
@entries.each do |range|
if range.size >= remaining
return (range.first + remaining) - 1
end
remaining -= range.size
end
0
end
end
def parse!
@format, entry_count = read(2, 'C*')
@length = entry_count * element_width
case format_sym
when :array_format
@items_count = entry_count
@entries = OneBasedArray.new(read(length, 'C*'))
when :range_format
@entries = []
@items_count = 0
entry_count.times do
code, num_left = read(element_width, element_format)
@entries << (code..(code + num_left))
@items_count += num_left + 1
end
when :supplemental
@entries = {}
@items_count = entry_count
entry_count.times do
code, glyph = read(element_width, element_format)
@entries[code] = glyph
end
end
end
def element_format(fmt = format_sym)
{
array_format: 'C',
range_format: 'CC',
supplemental: 'Cn',
}[fmt]
end
# @TODO: handle supplemental encoding (necessary?)
def element_width(fmt = format_sym)
case fmt
when :array_format then 1
when :range_format then 2
when :supplemental then 3
else
raise Error, "'#{fmt}' is an unsupported encoding format"
end
end
def format_sym
return :supplemental if supplemental?
case @format
when 0 then :array_format
when 1 then :range_format
else
raise Error, "unsupported charset format '#{fmt}'"
end
end
def format_int(sym = format_sym)
case sym
when :array_format then 0
when :range_format then 1
when :supplemental then 129
else
raise Error, "unsupported charset format '#{sym}'"
end
end
end
end
end
end
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# Predefined encodings.
module Encodings
autoload :EXPERT, 'ttfunk/table/cff/encodings/expert'
autoload :STANDARD, 'ttfunk/table/cff/encodings/standard'
end
end
end
end
@@ -0,0 +1,207 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Encodings
# CFF predefined Expert encoding.
EXPERT = OneBasedArray.new(
[
*[0] * 31,
1,
229,
230,
0,
231,
232,
233,
234,
235,
236,
237,
238,
13,
14,
15,
99,
239,
240,
241,
242,
243,
244,
245,
246,
247,
248,
27,
28,
249,
250,
251,
252,
0,
253,
254,
255,
256,
257,
0,
0,
0,
258,
0,
0,
259,
260,
261,
262,
0,
0,
263,
264,
265,
0,
266,
109,
110,
267,
268,
269,
0,
270,
271,
272,
273,
274,
275,
276,
277,
278,
279,
280,
281,
282,
283,
284,
285,
286,
287,
288,
289,
290,
291,
292,
293,
294,
295,
296,
297,
298,
299,
300,
301,
302,
303,
*[0] * 34,
304,
305,
306,
0,
0,
307,
308,
309,
310,
311,
0,
312,
0,
0,
313,
0,
0,
314,
315,
0,
0,
316,
317,
318,
0,
0,
0,
158,
155,
163,
319,
320,
321,
322,
323,
324,
325,
0,
0,
326,
150,
164,
169,
327,
328,
329,
330,
331,
332,
333,
334,
335,
336,
337,
338,
339,
340,
341,
342,
343,
344,
345,
346,
347,
348,
349,
350,
351,
352,
353,
354,
355,
356,
357,
358,
359,
360,
361,
362,
363,
364,
365,
366,
367,
368,
369,
370,
371,
372,
373,
374,
375,
376,
377,
378,
],
).freeze
end
end
end
end
@@ -0,0 +1,182 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
module Encodings
# CFF predefined Standard encoding.
STANDARD = OneBasedArray.new(
[
*[0] * 31,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
*[0] * 34,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
0,
111,
112,
113,
114,
0,
115,
116,
117,
118,
119,
120,
121,
122,
0,
123,
0,
124,
125,
126,
127,
128,
129,
130,
131,
0,
132,
133,
0,
134,
135,
136,
137,
*[0] * 16,
138,
0,
139,
*[0] * 4,
140,
141,
142,
143,
*[0] * 5,
144,
0,
0,
0,
145,
0,
0,
146,
147,
148,
149,
*[0] * 4,
],
).freeze
end
end
end
end
@@ -0,0 +1,193 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF FDSelect.
class FdSelector < TTFunk::SubTable
include Enumerable
# Array format.
ARRAY_FORMAT = 0
# Range format.
RANGE_FORMAT = 3
# Range entry size.
RANGE_ENTRY_SIZE = 3
# Array entry size.
ARRAY_ENTRY_SIZE = 1
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# Number of encoded items.
# @return [Integer]
attr_reader :items_count
# Number of entries.
# @return [Array<Integer>] if format is array.
# @return [Array<Array(Range, Integer)>] if format is range.
attr_reader :entries
# Number of glyphs.
# @return [Integer]
attr_reader :n_glyphs
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, file, offset, length = nil)
@top_dict = top_dict
super(file, offset, length)
end
# Get font dict index for glyph ID.
#
# @return [Integer]
def [](glyph_id)
case format_sym
when :array_format
entries[glyph_id]
when :range_format
if (entry = range_cache[glyph_id])
return entry
end
range, entry =
entries.bsearch { |rng, _|
if rng.cover?(glyph_id)
0
elsif glyph_id < rng.first
-1
else
1
end
}
range.each { |i| range_cache[i] = entry }
entry
end
end
# Iterate over font dicts for each glyph ID.
#
# @yieldparam [Integer] font dict index.
# @return [void]
def each
return to_enum(__method__) unless block_given?
items_count.times { |i| yield(self[i]) }
end
# Encode Font dict selector.
#
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [String]
def encode(charmap)
# get list of [new_gid, fd_index] pairs
new_indices =
charmap
.reject { |code, mapping| mapping[:new].zero? && !code.zero? }
.sort_by { |_code, mapping| mapping[:new] }
.map { |(_code, mapping)| [mapping[:new], self[mapping[:old]]] }
ranges = rangify_gids(new_indices)
total_range_size = ranges.size * RANGE_ENTRY_SIZE
total_array_size = new_indices.size * ARRAY_ENTRY_SIZE
''.b.tap do |result|
if total_array_size <= total_range_size
result << [ARRAY_FORMAT].pack('C')
result << new_indices.map(&:last).pack('C*')
else
result << [RANGE_FORMAT, ranges.size].pack('Cn')
ranges.each { |range| result << range.pack('nC') }
# "A sentinel GID follows the last range element and serves to
# delimit the last range in the array. (The sentinel GID is set
# equal to the number of glyphs in the font. That is, its value
# is 1 greater than the last GID in the font)."
result << [new_indices.size].pack('n')
end
end
end
private
def range_cache
@range_cache ||= {}
end
# values is an array of [new_gid, fd_index] pairs
def rangify_gids(values)
start_gid = 0
[].tap do |ranges|
values.each_cons(2) do |(_, first_idx), (sec_gid, sec_idx)|
if first_idx != sec_idx
ranges << [start_gid, first_idx]
start_gid = sec_gid
end
end
ranges << [start_gid, values.last[1]]
end
end
def parse!
@format = read(1, 'C').first
@length = 1
case format_sym
when :array_format
@n_glyphs = top_dict.charstrings_index.items_count
data = io.read(n_glyphs)
@length += data.bytesize
@items_count = data.bytesize
@entries = data.bytes
when :range_format
# +2 for sentinel GID, +2 for num_ranges
num_ranges = read(2, 'n').first
@length += (num_ranges * RANGE_ENTRY_SIZE) + 4
ranges = Array.new(num_ranges) { read(RANGE_ENTRY_SIZE, 'nC') }
@entries =
ranges.each_cons(2).map { |first, second|
first_gid, fd_index = first
second_gid, = second
[(first_gid...second_gid), fd_index]
}
# read the sentinel GID, otherwise known as the number of glyphs
# in the font
@n_glyphs = read(2, 'n').first
last_start_gid, last_fd_index = ranges.last
@entries << [(last_start_gid...(n_glyphs + 1)), last_fd_index]
@items_count = entries.reduce(0) { |sum, entry| sum + entry.first.size }
end
end
def format_sym
case @format
when ARRAY_FORMAT then :array_format
when RANGE_FORMAT then :range_format
else
raise Error, "unsupported fd select format '#{@format}'"
end
end
end
end
end
end
@@ -0,0 +1,91 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Font dict.
class FontDict < TTFunk::Table::Cff::Dict
# Length of placeholders.
PLACEHOLDER_LENGTH = 5
# Operators we care about in this dict.
OPERATORS = { private: 18 }.freeze
# Inverse operator mapping.
OPERATOR_CODES = OPERATORS.invert
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, file, offset, length = nil)
@top_dict = top_dict
super(file, offset, length)
end
# Encode dict.
#
# @return [TTFunk::EncodedString]
def encode
EncodedString.new do |result|
each do |operator, operands|
case OPERATOR_CODES[operator]
when :private
result << encode_private
else
operands.each { |operand| result << encode_operand(operand) }
end
result << encode_operator(operator)
end
end
end
# Finalize dict.
#
# @param new_cff_data [TTFunk::EncodedString]
# @return [void]
def finalize(new_cff_data)
encoded_private_dict = private_dict.encode
encoded_offset = encode_integer32(new_cff_data.length)
encoded_length = encode_integer32(encoded_private_dict.length)
new_cff_data.resolve_placeholder(:"private_length_#{@table_offset}", encoded_length)
new_cff_data.resolve_placeholder(:"private_offset_#{@table_offset}", encoded_offset)
private_dict.finalize(encoded_private_dict)
new_cff_data << encoded_private_dict
end
# Private dict.
#
# @return [TTFunk::Table::Cff::PrivateDict, nil]
def private_dict
@private_dict ||=
if (info = self[OPERATORS[:private]])
private_dict_length, private_dict_offset = info
PrivateDict.new(
file,
top_dict.cff_offset + private_dict_offset,
private_dict_length,
)
end
end
private
def encode_private
EncodedString.new do |result|
result << Placeholder.new(:"private_length_#{@table_offset}", length: PLACEHOLDER_LENGTH)
result << Placeholder.new(:"private_offset_#{@table_offset}", length: PLACEHOLDER_LENGTH)
end
end
end
end
end
end
@@ -0,0 +1,42 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Font Dict Index.
class FontIndex < TTFunk::Table::Cff::Index
# Top dict.
# @return [TTFunk::Table::Cff::TopDict]
attr_reader :top_dict
# @param top_dict [TTFunk::Table:Cff::TopDict]
# @param file [TTFunk::File]
# @param offset [Integer]
# @param length [Integer]
def initialize(top_dict, file, offset, length = nil)
super(file, offset, length)
@top_dict = top_dict
end
# Finalize index.
#
# @param new_cff_data [TTFunk::EncodedString]
# @return [void]
def finalize(new_cff_data)
each { |font_dict| font_dict.finalize(new_cff_data) }
end
private
def decode_item(_index, offset, length)
TTFunk::Table::Cff::FontDict.new(top_dict, file, offset, length)
end
def encode_items(*)
# Re-encode font dicts
map(&:encode)
end
end
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Header.
class Header < TTFunk::SubTable
# CFF table major version.
# @return [Integer]
attr_reader :major
# CFF table minor version.
# @return [Integer]
attr_reader :minor
# Size of the header itself.
# @return [Integer]
attr_reader :header_size
# Size of all offsets from beginning of table.
# @return [Integer]
attr_reader :absolute_offset_size
# Length of header.
#
# @return [Integer]
def length
4
end
# Encode header.
#
# @return [String]
def encode
[major, minor, header_size, absolute_offset_size].pack('C*')
end
private
def parse!
@major, @minor, @header_size, @absolute_offset_size = read(4, 'C*')
end
end
end
end
end
@@ -0,0 +1,158 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Index.
class Index < TTFunk::SubTable
include Enumerable
# Get value by index.
#
# @param index [Integer]
# @return [any]
def [](index)
return if index >= items_count
entry_cache[index] ||=
decode_item(
index,
data_reference_offset + offsets[index],
offsets[index + 1] - offsets[index],
)
end
# Iterate over index items.
#
# @overload each()
# @yieldparam item [any]
# @return [void]
# @overload each()
# @return [Enumerator]
def each(&block)
return to_enum(__method__) unless block
items_count.times do |i|
yield(self[i])
end
end
# Numer of items in this index.
#
# @return [Integer]
def items_count
items.length
end
# Encode index.
#
# @param args all arguments are passed to `encode_item` method.
# @return [TTFunk::EncodedString]
def encode(*args)
new_items = encode_items(*args)
if new_items.empty?
return [0].pack('n')
end
if new_items.length > 0xffff
raise Error, 'Too many items in a CFF index'
end
offsets_array =
new_items
.each_with_object([1]) { |item, offsets|
offsets << (offsets.last + item.length)
}
offset_size = (offsets_array.last.bit_length / 8.0).ceil
offsets_array.map! { |offset| encode_offset(offset, offset_size) }
EncodedString.new.concat(
[new_items.length, offset_size].pack('nC'),
*offsets_array,
*new_items,
)
end
private
attr_reader :items
attr_reader :offsets
attr_reader :data_reference_offset
def entry_cache
@entry_cache ||= {}
end
# Returns an array of EncodedString elements (plain strings,
# placeholders, or EncodedString instances). Each element is supposed to
# represent an encoded item.
#
# This is the place to do all the filtering, reordering, or individual
# item encoding.
#
# It gets all the arguments `encode` gets.
def encode_items(*)
items
end
# By default do nothing
def decode_item(index, _offset, _length)
items[index]
end
def encode_offset(offset, offset_size)
case offset_size
when 1
[offset].pack('C')
when 2
[offset].pack('n')
when 3
[offset].pack('N')[1..]
when 4
[offset].pack('N')
end
end
def parse!
@entry_cache = {}
num_entries = read(2, 'n').first
if num_entries.zero?
@length = 2
@items = []
return
end
offset_size = read(1, 'C').first
@offsets =
Array.new(num_entries + 1) {
unpack_offset(io.read(offset_size), offset_size)
}
@data_reference_offset = table_offset + 3 + (offsets.length * offset_size) - 1
@length =
2 + # num entries
1 + # offset size
(offsets.length * offset_size) + # offsets
offsets.last - 1 # items
@items =
offsets.each_cons(2).map { |offset, next_offset|
io.read(next_offset - offset)
}
end
def unpack_offset(offset_data, offset_size)
padding = "\x00" * (4 - offset_size)
(padding + offset_data).unpack1('N')
end
end
end
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'forwardable'
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Index with indexing starting at 1.
class OneBasedIndex
extend Forwardable
def_delegators :base_index,
:each,
:table_offset,
:items_count,
:length,
:encode
# Underlaying Index.
# @return [TTFunk::Table::Cff::Index]
attr_reader :base_index
# @param args [Array] all params are passed to the base index.
# @see Index
def initialize(*args)
@base_index = Index.new(*args)
end
# Get item by index.
#
# @param idx [Integer]
# @return [any]
# @raise [IndexError] when requested index is 0.
def [](idx)
if idx.zero?
raise IndexError,
"index #{idx} was outside the bounds of the index"
end
base_index[idx - 1]
end
end
end
end
end
@@ -0,0 +1,107 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# Path. Mostly used for CFF glyph outlines.
class Path
# Close path command.
CLOSE_PATH_CMD = [:close].freeze
# Commands in this path.
# @return [Array]
attr_reader :commands
# Number of contours in this path.
# @return [Integer]
attr_reader :number_of_contours
def initialize
@commands = []
@number_of_contours = 0
end
# Move implicit cursor to coordinates.
#
# @param x [Integer, Float]
# @param y [Integer, Float]
# @return [void]
def move_to(x, y)
@commands << [:move, x, y]
end
# Add a line to coordinates.
#
# @param x [Integer, Float]
# @param y [Integer, Float]
# @return [void]
def line_to(x, y)
@commands << [:line, x, y]
end
# Add a Bézier curve. Current position is the first control point, (`x1`,
# `y1`) is the second, (`x2`, `y2`) is the third, and (`x`, `y`) is the
# last control point.
#
# @param x1 [Integer, Float]
# @param y1 [Integer, Float]
# @param x2 [Integer, Float]
# @param y2 [Integer, Float]
# @param x [Integer, Float]
# @param y [Integer, Float]
# @return [void]
def curve_to(x1, y1, x2, y2, x, y) # rubocop: disable Metrics/ParameterLists
@commands << [:curve, x1, y1, x2, y2, x, y]
end
# Close current contour.
#
# @return [void]
def close_path
@commands << CLOSE_PATH_CMD
@number_of_contours += 1
end
# Reposition and scale path.
#
# @param x [Integer, Float] new horizontal position.
# @param y [Integer, Float] new vertical position.
# @param font_size [Integer, Float] font size.
# @param units_per_em [Integer] units per Em as defined in the font.
# @return [TTFunk::Table::Cff::Path]
def render(x: 0, y: 0, font_size: 72, units_per_em: 1000)
new_path = self.class.new
scale = 1.0 / units_per_em * font_size
commands.each do |cmd|
case cmd[:type]
when :move
new_path.move_to(x + (cmd[1] * scale), y + (-cmd[2] * scale))
when :line
new_path.line_to(x + (cmd[1] * scale), y + (-cmd[2] * scale))
when :curve
new_path.curve_to(
x + (cmd[1] * scale),
y + (-cmd[2] * scale),
x + (cmd[3] * scale),
y + (-cmd[4] * scale),
x + (cmd[5] * scale),
y + (-cmd[6] * scale),
)
when :close
new_path.close_path
end
end
new_path
end
private
def format_values(command)
command[1..].map { |k| format('%.2f', k) }.join(' ')
end
end
end
end
end
@@ -0,0 +1,104 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Private dict.
class PrivateDict < TTFunk::Table::Cff::Dict
# Default value of Default Width X.
DEFAULT_WIDTH_X_DEFAULT = 0
# Default value of Nominal Width X.
DEFAULT_WIDTH_X_NOMINAL = 0
# Length of placeholders.
PLACEHOLDER_LENGTH = 5
# Operators we care about in this dict.
OPERATORS = {
subrs: 19,
default_width_x: 20,
nominal_width_x: 21,
}.freeze
# Inverse operator mapping.
OPERATOR_CODES = OPERATORS.invert
# Encode dict.
#
# @return [TTFunk::EncodedString]
def encode
# TODO: use mapping to determine which subroutines are still used.
# For now, just encode them all.
EncodedString.new do |result|
each do |operator, operands|
case OPERATOR_CODES[operator]
when :subrs
result << encode_subrs
else
operands.each { |operand| result << encode_operand(operand) }
end
result << encode_operator(operator)
end
end
end
# Finalize dict.
#
# @param private_dict_data [TTFunk::EncodedString]
# @return [void]
def finalize(private_dict_data)
return unless subr_index
encoded_subr_index = subr_index.encode
encoded_offset = encode_integer32(private_dict_data.length)
private_dict_data.resolve_placeholder(:"subrs_#{@table_offset}", encoded_offset)
private_dict_data << encoded_subr_index
end
# Subroutine index.
#
# @return [TTFunk::Table::Cff::SubrIndex, nil]
def subr_index
@subr_index ||=
if (subr_offset = self[OPERATORS[:subrs]])
SubrIndex.new(file, table_offset + subr_offset.first)
end
end
# Default Width X.
#
# @return [Integer]
def default_width_x
if (width = self[OPERATORS[:default_width_x]])
width.first
else
DEFAULT_WIDTH_X_DEFAULT
end
end
# Nominal Width X.
#
# @return [Integer]
def nominal_width_x
if (width = self[OPERATORS[:nominal_width_x]])
width.first
else
DEFAULT_WIDTH_X_NOMINAL
end
end
private
def encode_subrs
EncodedString.new do |result|
result << Placeholder.new(:"subrs_#{@table_offset}", length: PLACEHOLDER_LENGTH)
end
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Subroutine index.
class SubrIndex < TTFunk::Table::Cff::Index
# Subroutine index biase. For correct subroutine selection the
# calculated bias must be added to the subroutine number operand before
# accessing the index.
# @return [Integer]
def bias
if items.length < 1240
107
elsif items.length < 33_900
1131
else
32_768
end
end
end
end
end
end
@@ -0,0 +1,254 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF top dict.
class TopDict < TTFunk::Table::Cff::Dict
# Default charstring type.
DEFAULT_CHARSTRING_TYPE = 2
# Length of placeholders for pointer operators.
POINTER_PLACEHOLDER_LENGTH = 5
# Length of placeholders for other operators.
PLACEHOLDER_LENGTH = 5
# Operators whose values are offsets that point to other parts
# of the file.
POINTER_OPERATORS = {
charset: 15,
encoding: 16,
charstrings_index: 17,
private: 18,
font_index: 1236,
font_dict_selector: 1237,
}.freeze
# All the operators we currently care about.
OPERATORS = {
**POINTER_OPERATORS,
ros: 1230,
charstring_type: 1206,
}.freeze
# Inverse operator mapping.
OPERATOR_CODES = OPERATORS.invert
# Encode dict.
#
# @return [TTFunk::EncodedString]
def encode(*)
EncodedString.new do |result|
each_with_index do |(operator, operands), _idx|
if operator == OPERATORS[:private]
result << encode_private
elsif pointer_operator?(operator)
result << Placeholder.new(
OPERATOR_CODES[operator],
length: POINTER_PLACEHOLDER_LENGTH,
)
else
operands.each { |operand| result << encode_operand(operand) }
end
result << encode_operator(operator)
end
end
end
# Finalize the table.
#
# @param new_cff_data [TTFunk::EncodedString]
# @param charmap [Hash{Integer => Hash}] keys are the charac codes,
# values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# @return [void]
def finalize(new_cff_data, charmap)
if charset
finalize_subtable(new_cff_data, :charset, charset.encode(charmap))
end
if encoding
finalize_subtable(new_cff_data, :encoding, encoding.encode(charmap))
end
if charstrings_index
finalize_subtable(new_cff_data, :charstrings_index, charstrings_index.encode(charmap))
end
if font_index
finalize_subtable(new_cff_data, :font_index, font_index.encode)
font_index.finalize(new_cff_data)
end
if font_dict_selector
finalize_subtable(new_cff_data, :font_dict_selector, font_dict_selector.encode(charmap))
end
if private_dict
encoded_private_dict = private_dict.encode
encoded_offset = encode_integer32(new_cff_data.length)
encoded_length = encode_integer32(encoded_private_dict.length)
new_cff_data.resolve_placeholder(:"private_length_#{@table_offset}", encoded_length)
new_cff_data.resolve_placeholder(:"private_offset_#{@table_offset}", encoded_offset)
private_dict.finalize(encoded_private_dict)
new_cff_data << encoded_private_dict
end
end
# Registry Ordering Supplement.
#
# @return [Array(Integer, Integer, Integer), nil]
def ros
self[OPERATORS[:ros]]
end
# Is Registry Ordering Supplement present in this dict?
#
# @return [Boolean]
def ros?
!ros.nil?
end
alias is_cid_font? ros?
# Charset specified in this dict.
#
# @return [TTFunk::Table::Cff::Charset, nil]
def charset
@charset ||=
if (charset_offset_or_id = self[OPERATORS[:charset]])
if charset_offset_or_id.empty?
Charset.new(self, file)
else
Charset.new(self, file, charset_offset_or_id.first)
end
end
end
# Encoding specified in this dict.
#
# @return [TTFunk::Table::Cff::Encoding, nil]
def encoding
# PostScript type 1 fonts, i.e. CID fonts, i.e. some fonts that use
# the CFF table, don't specify an encoding, so this can be nil
@encoding ||=
if (encoding_offset_or_id = self[OPERATORS[:encoding]])
Encoding.new(self, file, encoding_offset_or_id.first)
end
end
# Charstrings index specified in this dict.
#
# > OpenType fonts with TrueType outlines use a glyph index to specify
# and access glyphs within a font; e.g., to index within the `loca`
# table and thereby access glyph data in the `glyf` table. This
# concept is retained in OpenType CFF fonts, except that glyph data is
# accessed through the CharStrings INDEX of the CFF table.
#
# > --- [CFF — Compact Font Format Table](https://www.microsoft.com/typography/otspec/cff.htm)
#
# @return [TTFunk::Table::Cff::CharstringsIndex, nil]
def charstrings_index
@charstrings_index ||=
if (charstrings_offset = self[OPERATORS[:charstrings_index]])
CharstringsIndex.new(self, file, cff_offset + charstrings_offset.first)
end
end
# Charstring type specified in this dict.
#
# @return [Integer]
def charstring_type
@charstring_type =
self[OPERATORS[:charstring_type]] || DEFAULT_CHARSTRING_TYPE
end
# Font index specified in this dict.
#
# @return [TTFunk::Table::Cff::FontIndex, nil]
def font_index
@font_index ||=
if (font_index_offset = self[OPERATORS[:font_index]])
FontIndex.new(self, file, cff_offset + font_index_offset.first)
end
end
# Font dict selector specified in this dict.
#
# @return [TTFunk::Table::Cff::FdSelector, nil]
def font_dict_selector
@font_dict_selector ||=
if (fd_select_offset = self[OPERATORS[:font_dict_selector]])
FdSelector.new(self, file, cff_offset + fd_select_offset.first)
end
end
# Private dict specified in this dict.
#
# @return [TTFunk::Table::Cff::PrivateDict, nil]
def private_dict
@private_dict ||=
if (info = self[OPERATORS[:private]])
private_dict_length, private_dict_offset = info
PrivateDict.new(file, cff_offset + private_dict_offset, private_dict_length)
end
end
# CFF table in this file.
#
# @return [TTFunk::Table::Cff]
def cff
file.cff
end
# Ofsset of CFF table in the file.
#
# @return [Integer]
def cff_offset
cff.offset
end
private
def encode_private
EncodedString.new do |result|
result << Placeholder.new(
:"private_length_#{@table_offset}",
length: PLACEHOLDER_LENGTH,
)
result << Placeholder.new(
:"private_offset_#{@table_offset}",
length: PLACEHOLDER_LENGTH,
)
end
end
def finalize_subtable(new_cff_data, name, table_data)
encoded = encode_integer32(new_cff_data.length)
new_cff_data.resolve_placeholder(name, encoded)
new_cff_data << table_data
end
def pointer_operator?(operator)
POINTER_OPERATORS.include?(OPERATOR_CODES[operator])
end
def encode_charstring_type(charstring_type)
if charstring_type == DEFAULT_CHARSTRING_TYPE
''
else
encode_operand(charstring_type)
end
end
end
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cff < TTFunk::Table
# CFF Index conatining Top dict.
class TopIndex < TTFunk::Table::Cff::Index
private
def decode_item(_index, offset, length)
TTFunk::Table::Cff::TopDict.new(file, offset, length)
end
def encode_items(*)
# Re-encode the top dict
map(&:encode)
end
end
end
end
end
@@ -0,0 +1,60 @@
# frozen_string_literal: true
module TTFunk
class Table
# Character to Glyph Index Mapping (`cmap`) table.
class Cmap < Table
# Table version.
# @return [Integer]
attr_reader :version
# Encoding tables.
# @return [Array<TTFunk::Table::Cmap::Subtable>]
attr_reader :tables
# Encode table.
#
# @param charmap [Hash{Integer => Integer}]
# @param encoding [Symbol]
# @return [Hash]
# * `:charmap` (<tt>Hash{Integer => Hash}</tt>) keys are the characrers in
# `charset`, values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# that maps the characters in charmap to a
# * `:table` (<tt>String</tt>) - serialized table.
# * `:max_glyph_id` (<tt>Integer</tt>) - maximum glyph ID in the new font.
def self.encode(charmap, encoding)
result = Cmap::Subtable.encode(charmap, encoding)
# pack 'version' and 'table-count'
result[:table] = [0, 1, result.delete(:subtable)].pack('nnA*')
result
end
# Get Unicode encoding records.
#
# @return [Array<TTFunk::Table::Cmap::Subtable>]
def unicode
# Because most callers just call .first on the result, put tables with
# highest-number format first. Unsupported formats will be ignored.
@unicode ||=
@tables
.select { |table| table.unicode? && table.supported? }
.sort { |a, b| b.format <=> a.format }
end
private
def parse!
@version, table_count = read(4, 'nn')
@tables =
Array.new(table_count) do
Cmap::Subtable.new(file, offset)
end
end
end
end
end
require_relative 'cmap/subtable'
@@ -0,0 +1,73 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cmap
# Format 0: Byte encoding table.
#
# This module conditionally extends {TTFunk::Table::Cmap::Subtable}.
module Format00
# Language.
# @return [Integer]
attr_reader :language
# Code map.
# @return [Array<Integer>]
attr_reader :code_map
# Encode the encoding record to format 0.
#
# @param charmap [Hash{Integer => Integer}] a hash mapping character
# codes to glyph ids (where the glyph ids are from the original font).
# @return [Hash]
# * `:charmap` (<tt>Hash{Integer => Hash}</tt>) keys are the characrers in
# `charset`, values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# that maps the characters in charmap to a
# * `:subtable` (<tt>String</tt>) - serialized encoding record.
# * `:max_glyph_id` (<tt>Integer</tt>) - maximum glyph ID in the new font.
def self.encode(charmap)
next_id = 0
glyph_indexes = Array.new(256, 0)
glyph_map = { 0 => 0 }
new_map =
charmap.keys.sort.each_with_object({}) do |code, map|
glyph_map[charmap[code]] ||= next_id += 1
map[code] = { old: charmap[code], new: glyph_map[charmap[code]] }
glyph_indexes[code] = glyph_map[charmap[code]]
map
end
# format, length, language, indices
subtable = [0, 262, 0, *glyph_indexes].pack('nnnC*')
{ charmap: new_map, subtable: subtable, max_glyph_id: next_id + 1 }
end
# Get glyph ID for character code.
#
# @param code [Integer] character code.
# @return [Integer] glyph ID.
def [](code)
@code_map[code] || 0
end
# Is this encoding record format supported?
#
# @return [true]
def supported?
true
end
private
def parse_cmap!
@language = read(4, 'x2n')
@code_map = read(256, 'C*')
end
end
end
end
end
@@ -0,0 +1,160 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cmap
# Format 4: Segment mapping to delta values.
#
# This module conditionally extends {TTFunk::Table::Cmap::Subtable}.
module Format04
# Language.
# @return [Integer]
attr_reader :language
# Code map.
# @return [Hash{Integer => Integer}]
attr_reader :code_map
# Encode the encoding record to format 4.
#
# @param charmap [Hash{Integer => Integer}] a hash mapping character
# codes to glyph IDs from the original font.
# @return [Hash]
# * `:charmap` (<tt>Hash{Integer => Hash}</tt>) keys are the characrers in
# `charset`, values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# that maps the characters in charmap to a
# * `:subtable` (<tt>String</tt>) - serialized encoding record.
# * `:max_glyph_id` (<tt>Integer</tt>) - maximum glyph ID in the new font.
def self.encode(charmap)
end_codes = []
start_codes = []
next_id = 0
last = difference = nil
glyph_map = { 0 => 0 }
new_map =
charmap.keys.sort.each_with_object({}) do |code, map|
old = charmap[code]
glyph_map[old] ||= next_id += 1
map[code] = { old: old, new: glyph_map[old] }
delta = glyph_map[old] - code
if last.nil? || delta != difference
end_codes << last if last
start_codes << code
difference = delta
end
last = code
map
end
end_codes << last if last
end_codes << 0xFFFF
start_codes << 0xFFFF
segcount = start_codes.length
# build the conversion tables
deltas = []
range_offsets = []
glyph_indices = []
offset = 0
start_codes.zip(end_codes).each_with_index do |(a, b), segment|
if a == 0xFFFF
# We want the final 0xFFFF code to map to glyph 0.
# The glyph index is calculated as glyph = charcode + delta,
# which means that delta must be -0xFFFF to map character code
# 0xFFFF to glyph 0.
deltas << -0xFFFF
range_offsets << 0
break
end
start_glyph_id = new_map[a][:new]
if a - start_glyph_id >= 0x8000
deltas << 0
range_offsets << (2 * (glyph_indices.length + segcount - segment))
a.upto(b) { |code| glyph_indices << new_map[code][:new] }
else
deltas << (-a + start_glyph_id)
range_offsets << 0
end
offset += 2
end
# format, length, language
subtable = [
4, 16 + (8 * segcount) + (2 * glyph_indices.length), 0,
].pack('nnn')
search_range = 2 * (2**Integer(Math.log(segcount) / Math.log(2)))
entry_selector = Integer(Math.log(search_range / 2) / Math.log(2))
range_shift = (2 * segcount) - search_range
subtable << [
segcount * 2, search_range, entry_selector, range_shift,
].pack('nnnn')
subtable << end_codes.pack('n*') << "\0\0" << start_codes.pack('n*')
subtable << deltas.pack('n*') << range_offsets.pack('n*')
subtable << glyph_indices.pack('n*')
{ charmap: new_map, subtable: subtable, max_glyph_id: next_id + 1 }
end
# Get glyph ID for character code.
#
# @param code [Integer] character code.
# @return [Integer] glyph ID.
def [](code)
@code_map[code] || 0
end
# Is this encoding record format supported?
#
# @return [true]
def supported?
true
end
private
def parse_cmap!
length, @language, segcount_x2 = read(6, 'nnn')
segcount = segcount_x2 / 2
io.read(6) # skip searching hints
end_code = read(segcount_x2, 'n*')
io.read(2) # skip reserved value
start_code = read(segcount_x2, 'n*')
id_delta = read_signed(segcount)
id_range_offset = read(segcount_x2, 'n*')
glyph_ids = read(length - io.pos + @offset, 'n*')
@code_map = {}
end_code.each_with_index do |tail, i|
start_code[i].upto(tail) do |code|
if (id_range_offset[i]).zero?
glyph_id = code + id_delta[i]
else
index = (id_range_offset[i] / 2) + (code - start_code[i]) - (segcount - i)
# Because some TTF fonts are broken
glyph_id = glyph_ids[index] || 0
glyph_id += id_delta[i] if glyph_id != 0
end
@code_map[code] = glyph_id & 0xFFFF
end
end
end
end
end
end
end
@@ -0,0 +1,81 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cmap
# Format 6: Trimmed table mapping.
#
# This module conditionally extends {TTFunk::Table::Cmap::Subtable}.
module Format06
# Language.
# @return [Integer]
attr_reader :language
# Code map.
# @return [Hash{Integer => Integer}]
attr_reader :code_map
# Encode the encoding record to format 6.
#
# @param charmap [Hash{Integer => Integer}] a hash mapping character
# codes to glyph IDs from the original font.
# @return [Hash]
# * `:charmap` (<tt>Hash{Integer => Hash}</tt>) keys are the characrers in
# `charset`, values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# that maps the characters in charmap to a
# * `:subtable` (<tt>String</tt>) - serialized encoding record.
# * `:max_glyph_id` (<tt>Integer</tt>) - maximum glyph ID in the new font.
def self.encode(charmap)
next_id = 0
glyph_map = { 0 => 0 }
sorted_chars = charmap.keys.sort
low_char = sorted_chars.first
high_char = sorted_chars.last
entry_count = 1 + high_char - low_char
glyph_indexes = Array.new(entry_count, 0)
new_map =
charmap.keys.sort.each_with_object({}) do |code, map|
glyph_map[charmap[code]] ||= next_id += 1
map[code] = { old: charmap[code], new: glyph_map[charmap[code]] }
glyph_indexes[code - low_char] = glyph_map[charmap[code]]
end
subtable = [
6, 10 + (entry_count * 2), 0, low_char, entry_count, *glyph_indexes,
].pack('n*')
{ charmap: new_map, subtable: subtable, max_glyph_id: next_id + 1 }
end
# Get glyph ID for character code.
#
# @param code [Integer] character code.
# @return [Integer] glyph ID.
def [](code)
@code_map[code] || 0
end
# Is this encoding record format supported?
#
# @return [true]
def supported?
true
end
private
def parse_cmap!
@language, firstcode, entrycount = read(8, 'x2nnn')
@code_map = {}
(firstcode...(firstcode + entrycount)).each do |code|
@code_map[code] = read(2, 'n').first & 0xFFFF
end
end
end
end
end
end
@@ -0,0 +1,87 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cmap
# Format 10: Trimmed array.
#
# This module conditionally extends {TTFunk::Table::Cmap::Subtable}.
module Format10
# Language.
# @return [Integer]
attr_reader :language
# Code map.
# @return [Hash{Integer => Integer}]
attr_reader :code_map
# Encode the encoding record to format 10.
#
# @param charmap [Hash{Integer => Integer}] a hash mapping character
# codes to glyph IDs from the original font.
# @return [Hash]
# * `:charmap` (<tt>Hash{Integer => Hash}</tt>) keys are the characrers in
# `charset`, values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# that maps the characters in charmap to a
# * `:subtable` (<tt>String</tt>) - serialized encoding record.
# * `:max_glyph_id` (<tt>Integer</tt>) - maximum glyph ID in the new font.
def self.encode(charmap)
next_id = 0
glyph_map = { 0 => 0 }
sorted_chars = charmap.keys.sort
low_char = sorted_chars.first
high_char = sorted_chars.last
entry_count = 1 + high_char - low_char
glyph_indexes = Array.new(entry_count, 0)
new_map =
charmap.keys.sort.each_with_object({}) do |code, map|
glyph_map[charmap[code]] ||= next_id += 1
map[code] = { old: charmap[code], new: glyph_map[charmap[code]] }
glyph_indexes[code - low_char] = glyph_map[charmap[code]]
end
subtable = [
10, 0, 20 + (entry_count * 4), 0, low_char, entry_count,
*glyph_indexes,
].pack('nnN*')
{ charmap: new_map, subtable: subtable, max_glyph_id: next_id + 1 }
end
# Get glyph ID for character code.
#
# @param code [Integer] character code.
# @return [Integer] glyph ID.
def [](code)
@code_map[code] || 0
end
# Is this encoding record format supported?
#
# @return [true]
def supported?
true
end
private
def parse_cmap!
fractional_version, @language, firstcode, entrycount =
read(18, 'nx4NNN')
if fractional_version != 0
raise NotImplementedError,
"cmap version 10.#{fractional_version} is not supported"
end
@code_map = {}
(firstcode...(firstcode + entrycount)).each do |code|
@code_map[code] = read(2, 'n').first & 0xFFFF
end
end
end
end
end
end
@@ -0,0 +1,102 @@
# frozen_string_literal: true
module TTFunk
class Table
class Cmap
# Format 12: Segmented coverage.
#
# This module conditionally extends {TTFunk::Table::Cmap::Subtable}.
module Format12
# Language.
# @return [Integer]
attr_reader :language
# Code map.
# @return [Hash{Integer => Integer}]
attr_reader :code_map
# Encode the encoding record to format 12.
#
# @param charmap [Hash{Integer => Integer}] a hash mapping character
# codes to glyph IDs from the original font.
# @return [Hash]
# * `:charmap` (<tt>Hash{Integer => Hash}</tt>) keys are the characrers in
# `charset`, values are hashes:
# * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
# * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
# that maps the characters in charmap to a
# * `:subtable` (<tt>String</tt>) - serialized encoding record.
# * `:max_glyph_id` (<tt>Integer</tt>) - maximum glyph ID in the new font.
def self.encode(charmap)
next_id = 0
glyph_map = { 0 => 0 }
range_firstglyphs = []
range_firstcodes = []
range_lengths = []
last_glyph = last_code = -999
new_map =
charmap.keys.sort.each_with_object({}) do |code, map|
glyph_map[charmap[code]] ||= next_id += 1
map[code] = { old: charmap[code], new: glyph_map[charmap[code]] }
if code > last_code + 1 || glyph_map[charmap[code]] > last_glyph + 1
range_firstcodes << code
range_firstglyphs << glyph_map[charmap[code]]
range_lengths << 1
else
range_lengths.push(range_lengths.pop) + 1
end
last_code = code
last_glyph = glyph_map[charmap[code]]
end
subtable = [
12, 0, 16 + (12 * range_lengths.size), 0, range_lengths.size,
].pack('nnNNN')
range_lengths.each_with_index do |length, i|
firstglyph = range_firstglyphs[i]
firstcode = range_firstcodes[i]
subtable << [
firstcode, firstcode + length - 1, firstglyph,
].pack('NNN')
end
{ charmap: new_map, subtable: subtable, max_glyph_id: next_id + 1 }
end
# Get glyph ID for character code.
#
# @param code [Integer] character code.
# @return [Integer] glyph ID.
def [](code)
@code_map[code] || 0
end
# Is this encoding record format supported?
#
# @return [true]
def supported?
true
end
private
def parse_cmap!
fractional_version, @language, groupcount = read(14, 'nx4NN')
if fractional_version != 0
raise NotImplementedError,
"cmap version 12.#{fractional_version} is not supported"
end
@code_map = {}
(1..groupcount).each do
startchar, endchar, startglyph = read(12, 'NNN')
(0..(endchar - startchar)).each do |offset|
@code_map[startchar + offset] = startglyph + offset
end
end
end
end
end
end
end
@@ -0,0 +1,143 @@
# frozen_string_literal: true
require_relative '../../reader'
module TTFunk
class Table
class Cmap
# Character to Glyph Index encoding record.
# This class can be extended with a format-specific
#
# @see TTFunk::Table::Cmap::Format00
# @see TTFunk::Table::Cmap::Format04
# @see TTFunk::Table::Cmap::Format06
# @see TTFunk::Table::Cmap::Format10
# @see TTFunk::Table::Cmap::Format12
class Subtable
include Reader
# Platform ID.
# @return [Integer]
attr_reader :platform_id
# Platform-specific encoding ID.
# @return [Integere]
attr_reader :encoding_id
# Record encoding format.
# @return [Integer]
attr_reader :format
# Most used encoding mappings.
ENCODING_MAPPINGS = {
mac_roman: { platform_id: 1, encoding_id: 0 }.freeze,
# Use microsoft unicode, instead of generic unicode, for optimal
# Windows support
unicode: { platform_id: 3, encoding_id: 1 }.freeze,
unicode_ucs4: { platform_id: 3, encoding_id: 10 }.freeze,
}.freeze
# Encode encoding record.
#
# @param charmap [Hash{Integer => Integer}] keys are code points in the
# used encoding, values are Unicode code points.
# @param encoding [Symbol] - one of the encodign mapping in
# {ENCODING_MAPPINGS}
# @return [Hash]
# * `:platform_id` (<tt>Integer</tt>) - Platform ID of this encoding record.
# * `:encoding_id` (<tt>Integer</tt>) - Encodign ID of this encoding record.
# * `:subtable` (<tt>String</tt>) - encoded encoding record.
# * `:max_glyph_id` (<tt>Integer</tt>) - maximum glyph ID in this encoding
# record.
# * `:charmap` (<tt>Hash{Integer => Hash}</tt>) - keys are codepoints in this
# encoding record, values are hashes:
# * `:new` - new glyph ID.
# * `:old` - glyph ID in the original font.
def self.encode(charmap, encoding)
case encoding
when :mac_roman
result = Format00.encode(charmap)
when :unicode
result = Format04.encode(charmap)
when :unicode_ucs4
result = Format12.encode(charmap)
else
raise NotImplementedError,
"encoding #{encoding.inspect} is not supported"
end
mapping = ENCODING_MAPPINGS[encoding]
# platform-id, encoding-id, offset
result.merge(
platform_id: mapping[:platform_id],
encoding_id: mapping[:encoding_id],
subtable: [
mapping[:platform_id],
mapping[:encoding_id],
12,
result[:subtable],
].pack('nnNA*'),
)
end
# @param file [TTFunk::File]
# @param table_start [Integer]
def initialize(file, table_start)
@file = file
@platform_id, @encoding_id, @offset = read(8, 'nnN')
@offset += table_start
parse_from(@offset) do
@format = read(2, 'n').first
case @format
when 0 then extend(TTFunk::Table::Cmap::Format00)
when 4 then extend(TTFunk::Table::Cmap::Format04)
when 6 then extend(TTFunk::Table::Cmap::Format06)
when 10 then extend(TTFunk::Table::Cmap::Format10)
when 12 then extend(TTFunk::Table::Cmap::Format12)
end
parse_cmap!
end
end
# Is this an encoding record for Unicode?
#
# @return [Boolean]
def unicode?
(platform_id == 3 && (encoding_id == 1 || encoding_id == 10) && format != 0) ||
(platform_id.zero? && format != 0)
end
# Is this encoding record format supported?
#
# @return [Boolean]
def supported?
false
end
# Get glyph ID for character code.
#
# @param _code [Integer] character code.
# @return [Integer] glyph ID.
def [](_code)
raise NotImplementedError, "cmap format #{@format} is not supported"
end
private
def parse_cmap!
# do nothing...
end
end
end
end
end
require_relative 'format00'
require_relative 'format04'
require_relative 'format06'
require_relative 'format10'
require_relative 'format12'
@@ -0,0 +1,93 @@
# frozen_string_literal: true
module TTFunk
class Table
# Digital Signature (`DSIG`) table.
class Dsig < Table
# Signature record.
class SignatureRecord
# Format of the signature.
# @return [Integer]
attr_reader :format
# Length of signature in bytes.
# @return [Integer]
attr_reader :length
# Offset to the signature block from the beginning of the table.
# @return [Integer]
attr_reader :offset
# Signature PKCS#7 packet.
# @return [String]
attr_reader :signature
# @param format [Integer]
# @param length [Integer]
# @param offset [Integer]
# @param signature [String]
def initialize(format, length, offset, signature)
@format = format
@length = length
@offset = offset
@signature = signature
end
end
# Version umber of this table.
# @return [Integer]
attr_reader :version
# Permission flags.
# @return [Integer]
attr_reader :flags
# Signature records.
# @return [Array<SignatureRecord>]
attr_reader :signatures
# Table tag.
TAG = 'DSIG'
# Encode table.
#
# **Note**: all signatures will be lost. This encodes an empty table
# regardless whether the supplied table contains any signtaures or not.
#
# @param dsig [TTFunk::Table::Dsig]
# @return [String]
def self.encode(dsig)
return unless dsig
# Don't attempt to re-sign or anything - just use dummy values.
# Since we're subsetting that should be permissible.
[dsig.version, 0, 0].pack('Nnn')
end
# Table tag.
#
# @return [String]
def tag
TAG
end
private
def parse!
@version, num_signatures, @flags = read(8, 'Nnn')
@signatures =
Array.new(num_signatures) {
format, length, sig_offset = read(12, 'N3')
signature =
parse_from(offset + sig_offset) {
_, _, sig_length = read(8, 'nnN')
read(sig_length, 'C*')
}
SignatureRecord.new(format, length, sig_offset, signature)
}
end
end
end
end
@@ -0,0 +1,76 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Glyph Data (`glyf`) table.
class Glyf < Table
# Encode table.
#
# @param glyphs [Hash] a hash mapping (old) glyph-ids to glyph objects
# @param new_to_old [Hash{Integer => Integer}] a hash mapping new glyph
# IDs to glyph IDs in the original font.
# @param old_to_new [Hash{Integer => Integer}] a hash mapping old glyph
# IDs to new glyph IDs.
# @return [Hash]
# * `:table` (<tt>String</tt>) - encoded table.
# * `:offsets` (<tt>Array\<Integer></tt>) - glyph offsets in the table.
def self.encode(glyphs, new_to_old, old_to_new)
result = { table: +'', offsets: [] }
new_to_old.keys.sort.each do |new_id|
glyph = glyphs[new_to_old[new_id]]
result[:offsets] << result[:table].length
result[:table] << glyph.recode(old_to_new) if glyph
end
# include an offset at the end of the table, for use in computing the
# size of the last glyph
result[:offsets] << result[:table].length
result
end
# Get glyph by ID.
#
# @param glyph_id [Integer]
# @return [TTFunk::Table::Glyf::Simple, TTFunk::Table::Glyf::Compound,
# nil]
def for(glyph_id)
return @cache[glyph_id] if @cache.key?(glyph_id)
index = file.glyph_locations.index_of(glyph_id)
size = file.glyph_locations.size_of(glyph_id)
if size.zero? # blank glyph, e.g. space character
@cache[glyph_id] = nil
return
end
parse_from(offset + index) do
raw = io.read(size)
number_of_contours = to_signed(raw.unpack1('n'))
@cache[glyph_id] =
if number_of_contours == -1
Compound.new(glyph_id, raw)
else
Simple.new(glyph_id, raw)
end
end
end
private
def parse!
# because the glyf table is rather complex to parse, we defer
# the parse until we need a specific glyf, and then cache it.
@cache = {}
end
end
end
end
require_relative 'glyf/compound'
require_relative 'glyf/path_based'
require_relative 'glyf/simple'
@@ -0,0 +1,158 @@
# frozen_string_literal: true
require_relative '../../reader'
module TTFunk
class Table
class Glyf
# Composite TrueType glyph.
class Compound
include Reader
# Flags bit 0: arg1 and arg2 are words.
ARG_1_AND_2_ARE_WORDS = 0x0001
# Flags bit 3: there is a simple scale for the component.
WE_HAVE_A_SCALE = 0x0008
# Flags bit 5: at least one more glyph after this one.
MORE_COMPONENTS = 0x0020
# Flags bit 6: the x direction will use a different scale from the
# y direction.
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
# Flags bit 7: there is a 2 by 2 transformation that will be used to
# scale the component.
WE_HAVE_A_TWO_BY_TWO = 0x0080
# Flags bit 8: following the last component are instructions for the
# composite character.
WE_HAVE_INSTRUCTIONS = 0x0100
# Glyph ID.
# @return [Integer]
attr_reader :id
# Binary serialization of this glyph.
# @return [String]
attr_reader :raw
# Number of contours in this glyph.
# @return [Integer]
attr_reader :number_of_contours
# Minimum x for coordinate.
# @return [Integer]
attr_reader :x_min
# Minimum y for coordinate.
# @return [Integer]
attr_reader :y_min
# Maximum x for coordinate.
# @return [Integer]
attr_reader :x_max
# Maximum y for coordinate.
# @return [Integer]
attr_reader :y_max
# IDs of compound glyphs.
attr_reader :glyph_ids
# Component glyph.
#
# @!attribute [rw] flags
# Component flag.
# @return [Integer]
# @!attribute [rw] glyph_index
# Glyph index of component.
# @return [Integer]
# @!attribute [rw] arg1
# x-offset for component or point number.
# @return [Integer]
# @!attribute [rw] arg2
# y-offset for component or point number.
# @return [Integer]
# @!attribute [rw] transform
# Transformation.
# @return []
Component = Struct.new(:flags, :glyph_index, :arg1, :arg2, :transform)
# @param id [Integer] glyph ID.
# @param raw [String]
def initialize(id, raw)
@id = id
@raw = raw
io = StringIO.new(raw)
@number_of_contours, @x_min, @y_min, @x_max, @y_max =
io.read(10).unpack('n*').map { |i|
BinUtils.twos_comp_to_int(i, bit_width: 16)
}
# Because TTFunk only cares about glyphs insofar as they (1) provide
# a bounding box for each glyph, and (2) can be rewritten into a
# font subset, we don't really care about the rest of the glyph data
# except as a whole. Thus, we don't actually decompose the glyph
# into it's parts--all we really care about are the locations within
# the raw string where the component glyph ids are stored, so that
# when we rewrite this glyph into a subset we can rewrite the
# component glyph-ids so they are correct for the subset.
@glyph_ids = []
@glyph_id_offsets = []
offset = 10 # 2 bytes for each of num-contours, min x/y, max x/y
loop do
flags, glyph_id = @raw[offset, 4].unpack('n*')
@glyph_ids << glyph_id
@glyph_id_offsets << (offset + 2)
break if (flags & MORE_COMPONENTS).zero?
offset += 4
offset +=
if (flags & ARG_1_AND_2_ARE_WORDS).zero?
2
else
4
end
if flags & WE_HAVE_A_TWO_BY_TWO != 0
offset += 8
elsif flags & WE_HAVE_AN_X_AND_Y_SCALE != 0
offset += 4
elsif flags & WE_HAVE_A_SCALE != 0
offset += 2
end
end
end
# Is this a composite glyph?
# @return [true]
def compound?
true
end
# Recode glyph.
#
# @param mapping [Hash{Integer => Integer}] a hash mapping old glyph IDs
# to new glyph IDs.
# @return [String]
def recode(mapping)
result = raw.dup
new_ids = glyph_ids.map { |id| mapping[id] }
new_ids.zip(@glyph_id_offsets).each do |new_id, offset|
result[offset, 2] = [new_id].pack('n')
end
result
end
end
end
end
end
@@ -0,0 +1,84 @@
# frozen_string_literal: true
module TTFunk
class Table
class Glyf
# TrueType-compatible representation of a CFF glyph.
class PathBased
# Glyph outline.
# @return [TTFunk::Table::Cff::Path]
attr_reader :path
# Glyph horizontal metrics.
# @return [TTFunk::Table::Hmtx::HorizontalMetric]
attr_reader :horizontal_metrics
# Minimum X.
# @return [Integer, Float]
attr_reader :x_min
# Minimum Y.
# @return [Integer, Float]
attr_reader :y_min
# Maximum X.
# @return [Integer, Float]
attr_reader :x_max
# Maximum Y.
# @return [Integer, Float]
attr_reader :y_max
# Left side bearing.
# @return [Integer, Float]
attr_reader :left_side_bearing
# Rigth side bearing.
# @return [Integer, Float]
attr_reader :right_side_bearing
# @param path [TTFunk::Table::Cff::Path]
# @param horizontal_metrics [TTFunk::Table::Hmtx::HorizontalMetric]
def initialize(path, horizontal_metrics)
@path = path
@horizontal_metrics = horizontal_metrics
@x_min = 0
@y_min = 0
@x_max = horizontal_metrics.advance_width
@y_max = 0
path.commands.each do |command|
cmd, x, y = command
next if cmd == :close
@x_min = x if x < @x_min
@x_max = x if x > @x_max
@y_min = y if y < @y_min
@y_max = y if y > @y_max
end
@left_side_bearing = horizontal_metrics.left_side_bearing
@right_side_bearing =
horizontal_metrics.advance_width -
@left_side_bearing -
(@x_max - @x_min)
end
# Number of contour.
#
# @return [Integer]
def number_of_contours
path.number_of_contours
end
# Is this glyph compound?
#
# @return [false]
def compound?
false
end
end
end
end
end
@@ -0,0 +1,89 @@
# frozen_string_literal: true
require_relative '../../reader'
module TTFunk
class Table
class Glyf
# Simple TrueType glyph
class Simple
# Glyph ID.
# @return [Integer]
attr_reader :id
# Binary serialization of this glyph.
# @return [String]
attr_reader :raw
# Number of contours in this glyph.
# @return [Integer]
attr_reader :number_of_contours
# Minimum x for coordinate.
# @return [Integer]
attr_reader :x_min
# Minimum y for coordinate.
# @return [Integer]
attr_reader :y_min
# Maximum x for coordinate.
# @return [Integer]
attr_reader :x_max
# Maximum y for coordinate.
# @return [Integer]
attr_reader :y_max
# Point indices for the last point of each contour.
# @return [Array<Integer>]
attr_reader :end_points_of_contours
# Total number of bytes for instructions.
# @return [Integer]
attr_reader :instruction_length
# Instruction byte code.
# @return [Array<Integer>]
attr_reader :instructions
# @param id [Integer] glyph ID.
# @param raw [String]
def initialize(id, raw)
@id = id
@raw = raw
io = StringIO.new(raw)
@number_of_contours, @x_min, @y_min, @x_max, @y_max =
io.read(10).unpack('n*').map { |i|
BinUtils.twos_comp_to_int(i, bit_width: 16)
}
@end_points_of_contours = io.read(number_of_contours * 2).unpack('n*')
@instruction_length = io.read(2).unpack1('n')
@instructions = io.read(instruction_length).unpack('C*')
end
# Is this glyph compound?
# @return [false]
def compound?
false
end
# Recode glyph.
#
# @param _mapping Unused, here for API compatibility.
# @return [String]
def recode(_mapping)
raw
end
# End point index of last contour.
# @return [Integer]
def end_point_of_last_contour
end_points_of_contours.last + 1
end
end
end
end
end
@@ -0,0 +1,160 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Font Header (`head`) Table.
class Head < TTFunk::Table
# Table version.
# @return [Integer]
attr_reader :version
# Font revision.
# @return [Integer]
attr_reader :font_revision
# Checksum adjustment.
# @return [Integer]
attr_reader :checksum_adjustment
# Magic number.
# @return [Integer] must be `0x5F0F3CF5`
attr_reader :magic_number
# Flags.
# @return [Integer]
attr_reader :flags
# Units per Em.
# @return [Integer]
attr_reader :units_per_em
# Font creation time.
# @return [Integer] Long Date Time timestamp.
attr_reader :created
# Font modification time.
# @return [Integer] Long Date Time timestamp.
attr_reader :modified
# Minimum x coordinate across all glyph bounding boxes.
# @return [Integer]
attr_reader :x_min
# Minimum y coordinate across all glyph bounding boxes.
# @return [Integer]
attr_reader :y_min
# Maximum x coordinate across all glyph bounding boxes.
# @return [Integer]
attr_reader :x_max
# Maximum y coordinate across all glyph bounding boxes.
# @return [Integer]
attr_reader :y_max
# Mac font style.
# @return [Integer]
attr_reader :mac_style
# Smallest readable size in pixels.
# @return [Integer]
attr_reader :lowest_rec_ppem
# Font direction hint. Deprecated, set to 2.
# @return [Integer]
attr_reader :font_direction_hint
# Index to Location format.
# @return [Integer]
attr_reader :index_to_loc_format
# Glyph data format.
# @return [Integer]
attr_reader :glyph_data_format
class << self
# Long date time (used in TTF headers).
# January 1, 1904 00:00:00 UTC basis used by Long date time.
# @see https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html
# TrueType Font Tables
LONG_DATE_TIME_BASIS = Time.new(1904, 1, 1, 0, 0, 0, 0).to_i
private_constant :LONG_DATE_TIME_BASIS
# Encode table.
#
# @param head [TTFunk::Table::Head]
# @param loca [Hash] result of encoding Index to Location (`loca`) table
# @param mapping [Hash{Integer => Integer}] keys are new glyph IDs, values
# are old glyph IDs
# @return [EncodedString]
def encode(head, loca, mapping)
EncodedString.new do |table|
table <<
[head.version, head.font_revision].pack('N2') <<
Placeholder.new(:checksum, length: 4) <<
[
head.magic_number,
head.flags, head.units_per_em,
head.created, head.modified,
*min_max_values_for(head, mapping),
head.mac_style, head.lowest_rec_ppem, head.font_direction_hint,
loca[:type] || 0, head.glyph_data_format,
].pack('Nn2q>2n*')
end
end
# Convert Long Date Time timestamp to Time.
# @param ldt [Float, Integer]
# @return [Time]
def from_long_date_time(ldt)
Time.at(ldt + LONG_DATE_TIME_BASIS, in: 'UTC')
end
# Convert Time to Long Date Time timestamp
# @param time [Time]
# @return [Integer]
def to_long_date_time(time)
Integer(time) - LONG_DATE_TIME_BASIS
end
private
def min_max_values_for(head, mapping)
x_min = Min.new
x_max = Max.new
y_min = Min.new
y_max = Max.new
mapping.each_value do |old_glyph_id|
glyph = head.file.find_glyph(old_glyph_id)
next unless glyph
x_min << glyph.x_min
x_max << glyph.x_max
y_min << glyph.y_min
y_max << glyph.y_max
end
[
x_min.value_or(0), y_min.value_or(0),
x_max.value_or(0), y_max.value_or(0),
]
end
end
private
def parse!
@version, @font_revision, @check_sum_adjustment, @magic_number,
@flags, @units_per_em, @created, @modified = read(36, 'N4n2q>2')
@x_min, @y_min, @x_max, @y_max = read_signed(4)
@mac_style, @lowest_rec_ppem, @font_direction_hint,
@index_to_loc_format, @glyph_data_format = read(10, 'n*')
end
end
end
end
@@ -0,0 +1,145 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Horizontal Header (`hhea`) table.
class Hhea < Table
# Table version
# @return [Integer]
attr_reader :version
# Typographic ascent.
# @return [Integer]
attr_reader :ascent
# Typographic descent.
# @return [Integer]
attr_reader :descent
# Typographic line gap.
# @return [Integer]
attr_reader :line_gap
# Maximum advance width value in `hmtx` table.
# @return [Integer]
attr_reader :advance_width_max
# Minimum left sidebearing value in `hmtx` table for glyphs with contours
# (empty glyphs should be ignored).
# @return [Integer]
attr_reader :min_left_side_bearing
# Minimum right sidebearing value.
# @return [Integer]
attr_reader :min_right_side_bearing
# Maximum extent.
# @return [Integer]
attr_reader :x_max_extent
# Caret slope rise.
# @return [Integer]
attr_reader :caret_slope_rise
# @deprecated Use {caret_slope_rise} instead.
# @!parse attr_reader :carot_slope_rise
# @return [Integer]
def carot_slope_rise
@caret_slope_rise
end
# Caret slope run.
# @return [Integer]
attr_reader :caret_slope_run
# @deprecated Use {caret_slope_run} instead.
# @!parse attr_reader :carot_slope_run
# @return [Integer]
def carot_slope_run
@caret_slope_run
end
# Caret offset.
# @return [Integer]
attr_reader :caret_offset
# Metric data format. `0` for current format.
# @return [Integer]
attr_reader :metric_data_format
# Number of hMetric entries in `hmtx` table.
# @return [Integer]
attr_reader :number_of_metrics
class << self
# Encode table.
#
# @param hhea [TTFunk::Table::Hhea] table to encode.
# @param hmtx [TTFunk::Table::Hmtx]
# @param original [TTFunk::File] original font file.
# @param mapping [Hash{Integer => Integer}] keys are new glyph IDs, values
# are old glyph IDs
# @return [String]
def encode(hhea, hmtx, original, mapping)
''.b.tap do |table|
table << [hhea.version].pack('N')
table << [
hhea.ascent, hhea.descent, hhea.line_gap,
*min_max_values_for(original, mapping),
hhea.caret_slope_rise, hhea.caret_slope_run, hhea.caret_offset,
0, 0, 0, 0, hhea.metric_data_format, hmtx[:number_of_metrics],
].pack('n*')
end
end
private
def min_max_values_for(original, mapping)
min_lsb = Min.new
min_rsb = Min.new
max_aw = Max.new
max_extent = Max.new
mapping.each_value do |old_glyph_id|
horiz_metrics = original.horizontal_metrics.for(old_glyph_id)
next unless horiz_metrics
min_lsb << horiz_metrics.left_side_bearing
max_aw << horiz_metrics.advance_width
glyph = original.find_glyph(old_glyph_id)
next unless glyph
x_delta = glyph.x_max - glyph.x_min
min_rsb << (horiz_metrics.advance_width - horiz_metrics.left_side_bearing - x_delta)
max_extent << (horiz_metrics.left_side_bearing + x_delta)
end
[
max_aw.value_or(0), min_lsb.value_or(0),
min_rsb.value_or(0), max_extent.value_or(0),
]
end
end
private
def parse!
@version = read(4, 'N').first
@ascent, @descent, @line_gap = read_signed(3)
@advance_width_max = read(2, 'n').first
@min_left_side_bearing, @min_right_side_bearing, @x_max_extent,
@caret_slope_rise, @caret_slope_run, @caret_offset,
_reserved, _reserved, _reserved, _reserved,
@metric_data_format = read_signed(11)
@number_of_metrics = read(2, 'n').first
end
end
end
end
@@ -0,0 +1,87 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Horizontal Metrics (`hmtx`) table.
class Hmtx < Table
# Glyph horizontal metrics.
# @return [Array<HorizontalMetric>]
attr_reader :metrics
# Left side bearings.
# @return [Array<Ingteger>]
attr_reader :left_side_bearings
# Glyph widths.
# @return [Array<Integer>]
attr_reader :widths
# Encode table.
#
# @param hmtx [TTFunk::Table::Hmtx]
# @param mapping [Hash{Integer => Integer}] keys are new glyph IDs, values
# are old glyph IDs
# @return [Hash{:number_of_metrics => Integer, :table => String}]
# * `:number_of_metrics` - number of mertrics is the table.
# * `:table` - encoded table.
def self.encode(hmtx, mapping)
metrics =
mapping.keys.sort.map { |new_id|
metric = hmtx.for(mapping[new_id])
[metric.advance_width, metric.left_side_bearing]
}
{
number_of_metrics: metrics.length,
table: metrics.flatten.pack('n*'),
}
end
# Horyzontal glyph metric.
#
# @!attribute [rw] advance_width
# @return [Integer] Advance width.
# @!attribute [rw] left_side_bearing
# @return [Integer] Left side bearing.
HorizontalMetric = Struct.new(:advance_width, :left_side_bearing)
# Get horizontal metric for glyph.
#
# @param glyph_id [Integer]
# @return [HorizontalMetric]
def for(glyph_id)
@metrics[glyph_id] ||
metrics_cache[glyph_id] ||=
HorizontalMetric.new(
@metrics.last.advance_width,
@left_side_bearings[glyph_id - @metrics.length],
)
end
private
def metrics_cache
@metrics_cache ||= {}
end
def parse!
@metrics = []
file.horizontal_header.number_of_metrics.times do
advance = read(2, 'n').first
lsb = read_signed(1).first
@metrics.push(HorizontalMetric.new(advance, lsb))
end
lsb_count = file.maximum_profile.num_glyphs -
file.horizontal_header.number_of_metrics
@left_side_bearings = read_signed(lsb_count)
@widths = @metrics.map(&:advance_width)
@widths += [@widths.last] * @left_side_bearings.length
end
end
end
end
@@ -0,0 +1,103 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Kerning (`kern`) table
class Kern < Table
# Table version
# @return [Integer]
attr_reader :version
# Subtables.
# @return [Array<TTFunk::Table::Kern::Format0>]
attr_reader :tables
# Encode table.
#
# @param kerning [TTFunk::Table::Kern]
# @param mapping [Hash{Integer => Integer}] keys are new glyph IDs, values
# are old glyph IDs
# @return [String, nil]
def self.encode(kerning, mapping)
return unless kerning.exists? && kerning.tables.any?
tables = kerning.tables.filter_map { |table| table.recode(mapping) }
return if tables.empty?
[0, tables.length, tables.join].pack('nnA*')
end
private
def parse!
@version, num_tables = read(4, 'n*')
@tables = []
if @version == 1 # Mac OS X fonts
@version = (@version << 16) + num_tables
num_tables = read(4, 'N').first
parse_version_1_tables(num_tables)
else
parse_version_0_tables(num_tables)
end
end
def parse_version_0_tables(_num_tables)
# It looks like some MS fonts report their kerning subtable lengths
# wrong. In one case, the length was reported to be some 19366, and yet
# the table also claimed to hold 14148 pairs (each pair consisting of
# 6 bytes). You do the math!
#
# We're going to assume that the microsoft fonts hold only a single
# kerning subtable, which occupies the entire length of the kerning
# table. Worst case, we lose any other subtables that the font contains,
# but it's better than reading a truncated kerning table.
#
# And what's more, it appears to work. So.
version, length, coverage = read(6, 'n*')
format = coverage >> 8
add_table(
format,
version: version,
length: length,
coverage: coverage,
data: raw[10..],
vertical: (coverage & 0x1).zero?,
minimum: (coverage & 0x2 != 0),
cross: (coverage & 0x4 != 0),
override: (coverage & 0x8 != 0),
)
end
def parse_version_1_tables(num_tables)
num_tables.times do
length, coverage, tuple_index = read(8, 'Nnn')
format = coverage & 0x0FF
add_table(
format,
length: length,
coverage: coverage,
tuple_index: tuple_index,
data: io.read(length - 8),
vertical: (coverage & 0x8000 != 0),
cross: (coverage & 0x4000 != 0),
variation: (coverage & 0x2000 != 0),
)
end
end
def add_table(format, attributes = {})
if format.zero?
@tables << Kern::Format0.new(attributes)
end
# Unsupported kerning tables are silently ignored
end
end
end
end
require_relative 'kern/format0'
@@ -0,0 +1,90 @@
# frozen_string_literal: true
require_relative '../../reader'
module TTFunk
class Table
class Kern
# Format 0 kerning subtable.
class Format0
include Reader
# Subtable attributes.
# @return [Hash{Symbol => any}]
attr_reader :attributes
# Kerning pairs.
# @return [Hash{Array(Integer, Integer) => Integer}]
attr_reader :pairs
# @param attributes [Hash{Symbol => any}]
def initialize(attributes = {})
@attributes = attributes
num_pairs, *pairs = attributes.delete(:data).unpack('nx6n*')
@pairs = {}
num_pairs.times do |i|
# sanity check, in case there's a bad length somewhere
break if (i * 3) + 2 > pairs.length
left = pairs[i * 3]
right = pairs[(i * 3) + 1]
value = to_signed(pairs[(i * 3) + 2])
@pairs[[left, right]] = value
end
end
# Is this vertical kerning?
# @return [Boolean]
def vertical?
@attributes[:vertical]
end
# Is this horizontal kerning?
# @return [Boolean]
def horizontal?
!vertical?
end
# Is this cross-stream kerning?
# @return [Boolean]
def cross_stream?
@attributes[:cross]
end
# Recode this subtable using the specified mapping.
#
# @param mapping [Hash{Integer => Integer}] keys are new glyph IDs,
# values are old glyph IDs
# @return [String]
def recode(mapping)
subset = []
pairs.each do |(left, right), value|
if mapping[left] && mapping[right]
subset << [mapping[left], mapping[right], value]
end
end
return if subset.empty?
num_pairs = subset.length
search_range = 2 * (2**Integer(Math.log(num_pairs) / Math.log(2)))
entry_selector = Integer(Math.log(search_range / 2) / Math.log(2))
range_shift = (2 * num_pairs) - search_range
[
attributes[:version],
(num_pairs * 6) + 14,
attributes[:coverage],
num_pairs,
search_range,
entry_selector,
range_shift,
subset,
].flatten.pack('n*')
end
end
end
end
end
@@ -0,0 +1,64 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Index to Location table.
class Loca < Table
# Glyph ofsets
# @return [Array<Integer>]
attr_reader :offsets
# Encode table.
#
# @param offsets [Array<Integer>] an array of offsets, with each index
# corresponding to the glyph id with that index.
# @return [Hash] result hash:
# * `:type` - the type of offset (to be encoded in the 'head' table):
# * `0` - short offsets
# * `1` - long offsets
# * `:table` - encoded bytes
def self.encode(offsets)
long_offsets =
offsets.any? { |offset|
short_offset = offset / 2
short_offset * 2 != offset || short_offset > 0xffff
}
if long_offsets
{ type: 1, table: offsets.pack('N*') }
else
{ type: 0, table: offsets.map { |o| o / 2 }.pack('n*') }
end
end
# Glyph offset by ID.
#
# @param glyph_id [Integer]
# @return [Integer] - offset of the glyph in the `glyf` table
def index_of(glyph_id)
@offsets[glyph_id]
end
# Size of encoded glyph.
#
# @param glyph_id [Integer]
# @return [Integer]
def size_of(glyph_id)
@offsets[glyph_id + 1] - @offsets[glyph_id]
end
private
def parse!
type = file.header.index_to_loc_format.zero? ? 'n' : 'N'
@offsets = read(length, "#{type}*")
if file.header.index_to_loc_format.zero?
@offsets.map! { |v| v * 2 }
end
end
end
end
end
@@ -0,0 +1,227 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Maximum Profile (`maxp`) table
class Maxp < Table
# Default maximum levels of recursion.
DEFAULT_MAX_COMPONENT_DEPTH = 1
# Size of full table version 1.
MAX_V1_TABLE_LENGTH = 32
# Table version.
# @return [Integer]
attr_reader :version
# The number of glyphs in the font.
# @return [Integer]
attr_reader :num_glyphs
# Maximum points in a non-composite glyph.
# @return [Integer]
attr_reader :max_points
# Maximum contours in a non-composite glyph.
# @return [Integer]
attr_reader :max_contours
# Maximum points in a composite glyph.
# @return [Integer]
attr_reader :max_component_points
# Maximum contours in a composite glyph.
# @return [Integer]
attr_reader :max_component_contours
# Maximum zones.
# * 1 if instructions do not use the twilight zone (Z0)
# * 2 if instructions do use Z0
# @return [Integer]
attr_reader :max_zones
# Maximum points used in Z0.
# @return [Integer]
attr_reader :max_twilight_points
# Number of Storage Area locations.
# @return [Integer]
attr_reader :max_storage
# Number of FDEFs.
# @return [Integer]
attr_reader :max_function_defs
# Number of IDEFs.
# @return [Integer]
attr_reader :max_instruction_defs
# Maximum stack depth across Font Program, CVT Program and all glyph
# instructions.
# @return [Integer]
attr_reader :max_stack_elements
# Maximum byte count for glyph instructions.
# @return [Integer]
attr_reader :max_size_of_instructions
# Maximum number of components referenced at "top level" for any composite
# glyph.
# @return [Integer]
attr_reader :max_component_elements
# Maximum levels of recursion.
# @return [Integer]
attr_reader :max_component_depth
class << self
# Encode table.
#
# @param maxp [TTFunk::Table::Maxp]
# @param new2old_glyph [Hash{Integer => Integer}] keys are new glyph IDs, values
# are old glyph IDs.
# @return [String]
def encode(maxp, new2old_glyph)
''.b.tap do |table|
num_glyphs = new2old_glyph.length
table << [maxp.version, num_glyphs].pack('Nn')
if maxp.version == 0x10000
stats = stats_for(maxp, glyphs_from_ids(maxp, new2old_glyph.values))
table << [
stats[:max_points],
stats[:max_contours],
stats[:max_component_points],
stats[:max_component_contours],
# these all come from the fpgm and cvt tables, which
# we don't support at the moment
maxp.max_zones,
maxp.max_twilight_points,
maxp.max_storage,
maxp.max_function_defs,
maxp.max_instruction_defs,
maxp.max_stack_elements,
stats[:max_size_of_instructions],
stats[:max_component_elements],
stats[:max_component_depth],
].pack('n*')
end
end
end
private
def glyphs_from_ids(maxp, glyph_ids)
glyph_ids.each_with_object([]) do |glyph_id, ret|
if (glyph = maxp.file.glyph_outlines.for(glyph_id))
ret << glyph
end
end
end
def stats_for(maxp, glyphs)
stats_for_simple(maxp, glyphs)
.merge(stats_for_compound(maxp, glyphs))
.transform_values { |agg| agg.value_or(0) }
end
def stats_for_simple(_maxp, glyphs)
max_component_elements = Max.new
max_points = Max.new
max_contours = Max.new
max_size_of_instructions = Max.new
glyphs.each do |glyph|
if glyph.compound?
max_component_elements << glyph.glyph_ids.size
else
max_points << glyph.end_point_of_last_contour
max_contours << glyph.number_of_contours
max_size_of_instructions << glyph.instruction_length
end
end
{
max_component_elements: max_component_elements,
max_points: max_points,
max_contours: max_contours,
max_size_of_instructions: max_size_of_instructions,
}
end
def stats_for_compound(maxp, glyphs)
max_component_points = Max.new
max_component_depth = Max.new
max_component_contours = Max.new
glyphs.each do |glyph|
next unless glyph.compound?
stats = totals_for_compound(maxp, [glyph], 0)
max_component_points << stats[:total_points]
max_component_depth << stats[:max_depth]
max_component_contours << stats[:total_contours]
end
{
max_component_points: max_component_points,
max_component_depth: max_component_depth,
max_component_contours: max_component_contours,
}
end
def totals_for_compound(maxp, glyphs, depth)
total_points = Sum.new
total_contours = Sum.new
max_depth = Max.new(depth)
glyphs.each do |glyph|
if glyph.compound?
stats = totals_for_compound(maxp, glyphs_from_ids(maxp, glyph.glyph_ids), depth + 1)
total_points << stats[:total_points]
total_contours << stats[:total_contours]
max_depth << stats[:max_depth]
else
stats = stats_for_simple(maxp, [glyph])
total_points << stats[:max_points]
total_contours << stats[:max_contours]
end
end
{
total_points: total_points,
total_contours: total_contours,
max_depth: max_depth,
}
end
end
private
def parse!
@version, @num_glyphs = read(6, 'Nn')
if @version == 0x10000
@max_points, @max_contours, @max_component_points,
@max_component_contours, @max_zones, @max_twilight_points,
@max_storage, @max_function_defs, @max_instruction_defs,
@max_stack_elements, @max_size_of_instructions,
@max_component_elements = read(24, 'n*')
# a number of fonts omit these last two bytes for some reason,
# so we have to supply a default here to prevent nils
@max_component_depth =
if length == MAX_V1_TABLE_LENGTH
read(2, 'n').first
else
DEFAULT_MAX_COMPONENT_DEPTH
end
end
end
end
end
end
@@ -0,0 +1,290 @@
# frozen_string_literal: true
require_relative '../table'
require 'digest/sha1'
module TTFunk
class Table
# Naming (`name`) table
class Name < Table
# Name Record.
class NameString < ::String
# Platform ID.
# @return [Integer]
attr_reader :platform_id
# Platform-specific encoding ID.
# @return [Integer]
attr_reader :encoding_id
# Language ID.
# @return [Integer]
attr_reader :language_id
# @param text [String]
# @param platform_id [Integer]
# @param encoding_id [Integer]
# @param language_id [Integer]
def initialize(text, platform_id, encoding_id, language_id)
super(text)
@platform_id = platform_id
@encoding_id = encoding_id
@language_id = language_id
end
# Removes chracter incompatible with PostScript.
# @return [String] PostScript-compatible version of this string.
def strip_extended
stripped = gsub(/[\x00-\x19\x80-\xff]/n, '')
stripped = '[not-postscript]' if stripped.empty?
stripped
end
end
# Name records.
# @return [Array<Hash>]
attr_reader :entries
# Name strings.
# @return [Hash{Integer => NameString}]
attr_reader :strings
# Copyright notice.
# @return [Array<NameString>]
attr_reader :copyright
# Font Family names.
# @return [Array<NameString>]
attr_reader :font_family
# Font Subfamily names.
# @return [Array<NameString>]
attr_reader :font_subfamily
# Unique font identifiers.
# @return [Array<NameString>]
attr_reader :unique_subfamily
# Full font names.
# @return [Array<NameString>]
attr_reader :font_name
# Version strings.
# @return [Array<NameString>]
attr_reader :version
# Trademarks.
# @return [Array<NameString>]
attr_reader :trademark
# Manufacturer Names.
# @return [Array<NameString>]
attr_reader :manufacturer
# Designers.
# @return [Array<NameString>]
attr_reader :designer
# Descriptions.
# @return [Array<NameString>]
attr_reader :description
# Vendor URLs.
# @return [Array<NameString>]
attr_reader :vendor_url
# Designer URLs.
# @return [Array<NameString>]
attr_reader :designer_url
# License Descriptions.
# @return [Array<NameString>]
attr_reader :license
# License Info URLs.
# @return [Array<NameString>]
attr_reader :license_url
# Typographic Family names.
# @return [Array<NameString>]
attr_reader :preferred_family
# Typographic Subfamily names.
# @return [Array<NameString>]
attr_reader :preferred_subfamily
# Compatible Full Names.
# @return [Array<NameString>]
attr_reader :compatible_full
# Sample texts.
# @return [Array<NameString>]
attr_reader :sample_text
# Copyright notice ID.
COPYRIGHT_NAME_ID = 0
# Font Family name ID.
FONT_FAMILY_NAME_ID = 1
# Font Subfamily name ID.
FONT_SUBFAMILY_NAME_ID = 2
# Unique font identifier ID.
UNIQUE_SUBFAMILY_NAME_ID = 3
# Full font name that reflects all family and relevant subfamily
# descriptors ID.
FONT_NAME_NAME_ID = 4
# Version string ID.
VERSION_NAME_ID = 5
# PostScript name for the font ID.
POSTSCRIPT_NAME_NAME_ID = 6
# Trademark ID.
TRADEMARK_NAME_ID = 7
# Manufacturer Name ID.
MANUFACTURER_NAME_ID = 8
# Designer ID.
DESIGNER_NAME_ID = 9
# Description ID.
DESCRIPTION_NAME_ID = 10
# Vendor URL ID.
VENDOR_URL_NAME_ID = 11
# Designer URL ID.
DESIGNER_URL_NAME_ID = 12
# License Description ID.
LICENSE_NAME_ID = 13
# License Info URL ID.
LICENSE_URL_NAME_ID = 14
# Typographic Family name ID.
PREFERRED_FAMILY_NAME_ID = 16
# Typographic Subfamily name ID.
PREFERRED_SUBFAMILY_NAME_ID = 17
# Compatible Full ID.
COMPATIBLE_FULL_NAME_ID = 18
# Sample text ID.
SAMPLE_TEXT_NAME_ID = 19
# Encode table.
#
# @param names [TTFunk::Table::Name]
# @param key [String]
# @return [String]
def self.encode(names, key = '')
tag = Digest::SHA1.hexdigest(key)[0, 6]
postscript_name = NameString.new("#{tag}+#{names.postscript_name}", 1, 0, 0)
strings = names.strings.dup
strings[6] = [postscript_name]
str_count = strings.reduce(0) { |sum, (_, list)| sum + list.length }
table = [0, str_count, 6 + (12 * str_count)].pack('n*')
strtable = +''
items = []
strings.each do |id, list|
list.each do |string|
items << [id, string]
end
end
items =
items.sort_by { |id, string|
[string.platform_id, string.encoding_id, string.language_id, id]
}
items.each do |id, string|
table << [
string.platform_id, string.encoding_id, string.language_id, id,
string.length, strtable.length,
].pack('n*')
strtable << string
end
table << strtable
end
# PostScript name for the font.
# @return [String]
def postscript_name
return @postscript_name if @postscript_name
font_family.first || 'unnamed'
end
private
def parse!
count, string_offset = read(6, 'x2n*')
@entries = []
count.times do
platform, encoding, language, id, length, start_offset =
read(12, 'n*')
@entries << {
platform_id: platform,
encoding_id: encoding,
language_id: language,
name_id: id,
length: length,
offset: offset + string_offset + start_offset,
text: nil,
}
end
@strings = Hash.new { |h, k| h[k] = [] }
count.times do |i|
io.pos = @entries[i][:offset]
@entries[i][:text] = io.read(@entries[i][:length])
@strings[@entries[i][:name_id]] << NameString.new(
@entries[i][:text] || '',
@entries[i][:platform_id],
@entries[i][:encoding_id],
@entries[i][:language_id],
)
end
# should only be ONE postscript name
@copyright = @strings[COPYRIGHT_NAME_ID]
@font_family = @strings[FONT_FAMILY_NAME_ID]
@font_subfamily = @strings[FONT_SUBFAMILY_NAME_ID]
@unique_subfamily = @strings[UNIQUE_SUBFAMILY_NAME_ID]
@font_name = @strings[FONT_NAME_NAME_ID]
@version = @strings[VERSION_NAME_ID]
unless @strings[POSTSCRIPT_NAME_NAME_ID].empty?
@postscript_name = @strings[POSTSCRIPT_NAME_NAME_ID]
.first.strip_extended
end
@trademark = @strings[TRADEMARK_NAME_ID]
@manufacturer = @strings[MANUFACTURER_NAME_ID]
@designer = @strings[DESIGNER_NAME_ID]
@description = @strings[DESCRIPTION_NAME_ID]
@vendor_url = @strings[VENDOR_URL_NAME_ID]
@designer_url = @strings[DESIGNER_URL_NAME_ID]
@license = @strings[LICENSE_NAME_ID]
@license_url = @strings[LICENSE_URL_NAME_ID]
@preferred_family = @strings[PREFERRED_FAMILY_NAME_ID]
@preferred_subfamily = @strings[PREFERRED_SUBFAMILY_NAME_ID]
@compatible_full = @strings[COMPATIBLE_FULL_NAME_ID]
@sample_text = @strings[SAMPLE_TEXT_NAME_ID]
end
end
end
end
@@ -0,0 +1,635 @@
# frozen_string_literal: true
require_relative '../table'
require 'set'
module TTFunk
class Table
# OS/2 and Windows Metrics (`OS/2`) table
class OS2 < Table
# Table version.
# @return [Integer]
attr_reader :version
# Average weighted escapement.
# @return [Integer]
attr_reader :ave_char_width
# Weight class.
# @return [Integer]
attr_reader :weight_class
# Width class.
# @return [Integer]
attr_reader :width_class
# Type flags.
# @return [Integer]
attr_reader :type
# Subscript horizontal font size.
# @return [Integer]
attr_reader :y_subscript_x_size
# Subscript vertical font size.
# @return [Integer]
attr_reader :y_subscript_y_size
# Subscript x offset.
# @return [Integer]
attr_reader :y_subscript_x_offset
# Subscript y offset.
# @return [Integer]
attr_reader :y_subscript_y_offset
# Superscript horizontal font size.
# @return [Integer]
attr_reader :y_superscript_x_size
# Superscript vertical font size.
# @return [Integer]
attr_reader :y_superscript_y_size
# Superscript x offset.
# @return [Integer]
attr_reader :y_superscript_x_offset
# Superscript y offset.
# @return [Integer]
attr_reader :y_superscript_y_offset
# Strikeout size.
# @return [Integer]
attr_reader :y_strikeout_size
# Strikeout position.
# @return [Integer]
attr_reader :y_strikeout_position
# Font-family class and subclass.
# @return [Integer]
attr_reader :family_class
# PANOSE classification number.
# @return [Integer]
attr_reader :panose
# Unicode Character Range.
# @return [Integer]
attr_reader :char_range
# Font Vendor Identification.
# @return [String]
attr_reader :vendor_id
# Font selection flags.
# @return [Integer]
attr_reader :selection
# The minimum Unicode index (character code) in this font.
# @return [Integer]
attr_reader :first_char_index
# The maximum Unicode index (character code) in this font.
# @return [Integer]
attr_reader :last_char_index
# The typographic ascender for this font.
# @return [Integer]
attr_reader :ascent
# The typographic descender for this font.
# @return [Integer]
attr_reader :descent
# The typographic line gap for this font.
# @return [Integer]
attr_reader :line_gap
# The "Windows ascender" metric.
# @return [Integer]
attr_reader :win_ascent
# The "Windows descender" metric.
# @return [Integer]
attr_reader :win_descent
# Code Page Character Range.
# @return [Integer]
attr_reader :code_page_range
# The distance between the baseline and the approximate height of
# non-ascending lowercase letters.
# @return [Integer]
attr_reader :x_height
# The distance between the baseline and the approximate height of
# uppercase letters.
# @return [Integer]
attr_reader :cap_height
# The Unicode code point, in UTF-16 encoding, of a character that can be
# used for a default glyph if a requested character is not supported in
# the font.
# @return [Integer]
attr_reader :default_char
# The Unicode code point, in UTF-16 encoding, of a character that can be
# used as a default break character.
# @return [Integer]
attr_reader :break_char
# The maximum length of a target glyph context for any feature in this
# font.
# @return [Integer]
attr_reader :max_context
# Code page bits.
CODE_PAGE_BITS = {
1252 => 0,
1250 => 1,
1251 => 2,
1253 => 3,
1254 => 4,
1255 => 5,
1256 => 6,
1257 => 7,
1258 => 8,
874 => 16,
932 => 17,
936 => 18,
949 => 19,
950 => 20,
1361 => 21,
10_000 => 29,
869 => 48,
866 => 49,
865 => 50,
864 => 51,
863 => 52,
862 => 53,
861 => 54,
860 => 55,
857 => 56,
855 => 57,
852 => 58,
775 => 59,
737 => 60,
708 => 61,
850 => 62,
437 => 63,
}.freeze
# Unicode blocks.
UNICODE_BLOCKS = {
(0x0000..0x007F) => 0,
(0x0080..0x00FF) => 1,
(0x0100..0x017F) => 2,
(0x0180..0x024F) => 3,
(0x0250..0x02AF) => 4,
(0x1D00..0x1D7F) => 4,
(0x1D80..0x1DBF) => 4,
(0x02B0..0x02FF) => 5,
(0xA700..0xA71F) => 5,
(0x0300..0x036F) => 6,
(0x1DC0..0x1DFF) => 6,
(0x0370..0x03FF) => 7,
(0x2C80..0x2CFF) => 8,
(0x0400..0x04FF) => 9,
(0x0500..0x052F) => 9,
(0x2DE0..0x2DFF) => 9,
(0xA640..0xA69F) => 9,
(0x0530..0x058F) => 10,
(0x0590..0x05FF) => 11,
(0xA500..0xA63F) => 12,
(0x0600..0x06FF) => 13,
(0x0750..0x077F) => 13,
(0x07C0..0x07FF) => 14,
(0x0900..0x097F) => 15,
(0x0980..0x09FF) => 16,
(0x0A00..0x0A7F) => 17,
(0x0A80..0x0AFF) => 18,
(0x0B00..0x0B7F) => 19,
(0x0B80..0x0BFF) => 20,
(0x0C00..0x0C7F) => 21,
(0x0C80..0x0CFF) => 22,
(0x0D00..0x0D7F) => 23,
(0x0E00..0x0E7F) => 24,
(0x0E80..0x0EFF) => 25,
(0x10A0..0x10FF) => 26,
(0x2D00..0x2D2F) => 26,
(0x1B00..0x1B7F) => 27,
(0x1100..0x11FF) => 28,
(0x1E00..0x1EFF) => 29,
(0x2C60..0x2C7F) => 29,
(0xA720..0xA7FF) => 29,
(0x1F00..0x1FFF) => 30,
(0x2000..0x206F) => 31,
(0x2E00..0x2E7F) => 31,
(0x2070..0x209F) => 32,
(0x20A0..0x20CF) => 33,
(0x20D0..0x20FF) => 34,
(0x2100..0x214F) => 35,
(0x2150..0x218F) => 36,
(0x2190..0x21FF) => 37,
(0x27F0..0x27FF) => 37,
(0x2900..0x297F) => 37,
(0x2B00..0x2BFF) => 37,
(0x2200..0x22FF) => 38,
(0x2A00..0x2AFF) => 38,
(0x27C0..0x27EF) => 38,
(0x2980..0x29FF) => 38,
(0x2300..0x23FF) => 39,
(0x2400..0x243F) => 40,
(0x2440..0x245F) => 41,
(0x2460..0x24FF) => 42,
(0x2500..0x257F) => 43,
(0x2580..0x259F) => 44,
(0x25A0..0x25FF) => 45,
(0x2600..0x26FF) => 46,
(0x2700..0x27BF) => 47,
(0x3000..0x303F) => 48,
(0x3040..0x309F) => 49,
(0x30A0..0x30FF) => 50,
(0x31F0..0x31FF) => 50,
(0x3100..0x312F) => 51,
(0x31A0..0x31BF) => 51,
(0x3130..0x318F) => 52,
(0xA840..0xA87F) => 53,
(0x3200..0x32FF) => 54,
(0x3300..0x33FF) => 55,
(0xAC00..0xD7AF) => 56,
(0xD800..0xDFFF) => 57,
(0x10900..0x1091F) => 58,
(0x4E00..0x9FFF) => 59,
(0x2E80..0x2EFF) => 59,
(0x2F00..0x2FDF) => 59,
(0x2FF0..0x2FFF) => 59,
(0x3400..0x4DBF) => 59,
(0x20000..0x2A6DF) => 59,
(0x3190..0x319F) => 59,
(0xE000..0xF8FF) => 60,
(0x31C0..0x31EF) => 61,
(0xF900..0xFAFF) => 61,
(0x2F800..0x2FA1F) => 61,
(0xFB00..0xFB4F) => 62,
(0xFB50..0xFDFF) => 63,
(0xFE20..0xFE2F) => 64,
(0xFE10..0xFE1F) => 65,
(0xFE30..0xFE4F) => 65,
(0xFE50..0xFE6F) => 66,
(0xFE70..0xFEFF) => 67,
(0xFF00..0xFFEF) => 68,
(0xFFF0..0xFFFF) => 69,
(0x0F00..0x0FFF) => 70,
(0x0700..0x074F) => 71,
(0x0780..0x07BF) => 72,
(0x0D80..0x0DFF) => 73,
(0x1000..0x109F) => 74,
(0x1200..0x137F) => 75,
(0x1380..0x139F) => 75,
(0x2D80..0x2DDF) => 75,
(0x13A0..0x13FF) => 76,
(0x1400..0x167F) => 77,
(0x1680..0x169F) => 78,
(0x16A0..0x16FF) => 79,
(0x1780..0x17FF) => 80,
(0x19E0..0x19FF) => 80,
(0x1800..0x18AF) => 81,
(0x2800..0x28FF) => 82,
(0xA000..0xA48F) => 83,
(0xA490..0xA4CF) => 83,
(0x1700..0x171F) => 84,
(0x1720..0x173F) => 84,
(0x1740..0x175F) => 84,
(0x1760..0x177F) => 84,
(0x10300..0x1032F) => 85,
(0x10330..0x1034F) => 86,
(0x10400..0x1044F) => 87,
(0x1D000..0x1D0FF) => 88,
(0x1D100..0x1D1FF) => 88,
(0x1D200..0x1D24F) => 88,
(0x1D400..0x1D7FF) => 89,
(0xF0000..0xFFFFD) => 90,
(0x100000..0x10FFFD) => 90,
(0xFE00..0xFE0F) => 91,
(0xE0100..0xE01EF) => 91,
(0xE0000..0xE007F) => 92,
(0x1900..0x194F) => 93,
(0x1950..0x197F) => 94,
(0x1980..0x19DF) => 95,
(0x1A00..0x1A1F) => 96,
(0x2C00..0x2C5F) => 97,
(0x2D30..0x2D7F) => 98,
(0x4DC0..0x4DFF) => 99,
(0xA800..0xA82F) => 100,
(0x10000..0x1007F) => 101,
(0x10080..0x100FF) => 101,
(0x10100..0x1013F) => 101,
(0x10140..0x1018F) => 102,
(0x10380..0x1039F) => 103,
(0x103A0..0x103DF) => 104,
(0x10450..0x1047F) => 105,
(0x10480..0x104AF) => 106,
(0x10800..0x1083F) => 107,
(0x10A00..0x10A5F) => 108,
(0x1D300..0x1D35F) => 109,
(0x12000..0x123FF) => 110,
(0x12400..0x1247F) => 110,
(0x1D360..0x1D37F) => 111,
(0x1B80..0x1BBF) => 112,
(0x1C00..0x1C4F) => 113,
(0x1C50..0x1C7F) => 114,
(0xA880..0xA8DF) => 115,
(0xA900..0xA92F) => 116,
(0xA930..0xA95F) => 117,
(0xAA00..0xAA5F) => 118,
(0x10190..0x101CF) => 119,
(0x101D0..0x101FF) => 120,
(0x102A0..0x102DF) => 121,
(0x10280..0x1029F) => 121,
(0x10920..0x1093F) => 121,
(0x1F030..0x1F09F) => 122,
(0x1F000..0x1F02F) => 122,
}.freeze
# Indicates that font supports supplementary characters.
UNICODE_MAX = 0xFFFF
# Unicode block ranges.
UNICODE_RANGES = UNICODE_BLOCKS.keys.sort_by(&:max).freeze
# Start chracter for average character width calculation.
LOWERCASE_START = 'a'.ord
# End chracter for average character width calculation.
LOWERCASE_END = 'z'.ord
# Number of chracters for average character width calculation.
LOWERCASE_COUNT = (LOWERCASE_END - LOWERCASE_START) + 1
# Space character code point.
CODEPOINT_SPACE = 32
# Error message for missing space character.
SPACE_GLYPH_MISSING_ERROR = "Space glyph (0x#{CODEPOINT_SPACE.to_s(16)}) must be included in the font"
# Used to calculate the xAvgCharWidth field.
# From https://docs.microsoft.com/en-us/typography/opentype/spec/os2:
#
# "When first defined, the specification was biased toward Basic Latin
# characters, and it was thought that the xAvgCharWidth value could be
# used to estimate the average length of lines of text. A formula for
# calculating xAvgCharWidth was provided using frequency-of-use
# weighting factors for lowercase letters a - z."
#
# The array below contains 26 weight values which correspond to the
# 26 letters in the Latin alphabet. Each weight is the relative
# frequency of that letter in the English language.
WEIGHT_SPACE = 166
# chracter weights for average character width calculation.
WEIGHT_LOWERCASE = [
64, 14, 27, 35, 100, 20, 14, 42, 63, 3, 6, 35, 20,
56, 56, 17, 4, 49, 56, 71, 31, 10, 18, 3, 18, 2,
].freeze
# Table tag.
# @return [String]
def tag
'OS/2'
end
class << self
# Encode table.
#
# @param os2 [TTFunk::Table::OS2]
# @param subset [TTFunk::Subset::MacRoman, TTFunk::Subset::Windows1252,
# TTFunk::Subset::Unicode, TTFunk::Subset::Unicode8Bit]
# @return [String]
def encode(os2, subset)
result = ''.b
result << [
os2.version, avg_char_width_for(os2, subset), os2.weight_class,
os2.width_class, os2.type, os2.y_subscript_x_size,
os2.y_subscript_y_size, os2.y_subscript_x_offset,
os2.y_subscript_y_offset, os2.y_superscript_x_size,
os2.y_superscript_y_size, os2.y_superscript_x_offset,
os2.y_superscript_y_offset, os2.y_strikeout_size,
os2.y_strikeout_position, os2.family_class,
].pack('n*')
result << os2.panose
new_char_range = unicode_blocks_for(os2, os2.char_range, subset)
result << BinUtils
.slice_int(
new_char_range.value,
bit_width: 32,
slice_count: 4,
)
.pack('N*')
result << os2.vendor_id
new_cmap_table = subset.new_cmap_table[:charmap]
code_points = new_cmap_table
.keys
.select { |k| (new_cmap_table[k][:new]).positive? }
.sort
# "This value depends on which character sets the font supports.
# This field cannot represent supplementary character values
# (codepoints greater than 0xFFFF). Fonts that support
# supplementary characters should set the value in this field
# to 0xFFFF."
first_char_index = [code_points.first || 0, UNICODE_MAX].min
last_char_index = [code_points.last || 0, UNICODE_MAX].min
result << [
os2.selection, first_char_index, last_char_index,
].pack('n*')
if os2.version.positive?
result << [
os2.ascent, os2.descent, os2.line_gap,
os2.win_ascent, os2.win_descent,
].pack('n*')
result << BinUtils
.slice_int(
code_pages_for(subset).value,
bit_width: 32,
slice_count: 2,
)
.pack('N*')
if os2.version > 1
result << [
os2.x_height, os2.cap_height, os2.default_char,
os2.break_char, os2.max_context,
].pack('n*')
end
end
result
end
private
def code_pages_for(subset)
field = BitField.new(0)
return field if subset.unicode?
field.on(CODE_PAGE_BITS[subset.code_page])
field
end
def unicode_blocks_for(os2, original_field, subset)
field = BitField.new(0)
return field unless subset.unicode?
subset_code_points = Set.new(subset.new_cmap_table[:charmap].keys)
original_code_point_groups = group_original_code_points_by_bit(os2)
original_code_point_groups.each do |bit, code_points|
next if original_field.off?(bit)
if code_points.any? { |cp| subset_code_points.include?(cp) }
field.on(bit)
end
end
field
end
def group_original_code_points_by_bit(os2)
Hash.new { |h, k| h[k] = [] }.tap do |result|
code_points = os2.file.cmap.unicode.first.code_map.keys.sort
UNICODE_RANGES.each do |r|
code_points = code_points.drop_while { |p| p < r.min }
code_points.take_while { |p| p <= r.max }.each do |code_point|
if (bit = UNICODE_BLOCKS[r])
result[bit] << code_point
end
end
code_points = code_points.drop_while { |p| p <= r.max }
end
end
end
def avg_char_width_for(os2, subset)
if subset.microsoft_symbol?
avg_ms_symbol_char_width_for(os2, subset)
else
avg_weighted_char_width_for(os2, subset)
end
end
def avg_ms_symbol_char_width_for(os2, subset)
total_width = 0
num_glyphs = 0
# use new -> old glyph mapping in order to include compound glyphs
# in the calculation
subset.new_to_old_glyph.each_value do |old_gid|
if (metric = os2.file.horizontal_metrics.for(old_gid))
total_width += metric.advance_width
num_glyphs += 1 if metric.advance_width.positive?
end
end
return 0 if num_glyphs.zero?
total_width / num_glyphs # this should be a whole number
end
def avg_weighted_char_width_for(os2, subset)
# make sure the subset includes the space char
unless subset.to_unicode_map[CODEPOINT_SPACE]
raise SPACE_GLYPH_MISSING_ERROR
end
space_gid = os2.file.cmap.unicode.first[CODEPOINT_SPACE]
space_hm = os2.file.horizontal_metrics.for(space_gid)
return 0 unless space_hm
total_weight = space_hm.advance_width * WEIGHT_SPACE
num_lowercase = 0
# calculate the weighted sum of all the lowercase widths in
# the subset
LOWERCASE_START.upto(LOWERCASE_END) do |lowercase_cp|
# make sure the subset includes the character
next unless subset.to_unicode_map[lowercase_cp]
lowercase_gid = os2.file.cmap.unicode.first[lowercase_cp]
lowercase_hm = os2.file.horizontal_metrics.for(lowercase_gid)
num_lowercase += 1
total_weight += lowercase_hm.advance_width *
WEIGHT_LOWERCASE[lowercase_cp - 'a'.ord]
end
# return if all lowercase characters are present in the subset
return total_weight / 1000 if num_lowercase == LOWERCASE_COUNT
# If not all lowercase characters are present in the subset, take
# the average width of all the subsetted characters. This differs
# from avg_ms_char_width_for in that it includes zero-width glyphs
# in the calculation.
total_width = 0
num_glyphs = subset.new_to_old_glyph.size
# use new -> old glyph mapping in order to include compound glyphs
# in the calculation
subset.new_to_old_glyph.each_value do |old_gid|
if (metric = os2.file.horizontal_metrics.for(old_gid))
total_width += metric.advance_width
end
end
return 0 if num_glyphs.zero?
total_width / num_glyphs # this should be a whole number
end
end
private
def parse!
@version = read(2, 'n').first
@ave_char_width = read_signed(1).first
@weight_class, @width_class = read(4, 'nn')
@type, @y_subscript_x_size, @y_subscript_y_size, @y_subscript_x_offset,
@y_subscript_y_offset, @y_superscript_x_size, @y_superscript_y_size,
@y_superscript_x_offset, @y_superscript_y_offset, @y_strikeout_size,
@y_strikeout_position, @family_class = read_signed(12)
@panose = io.read(10)
@char_range = BitField.new(BinUtils.stitch_int(read(16, 'N*'), bit_width: 32))
@vendor_id = io.read(4)
@selection, @first_char_index, @last_char_index = read(6, 'n*')
if @version.positive?
@ascent, @descent, @line_gap = read_signed(3)
@win_ascent, @win_descent = read(4, 'nn')
@code_page_range = BitField.new(BinUtils.stitch_int(read(8, 'N*'), bit_width: 32))
if @version > 1
@x_height, @cap_height = read_signed(2)
@default_char, @break_char, @max_context = read(6, 'nnn')
# Set this to zero until GSUB/GPOS support has been implemented.
# This value is calculated via those tables, and should be set to
# zero if the data is not available.
@max_context = 0
end
end
end
end
end
end
@@ -0,0 +1,157 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# PostScript (`post`) table.
#
# This class can be extended with version-specific modules.
#
# @see TTFunk::Table::Post::Format10
# @see TTFunk::Table::Post::Format20
# @see TTFunk::Table::Post::Format30
# @see TTFunk::Table::Post::Format40
class Post < Table
# Table version.
# @return [Integer]
attr_reader :format
# Italic angle in counter-clockwise degrees from the vertical.
# @return [Integer]
attr_reader :italic_angle
# Suggested distance of the top of the underline from the baseline
# @return [Integer]
attr_reader :underline_position
# Suggested values for the underline thickness.
# @return [Integer]
attr_reader :underline_thickness
# 0 if the font is proportionally spaced, non-zero if the font is not
# proportionally spaced.
# @return [Integer]
attr_reader :fixed_pitch
# Minimum memory usage when an OpenType font is downloaded.
# @return [Integer]
attr_reader :min_mem_type42
# Maximum memory usage when an OpenType font is downloaded.
# @return [Integer]
attr_reader :max_mem_type42
# Minimum memory usage when an OpenType font is downloaded as
# a Type 1 font.
# @return [Integer]
attr_reader :min_mem_type1
# Maximum memory usage when an OpenType font is downloaded as
# a Type 1 font.
# @return [Integer]
attr_reader :max_mem_type1
# Version-specific fields.
# @return [TTFunk::Table::Post::Format10, TTFunk::Table::Post::Format20,
# TTFunk::Table::Post::Format30, TTFunk::Table::Post::Format40]
attr_reader :subtable
# Encode table.
#
# @param post [TTFunk::Table::Post]
# @param mapping [Hash{Integer => Integer}] keys are new glyph IDs, values
# are old glyph IDs
# @return [String, nil]
def self.encode(post, mapping)
return if post.nil?
post.recode(mapping)
end
# Is this font monospaced?
#
# @return [Boolean]
def fixed_pitch?
@fixed_pitch != 0
end
# Get glyph name for character code.
#
# This is a placeholder.
#
# @param _code [Integer]
# @return [String]
def glyph_for(_code)
'.notdef'
end
# Re-encode this table.
#
# @param mapping [Hash{Integer => Integer}] keys are new glyph IDs, values
# are old glyph IDs
# @return [String]
def recode(mapping)
return raw if format == 0x00030000
table = raw[0, 32]
table[0, 4] = [0x00020000].pack('N')
index = []
strings = []
mapping.keys.sort.each do |new_id|
post_glyph = glyph_for(mapping[new_id])
position = Format10::POSTSCRIPT_GLYPHS.index(post_glyph)
if position
index << position
else
index << (257 + strings.length)
strings << post_glyph
end
end
table << [mapping.length, *index].pack('n*')
strings.each do |string|
table << [string.length, string].pack('CA*')
end
table
end
private
def parse!
@format, @italic_angle, @underline_position, @underline_thickness,
@fixed_pitch, @min_mem_type42, @max_mem_type42,
@min_mem_type1, @max_mem_type1 = read(32, 'N2n2N*')
@subtable =
case @format
when 0x00010000
extend(Post::Format10)
when 0x00020000
extend(Post::Format20)
when 0x00025000
raise NotImplementedError,
'Post format 2.5 is not supported by TTFunk'
when 0x00030000
extend(Post::Format30)
when 0x00040000
extend(Post::Format40)
end
parse_format!
end
def parse_format!
warn(Kernel.format('postscript table format 0x%08X is not supported', @format))
end
end
end
end
require_relative 'post/format10'
require_relative 'post/format20'
require_relative 'post/format30'
require_relative 'post/format40'
@@ -0,0 +1,58 @@
# frozen_string_literal: true
module TTFunk
class Table
class Post
# Version 1.0 provides glyph names for standard 258 glyphs in the standard
# Macintosh TrueType font file.
module Format10
# Glyph names.
POSTSCRIPT_GLYPHS = %w[
.notdef .null nonmarkingreturn space exclam quotedbl numbersign dollar
percent ampersand quotesingle parenleft parenright asterisk plus comma
hyphen period slash zero one two three four five six seven eight nine
colon semicolon less equal greater question at
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
bracketleft backslash bracketright asciicircum underscore grave
a b c d e f g h i j k l m n o p q r s t u v w x y z
braceleft bar braceright asciitilde Adieresis Aring Ccedilla Eacute
Ntilde Odieresis Udieresis aacute agrave acircumflex adieresis atilde
aring ccedilla eacute egrave ecircumflex edieresis iacute igrave
icircumflex idieresis ntilde oacute ograve ocircumflex odieresis
otilde uacute ugrave ucircumflex udieresis dagger degree cent sterling
section bullet paragraph germandbls registered copyright trademark
acute dieresis notequal AE Oslash infinity plusminus lessequal
greaterequal yen mu partialdiff summation product pi integral
ordfeminine ordmasculine Omega ae oslash questiondown exclamdown
logicalnot radical florin approxequal Delta guillemotleft
guillemotright ellipsis nonbreakingspace Agrave Atilde Otilde OE oe
endash emdash quotedblleft quotedblright quoteleft quoteright divide
lozenge ydieresis Ydieresis fraction currency guilsinglleft
guilsinglright fi fl daggerdbl periodcentered quotesinglbase
quotedblbase perthousand Acircumflex Ecircumflex Aacute Edieresis
Egrave Iacute Icircumflex Idieresis Igrave Oacute Ocircumflex apple
Ograve Uacute Ucircumflex Ugrave dotlessi circumflex tilde macron
breve dotaccent ring cedilla hungarumlaut ogonek caron Lslash lslash
Scaron scaron Zcaron zcaron brokenbar Eth eth Yacute yacute Thorn
thorn minus multiply onesuperior twosuperior threesuperior onehalf
onequarter threequarters franc Gbreve gbreve Idotaccent Scedilla
scedilla Cacute cacute Ccaron ccaron dcroat
].freeze
# Get glyph name for character code.
#
# @param code [Integer]
# @return [String]
def glyph_for(code)
POSTSCRIPT_GLYPHS[code] || '.notdef'
end
private
def parse_format!
# do nothing. Format 1 is easy-sauce.
end
end
end
end
end
@@ -0,0 +1,48 @@
# frozen_string_literal: true
require_relative 'format10'
require 'stringio'
module TTFunk
class Table
class Post
# Version 2.0 is used for fonts that use glyph names that are not in the
# set of Macintosh glyph names. A given font may map some of its glyphs to
# the standard Macintosh glyph names, and some to its own custom names.
# A version 2.0 `post` table can be used in fonts with TrueType or CFF
# version 2 outlines.
module Format20
include Format10
# Get glyph name for character code.
#
# @param code [Integer]
# @return [String]
def glyph_for(code)
index = @glyph_name_index[code]
return '.notdef' unless index
if index <= 257
POSTSCRIPT_GLYPHS[index]
else
@names[index - 258] || '.notdef'
end
end
private
def parse_format!
number_of_glyphs = read(2, 'n').first
@glyph_name_index = read(number_of_glyphs * 2, 'n*')
@names = []
strings = StringIO.new(io.read(offset + length - io.pos))
until strings.eof?
length = strings.read(1).unpack1('C')
@names << strings.read(length)
end
end
end
end
end
end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module TTFunk
class Table
class Post
# Version 3.0 specifies that no PostScript name information is provided
# for the glyphs in this font file.
module Format30
# Get glyph name for character code.
#
# @param _code [Integer]
# @return [String]
def glyph_for(_code)
'.notdef'
end
private
def parse_format!
# do nothing. Format 3 is easy-sauce.
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module TTFunk
class Table
class Post
# Version 4.0 names glyphs by their character code.
module Format40
# Get glyph name for character code.
#
# @param code [Integer]
# @return [String]
def glyph_for(code)
@map[code] || 0xFFFF
end
private
def parse_format!
@map = read(file.maximum_profile.num_glyphs * 2, 'N*')
end
end
end
end
end
@@ -0,0 +1,103 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Standard Bitmap Graphics (`sbix`) table.
class Sbix < Table
# Table version.
# @return [Integer]
attr_reader :version
# Flags.
# @return [Integer]
attr_reader :flags
# Number of bitmap strikes.
# @return [Integer]
attr_reader :num_strikes
# Strikes.
# @return [Array<Hash>]
attr_reader :strikes
# Bitmap Data.
#
# @!attribute [rw] x
# The horizontal (x-axis) position of the left edge of the bitmap
# graphic in relation to the glyph design space origin.
# @!attribute [rw] y
# The vertical (y-axis) position of the bottom edge of the bitmap
# graphic in relation to the glyph design space origin.
# @!attribute [rw] type
# Indicates the format of the embedded graphic data: one of `jpg `,
# `png `, `tiff`, or the special format `dupe`.
# @!attribute [rw] data
# The actual embedded graphic data.
# @!attribute [rw] ppem
# The PPEM size for which this strike was designed.
# @!attribute [rw] resolution
# The device pixel density (in PPI) for which this strike was designed.
BitmapData = Struct.new(:x, :y, :type, :data, :ppem, :resolution)
# Get bitmap for glyph strike.
#
# @param glyph_id [Integer]
# @param strike_index [Integer]
# @return [BitmapData]
def bitmap_data_for(glyph_id, strike_index)
strike = strikes[strike_index]
return if strike.nil?
glyph_offset = strike[:glyph_data_offset][glyph_id]
next_glyph_offset = strike[:glyph_data_offset][glyph_id + 1]
if glyph_offset && next_glyph_offset
bytes = next_glyph_offset - glyph_offset
if bytes.positive?
parse_from(offset + strike[:offset] + glyph_offset) {
x, y, type = read(8, 's2A4')
data = StringIO.new(io.read(bytes - 8))
BitmapData.new(x, y, type, data, strike[:ppem], strike[:resolution])
}
end
end
end
# Get all bitmaps for glyph.
#
# @param glyph_id [Integer]
# @return [Array<BitmapData>]
def all_bitmap_data_for(glyph_id)
strikes.each_index.filter_map { |strike_index|
bitmap_data_for(glyph_id, strike_index)
}
end
private
def parse!
@version, @flags, @num_strikes = read(8, 'n2N')
strike_offsets = Array.new(num_strikes) { read(4, 'N').first }
@strikes =
strike_offsets.map { |strike_offset|
parse_from(offset + strike_offset) {
ppem, resolution = read(4, 'n2')
data_offsets =
Array.new(file.maximum_profile.num_glyphs + 1) {
read(4, 'N').first
}
{
ppem: ppem,
resolution: resolution,
offset: strike_offset,
glyph_data_offset: data_offsets,
}
}
}
end
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# A table that TTFunk doesn't decode but preserve.
class Simple < Table
# Table tag
# @return [String]
attr_reader :tag
# @param file [TTFunk::File]
# @param tag [String]
def initialize(file, tag)
@tag = tag
super(file)
end
end
end
end
@@ -0,0 +1,82 @@
# frozen_string_literal: true
require_relative '../table'
module TTFunk
class Table
# Vertical Origin (`VORG`) table.
class Vorg < Table
# Table tag.
TAG = 'VORG'
# Table major version.
# @return [Integer]
attr_reader :major_version
# Table minor version.
# @return [Integer]
attr_reader :minor_version
# The default y coordinate of a glyph’s vertical origin.
# @return [Integer]
attr_reader :default_vert_origin_y
# Number of vertical origin metrics.
# @return [Integer]
attr_reader :count
# Encode table.
#
# @return [String]
def self.encode(vorg)
return unless vorg
''.b.tap do |table|
table << [
vorg.major_version, vorg.minor_version,
vorg.default_vert_origin_y, vorg.count,
].pack('n*')
vorg.origins.each_pair do |glyph_id, vert_origin_y|
table << [glyph_id, vert_origin_y].pack('n*')
end
end
end
# Get vertical origina for glyph by ID.
#
# @param glyph_id [Integer]
# @return [Integer]
def for(glyph_id)
@origins.fetch(glyph_id, default_vert_origin_y)
end
# Table tag.
#
# @return [String]
def tag
TAG
end
# Origins map.
#
# @return [Hash{Integer => Integer}]
def origins
@origins ||= {}
end
private
def parse!
@major_version, @minor_version = read(4, 'n*')
@default_vert_origin_y = read_signed(1).first
@count = read(2, 'n').first
count.times do
glyph_id = read(2, 'n').first
origins[glyph_id] = read_signed(1).first
end
end
end
end
end
@@ -0,0 +1,218 @@
# frozen_string_literal: true
module TTFunk
# Encodes a TrueType font subset to its binary representation.
class TTFEncoder
# Optimal table order according to TrueType specification.
OPTIMAL_TABLE_ORDER = [
'head', 'hhea', 'maxp', 'OS/2', 'hmtx', 'LTSH', 'VDMX',
'hdmx', 'cmap', 'fpgm', 'prep', 'cvt ', 'loca', 'glyf',
'kern', 'name', 'post', 'gasp', 'PCLT',
].freeze
# Original font.
# @return [TTFunk::File]
attr_reader :original
# Subset to encode.
# @return [TTFunk::Subset]
attr_reader :subset
# Encoding options.
# @return [Hash]
attr_reader :options
# @param original [TTFunk::File]
# @param subset [TTFunk::Subset]
# @param options [Hash]
# @option options :kerning [Boolean] whether to encode Kerning (`kern`)
# table.
def initialize(original, subset, options = {})
@original = original
@subset = subset
@options = options
end
# Encode the font subset.
#
# @return [String]
def encode
# https://www.microsoft.com/typography/otspec/otff.htm#offsetTable
search_range = (2**Math.log2(tables.length).floor) * 16
entry_selector = Integer(Math.log2(2**Math.log2(tables.length).floor))
range_shift = (tables.length * 16) - search_range
range_shift = 0 if range_shift.negative?
newfont = EncodedString.new
newfont << [
original.directory.scaler_type,
tables.length,
search_range,
entry_selector,
range_shift,
].pack('Nn*')
# Tables are supposed to be listed in ascending order whereas there is a
# known optimal order for table data.
tables.keys.sort.each do |tag|
newfont << [tag, checksum(tables[tag])].pack('A4N')
newfont << Placeholder.new(tag, length: 4)
newfont << [tables[tag].length].pack('N')
end
optimal_table_order.each do |optimal_tag|
next unless tables.include?(optimal_tag)
newfont.resolve_placeholder(optimal_tag, [newfont.length].pack('N'))
newfont << tables[optimal_tag]
newfont.align!(4)
end
sum = checksum(newfont)
adjustment = 0xB1B0AFBA - sum
newfont.resolve_placeholder(:checksum, [adjustment].pack('N'))
newfont.string
end
private
def optimal_table_order
OPTIMAL_TABLE_ORDER +
(tables.keys - ['DSIG'] - OPTIMAL_TABLE_ORDER) +
['DSIG']
end
# "mandatory" tables. Every font should ("should") have these
def cmap_table
@cmap_table ||= subset.new_cmap_table
end
def glyf_table
@glyf_table ||= TTFunk::Table::Glyf.encode(glyphs, new_to_old_glyph, old_to_new_glyph)
end
def loca_table
@loca_table ||= TTFunk::Table::Loca.encode(glyf_table[:offsets])
end
def hmtx_table
@hmtx_table ||= TTFunk::Table::Hmtx.encode(original.horizontal_metrics, new_to_old_glyph)
end
def hhea_table
@hhea_table = TTFunk::Table::Hhea.encode(original.horizontal_header, hmtx_table, original, new_to_old_glyph)
end
def maxp_table
@maxp_table ||= TTFunk::Table::Maxp.encode(original.maximum_profile, old_to_new_glyph)
end
def post_table
@post_table ||= TTFunk::Table::Post.encode(original.postscript, new_to_old_glyph)
end
def name_table
@name_table ||= TTFunk::Table::Name.encode(original.name, glyf_table.fetch(:table, ''))
end
def head_table
@head_table ||= TTFunk::Table::Head.encode(original.header, loca_table, new_to_old_glyph)
end
# "optional" tables. Fonts may omit these if they do not need them.
# Because they apply globally, we can simply copy them over, without
# modification, if they exist.
def os2_table
@os2_table ||= TTFunk::Table::OS2.encode(original.os2, subset)
end
def cvt_table
@cvt_table ||= TTFunk::Table::Simple.new(original, 'cvt ').raw
end
def fpgm_table
@fpgm_table ||= TTFunk::Table::Simple.new(original, 'fpgm').raw
end
def prep_table
@prep_table ||= TTFunk::Table::Simple.new(original, 'prep').raw
end
def gasp_table
@gasp_table ||= TTFunk::Table::Simple.new(original, 'gasp').raw
end
def kern_table
# for PDFs, the kerning info is all included in the PDF as the text is
# drawn. Thus, the PDF readers do not actually use the kerning info in
# embedded fonts. If the library is used for something else, the
# generated subfont may need a kerning table... in that case, you need
# to opt into it.
if options[:kerning]
@kern_table ||= TTFunk::Table::Kern.encode(original.kerning, old_to_new_glyph)
end
end
def vorg_table
@vorg_table ||= TTFunk::Table::Vorg.encode(original.vertical_origins)
end
def dsig_table
@dsig_table ||= TTFunk::Table::Dsig.encode(original.digital_signature)
end
def tables
@tables ||= {
'cmap' => cmap_table[:table],
'glyf' => glyf_table[:table],
'loca' => loca_table[:table],
'kern' => kern_table,
'hmtx' => hmtx_table[:table],
'hhea' => hhea_table,
'maxp' => maxp_table,
'OS/2' => os2_table,
'post' => post_table,
'name' => name_table,
'head' => head_table,
'prep' => prep_table,
'fpgm' => fpgm_table,
'cvt ' => cvt_table,
'VORG' => vorg_table,
'DSIG' => dsig_table,
'gasp' => gasp_table,
}.compact
end
def glyphs
subset.glyphs
end
def new_to_old_glyph
subset.new_to_old_glyph
end
def old_to_new_glyph
subset.old_to_new_glyph
end
def checksum(data)
align(raw(data), 4).unpack('N*').sum & 0xFFFF_FFFF
end
def raw(data)
data.respond_to?(:unresolved_string) ? data.unresolved_string : data
end
def align(data, width)
if (data.length % width).positive?
data + ("\0" * (width - (data.length % width)))
else
data
end
end
end
end