This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,48 @@
|
||||
# TTFunk
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user