1113 lines
29 KiB
Go
1113 lines
29 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"io"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// === Поиск системного TTF шрифта ===
|
||
|
||
// systemFontDirs возвращает директории со шрифтами для текущей ОС
|
||
func systemFontDirs() []string {
|
||
switch runtime.GOOS {
|
||
case "windows":
|
||
windir := os.Getenv("WINDIR")
|
||
if windir == "" {
|
||
windir = `C:\Windows`
|
||
}
|
||
return []string{
|
||
filepath.Join(windir, "Fonts"),
|
||
}
|
||
case "darwin":
|
||
home, _ := os.UserHomeDir()
|
||
return []string{
|
||
"/Library/Fonts",
|
||
"/System/Library/Fonts",
|
||
filepath.Join(home, "Library/Fonts"),
|
||
}
|
||
default: // Linux и прочие Unix
|
||
home, _ := os.UserHomeDir()
|
||
return []string{
|
||
"/usr/share/fonts",
|
||
"/usr/local/share/fonts",
|
||
filepath.Join(home, ".fonts"),
|
||
filepath.Join(home, ".local/share/fonts"),
|
||
"/usr/share/fonts/truetype",
|
||
"/usr/share/fonts/TTF",
|
||
}
|
||
}
|
||
}
|
||
|
||
// candidateFonts — имена шрифтов с кириллицей в порядке предпочтения
|
||
var candidateFonts = []string{
|
||
// Liberation (стандарт в большинстве Linux дистрибутивов)
|
||
"LiberationSans-Regular.ttf",
|
||
"LiberationSans-Bold.ttf",
|
||
// DejaVu (почти везде есть)
|
||
"DejaVuSans.ttf",
|
||
"DejaVuSans-Bold.ttf",
|
||
"dejavu-sans.book.ttf",
|
||
// FreeSans (GNU)
|
||
"FreeSans.ttf",
|
||
"FreeSerif.ttf",
|
||
// Ubuntu Font Family
|
||
"Ubuntu-R.ttf",
|
||
"Ubuntu-B.ttf",
|
||
// Roboto (Android/Chrome)
|
||
"Roboto-Regular.ttf",
|
||
"Roboto-Bold.ttf",
|
||
// Windows
|
||
"arial.ttf",
|
||
"Arial.ttf",
|
||
"arialbd.ttf", // Arial Bold
|
||
"times.ttf",
|
||
"cour.ttf",
|
||
// macOS
|
||
"Arial.ttf",
|
||
"Helvetica.ttf",
|
||
"Geneva.ttf",
|
||
// Noto (универсальный)
|
||
"NotoSans-Regular.ttf",
|
||
"NotoSans-Bold.ttf",
|
||
"NotoSerif-Regular.ttf",
|
||
}
|
||
|
||
// FindCyrillicFont ищет подходящий TTF шрифт с кириллицей.
|
||
// Возвращает путь к файлу или "" если не найден.
|
||
func FindCyrillicFont() string {
|
||
dirs := systemFontDirs()
|
||
|
||
// Сначала ищем по именам-кандидатам
|
||
for _, dir := range dirs {
|
||
for _, name := range candidateFonts {
|
||
path := filepath.Join(dir, name)
|
||
if _, err := os.Stat(path); err == nil {
|
||
return path
|
||
}
|
||
}
|
||
}
|
||
|
||
// Если не нашли — рекурсивно обходим директории
|
||
for _, dir := range dirs {
|
||
if path := walkForTTF(dir); path != "" {
|
||
return path
|
||
}
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
// walkForTTF рекурсивно ищет первый TTF файл в директории
|
||
func walkForTTF(dir string) string {
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
// Сначала файлы, потом поддиректории
|
||
for _, e := range entries {
|
||
if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".ttf") {
|
||
return filepath.Join(dir, e.Name())
|
||
}
|
||
}
|
||
for _, e := range entries {
|
||
if e.IsDir() {
|
||
if path := walkForTTF(filepath.Join(dir, e.Name())); path != "" {
|
||
return path
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// === Парсинг TTF ===
|
||
|
||
// TTFFont — распарсенный TTF шрифт
|
||
type TTFFont struct {
|
||
path string
|
||
unitsPerEm uint16
|
||
|
||
// cmap: Unicode codepoint → glyph ID
|
||
cmap map[rune]uint16
|
||
|
||
// hmtx: glyph ID → advance width (в font units)
|
||
advanceWidths []uint16
|
||
|
||
// loca: glyph ID → offset в glyf таблице
|
||
locaOffsets []uint32
|
||
locaFormat int // 0=short, 1=long
|
||
|
||
// Сырые данные таблиц для встраивания
|
||
rawGlyf []byte
|
||
rawHead []byte
|
||
rawHhea []byte
|
||
rawHmtx []byte
|
||
rawMaxp []byte
|
||
rawOS2 []byte
|
||
rawPost []byte
|
||
rawCmap []byte // синтетическая cmap для PDF
|
||
rawName []byte
|
||
|
||
numGlyphs int
|
||
}
|
||
|
||
// ttfTable — запись в таблице TTF
|
||
type ttfTable struct {
|
||
tag string
|
||
checksum uint32
|
||
offset uint32
|
||
length uint32
|
||
}
|
||
|
||
// LoadTTF загружает и парсит TTF файл
|
||
func LoadTTF(path string) (*TTFFont, error) {
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("reading font: %w", err)
|
||
}
|
||
|
||
f := &TTFFont{
|
||
path: path,
|
||
cmap: make(map[rune]uint16),
|
||
}
|
||
|
||
if err := f.parse(data); err != nil {
|
||
return nil, fmt.Errorf("parsing font %s: %w", path, err)
|
||
}
|
||
|
||
return f, nil
|
||
}
|
||
|
||
func (f *TTFFont) parse(data []byte) error {
|
||
r := &ttfReader{data: data}
|
||
|
||
// Офсетная таблица
|
||
sfVersion := r.readU32()
|
||
if sfVersion != 0x00010000 && sfVersion != 0x74727565 { // 'true'
|
||
return fmt.Errorf("not a TTF font (sfVersion=%08x)", sfVersion)
|
||
}
|
||
numTables := int(r.readU16())
|
||
r.skip(6) // searchRange, entrySelector, rangeShift
|
||
|
||
// Читаем таблицы
|
||
tables := make(map[string]ttfTable, numTables)
|
||
for i := 0; i < numTables; i++ {
|
||
tag := string(r.readBytes(4))
|
||
checksum := r.readU32()
|
||
offset := r.readU32()
|
||
length := r.readU32()
|
||
tables[tag] = ttfTable{tag, checksum, offset, length}
|
||
}
|
||
|
||
// head — units per em
|
||
if t, ok := tables["head"]; ok {
|
||
f.rawHead = safeSlice(data, t.offset, t.length)
|
||
hr := &ttfReader{data: f.rawHead}
|
||
hr.skip(18) // version, fontRevision, checkSumAdjustment, magicNumber, flags
|
||
f.unitsPerEm = hr.readU16()
|
||
}
|
||
if f.unitsPerEm == 0 {
|
||
f.unitsPerEm = 1000
|
||
}
|
||
|
||
// maxp — количество глифов
|
||
if t, ok := tables["maxp"]; ok {
|
||
f.rawMaxp = safeSlice(data, t.offset, t.length)
|
||
mr := &ttfReader{data: f.rawMaxp}
|
||
mr.skip(4) // version
|
||
f.numGlyphs = int(mr.readU16())
|
||
}
|
||
|
||
// hhea
|
||
if t, ok := tables["hhea"]; ok {
|
||
f.rawHhea = safeSlice(data, t.offset, t.length)
|
||
}
|
||
|
||
// hmtx — ширины глифов
|
||
if t, ok := tables["hmtx"]; ok {
|
||
f.rawHmtx = safeSlice(data, t.offset, t.length)
|
||
f.parseHmtx(tables)
|
||
}
|
||
|
||
// loca + glyf
|
||
if t, ok := tables["head"]; ok {
|
||
hr := &ttfReader{data: safeSlice(data, t.offset, t.length)}
|
||
hr.skip(50) // до indexToLocFormat
|
||
f.locaFormat = int(hr.readI16())
|
||
}
|
||
if t, ok := tables["loca"]; ok {
|
||
f.parseLoca(safeSlice(data, t.offset, t.length))
|
||
}
|
||
if t, ok := tables["glyf"]; ok {
|
||
f.rawGlyf = safeSlice(data, t.offset, t.length)
|
||
}
|
||
|
||
// OS/2, post, name
|
||
if t, ok := tables["OS/2"]; ok {
|
||
f.rawOS2 = safeSlice(data, t.offset, t.length)
|
||
}
|
||
if t, ok := tables["post"]; ok {
|
||
f.rawPost = safeSlice(data, t.offset, t.length)
|
||
}
|
||
if t, ok := tables["name"]; ok {
|
||
f.rawName = safeSlice(data, t.offset, t.length)
|
||
}
|
||
|
||
// cmap — Unicode маппинг
|
||
if t, ok := tables["cmap"]; ok {
|
||
f.parseCmap(safeSlice(data, t.offset, t.length))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (f *TTFFont) parseHmtx(tables map[string]ttfTable) {
|
||
// Количество hMetrics из hhea
|
||
numHMetrics := 0
|
||
if t, ok := tables["hhea"]; ok {
|
||
hr := &ttfReader{data: safeSlice([]byte(nil), t.offset, 0)}
|
||
_ = hr
|
||
// Читаем напрямую из rawHhea
|
||
if len(f.rawHhea) >= 36 {
|
||
numHMetrics = int(binary.BigEndian.Uint16(f.rawHhea[34:36]))
|
||
}
|
||
}
|
||
if numHMetrics == 0 {
|
||
numHMetrics = f.numGlyphs
|
||
}
|
||
|
||
r := &ttfReader{data: f.rawHmtx}
|
||
f.advanceWidths = make([]uint16, f.numGlyphs)
|
||
lastAW := uint16(0)
|
||
for i := 0; i < f.numGlyphs; i++ {
|
||
if i < numHMetrics {
|
||
lastAW = r.readU16()
|
||
r.skip(2) // lsb
|
||
}
|
||
f.advanceWidths[i] = lastAW
|
||
}
|
||
}
|
||
|
||
func (f *TTFFont) parseLoca(data []byte) {
|
||
r := &ttfReader{data: data}
|
||
n := f.numGlyphs + 1
|
||
f.locaOffsets = make([]uint32, n)
|
||
if f.locaFormat == 0 {
|
||
for i := 0; i < n; i++ {
|
||
f.locaOffsets[i] = uint32(r.readU16()) * 2
|
||
}
|
||
} else {
|
||
for i := 0; i < n; i++ {
|
||
f.locaOffsets[i] = r.readU32()
|
||
}
|
||
}
|
||
}
|
||
|
||
func (f *TTFFont) parseCmap(data []byte) {
|
||
r := &ttfReader{data: data}
|
||
r.skip(2) // version
|
||
numTables := int(r.readU16())
|
||
|
||
type cmapRecord struct {
|
||
platformID uint16
|
||
encodingID uint16
|
||
offset uint32
|
||
}
|
||
records := make([]cmapRecord, numTables)
|
||
for i := range records {
|
||
records[i].platformID = r.readU16()
|
||
records[i].encodingID = r.readU16()
|
||
records[i].offset = r.readU32()
|
||
}
|
||
|
||
// Приоритет: платформа 3 (Windows) encoding 1 (Unicode BMP),
|
||
// затем платформа 0 (Unicode)
|
||
var bestOffset uint32
|
||
bestPriority := -1
|
||
|
||
for _, rec := range records {
|
||
priority := -1
|
||
if rec.platformID == 3 && rec.encodingID == 1 {
|
||
priority = 2 // Windows Unicode BMP — лучший
|
||
} else if rec.platformID == 0 {
|
||
priority = 1
|
||
} else if rec.platformID == 1 && rec.encodingID == 0 {
|
||
priority = 0
|
||
}
|
||
if priority > bestPriority {
|
||
bestPriority = priority
|
||
bestOffset = rec.offset
|
||
}
|
||
}
|
||
|
||
if bestOffset == 0 || bestOffset >= uint32(len(data)) {
|
||
return
|
||
}
|
||
|
||
f.parseCmapFormat(data[bestOffset:])
|
||
}
|
||
|
||
func (f *TTFFont) parseCmapFormat(data []byte) {
|
||
if len(data) < 2 {
|
||
return
|
||
}
|
||
format := binary.BigEndian.Uint16(data[0:2])
|
||
|
||
switch format {
|
||
case 4:
|
||
f.parseCmapFormat4(data)
|
||
case 12:
|
||
f.parseCmapFormat12(data)
|
||
}
|
||
}
|
||
|
||
// Format 4 — наиболее распространённый для BMP
|
||
func (f *TTFFont) parseCmapFormat4(data []byte) {
|
||
if len(data) < 14 {
|
||
return
|
||
}
|
||
segCount := int(binary.BigEndian.Uint16(data[6:8])) / 2
|
||
offset := 14
|
||
|
||
endCodes := make([]uint16, segCount)
|
||
startCodes := make([]uint16, segCount)
|
||
idDeltas := make([]int16, segCount)
|
||
idRangeOffsets := make([]uint16, segCount)
|
||
|
||
for i := range endCodes {
|
||
if offset+2 > len(data) {
|
||
return
|
||
}
|
||
endCodes[i] = binary.BigEndian.Uint16(data[offset : offset+2])
|
||
offset += 2
|
||
}
|
||
offset += 2 // reservedPad
|
||
|
||
for i := range startCodes {
|
||
if offset+2 > len(data) {
|
||
return
|
||
}
|
||
startCodes[i] = binary.BigEndian.Uint16(data[offset : offset+2])
|
||
offset += 2
|
||
}
|
||
for i := range idDeltas {
|
||
if offset+2 > len(data) {
|
||
return
|
||
}
|
||
idDeltas[i] = int16(binary.BigEndian.Uint16(data[offset : offset+2]))
|
||
offset += 2
|
||
}
|
||
rangeOffsetBase := offset
|
||
for i := range idRangeOffsets {
|
||
if offset+2 > len(data) {
|
||
return
|
||
}
|
||
idRangeOffsets[i] = binary.BigEndian.Uint16(data[offset : offset+2])
|
||
offset += 2
|
||
}
|
||
|
||
for i := 0; i < segCount; i++ {
|
||
if startCodes[i] == 0xFFFF {
|
||
break
|
||
}
|
||
for cp := uint32(startCodes[i]); cp <= uint32(endCodes[i]); cp++ {
|
||
var glyphID uint16
|
||
if idRangeOffsets[i] == 0 {
|
||
glyphID = uint16((int32(cp) + int32(idDeltas[i])) & 0xFFFF)
|
||
} else {
|
||
idx := rangeOffsetBase + i*2 + int(idRangeOffsets[i]) + (int(cp)-int(startCodes[i]))*2
|
||
if idx+2 <= len(data) {
|
||
glyphID = binary.BigEndian.Uint16(data[idx : idx+2])
|
||
if glyphID != 0 {
|
||
glyphID = uint16((int32(glyphID) + int32(idDeltas[i])) & 0xFFFF)
|
||
}
|
||
}
|
||
}
|
||
if glyphID != 0 {
|
||
f.cmap[rune(cp)] = glyphID
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Format 12 — полный Unicode (включая supplementary planes)
|
||
func (f *TTFFont) parseCmapFormat12(data []byte) {
|
||
if len(data) < 16 {
|
||
return
|
||
}
|
||
numGroups := int(binary.BigEndian.Uint32(data[12:16]))
|
||
offset := 16
|
||
for i := 0; i < numGroups; i++ {
|
||
if offset+12 > len(data) {
|
||
break
|
||
}
|
||
startChar := binary.BigEndian.Uint32(data[offset : offset+4])
|
||
endChar := binary.BigEndian.Uint32(data[offset+4 : offset+8])
|
||
startGlyph := binary.BigEndian.Uint32(data[offset+8 : offset+12])
|
||
offset += 12
|
||
for cp := startChar; cp <= endChar; cp++ {
|
||
glyph := startGlyph + (cp - startChar)
|
||
if glyph < 65535 {
|
||
f.cmap[rune(cp)] = uint16(glyph)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// GlyphID возвращает glyph ID для символа (0 = .notdef)
|
||
func (f *TTFFont) GlyphID(r rune) uint16 {
|
||
if id, ok := f.cmap[r]; ok {
|
||
return id
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// AdvanceWidth возвращает ширину глифа в 1000-ных единицах (как в PDF)
|
||
func (f *TTFFont) AdvanceWidth(glyphID uint16) int {
|
||
if f.unitsPerEm == 0 || int(glyphID) >= len(f.advanceWidths) {
|
||
return 500
|
||
}
|
||
aw := f.advanceWidths[glyphID]
|
||
return int(math.Round(float64(aw) * 1000.0 / float64(f.unitsPerEm)))
|
||
}
|
||
|
||
// HasCyrillic проверяет что шрифт содержит кириллические глифы
|
||
func (f *TTFFont) HasCyrillic() bool {
|
||
// Проверяем несколько типичных кириллических символов
|
||
for _, r := range []rune{'А', 'а', 'Я', 'я', 'Ё', 'ё'} {
|
||
if id := f.GlyphID(r); id != 0 {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// === Встраивание шрифта в PDF ===
|
||
|
||
// EmbeddedFont — данные для встраивания в PDF
|
||
type EmbeddedFont struct {
|
||
font *TTFFont
|
||
usedGlyphs map[uint16]bool // глифы использованные в документе
|
||
objID int // ID объекта шрифта в PDF
|
||
}
|
||
|
||
// NewEmbeddedFont создаёт контейнер для встраивания
|
||
func NewEmbeddedFont(font *TTFFont) *EmbeddedFont {
|
||
return &EmbeddedFont{
|
||
font: font,
|
||
usedGlyphs: map[uint16]bool{0: true}, // .notdef всегда включаем
|
||
}
|
||
}
|
||
|
||
// UseRune регистрирует использование символа
|
||
func (ef *EmbeddedFont) UseRune(r rune) uint16 {
|
||
gid := ef.font.GlyphID(r)
|
||
ef.usedGlyphs[gid] = true
|
||
return gid
|
||
}
|
||
|
||
// GlyphWidth возвращает ширину глифа для символа в 1/1000 pt
|
||
func (ef *EmbeddedFont) GlyphWidth(r rune) int {
|
||
gid := ef.font.GlyphID(r)
|
||
return ef.font.AdvanceWidth(gid)
|
||
}
|
||
|
||
// buildSubsetGlyf строит подмножество glyf таблицы для использованных глифов.
|
||
// Возвращает: новый glyf blob, новые loca offsets, маппинг старый glyph ID → новый.
|
||
func (ef *EmbeddedFont) buildSubset() (glyf []byte, loca []uint32, oldToNew map[uint16]uint16) {
|
||
font := ef.font
|
||
|
||
// Собираем все нужные глифы (включаем составные — composite glyphs)
|
||
needed := make(map[uint16]bool)
|
||
var collect func(uint16)
|
||
collect = func(gid uint16) {
|
||
if needed[gid] {
|
||
return
|
||
}
|
||
needed[gid] = true
|
||
// Проверяем composite glyph
|
||
if int(gid) < len(font.locaOffsets)-1 {
|
||
off := font.locaOffsets[gid]
|
||
end := font.locaOffsets[gid+1]
|
||
if off < end && int(end) <= len(font.rawGlyf) && int(off)+10 <= len(font.rawGlyf) {
|
||
numContours := int16(binary.BigEndian.Uint16(font.rawGlyf[off : off+2]))
|
||
if numContours < 0 { // composite
|
||
pos := int(off) + 10
|
||
for pos+4 <= int(end) {
|
||
flags := binary.BigEndian.Uint16(font.rawGlyf[pos : pos+2])
|
||
compGlyph := binary.BigEndian.Uint16(font.rawGlyf[pos+2 : pos+4])
|
||
collect(compGlyph)
|
||
pos += 4
|
||
if flags&0x0001 != 0 { // ARG_1_AND_2_ARE_WORDS
|
||
pos += 4
|
||
} else {
|
||
pos += 2
|
||
}
|
||
if flags&0x0008 != 0 { // WE_HAVE_A_SCALE
|
||
pos += 2
|
||
} else if flags&0x0040 != 0 { // WE_HAVE_AN_X_AND_Y_SCALE
|
||
pos += 4
|
||
} else if flags&0x0080 != 0 { // WE_HAVE_A_TWO_BY_TWO
|
||
pos += 8
|
||
}
|
||
if flags&0x0020 == 0 { // не MORE_COMPONENTS
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for gid := range ef.usedGlyphs {
|
||
collect(gid)
|
||
}
|
||
|
||
// Упорядочиваем
|
||
sortedGIDs := make([]uint16, 0, len(needed))
|
||
for gid := range needed {
|
||
sortedGIDs = append(sortedGIDs, gid)
|
||
}
|
||
sort.Slice(sortedGIDs, func(i, j int) bool { return sortedGIDs[i] < sortedGIDs[j] })
|
||
|
||
// Маппинг старый → новый
|
||
oldToNew = make(map[uint16]uint16, len(sortedGIDs))
|
||
for newID, oldID := range sortedGIDs {
|
||
oldToNew[oldID] = uint16(newID)
|
||
}
|
||
|
||
// Строим новый glyf и loca
|
||
loca = make([]uint32, len(sortedGIDs)+1)
|
||
var glyf_buf bytes.Buffer
|
||
|
||
for i, oldGID := range sortedGIDs {
|
||
loca[i] = uint32(glyf_buf.Len())
|
||
if int(oldGID) < len(font.locaOffsets)-1 {
|
||
off := font.locaOffsets[oldGID]
|
||
end := font.locaOffsets[oldGID+1]
|
||
if off < end && int(end) <= len(font.rawGlyf) {
|
||
glyfData := font.rawGlyf[off:end]
|
||
// Для composite глифов обновляем glyph ID внутри
|
||
numContours := int16(binary.BigEndian.Uint16(glyfData[0:2]))
|
||
if numContours < 0 {
|
||
updated := make([]byte, len(glyfData))
|
||
copy(updated, glyfData)
|
||
pos := 10
|
||
for pos+4 <= len(updated) {
|
||
flags := binary.BigEndian.Uint16(updated[pos : pos+2])
|
||
oldCGID := binary.BigEndian.Uint16(updated[pos+2 : pos+4])
|
||
if newCGID, ok := oldToNew[oldCGID]; ok {
|
||
binary.BigEndian.PutUint16(updated[pos+2:pos+4], newCGID)
|
||
}
|
||
pos += 4
|
||
if flags&0x0001 != 0 {
|
||
pos += 4
|
||
} else {
|
||
pos += 2
|
||
}
|
||
if flags&0x0008 != 0 {
|
||
pos += 2
|
||
} else if flags&0x0040 != 0 {
|
||
pos += 4
|
||
} else if flags&0x0080 != 0 {
|
||
pos += 8
|
||
}
|
||
if flags&0x0020 == 0 {
|
||
break
|
||
}
|
||
}
|
||
glyf_buf.Write(updated)
|
||
} else {
|
||
glyf_buf.Write(glyfData)
|
||
}
|
||
// Выравнивание по 4 байтам
|
||
for glyf_buf.Len()%4 != 0 {
|
||
glyf_buf.WriteByte(0)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
loca[len(sortedGIDs)] = uint32(glyf_buf.Len())
|
||
|
||
return glyf_buf.Bytes(), loca, oldToNew
|
||
}
|
||
|
||
// WriteToPDF записывает встроенный шрифт в PDF и возвращает словарь шрифта
|
||
func (ef *EmbeddedFont) WriteToPDF(doc *pdfDoc, fontName string) (fontDictStr string) {
|
||
font := ef.font
|
||
glyf, loca, oldToNew := ef.buildSubset()
|
||
|
||
numNewGlyphs := len(oldToNew)
|
||
|
||
// Строим hmtx для подмножества
|
||
hmtx := make([]byte, numNewGlyphs*4)
|
||
sortedNew := make([]uint16, numNewGlyphs)
|
||
for old, newID := range oldToNew {
|
||
sortedNew[newID] = old
|
||
}
|
||
for newID, oldGID := range sortedNew {
|
||
aw := uint16(500)
|
||
if int(oldGID) < len(font.advanceWidths) {
|
||
aw = font.advanceWidths[oldGID]
|
||
}
|
||
binary.BigEndian.PutUint16(hmtx[newID*4:], aw)
|
||
// lsb = 0
|
||
}
|
||
|
||
// Строим loca в long формате
|
||
locaBytes := make([]byte, (numNewGlyphs+1)*4)
|
||
for i, off := range loca {
|
||
binary.BigEndian.PutUint32(locaBytes[i*4:], off)
|
||
}
|
||
|
||
// Синтетическая cmap format 4 для подмножества
|
||
cmapData := buildCmapFormat4(ef, oldToNew)
|
||
|
||
// Обновляем hhea (numHMetrics)
|
||
hhea := make([]byte, len(font.rawHhea))
|
||
copy(hhea, font.rawHhea)
|
||
if len(hhea) >= 36 {
|
||
binary.BigEndian.PutUint16(hhea[34:36], uint16(numNewGlyphs))
|
||
}
|
||
|
||
// Обновляем maxp (numGlyphs)
|
||
maxp := make([]byte, len(font.rawMaxp))
|
||
copy(maxp, font.rawMaxp)
|
||
if len(maxp) >= 6 {
|
||
binary.BigEndian.PutUint16(maxp[4:6], uint16(numNewGlyphs))
|
||
}
|
||
|
||
// Обновляем head (indexToLocFormat = 1 = long)
|
||
head := make([]byte, len(font.rawHead))
|
||
copy(head, font.rawHead)
|
||
if len(head) >= 52 {
|
||
binary.BigEndian.PutUint16(head[50:52], 1)
|
||
// Сбрасываем checkSumAdjustment
|
||
binary.BigEndian.PutUint32(head[8:12], 0)
|
||
}
|
||
|
||
// Собираем TTF subset
|
||
tableData := map[string][]byte{
|
||
"cmap": cmapData,
|
||
"glyf": glyf,
|
||
"head": head,
|
||
"hhea": hhea,
|
||
"hmtx": hmtx,
|
||
"loca": locaBytes,
|
||
"maxp": maxp,
|
||
}
|
||
if len(font.rawOS2) > 0 {
|
||
tableData["OS/2"] = font.rawOS2
|
||
}
|
||
if len(font.rawPost) > 0 {
|
||
tableData["post"] = buildMinimalPost()
|
||
}
|
||
|
||
subsetTTF := buildTTF(tableData)
|
||
|
||
// Объект FontFile2
|
||
fontFileID := doc.newObject()
|
||
doc.write(fmt.Sprintf(
|
||
"<< /Length %d /Length1 %d >>\nstream\n",
|
||
len(subsetTTF), len(subsetTTF),
|
||
))
|
||
doc.buf.Write(subsetTTF)
|
||
doc.write("\nendstream\nendobj\n\n")
|
||
|
||
// Объект FontDescriptor
|
||
descID := doc.newObject()
|
||
doc.write(fmt.Sprintf(
|
||
"<< /Type /FontDescriptor /FontName /%s\n"+
|
||
" /Flags 32 /FontBBox [-500 -200 1500 1000]\n"+
|
||
" /ItalicAngle 0 /Ascent 800 /Descent -200\n"+
|
||
" /CapHeight 700 /StemV 80\n"+
|
||
" /FontFile2 %d 0 R\n"+
|
||
">>\nendobj\n\n",
|
||
fontName, fontFileID,
|
||
))
|
||
|
||
// Строим массив ширин для PDF (W array для Type0)
|
||
// Для простоты используем CIDFont с диапазонами
|
||
widths := buildWidthsArray(ef, oldToNew)
|
||
|
||
// Объект CIDFont
|
||
cidID := doc.newObject()
|
||
doc.write(fmt.Sprintf(
|
||
"<< /Type /Font /Subtype /CIDFontType2\n"+
|
||
" /BaseFont /%s\n"+
|
||
" /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>\n"+
|
||
" /FontDescriptor %d 0 R\n"+
|
||
" /DW 500\n"+
|
||
" /W %s\n"+
|
||
">>\nendobj\n\n",
|
||
fontName, descID, widths,
|
||
))
|
||
|
||
// ToUnicode CMap
|
||
toUnicodeID := buildToUnicodeCMap(doc, ef, oldToNew)
|
||
|
||
// Объект Type0 Font (основной)
|
||
fontObjID := doc.newObject()
|
||
doc.write(fmt.Sprintf(
|
||
"<< /Type /Font /Subtype /Type0\n"+
|
||
" /BaseFont /%s\n"+
|
||
" /Encoding /Identity-H\n"+
|
||
" /DescendantFonts [%d 0 R]\n"+
|
||
" /ToUnicode %d 0 R\n"+
|
||
">>\nendobj\n\n",
|
||
fontName, cidID, toUnicodeID,
|
||
))
|
||
|
||
ef.objID = fontObjID
|
||
return fmt.Sprintf("%d 0 R", fontObjID)
|
||
}
|
||
|
||
// === Вспомогательные для построения TTF subset ===
|
||
|
||
// buildCmapFormat4 строит cmap format 4 для подмножества глифов
|
||
func buildCmapFormat4(ef *EmbeddedFont, oldToNew map[uint16]uint16) []byte {
|
||
// Собираем маппинг Unicode → новый glyph ID
|
||
type mapping struct {
|
||
cp uint16
|
||
newID uint16
|
||
}
|
||
var mappings []mapping
|
||
for r, oldID := range ef.font.cmap {
|
||
if newID, ok := oldToNew[oldID]; ok && r <= 0xFFFF {
|
||
mappings = append(mappings, mapping{uint16(r), newID})
|
||
}
|
||
}
|
||
sort.Slice(mappings, func(i, j int) bool { return mappings[i].cp < mappings[j].cp })
|
||
|
||
// Строим сегменты для format 4
|
||
type seg struct {
|
||
start, end uint16
|
||
delta int16
|
||
}
|
||
var segs []seg
|
||
|
||
if len(mappings) > 0 {
|
||
cur := seg{mappings[0].cp, mappings[0].cp, int16(mappings[0].newID) - int16(mappings[0].cp)}
|
||
for _, m := range mappings[1:] {
|
||
expectedDelta := int16(m.newID) - int16(m.cp)
|
||
if m.cp == cur.end+1 && expectedDelta == cur.delta {
|
||
cur.end = m.cp
|
||
} else {
|
||
segs = append(segs, cur)
|
||
cur = seg{m.cp, m.cp, expectedDelta}
|
||
}
|
||
}
|
||
segs = append(segs, cur)
|
||
}
|
||
segs = append(segs, seg{0xFFFF, 0xFFFF, 1}) // финальный сегмент
|
||
|
||
segCount := len(segs)
|
||
tableLen := 14 + segCount*8 + 2
|
||
buf := make([]byte, tableLen)
|
||
|
||
binary.BigEndian.PutUint16(buf[0:], 4)
|
||
binary.BigEndian.PutUint16(buf[2:], uint16(tableLen))
|
||
binary.BigEndian.PutUint16(buf[4:], 0) // language
|
||
binary.BigEndian.PutUint16(buf[6:], uint16(segCount*2))
|
||
// searchRange, entrySelector, rangeShift — пропускаем (не критично)
|
||
|
||
off := 14
|
||
for _, s := range segs {
|
||
binary.BigEndian.PutUint16(buf[off:], s.end)
|
||
off += 2
|
||
}
|
||
binary.BigEndian.PutUint16(buf[off:], 0) // pad
|
||
off += 2
|
||
for _, s := range segs {
|
||
binary.BigEndian.PutUint16(buf[off:], s.start)
|
||
off += 2
|
||
}
|
||
for _, s := range segs {
|
||
binary.BigEndian.PutUint16(buf[off:], uint16(s.delta))
|
||
off += 2
|
||
}
|
||
for range segs {
|
||
binary.BigEndian.PutUint16(buf[off:], 0) // idRangeOffset = 0
|
||
off += 2
|
||
}
|
||
|
||
// Полная cmap таблица
|
||
full := make([]byte, 4+8+tableLen)
|
||
binary.BigEndian.PutUint16(full[0:], 0) // version
|
||
binary.BigEndian.PutUint16(full[2:], 1) // numTables
|
||
binary.BigEndian.PutUint16(full[4:], 3) // platformID = Windows
|
||
binary.BigEndian.PutUint16(full[6:], 1) // encodingID = Unicode BMP
|
||
binary.BigEndian.PutUint32(full[8:], 12) // offset
|
||
copy(full[12:], buf)
|
||
return full
|
||
}
|
||
|
||
// buildWidthsArray строит массив ширин для CIDFont /W
|
||
func buildWidthsArray(ef *EmbeddedFont, oldToNew map[uint16]uint16) string {
|
||
type entry struct {
|
||
newID int
|
||
width int
|
||
}
|
||
var entries []entry
|
||
for oldID, newID := range oldToNew {
|
||
w := ef.font.AdvanceWidth(oldID)
|
||
entries = append(entries, entry{int(newID), w})
|
||
}
|
||
sort.Slice(entries, func(i, j int) bool { return entries[i].newID < entries[j].newID })
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString("[")
|
||
for _, e := range entries {
|
||
sb.WriteString(fmt.Sprintf("%d [%d] ", e.newID, e.width))
|
||
}
|
||
sb.WriteString("]")
|
||
return sb.String()
|
||
}
|
||
|
||
// buildToUnicodeCMap записывает ToUnicode CMap объект и возвращает его ID
|
||
func buildToUnicodeCMap(doc *pdfDoc, ef *EmbeddedFont, oldToNew map[uint16]uint16) int {
|
||
// Обратный маппинг: новый glyph ID → Unicode
|
||
newIDtoUnicode := make(map[uint16]rune)
|
||
for r, oldID := range ef.font.cmap {
|
||
if newID, ok := oldToNew[oldID]; ok {
|
||
newIDtoUnicode[newID] = r
|
||
}
|
||
}
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString("/CIDInit /ProcSet findresource begin\n")
|
||
sb.WriteString("12 dict begin\nbegincmap\n")
|
||
sb.WriteString("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n")
|
||
sb.WriteString("/CMapName /Adobe-Identity-UCS def\n")
|
||
sb.WriteString("/CMapType 2 def\n")
|
||
sb.WriteString("1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n")
|
||
|
||
// Группируем по 100
|
||
type pair struct {
|
||
gid uint16
|
||
r rune
|
||
}
|
||
var pairs []pair
|
||
for gid, r := range newIDtoUnicode {
|
||
pairs = append(pairs, pair{gid, r})
|
||
}
|
||
sort.Slice(pairs, func(i, j int) bool { return pairs[i].gid < pairs[j].gid })
|
||
|
||
for i := 0; i < len(pairs); i += 100 {
|
||
end := i + 100
|
||
if end > len(pairs) {
|
||
end = len(pairs)
|
||
}
|
||
chunk := pairs[i:end]
|
||
sb.WriteString(fmt.Sprintf("%d beginbfchar\n", len(chunk)))
|
||
for _, p := range chunk {
|
||
sb.WriteString(fmt.Sprintf("<%04X> <%04X>\n", p.gid, p.r))
|
||
}
|
||
sb.WriteString("endbfchar\n")
|
||
}
|
||
|
||
sb.WriteString("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n")
|
||
|
||
cmap := sb.String()
|
||
id := doc.newObject()
|
||
doc.write(fmt.Sprintf("<< /Length %d >>\nstream\n", len(cmap)))
|
||
doc.write(cmap)
|
||
doc.write("\nendstream\nendobj\n\n")
|
||
return id
|
||
}
|
||
|
||
// buildMinimalPost строит минимальную post таблицу (version 3.0 — без данных глифов)
|
||
func buildMinimalPost() []byte {
|
||
b := make([]byte, 32)
|
||
binary.BigEndian.PutUint32(b[0:], 0x00030000) // version 3.0
|
||
// italicAngle, underlinePosition и т.д. — нули
|
||
return b
|
||
}
|
||
|
||
// buildTTF собирает TTF файл из таблиц
|
||
func buildTTF(tables map[string][]byte) []byte {
|
||
// Имена таблиц в алфавитном порядке (требование TTF)
|
||
names := make([]string, 0, len(tables))
|
||
for name := range tables {
|
||
names = append(names, name)
|
||
}
|
||
sort.Strings(names)
|
||
|
||
numTables := len(names)
|
||
headerSize := 12 + numTables*16
|
||
|
||
// Вычисляем searchRange, entrySelector, rangeShift
|
||
sr := 1
|
||
for sr*2 <= numTables {
|
||
sr *= 2
|
||
}
|
||
searchRange := sr * 16
|
||
entrySelector := 0
|
||
for i := 1; i < sr; i *= 2 {
|
||
entrySelector++
|
||
}
|
||
rangeShift := numTables*16 - searchRange
|
||
|
||
var header bytes.Buffer
|
||
writeU32(&header, 0x00010000) // sfVersion
|
||
writeU16(&header, uint16(numTables))
|
||
writeU16(&header, uint16(searchRange))
|
||
writeU16(&header, uint16(entrySelector))
|
||
writeU16(&header, uint16(rangeShift))
|
||
|
||
// Вычисляем офсеты таблиц
|
||
offset := uint32(headerSize)
|
||
type tableEntry struct {
|
||
name string
|
||
data []byte
|
||
offset uint32
|
||
}
|
||
entries := make([]tableEntry, len(names))
|
||
for i, name := range names {
|
||
data := tables[name]
|
||
entries[i] = tableEntry{name, data, offset}
|
||
offset += uint32(len(data))
|
||
for offset%4 != 0 {
|
||
offset++
|
||
}
|
||
}
|
||
|
||
// Записываем directory
|
||
for _, e := range entries {
|
||
header.WriteString(fmt.Sprintf("%-4s", e.name))
|
||
writeU32(&header, ttfChecksum(e.data))
|
||
writeU32(&header, e.offset)
|
||
writeU32(&header, uint32(len(e.data)))
|
||
}
|
||
|
||
// Собираем финальный файл
|
||
var out bytes.Buffer
|
||
out.Write(header.Bytes())
|
||
for _, e := range entries {
|
||
out.Write(e.data)
|
||
for out.Len()%4 != 0 {
|
||
out.WriteByte(0)
|
||
}
|
||
}
|
||
|
||
// Обновляем checkSumAdjustment в head
|
||
result := out.Bytes()
|
||
total := ttfChecksumBytes(result)
|
||
adjustment := 0xB1B0AFBA - total
|
||
// Находим head таблицу
|
||
for _, e := range entries {
|
||
if e.name == "head" && int(e.offset)+12 <= len(result) {
|
||
binary.BigEndian.PutUint32(result[e.offset+8:e.offset+12], uint32(adjustment))
|
||
break
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
func ttfChecksum(data []byte) uint32 {
|
||
return ttfChecksumBytes(data)
|
||
}
|
||
|
||
func ttfChecksumBytes(data []byte) uint32 {
|
||
var sum uint32
|
||
for i := 0; i+3 < len(data); i += 4 {
|
||
sum += binary.BigEndian.Uint32(data[i : i+4])
|
||
}
|
||
// Обрабатываем хвост
|
||
rem := len(data) % 4
|
||
if rem > 0 {
|
||
var tmp [4]byte
|
||
copy(tmp[:], data[len(data)-rem:])
|
||
sum += binary.BigEndian.Uint32(tmp[:])
|
||
}
|
||
return sum
|
||
}
|
||
|
||
func writeU16(w io.Writer, v uint16) {
|
||
var b [2]byte
|
||
binary.BigEndian.PutUint16(b[:], v)
|
||
w.Write(b[:])
|
||
}
|
||
|
||
func writeU32(w io.Writer, v uint32) {
|
||
var b [4]byte
|
||
binary.BigEndian.PutUint32(b[:], v)
|
||
w.Write(b[:])
|
||
}
|
||
|
||
// === Вспомогательный reader для TTF ===
|
||
|
||
type ttfReader struct {
|
||
data []byte
|
||
pos int
|
||
}
|
||
|
||
func (r *ttfReader) readU8() uint8 {
|
||
if r.pos >= len(r.data) {
|
||
return 0
|
||
}
|
||
v := r.data[r.pos]
|
||
r.pos++
|
||
return v
|
||
}
|
||
|
||
func (r *ttfReader) readU16() uint16 {
|
||
if r.pos+2 > len(r.data) {
|
||
return 0
|
||
}
|
||
v := binary.BigEndian.Uint16(r.data[r.pos : r.pos+2])
|
||
r.pos += 2
|
||
return v
|
||
}
|
||
|
||
func (r *ttfReader) readI16() int16 {
|
||
return int16(r.readU16())
|
||
}
|
||
|
||
func (r *ttfReader) readU32() uint32 {
|
||
if r.pos+4 > len(r.data) {
|
||
return 0
|
||
}
|
||
v := binary.BigEndian.Uint32(r.data[r.pos : r.pos+4])
|
||
r.pos += 4
|
||
return v
|
||
}
|
||
|
||
func (r *ttfReader) readBytes(n int) []byte {
|
||
if r.pos+n > len(r.data) {
|
||
n = len(r.data) - r.pos
|
||
}
|
||
v := r.data[r.pos : r.pos+n]
|
||
r.pos += n
|
||
return v
|
||
}
|
||
|
||
func (r *ttfReader) skip(n int) {
|
||
r.pos += n
|
||
if r.pos > len(r.data) {
|
||
r.pos = len(r.data)
|
||
}
|
||
}
|
||
|
||
func safeSlice(data []byte, offset, length uint32) []byte {
|
||
if data == nil {
|
||
return nil
|
||
}
|
||
end := offset + length
|
||
if int(offset) >= len(data) {
|
||
return nil
|
||
}
|
||
if int(end) > len(data) {
|
||
end = uint32(len(data))
|
||
}
|
||
result := make([]byte, end-offset)
|
||
copy(result, data[offset:end])
|
||
return result
|
||
}
|