grimoir/pdf_font.go
2026-05-07 09:52:06 +03:00

1113 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}