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> \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 }