diff --git a/coloremoji.go b/coloremoji.go new file mode 100644 index 00000000..c99e8657 --- /dev/null +++ b/coloremoji.go @@ -0,0 +1,240 @@ +package gofpdf + +import ( + "fmt" + "strings" +) + +// ColorEmojiRenderer handles rendering of color emoji glyphs +type ColorEmojiRenderer struct { + utf8File *utf8FontFile + unitsPerEm int +} + +// NewColorEmojiRenderer creates a new color emoji renderer +func NewColorEmojiRenderer(utf8File *utf8FontFile) *ColorEmojiRenderer { + return &ColorEmojiRenderer{ + utf8File: utf8File, + unitsPerEm: utf8File.GetUnitsPerEm(), + } +} + +// RenderColorGlyph renders a color glyph to PDF path operators +// x, y are the position in PDF user units +// fontSize is the font size in points +// k is the scale factor (points per user unit) +func (r *ColorEmojiRenderer) RenderColorGlyph(glyphID uint16, x, y, fontSize, k float64) string { + if r.utf8File == nil || !r.utf8File.HasColorGlyphs() { + return "" + } + + layers := r.utf8File.GetColorGlyphLayers(glyphID) + if layers == nil { + return "" + } + + var result strings.Builder + scale := fontSize / float64(r.unitsPerEm) + + // Render layers back-to-front (first layer is bottom) + for _, layer := range layers { + color := r.utf8File.GetPaletteColor(layer.PaletteIndex) + outline := r.utf8File.ParseGlyphOutline(layer.GlyphID) + + if outline == nil || len(outline.Contours) == 0 { + continue + } + + // Save graphics state + result.WriteString("q ") + + // Set fill color (RGB normalized to 0-1) + if color.A < 255 { + // Handle transparency + alpha := float64(color.A) / 255.0 + result.WriteString(fmt.Sprintf("%.3f %.3f %.3f rg ", + float64(color.R)/255.0, + float64(color.G)/255.0, + float64(color.B)/255.0)) + // Note: Full alpha support requires ExtGState which is more complex + // For now we just use the RGB values + _ = alpha + } else { + result.WriteString(fmt.Sprintf("%.3f %.3f %.3f rg ", + float64(color.R)/255.0, + float64(color.G)/255.0, + float64(color.B)/255.0)) + } + + // Convert outline to PDF path + pathStr := glyphOutlineToPDFPath(outline, x, y, scale, k) + result.WriteString(pathStr) + + // Fill the path + result.WriteString("f ") + + // Restore graphics state + result.WriteString("Q ") + } + + return result.String() +} + +// glyphOutlineToPDFPath converts a glyph outline to PDF path operators +// x, y: position in user units +// scale: fontSize / unitsPerEm +// k: points per user unit +func glyphOutlineToPDFPath(outline *GlyphOutline, x, y, scale, k float64) string { + if outline == nil || len(outline.Contours) == 0 { + return "" + } + + var result strings.Builder + + for _, contour := range outline.Contours { + if len(contour) < 2 { + continue + } + + pathOps := contourToPDFOps(contour, x, y, scale, k) + result.WriteString(pathOps) + } + + return result.String() +} + +// contourToPDFOps converts a single contour to PDF path operators +func contourToPDFOps(contour GlyphContour, baseX, baseY, scale, k float64) string { + if len(contour) < 2 { + return "" + } + + var result strings.Builder + + // Helper to transform coordinates + transform := func(pt GlyphPoint) (float64, float64) { + // Apply scale and flip Y for PDF coordinate system + px := baseX + pt.X*scale + py := baseY + pt.Y*scale // Y is bottom-up in PDF + // Convert to points + return px * k, py * k + } + + // Find the first on-curve point or create one + startIdx := -1 + for i, pt := range contour { + if pt.OnCurve { + startIdx = i + break + } + } + + // If no on-curve point, start with midpoint between first two off-curve points + if startIdx == -1 { + // All points are off-curve, create implicit on-curve point + p0 := contour[0] + p1 := contour[1] + midX := (p0.X + p1.X) / 2 + midY := (p0.Y + p1.Y) / 2 + px, py := transform(GlyphPoint{X: midX, Y: midY, OnCurve: true}) + result.WriteString(fmt.Sprintf("%.2f %.2f m ", px, py)) + startIdx = 0 + } else { + // Move to first on-curve point + px, py := transform(contour[startIdx]) + result.WriteString(fmt.Sprintf("%.2f %.2f m ", px, py)) + } + + // Process remaining points + n := len(contour) + i := (startIdx + 1) % n + for count := 0; count < n; count++ { + curr := contour[i] + next := contour[(i+1)%n] + + if curr.OnCurve { + // Line to on-curve point + px, py := transform(curr) + result.WriteString(fmt.Sprintf("%.2f %.2f l ", px, py)) + } else { + // Quadratic bezier - we have an off-curve control point + // TrueType uses quadratic beziers, PDF uses cubic + // Convert quadratic to cubic: + // C1 = P0 + 2/3 * (P1 - P0) + // C2 = P2 + 2/3 * (P1 - P2) + + // Get the previous point (start of curve) + prevIdx := (i - 1 + n) % n + prev := contour[prevIdx] + + // If previous point is also off-curve, use implicit midpoint + var p0X, p0Y float64 + if !prev.OnCurve { + p0X = (prev.X + curr.X) / 2 + p0Y = (prev.Y + curr.Y) / 2 + } else { + p0X = prev.X + p0Y = prev.Y + } + + // Get the end point + var p2X, p2Y float64 + if next.OnCurve { + p2X = next.X + p2Y = next.Y + } else { + // Implicit midpoint between two off-curve points + p2X = (curr.X + next.X) / 2 + p2Y = (curr.Y + next.Y) / 2 + } + + // Control point (off-curve) + p1X := curr.X + p1Y := curr.Y + + // Convert quadratic to cubic bezier control points + c1X := p0X + 2.0/3.0*(p1X-p0X) + c1Y := p0Y + 2.0/3.0*(p1Y-p0Y) + c2X := p2X + 2.0/3.0*(p1X-p2X) + c2Y := p2Y + 2.0/3.0*(p1Y-p2Y) + + // Transform all points + c1px, c1py := transform(GlyphPoint{X: c1X, Y: c1Y}) + c2px, c2py := transform(GlyphPoint{X: c2X, Y: c2Y}) + epx, epy := transform(GlyphPoint{X: p2X, Y: p2Y}) + + result.WriteString(fmt.Sprintf("%.2f %.2f %.2f %.2f %.2f %.2f c ", + c1px, c1py, c2px, c2py, epx, epy)) + + // If next point is on-curve and not the implicit midpoint, skip it + // as we've already used it as the endpoint + if next.OnCurve { + i = (i + 1) % n + count++ + } + } + + i = (i + 1) % n + } + + // Close the path + result.WriteString("h ") + + return result.String() +} + +// IsColorGlyph checks if a glyph ID has color layers +func (r *ColorEmojiRenderer) IsColorGlyph(glyphID uint16) bool { + if r.utf8File == nil || !r.utf8File.HasColorGlyphs() { + return false + } + return r.utf8File.GetColorGlyphLayers(glyphID) != nil +} + +// GetGlyphLayers returns the color layers for a glyph +func (r *ColorEmojiRenderer) GetGlyphLayers(glyphID uint16) []LayerRecord { + if r.utf8File == nil { + return nil + } + return r.utf8File.GetColorGlyphLayers(glyphID) +} diff --git a/coloremoji_test.go b/coloremoji_test.go new file mode 100644 index 00000000..bbd68044 --- /dev/null +++ b/coloremoji_test.go @@ -0,0 +1,147 @@ +package gofpdf + +import ( + "os" + "testing" +) + +func TestCOLRCPALParsing(t *testing.T) { + // Test with a font that doesn't have COLR/CPAL tables + fontPath := "font/calligra.ttf" + fontData, err := os.ReadFile(fontPath) + if err != nil { + t.Skipf("Font file not found: %s", fontPath) + return + } + + reader := fileReader{readerPosition: 0, array: fontData} + utf8File := newUTF8Font(&reader) + err = utf8File.parseFile() + if err != nil { + t.Fatalf("Failed to parse font: %v", err) + } + + // Calligra should not have color glyphs + if utf8File.HasColorGlyphs() { + t.Error("Expected calligra font to not have color glyphs") + } +} + +func TestGlyphOutlineParsing(t *testing.T) { + // Test glyph outline parsing with a regular font + fontPath := "font/calligra.ttf" + fontData, err := os.ReadFile(fontPath) + if err != nil { + t.Skipf("Font file not found: %s", fontPath) + return + } + + reader := fileReader{readerPosition: 0, array: fontData} + utf8File := newUTF8Font(&reader) + err = utf8File.parseFile() + if err != nil { + t.Fatalf("Failed to parse font: %v", err) + } + + // We need to initialize symbolPosition by calling GenerateCutFont or similar + // For now, just check that the method doesn't panic + utf8File.fileReader.readerPosition = 0 + utf8File.symbolPosition = make([]int, 0) + utf8File.charSymbolDictionary = make(map[int]int) + utf8File.tableDescriptions = make(map[string]*tableDescription) + utf8File.outTablesData = make(map[string][]byte) + utf8File.skip(4) + utf8File.generateTableDescriptions() + + // Parse loca table to get symbol positions + utf8File.SeekTable("head") + utf8File.skip(50) + locaFormat := utf8File.readUint16() + + utf8File.SeekTable("maxp") + utf8File.skip(4) + numSymbols := utf8File.readUint16() + + utf8File.parseLOCATable(locaFormat, numSymbols) + + // Try to parse a glyph outline + outline := utf8File.ParseGlyphOutline(0) + if outline == nil { + t.Log("Glyph 0 has no outline (may be .notdef)") + } + + // Test with a higher glyph ID that likely has an outline + outline = utf8File.ParseGlyphOutline(10) + if outline != nil && len(outline.Contours) > 0 { + t.Logf("Glyph 10 has %d contours", len(outline.Contours)) + } +} + +func TestColorEmojiAPI(t *testing.T) { + pdf := New("P", "mm", "A4", "font") + pdf.AddPage() + pdf.AddUTF8Font("DejaVu", "", "DejaVuSansCondensed.ttf") + pdf.SetFont("DejaVu", "", 14) + + // Test the API methods exist and work + pdf.SetColorEmojiEnabled(true) + if pdf.colorEmojiEnabled != true { + t.Error("Expected colorEmojiEnabled to be true") + } + + // DejaVu doesn't have color glyphs, so HasColorEmoji should return false + if pdf.HasColorEmoji() { + t.Error("Expected HasColorEmoji to return false for DejaVu font") + } + + pdf.SetColorEmojiEnabled(false) + if pdf.colorEmojiEnabled != false { + t.Error("Expected colorEmojiEnabled to be false") + } +} + +func TestGlyphOutlineTransforms(t *testing.T) { + // Create a simple test outline + outline := &GlyphOutline{ + Bounds: [4]int16{0, 0, 100, 100}, + Contours: []GlyphContour{ + { + {X: 0, Y: 0, OnCurve: true}, + {X: 100, Y: 0, OnCurve: true}, + {X: 100, Y: 100, OnCurve: true}, + {X: 0, Y: 100, OnCurve: true}, + }, + }, + } + + // Test scale + scaled := ScaleOutline(outline, 2.0) + if scaled.Contours[0][1].X != 200 { + t.Errorf("Expected scaled X to be 200, got %f", scaled.Contours[0][1].X) + } + + // Test translate + translated := TranslateOutline(outline, 50, 50) + if translated.Contours[0][0].X != 50 || translated.Contours[0][0].Y != 50 { + t.Errorf("Expected translated point to be (50, 50), got (%f, %f)", + translated.Contours[0][0].X, translated.Contours[0][0].Y) + } + + // Test Y flip + flipped := FlipY(outline) + if flipped.Contours[0][2].Y != -100 { + t.Errorf("Expected flipped Y to be -100, got %f", flipped.Contours[0][2].Y) + } +} + +func TestColorRecordParsing(t *testing.T) { + // Test that GetPaletteColor returns a default color when no CPAL table exists + reader := fileReader{readerPosition: 0, array: []byte{}} + utf8File := newUTF8Font(&reader) + + color := utf8File.GetPaletteColor(0) + if color.R != 0 || color.G != 0 || color.B != 0 || color.A != 255 { + t.Errorf("Expected default color (0,0,0,255), got (%d,%d,%d,%d)", + color.R, color.G, color.B, color.A) + } +} diff --git a/def.go b/def.go index 6a7030f9..3308cd22 100644 --- a/def.go +++ b/def.go @@ -23,6 +23,7 @@ import ( "encoding/json" "fmt" "io" + "strconv" "time" ) @@ -602,6 +603,7 @@ type Fpdf struct { } spotColorMap map[string]spotColorType // Map of named ink-based colors userUnderlineThickness float64 // A custom user underline thickness multiplier. + colorEmojiEnabled bool // Enable color emoji rendering } type encType struct { @@ -697,22 +699,62 @@ type FontDescType struct { } type fontDefType struct { - Tp string // "Core", "TrueType", ... - Name string // "Courier-Bold", ... - Desc FontDescType // Font descriptor - Up int // Underline position - Ut int // Underline thickness - Cw []int // Character width by ordinal - Enc string // "cp1252", ... - Diff string // Differences from reference encoding - File string // "Redressed.z" - Size1, Size2 int // Type1 values - OriginalSize int // Size of uncompressed font file - N int // Set by font loader - DiffN int // Position of diff in app array, set by font loader - i string // 1-based position in font list, set by font loader, not this program - utf8File *utf8FontFile // UTF-8 font - usedRunes map[int]int // Array of used runes + Tp string // "Core", "TrueType", ... + Name string // "Courier-Bold", ... + Desc FontDescType // Font descriptor + Up int // Underline position + Ut int // Underline thickness + Cw map[int]int // Character width by ordinal + Enc string // "cp1252", ... + Diff string // Differences from reference encoding + File string // "Redressed.z" + Size1, Size2 int // Type1 values + OriginalSize int // Size of uncompressed font file + N int // Set by font loader + DiffN int // Position of diff in app array, set by font loader + i string // 1-based position in font list, set by font loader, not this program + utf8File *utf8FontFile // UTF-8 font + usedRunes map[int]int // Array of used runes + runeToCid map[int]int // Map of rune to CID (for remapping) + nextFreeCID int // Next available CID for remapping + HasColorGlyphs bool // True if font has COLR/CPAL color glyph data +} + +// UnmarshalJSON handles both array (legacy) and map (new) formats for Cw +func (f *fontDefType) UnmarshalJSON(data []byte) error { + type Alias fontDefType + aux := &struct { + Cw interface{} + *Alias + }{ + Alias: (*Alias)(f), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + f.Cw = make(map[int]int) + if aux.Cw != nil { + switch v := aux.Cw.(type) { + case []interface{}: + for i, val := range v { + if fVal, ok := val.(float64); ok { + if fVal != 0 { + f.Cw[i] = int(fVal) + } + } + } + case map[string]interface{}: + for k, val := range v { + if fVal, ok := val.(float64); ok { + if i, err := strconv.Atoi(k); err == nil { + f.Cw[i] = int(fVal) + } + } + } + } + } + return nil } // generateFontID generates a font Id from the font definition diff --git a/font.go b/font.go index 29417bb0..a309c764 100644 --- a/font.go +++ b/font.go @@ -344,7 +344,13 @@ func makeDefinitionFile(fileStr, tpStr, encodingFileStr string, embed bool, encL // dump(def.Desc.FontBBox) def.Up = info.UnderlinePosition def.Ut = info.UnderlineThickness - def.Cw = info.Widths + // def.Cw = info.Widths + def.Cw = make(map[int]int) + for i, w := range info.Widths { + if w != 0 { + def.Cw[i] = w + } + } def.Enc = baseNoExt(encodingFileStr) // fmt.Printf("encodingFileStr [%s], def.Enc [%s]\n", encodingFileStr, def.Enc) // fmt.Printf("reference [%s]\n", filepath.Join(filepath.Dir(encodingFileStr), "cp1252.map")) diff --git a/fpdf.go b/fpdf.go index 3be1cdc2..a98211bb 100644 --- a/fpdf.go +++ b/fpdf.go @@ -963,9 +963,10 @@ func (f *Fpdf) GetStringSymbolWidth(s string) int { unicode := []rune(s) for _, char := range unicode { intChar := int(char) - if len(f.currentFont.Cw) >= intChar && f.currentFont.Cw[intChar] > 0 { - if f.currentFont.Cw[intChar] != 65535 { - w += f.currentFont.Cw[intChar] + width, ok := f.currentFont.Cw[intChar] + if ok && width > 0 { + if width != 65535 { + w += width } } else if f.currentFont.Desc.MissingWidth != 0 { w += f.currentFont.Desc.MissingWidth @@ -974,11 +975,22 @@ func (f *Fpdf) GetStringSymbolWidth(s string) int { } } } else { - for _, ch := range []byte(s) { - if ch == 0 { + for _, char := range []byte(s) { + if char == 0 { break } - w += f.currentFont.Cw[ch] + ch := int(char) + if width, ok := f.currentFont.Cw[ch]; ok { + w += width + } else { + // Default behavior for non-existent char in map (should ideally not happen for byte fonts if properly initialized) + // Or assume missing width + if f.currentFont.Desc.MissingWidth != 0 { + w += f.currentFont.Desc.MissingWidth + } else { + w += 500 + } + } } } return w @@ -1700,6 +1712,7 @@ func (f *Fpdf) addFont(familyStr, styleStr, fileStr string, isUTF8 bool) { } reader := fileReader{readerPosition: 0, array: utf8Bytes} utf8File := newUTF8Font(&reader) + // fmt.Printf("File size: %d bytes\n", originalSize) err = utf8File.parseFile() if err != nil { f.SetError(err) @@ -1724,15 +1737,20 @@ func (f *Fpdf) addFont(familyStr, styleStr, fileStr string, isUTF8 bool) { sbarr = makeSubsetRange(32) } def := fontDefType{ - Tp: Type, - Name: fontKey, - Desc: desc, - Up: int(round(utf8File.UnderlinePosition)), - Ut: round(utf8File.UnderlineThickness), - Cw: utf8File.CharWidths, - usedRunes: sbarr, - File: fileStr, - utf8File: utf8File, + Tp: Type, + Name: fontKey, + Desc: desc, + Up: int(round(utf8File.UnderlinePosition)), + Ut: round(utf8File.UnderlineThickness), + Cw: utf8File.CharWidths, + usedRunes: sbarr, + File: fileStr, + utf8File: utf8File, + runeToCid: make(map[int]int), + HasColorGlyphs: utf8File.HasColorGlyphs(), + } + for cid, r := range sbarr { + def.runeToCid[r] = cid } def.i, _ = generateFontID(def) f.fonts[fontKey] = def @@ -1861,14 +1879,19 @@ func (f *Fpdf) addFontFromBytes(familyStr, styleStr string, jsonFileBytes, zFile sbarr = makeSubsetRange(32) } def := fontDefType{ - Tp: Type, - Name: fontkey, - Desc: desc, - Up: int(round(utf8File.UnderlinePosition)), - Ut: round(utf8File.UnderlineThickness), - Cw: utf8File.CharWidths, - utf8File: utf8File, - usedRunes: sbarr, + Tp: Type, + Name: fontkey, + Desc: desc, + Up: int(round(utf8File.UnderlinePosition)), + Ut: round(utf8File.UnderlineThickness), + Cw: utf8File.CharWidths, + utf8File: utf8File, + usedRunes: sbarr, + runeToCid: make(map[int]int), + HasColorGlyphs: utf8File.HasColorGlyphs(), + } + for cid, r := range sbarr { + def.runeToCid[r] = cid } def.i, _ = generateFontID(def) f.fonts[fontkey] = def @@ -2201,21 +2224,186 @@ func (f *Fpdf) Bookmark(txtStr string, level int, y float64) { f.outlines = append(f.outlines, outlineType{text: txtStr, level: level, y: y, p: f.PageNo(), prev: -1, last: -1, next: -1, first: -1}) } +// SetColorEmojiEnabled enables or disables color emoji rendering. +// When enabled and the current font has COLR/CPAL color glyph data, +// emoji will be rendered as multi-colored vector paths. +func (f *Fpdf) SetColorEmojiEnabled(enabled bool) { + f.colorEmojiEnabled = enabled +} + +// HasColorEmoji returns true if the current font has color emoji support +// and color emoji rendering is enabled. +func (f *Fpdf) HasColorEmoji() bool { + return f.colorEmojiEnabled && f.currentFont.HasColorGlyphs +} + +// isColorGlyph checks if a rune maps to a color glyph in the current font +func (f *Fpdf) isColorGlyph(r rune) bool { + if !f.HasColorEmoji() || f.currentFont.utf8File == nil { + return false + } + + // Get the glyph ID for this rune + glyphID, ok := f.currentFont.utf8File.charSymbolDictionary[int(r)] + if !ok { + return false + } + + return f.currentFont.utf8File.GetColorGlyphLayers(uint16(glyphID)) != nil +} + +// renderColorGlyph renders a single color glyph at the given position +func (f *Fpdf) renderColorGlyph(r rune, x, y float64) string { + if f.currentFont.utf8File == nil { + return "" + } + + glyphID, ok := f.currentFont.utf8File.charSymbolDictionary[int(r)] + if !ok { + return "" + } + + renderer := NewColorEmojiRenderer(f.currentFont.utf8File) + // Pass flipped Y (distance from bottom) and user-unit font size + return renderer.RenderColorGlyph(uint16(glyphID), x, f.h-y, f.fontSize, f.k) +} + +// textWithColorEmoji renders text that may contain color emoji +func (f *Fpdf) textWithColorEmoji(x, y float64, txtStr string) { + if f.isRTL { + txtStr = reverseText(txtStr) + x -= f.GetStringWidth(txtStr) + } + + var s strings.Builder + currentX := x + + for _, r := range txtStr { + charWidth := f.GetStringWidth(string(r)) + + if f.isColorGlyph(r) { + // Render as color glyph (vector graphics) + colorPath := f.renderColorGlyph(r, currentX, y) + if colorPath != "" { + s.WriteString(colorPath) + } + + // Render as invisible text (for copy-paste) + // Mode 3: Neither fill nor stroke (invisible) + txt2 := f.escape(f.stringToCIDs(string(r))) + // Save state (q), Set Text Render Mode (3 Tr), Text Object, Restore (Q) + // Note: q/Q saves graphics state (including Tr). + // We place invisible text at same position. + invisibleTextOp := sprintf("q 3 Tr BT %.2f %.2f Td (%s) Tj ET Q", currentX*f.k, (f.h-y)*f.k, txt2) + s.WriteString(invisibleTextOp) + } else { + // Render as regular text + txt2 := f.escape(f.stringToCIDs(string(r))) + textOp := sprintf("BT %.2f %.2f Td (%s) Tj ET", currentX*f.k, (f.h-y)*f.k, txt2) + if f.colorFlag { + textOp = sprintf("q %s %s Q", f.color.text.str, textOp) + } + s.WriteString(textOp) + s.WriteString(" ") + } + currentX += charWidth + } + + if f.underline && txtStr != "" { + s.WriteString(" ") + s.WriteString(f.dounderline(x, y, txtStr)) + } + if f.strikeout && txtStr != "" { + s.WriteString(" ") + s.WriteString(f.dostrikeout(x, y, txtStr)) + } + + f.out(s.String()) +} + +// textContainsColorEmoji checks if a string contains any color emoji characters +func (f *Fpdf) textContainsColorEmoji(txtStr string) bool { + for _, r := range txtStr { + if f.isColorGlyph(r) { + return true + } + } + return false +} + +func (f *Fpdf) getOrAssignCID(r int) int { + if cid, ok := f.currentFont.runeToCid[r]; ok { + return cid + } + + cid := r + // If the rune is in BMP and not already used as a CID for another rune (identity mapping), use it. + // But we must check if 'cid' is already occupied by a different rune? + // If runeToCid is empty initially, and usedRunes is empty. + // We want to prefer Identity. + // Check if this CID slot is free in usedRunes. + // Note: usedRunes[cid] = original_rune + if r < 0xFFFF { + if original, used := f.currentFont.usedRunes[r]; !used || original == r { + cid = r + } else { + cid = f.findNextFreeCID() + } + } else { + cid = f.findNextFreeCID() + } + + f.currentFont.runeToCid[r] = cid + f.currentFont.usedRunes[cid] = r + return cid +} + +func (f *Fpdf) findNextFreeCID() int { + // Start searching from PUA + start := 0xE000 + for i := start; i < 0xFFFF; i++ { + if _, used := f.currentFont.usedRunes[i]; !used { + return i + } + } + // If PUA full, search from beginning? + for i := 32; i < 0xE000; i++ { + if _, used := f.currentFont.usedRunes[i]; !used { + return i + } + } + // Fallback to 0 if full (should panic?) + return 0 +} + +func (f *Fpdf) stringToCIDs(s string) string { + var b bytes.Buffer + for _, r := range s { + cid := f.getOrAssignCID(int(r)) + b.WriteByte(byte(cid >> 8)) + b.WriteByte(byte(cid)) + } + return b.String() +} + // Text prints a character string. The origin (x, y) is on the left of the // first character at the baseline. This method permits a string to be placed // precisely on the page, but it is usually easier to use Cell(), MultiCell() // or Write() which are the standard methods to print text. func (f *Fpdf) Text(x, y float64, txtStr string) { + // Check if we need to handle color emoji + if f.isCurrentUTF8 && f.HasColorEmoji() && f.textContainsColorEmoji(txtStr) { + f.textWithColorEmoji(x, y, txtStr) + return + } + var txt2 string if f.isCurrentUTF8 { if f.isRTL { txtStr = reverseText(txtStr) x -= f.GetStringWidth(txtStr) } - txt2 = f.escape(utf8toutf16(txtStr, false)) - for _, uni := range []rune(txtStr) { - f.currentFont.usedRunes[int(uni)] = int(uni) - } + txt2 = f.escape(f.stringToCIDs(txtStr)) } else { txt2 = f.escape(txtStr) } @@ -2424,10 +2612,10 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, txtStr = reverseText(txtStr) } wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) - for _, uni := range []rune(txtStr) { - f.currentFont.usedRunes[int(uni)] = int(uni) - } - space := f.escape(utf8toutf16(" ", false)) + // for _, uni := range []rune(txtStr) { + // f.currentFont.usedRunes[int(uni)] = int(uni) + // } + space := f.escape(f.stringToCIDs(" ")) strSize := f.GetStringSymbolWidth(txtStr) s.printf("BT 0 Tw %.2f %.2f Td [", (f.x+dx)*k, (f.h-(f.y+.5*h+.3*f.fontSize))*k) t := strings.Split(txtStr, " ") @@ -2435,7 +2623,7 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, numt := len(t) for i := 0; i < numt; i++ { tx := t[i] - tx = "(" + f.escape(utf8toutf16(tx, false)) + ")" + tx = "(" + f.escape(f.stringToCIDs(tx)) + ")" s.printf("%s ", tx) if (i + 1) < numt { s.printf("%.3f(%s) ", -shift, space) @@ -2448,10 +2636,10 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, if f.isRTL { txtStr = reverseText(txtStr) } - txt2 = f.escape(utf8toutf16(txtStr, false)) - for _, uni := range []rune(txtStr) { - f.currentFont.usedRunes[int(uni)] = int(uni) - } + txt2 = f.escape(f.stringToCIDs(txtStr)) + // for _, uni := range []rune(txtStr) { + // f.currentFont.usedRunes[int(uni)] = int(uni) + // } } else { txt2 = strings.Replace(txtStr, "\\", "\\\\", -1) @@ -2545,7 +2733,7 @@ func (f *Fpdf) SplitLines(txt []byte, w float64) [][]byte { l := 0 for i < nb { c := s[i] - l += cw[c] + l += cw[int(c)] if c == ' ' || c == '\t' || c == '\n' { sep = i } @@ -2709,14 +2897,14 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill ls = l ns++ } - if int(c) >= len(cw) { - f.err = fmt.Errorf("character outside the supported range: %s", string(c)) - return - } - if cw[int(c)] == 0 { //Marker width 0 used for missing symbols + // if int(c) >= len(cw) { + // f.err = fmt.Errorf("character outside the supported range: %s", string(c)) + // return + // } + if width, ok := cw[int(c)]; !ok || width == 0 { //Marker width 0 used for missing symbols l += f.currentFont.Desc.MissingWidth - } else if cw[int(c)] != 65535 { //Marker width 65535 used for zero width symbols - l += cw[int(c)] + } else if width != 65535 { //Marker width 65535 used for zero width symbols + l += width } if l > wmax { // Automatic line break @@ -2836,7 +3024,12 @@ func (f *Fpdf) write(h float64, txtStr string, link int, linkStr string) { if c == ' ' { sep = i } - l += float64(cw[int(c)]) + // l += float64(cw[int(c)]) + if width, ok := cw[int(c)]; ok { + l += float64(width) + } else { + l += float64(f.currentFont.Desc.MissingWidth) + } if l > wmax { // Automatic line break if sep == -1 { @@ -2925,7 +3118,7 @@ func (f *Fpdf) WriteLinkID(h float64, displayStr string, linkID int) { // // width indicates the width of the box the text will be drawn in. This is in // the unit of measure specified in New(). If it is set to 0, the bounding box -//of the page will be taken (pageWidth - leftMargin - rightMargin). +// of the page will be taken (pageWidth - leftMargin - rightMargin). // // lineHeight indicates the line height in the unit of measure specified in // New(). @@ -4079,7 +4272,11 @@ func (f *Fpdf) putfonts() { var s fmtBuffer s.WriteString("[") for j := 32; j < 256; j++ { - s.printf("%d ", font.Cw[j]) + if width, ok := font.Cw[j]; ok { + s.printf("%d ", width) + } else { + s.WriteString("0 ") + } } s.WriteString("]") f.out(s.String()) @@ -4200,16 +4397,24 @@ func (f *Fpdf) generateCIDFontMap(font *fontDefType, LastRune int) { // for each character for cid := startCid; cid < cwLen; cid++ { - if font.Cw[cid] == 0x00 { + runa, used := font.usedRunes[cid] + if cid > 255 && (!used || runa == 0) { continue } - width := font.Cw[cid] - if width == 65535 { - width = 0 + if !used { + runa = cid } - if numb, OK := font.usedRunes[cid]; cid > 255 && (!OK || numb == 0) { + + width, ok := font.Cw[runa] + if !ok || width == 0x00 { continue } + if width == 65535 { + width = 0 + } + // if numb, OK := font.usedRunes[cid]; cid > 255 && (!OK || numb == 0) { + // continue + // } if cid == prevCid+1 { if width == prevWidth { diff --git a/glyphoutline.go b/glyphoutline.go new file mode 100644 index 00000000..4c297122 --- /dev/null +++ b/glyphoutline.go @@ -0,0 +1,374 @@ +package gofpdf + +import ( + "encoding/binary" + "math" +) + +// GlyphPoint represents a point in a glyph outline +type GlyphPoint struct { + X, Y float64 + OnCurve bool +} + +// GlyphContour is a closed contour made up of points +type GlyphContour []GlyphPoint + +// GlyphOutline contains all contours of a glyph +type GlyphOutline struct { + Contours []GlyphContour + Bounds [4]int16 // xMin, yMin, xMax, yMax +} + +// TrueType simple glyph flags +const ( + glyphOnCurve = 1 << 0 + glyphXShortVector = 1 << 1 + glyphYShortVector = 1 << 2 + glyphRepeat = 1 << 3 + glyphXSameOrPosShort = 1 << 4 + glyphYSameOrPosShort = 1 << 5 +) + +// Composite glyph flags (reusing from utf8fontfile.go constants) +const ( + compositeArg1And2AreWords = 1 << 0 + compositeArgsAreXYValues = 1 << 1 + compositeRoundXYToGrid = 1 << 2 + compositeWeHaveAScale = 1 << 3 + compositeMoreComponents = 1 << 5 + compositeWeHaveAnXYScale = 1 << 6 + compositeWeHaveATwoByTwo = 1 << 7 + compositeWeHaveInstr = 1 << 8 + compositeUseMyMetrics = 1 << 9 + compositeOverlapCompound = 1 << 10 +) + +// ParseGlyphOutline extracts the outline of a glyph from the glyf table +func (utf *utf8FontFile) ParseGlyphOutline(glyphID uint16) *GlyphOutline { + if len(utf.symbolPosition) == 0 { + return nil + } + + if int(glyphID) >= len(utf.symbolPosition)-1 { + return nil + } + + glyfData := utf.getTableData("glyf") + if glyfData == nil { + return nil + } + + symbolPos := utf.symbolPosition[glyphID] + symbolLen := utf.symbolPosition[glyphID+1] - symbolPos + + if symbolLen == 0 { + // Empty glyph (like space) + return &GlyphOutline{} + } + + data := glyfData[symbolPos : symbolPos+symbolLen] + return utf.parseGlyphData(data, glyfData) +} + +func (utf *utf8FontFile) parseGlyphData(data []byte, glyfData []byte) *GlyphOutline { + if len(data) < 10 { + return nil + } + + numContours := int16(binary.BigEndian.Uint16(data[0:2])) + xMin := int16(binary.BigEndian.Uint16(data[2:4])) + yMin := int16(binary.BigEndian.Uint16(data[4:6])) + xMax := int16(binary.BigEndian.Uint16(data[6:8])) + yMax := int16(binary.BigEndian.Uint16(data[8:10])) + + outline := &GlyphOutline{ + Bounds: [4]int16{xMin, yMin, xMax, yMax}, + } + + if numContours >= 0 { + // Simple glyph + utf.parseSimpleGlyph(data[10:], int(numContours), outline) + } else { + // Composite glyph + utf.parseCompositeGlyph(data[10:], glyfData, outline) + } + + return outline +} + +func (utf *utf8FontFile) parseSimpleGlyph(data []byte, numContours int, outline *GlyphOutline) { + if numContours == 0 || len(data) < numContours*2 { + return + } + + // Read end points of contours + endPtsOfContours := make([]uint16, numContours) + for i := 0; i < numContours; i++ { + endPtsOfContours[i] = binary.BigEndian.Uint16(data[i*2 : i*2+2]) + } + + numPoints := int(endPtsOfContours[numContours-1]) + 1 + offset := numContours * 2 + + // Skip instruction length and instructions + if offset+2 > len(data) { + return + } + instructionLength := int(binary.BigEndian.Uint16(data[offset : offset+2])) + offset += 2 + instructionLength + + if offset > len(data) { + return + } + + // Read flags + flags := make([]byte, numPoints) + for i := 0; i < numPoints; { + if offset >= len(data) { + return + } + flag := data[offset] + offset++ + flags[i] = flag + i++ + + if (flag & glyphRepeat) != 0 { + if offset >= len(data) { + return + } + repeatCount := int(data[offset]) + offset++ + for j := 0; j < repeatCount && i < numPoints; j++ { + flags[i] = flag + i++ + } + } + } + + // Read X coordinates + xCoords := make([]int, numPoints) + var x int + for i := 0; i < numPoints; i++ { + flag := flags[i] + if (flag & glyphXShortVector) != 0 { + if offset >= len(data) { + return + } + dx := int(data[offset]) + offset++ + if (flag & glyphXSameOrPosShort) != 0 { + x += dx + } else { + x -= dx + } + } else if (flag & glyphXSameOrPosShort) == 0 { + if offset+2 > len(data) { + return + } + dx := int(int16(binary.BigEndian.Uint16(data[offset : offset+2]))) + offset += 2 + x += dx + } + // else: x is same as previous + xCoords[i] = x + } + + // Read Y coordinates + yCoords := make([]int, numPoints) + var y int + for i := 0; i < numPoints; i++ { + flag := flags[i] + if (flag & glyphYShortVector) != 0 { + if offset >= len(data) { + return + } + dy := int(data[offset]) + offset++ + if (flag & glyphYSameOrPosShort) != 0 { + y += dy + } else { + y -= dy + } + } else if (flag & glyphYSameOrPosShort) == 0 { + if offset+2 > len(data) { + return + } + dy := int(int16(binary.BigEndian.Uint16(data[offset : offset+2]))) + offset += 2 + y += dy + } + // else: y is same as previous + yCoords[i] = y + } + + // Build contours + outline.Contours = make([]GlyphContour, numContours) + pointIdx := 0 + for c := 0; c < numContours; c++ { + endPt := int(endPtsOfContours[c]) + contourLen := endPt - pointIdx + 1 + contour := make(GlyphContour, contourLen) + + for i := 0; i < contourLen; i++ { + contour[i] = GlyphPoint{ + X: float64(xCoords[pointIdx]), + Y: float64(yCoords[pointIdx]), + OnCurve: (flags[pointIdx] & glyphOnCurve) != 0, + } + pointIdx++ + } + outline.Contours[c] = contour + } +} + +func (utf *utf8FontFile) parseCompositeGlyph(data []byte, glyfData []byte, outline *GlyphOutline) { + offset := 0 + flags := uint16(compositeMoreComponents) + + for (flags & compositeMoreComponents) != 0 { + if offset+4 > len(data) { + return + } + + flags = binary.BigEndian.Uint16(data[offset : offset+2]) + glyphIndex := binary.BigEndian.Uint16(data[offset+2 : offset+4]) + offset += 4 + + // Read transform arguments + var arg1, arg2 int + if (flags & compositeArg1And2AreWords) != 0 { + if offset+4 > len(data) { + return + } + arg1 = int(int16(binary.BigEndian.Uint16(data[offset : offset+2]))) + arg2 = int(int16(binary.BigEndian.Uint16(data[offset+2 : offset+4]))) + offset += 4 + } else { + if offset+2 > len(data) { + return + } + arg1 = int(int8(data[offset])) + arg2 = int(int8(data[offset+1])) + offset += 2 + } + + // Initialize transform matrix + var a, b, c, d float64 = 1, 0, 0, 1 + var e, f float64 = 0, 0 + + if (flags & compositeArgsAreXYValues) != 0 { + e = float64(arg1) + f = float64(arg2) + } + + // Read scale/transform + if (flags & compositeWeHaveAScale) != 0 { + if offset+2 > len(data) { + return + } + scale := read2Dot14(data[offset : offset+2]) + offset += 2 + a = scale + d = scale + } else if (flags & compositeWeHaveAnXYScale) != 0 { + if offset+4 > len(data) { + return + } + a = read2Dot14(data[offset : offset+2]) + d = read2Dot14(data[offset+2 : offset+4]) + offset += 4 + } else if (flags & compositeWeHaveATwoByTwo) != 0 { + if offset+8 > len(data) { + return + } + a = read2Dot14(data[offset : offset+2]) + b = read2Dot14(data[offset+2 : offset+4]) + c = read2Dot14(data[offset+4 : offset+6]) + d = read2Dot14(data[offset+6 : offset+8]) + offset += 8 + } + + // Get component glyph outline + if int(glyphIndex) < len(utf.symbolPosition)-1 { + compPos := utf.symbolPosition[glyphIndex] + compLen := utf.symbolPosition[glyphIndex+1] - compPos + + if compLen > 0 && compPos+compLen <= len(glyfData) { + compData := glyfData[compPos : compPos+compLen] + compOutline := utf.parseGlyphData(compData, glyfData) + + if compOutline != nil { + // Transform and add component contours + for _, contour := range compOutline.Contours { + transformedContour := make(GlyphContour, len(contour)) + for i, pt := range contour { + // Apply 2D affine transform + newX := a*pt.X + c*pt.Y + e + newY := b*pt.X + d*pt.Y + f + transformedContour[i] = GlyphPoint{ + X: newX, + Y: newY, + OnCurve: pt.OnCurve, + } + } + outline.Contours = append(outline.Contours, transformedContour) + } + } + } + } + } +} + +// read2Dot14 reads a 2.14 fixed-point number +func read2Dot14(data []byte) float64 { + val := int16(binary.BigEndian.Uint16(data)) + return float64(val) / 16384.0 +} + +// TransformOutline applies a 2D affine transform to a glyph outline +func TransformOutline(outline *GlyphOutline, a, b, c, d, e, f float64) *GlyphOutline { + if outline == nil { + return nil + } + + result := &GlyphOutline{ + Bounds: outline.Bounds, // Bounds would need recalculating for accuracy + Contours: make([]GlyphContour, len(outline.Contours)), + } + + for i, contour := range outline.Contours { + result.Contours[i] = make(GlyphContour, len(contour)) + for j, pt := range contour { + result.Contours[i][j] = GlyphPoint{ + X: a*pt.X + c*pt.Y + e, + Y: b*pt.X + d*pt.Y + f, + OnCurve: pt.OnCurve, + } + } + } + + return result +} + +// ScaleOutline scales a glyph outline by a factor +func ScaleOutline(outline *GlyphOutline, scale float64) *GlyphOutline { + return TransformOutline(outline, scale, 0, 0, scale, 0, 0) +} + +// TranslateOutline translates a glyph outline by (dx, dy) +func TranslateOutline(outline *GlyphOutline, dx, dy float64) *GlyphOutline { + return TransformOutline(outline, 1, 0, 0, 1, dx, dy) +} + +// FlipY flips the Y coordinates (for PDF coordinate system) +func FlipY(outline *GlyphOutline) *GlyphOutline { + return TransformOutline(outline, 1, 0, 0, -1, 0, 0) +} + +// RotateOutline rotates a glyph outline by angle (in radians) +func RotateOutline(outline *GlyphOutline, angle float64) *GlyphOutline { + cos := math.Cos(angle) + sin := math.Sin(angle) + return TransformOutline(outline, cos, sin, -sin, cos, 0, 0) +} diff --git a/splittext.go b/splittext.go index 525f93b0..b203e298 100644 --- a/splittext.go +++ b/splittext.go @@ -25,7 +25,7 @@ func (f *Fpdf) SplitText(txt string, w float64) (lines []string) { l := 0 for i < nb { c := s[i] - l += cw[c] + l += cw[int(c)] if unicode.IsSpace(c) || isChinese(c) { sep = i } diff --git a/utf8fontfile.go b/utf8fontfile.go index 0e1a17a7..8bb183a4 100644 --- a/utf8fontfile.go +++ b/utf8fontfile.go @@ -34,6 +34,41 @@ const symbol2x2 = 1 << 7 // CID map Init const toUnicode = "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo\n<> def\n/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000> \nendcodespacerange\n1 beginbfrange\n<0000> <0000>\nendbfrange\nendcmap\nCMapName currentdict /CMap defineresource pop\nend\nend" +// ColorRecord represents an RGBA color from the CPAL table +type ColorRecord struct { + R, G, B, A uint8 +} + +// LayerRecord represents a single layer in a COLR glyph +type LayerRecord struct { + GlyphID uint16 + PaletteIndex uint16 +} + +// BaseGlyphRecord maps a base glyph to its color layers +type BaseGlyphRecord struct { + GlyphID uint16 + FirstLayerIdx uint16 + NumLayers uint16 +} + +// COLRTable holds parsed COLR table data +type COLRTable struct { + Version uint16 + BaseGlyphRecords []BaseGlyphRecord + LayerRecords []LayerRecord + BaseGlyphListOffset int + LayerListOffset int + ClipListOffset int +} + +// CPALTable holds parsed CPAL table data +type CPALTable struct { + NumPaletteEntries uint16 + NumPalettes uint16 + ColorRecords []ColorRecord +} + type utf8FontFile struct { fileReader *fileReader LastRune int @@ -51,10 +86,13 @@ type utf8FontFile struct { Flags int UnderlinePosition float64 UnderlineThickness float64 - CharWidths []int + CharWidths map[int]int DefaultWidth float64 symbolData map[int]map[string][]int CodeSymbolDictionary map[int]int + colrTable *COLRTable + cpalTable *CPALTable + hasColorGlyphs bool } type tableDescription struct { @@ -189,7 +227,7 @@ func (utf *utf8FontFile) skip(delta int) { _, _ = utf.fileReader.seek(int64(delta), 1) } -//SeekTable position +// SeekTable position func (utf *utf8FontFile) SeekTable(name string) int { return utf.seekTable(name, 0) } @@ -332,7 +370,7 @@ func (utf *utf8FontFile) parseNAMETable() int { return format } -func (utf *utf8FontFile) parseHEADTable() { +func (utf *utf8FontFile) parseHEADTable() int { utf.SeekTable("head") utf.skip(18) utf.fontElementSize = utf.readUint16() @@ -344,12 +382,13 @@ func (utf *utf8FontFile) parseHEADTable() { yMax := utf.readInt16() utf.Bbox = fontBoxType{int(float64(xMin) * scale), int(float64(yMin) * scale), int(float64(xMax) * scale), int(float64(yMax) * scale)} utf.skip(3 * 2) - _ = utf.readUint16() + indexToLocFormat := utf.readUint16() symbolDataFormat := utf.readUint16() if symbolDataFormat != 0 { fmt.Printf("Unknown symbol data format %d\n", symbolDataFormat) - return + return 0 } + return indexToLocFormat } func (utf *utf8FontFile) parseHHEATable() int { @@ -456,14 +495,18 @@ func (utf *utf8FontFile) parseCMAPTable(format int) int { coded := utf.readUint16() position := utf.readUint32() oldReaderPosition := utf.fileReader.readerPosition - if (system == 3 && coded == 1) || system == 0 { // Microsoft, Unicode + // System 3: Windows + // Coded 1: Unicode BMP (UCS-2) + // Coded 10: Unicode Full (UCS-4) + if (system == 3 && (coded == 1 || coded == 10)) || system == 0 { format = utf.getUint16(cmapPosition + position) - if format == 4 { - if cidCMAPPosition == 0 { - cidCMAPPosition = cmapPosition + position - } + if format == 12 { + cidCMAPPosition = cmapPosition + position break } + if format == 4 { + cidCMAPPosition = cmapPosition + position + } } utf.seek(int(oldReaderPosition)) } @@ -474,14 +517,306 @@ func (utf *utf8FontFile) parseCMAPTable(format int) int { return cidCMAPPosition } +// parseCOLRTable parses the COLR (Color) table for color glyph definitions +func (utf *utf8FontFile) parseCOLRTable() { + if utf.tableDescriptions["COLR"] == nil { + return + } + + utf.SeekTable("COLR") + version := uint16(utf.readUint16()) + fmt.Printf("COLR version: %d\n", version) + // if version > 0 { + // // Only COLR v0 is supported for now + // return + // } + + numBaseGlyphRecords := utf.readUint16() + baseGlyphRecordsOffset := utf.readUint32() + layerRecordsOffset := utf.readUint32() + numLayerRecords := utf.readUint16() + + colr := &COLRTable{ + Version: version, + BaseGlyphRecords: make([]BaseGlyphRecord, numBaseGlyphRecords), + LayerRecords: make([]LayerRecord, numLayerRecords), + } + + if version == 1 { + colr.BaseGlyphListOffset = utf.readUint32() + colr.LayerListOffset = utf.readUint32() + colr.ClipListOffset = utf.readUint32() + } + + // Parse base glyph records + tableStart := utf.tableDescriptions["COLR"].position + utf.seek(tableStart + baseGlyphRecordsOffset) + for i := 0; i < int(numBaseGlyphRecords); i++ { + colr.BaseGlyphRecords[i] = BaseGlyphRecord{ + GlyphID: uint16(utf.readUint16()), + FirstLayerIdx: uint16(utf.readUint16()), + NumLayers: uint16(utf.readUint16()), + } + } + + // Parse layer records + utf.seek(tableStart + layerRecordsOffset) + for i := 0; i < int(numLayerRecords); i++ { + colr.LayerRecords[i] = LayerRecord{ + GlyphID: uint16(utf.readUint16()), + PaletteIndex: uint16(utf.readUint16()), + } + } + + utf.colrTable = colr +} + +// parseCPALTable parses the CPAL (Color Palette) table +func (utf *utf8FontFile) parseCPALTable() { + if utf.tableDescriptions["CPAL"] == nil { + return + } + + utf.SeekTable("CPAL") + version := utf.readUint16() + numPaletteEntries := uint16(utf.readUint16()) + numPalettes := uint16(utf.readUint16()) + numColorRecords := utf.readUint16() + colorRecordsArrayOffset := utf.readUint32() + + // Skip palette entry labels offset array (one uint16 per palette) + // For v0, we just need the first palette + + cpal := &CPALTable{ + NumPaletteEntries: numPaletteEntries, + NumPalettes: numPalettes, + ColorRecords: make([]ColorRecord, numColorRecords), + } + + // Parse color records (BGRA format in the font file) + tableStart := utf.tableDescriptions["CPAL"].position + utf.seek(tableStart + colorRecordsArrayOffset) + for i := 0; i < int(numColorRecords); i++ { + data := utf.fileReader.Read(4) + // CPAL stores colors as BGRA + cpal.ColorRecords[i] = ColorRecord{ + B: data[0], + G: data[1], + R: data[2], + A: data[3], + } + } + + utf.cpalTable = cpal + _ = version // version is read but not used yet (v0 and v1 have same color record format) +} + +// GetColorGlyphLayers returns the color layers for a glyph, or nil if not a color glyph +func (utf *utf8FontFile) GetColorGlyphLayers(glyphID uint16) []LayerRecord { + if utf.colrTable == nil { + return nil + } + + // 1. Try V0 records + if len(utf.colrTable.BaseGlyphRecords) > 0 { + records := utf.colrTable.BaseGlyphRecords + lo, hi := 0, len(records)-1 + for lo <= hi { + mid := (lo + hi) / 2 + if records[mid].GlyphID == glyphID { + firstIdx := records[mid].FirstLayerIdx + numLayers := records[mid].NumLayers + return utf.colrTable.LayerRecords[firstIdx : firstIdx+numLayers] + } + if records[mid].GlyphID < glyphID { + lo = mid + 1 + } else { + hi = mid - 1 + } + } + } + + // 2. Try V1 records if available + if utf.colrTable.Version == 1 && utf.colrTable.BaseGlyphListOffset != 0 { + return utf.getV1Layers(glyphID) + } + + return nil +} + +func (utf *utf8FontFile) getV1Layers(glyphID uint16) []LayerRecord { + colrStart := utf.tableDescriptions["COLR"].position + listStart := colrStart + utf.colrTable.BaseGlyphListOffset + + utf.seek(listStart) + numRecords := utf.readUint32() + + // Binary search in V1 BaseGlyphList + lo, hi := 0, int(numRecords)-1 + recordSize := 6 // GlyphID(2) + PaintOffset(4) + + for lo <= hi { + mid := (lo + hi) / 2 + utf.seek(listStart + 4 + mid*recordSize) + gid := uint16(utf.readUint16()) + + if gid == glyphID { + paintOffset := utf.readUint32() + absPaintOffset := listStart + int(paintOffset) + return utf.parseV1Paint(absPaintOffset, colrStart, 0) + } + if gid < glyphID { + lo = mid + 1 + } else { + hi = mid - 1 + } + } + + return nil +} + +func (utf *utf8FontFile) parseV1Paint(offset, colrStart, depth int) []LayerRecord { + if depth > 10 { + return nil + } + + utf.seek(offset) + val := utf.readUint16() + format := uint8(val >> 8) + + if format == 1 { // PaintColrLayers + // Format(1), NumLayers(1), FirstLayerIndex(4) + numLayers := uint8(val & 0xFF) + firstLayerIndex := utf.readUint32() + + layerListStart := colrStart + utf.colrTable.LayerListOffset + utf.seek(layerListStart) + numTotalLayers := utf.readUint32() + + if uint32(firstLayerIndex)+uint32(numLayers) > uint32(numTotalLayers) { + return nil + } + + layers := make([]LayerRecord, 0, numLayers) + + // Iterate layers + for i := 0; i < int(numLayers); i++ { + idx := int(firstLayerIndex) + i + utf.seek(layerListStart + 4 + idx*4) + paintOffset := utf.readUint32() + + // Recurse + subLayers := utf.parseV1Paint(layerListStart+int(paintOffset), colrStart, depth+1) + if subLayers != nil { + layers = append(layers, subLayers...) + } + } + return layers + } else if format == 10 { // PaintGlyph + // Format(1), PaintOffset(3), GlyphID(2) + pOffsetHigh := uint32(val & 0xFF) + pOffsetLow := uint16(utf.readUint16()) + paintOffset := (pOffsetHigh << 16) | uint32(pOffsetLow) + glyphID := uint16(utf.readUint16()) + + // Get color from sub-paint + paletteIdx := utf.parseV1Color(offset + int(paintOffset), 0) + + return []LayerRecord{{ + GlyphID: glyphID, + PaletteIndex: paletteIdx, + }} + } else if format == 12 || format == 14 { // PaintRotate or PaintTranslate + // Format(1), PaintOffset(3), ... + pOffsetHigh := uint32(val & 0xFF) + pOffsetLow := uint16(utf.readUint16()) + paintOffset := (pOffsetHigh << 16) | uint32(pOffsetLow) + + // Follow sub-paint (ignoring transform) + return utf.parseV1Paint(offset + int(paintOffset), colrStart, depth+1) + } + + return nil +} + +func (utf *utf8FontFile) parseV1Color(offset, depth int) uint16 { + if depth > 10 { + return 0xFFFF + } + utf.seek(offset) + val := utf.readUint16() + format := uint8(val >> 8) + + if format == 2 { // PaintSolid + // Format(1), PaletteIndex(2) + utf.seek(offset + 1) + return uint16(utf.readUint16()) + } else if format == 4 || format == 6 { // Linear or Radial Gradient + // Format(1), ColorLineOffset(3), ... + pOffsetHigh := uint32(val & 0xFF) + pOffsetLow := uint16(utf.readUint16()) + colorLineOffset := (pOffsetHigh << 16) | uint32(pOffsetLow) + + // Read ColorLine + clOffset := offset + int(colorLineOffset) + utf.seek(clOffset) + // Extend(1), NumStops(2) + // We need to read byte then uint16. + // utf.readUint16 reads 2 bytes. + // First byte is Extend. + // Next 2 bytes is NumStops. + _ = utf.readUint16() // Extend + NumStopsHigh? No. + // Structure: extend(1), numStops(2). Total 3 bytes. + utf.seek(clOffset + 1) + numStops := utf.readUint16() + + if numStops > 0 { + // ColorStop: StopOffset(2), PaletteIndex(2), Alpha(2) + // First stop at offset + 3 + 0*6 + 2 (to get PaletteIndex) + utf.seek(clOffset + 3 + 2) + return uint16(utf.readUint16()) + } + } else if format >= 12 && format <= 30 { // Transform/Translate/Rotate/Skew... + // Format(1), PaintOffset(3) + pOffsetHigh := uint32(val & 0xFF) + pOffsetLow := uint16(utf.readUint16()) + paintOffset := (pOffsetHigh << 16) | uint32(pOffsetLow) + return utf.parseV1Color(offset+int(paintOffset), depth+1) + } + return 0xFFFF // Invalid/Fallback +} + +// GetPaletteColor returns the color at the given palette index +func (utf *utf8FontFile) GetPaletteColor(paletteIndex uint16) ColorRecord { + if utf.cpalTable == nil || int(paletteIndex) >= len(utf.cpalTable.ColorRecords) { + return ColorRecord{R: 0, G: 0, B: 0, A: 255} + } + return utf.cpalTable.ColorRecords[paletteIndex] +} + +// HasColorGlyphs returns true if the font has color glyph data +func (utf *utf8FontFile) HasColorGlyphs() bool { + return utf.hasColorGlyphs +} + +// GetUnitsPerEm returns the font's units per em +func (utf *utf8FontFile) GetUnitsPerEm() int { + return utf.fontElementSize +} + func (utf *utf8FontFile) parseTables() { f := utf.parseNAMETable() - utf.parseHEADTable() + LocaFormat := utf.parseHEADTable() n := utf.parseHHEATable() w := utf.parseOS2Table() utf.parsePOSTTable(w) runeCMAPPosition := utf.parseCMAPTable(f) + // Parse color tables (COLR/CPAL) + utf.parseCOLRTable() + utf.parseCPALTable() + utf.hasColorGlyphs = utf.colrTable != nil && utf.cpalTable != nil + utf.SeekTable("maxp") utf.skip(4) numSymbols := utf.readUint16() @@ -490,8 +825,13 @@ func (utf *utf8FontFile) parseTables() { charSymbolDictionary := make(map[int]int) utf.generateSCCSDictionaries(runeCMAPPosition, symbolCharDictionary, charSymbolDictionary) + // Save the dictionary to the struct + utf.charSymbolDictionary = charSymbolDictionary + scale := 1000.0 / float64(utf.fontElementSize) utf.parseHMTXTable(n, numSymbols, symbolCharDictionary, scale) + + utf.parseLOCATable(LocaFormat, numSymbols) } func (utf *utf8FontFile) generateCMAP() map[int][]int { @@ -504,12 +844,15 @@ func (utf *utf8FontFile) generateCMAP() map[int][]int { coder := utf.readUint16() position := utf.readUint32() oldPosition := utf.fileReader.readerPosition - if (system == 3 && coder == 1) || system == 0 { + if (system == 3 && (coder == 1 || coder == 10)) || system == 0 { format := utf.getUint16(cmapPosition + position) - if format == 4 { + if format == 12 { runeCmapPosition = cmapPosition + position break } + if format == 4 { + runeCmapPosition = cmapPosition + position + } } utf.seek(int(oldPosition)) } @@ -531,13 +874,26 @@ func (utf *utf8FontFile) generateCMAP() map[int][]int { func (utf *utf8FontFile) parseSymbols(usedRunes map[int]int) (map[int]int, map[int]int, map[int]int, []int) { symbolCollection := map[int]int{0: 0} charSymbolPairCollection := make(map[int]int) - for _, char := range usedRunes { + for cid, char := range usedRunes { if _, OK := utf.charSymbolDictionary[char]; OK { - symbolCollection[utf.charSymbolDictionary[char]] = char - charSymbolPairCollection[char] = utf.charSymbolDictionary[char] - + glyphID := utf.charSymbolDictionary[char] + symbolCollection[glyphID] = char + charSymbolPairCollection[cid] = glyphID + + // If this is a color glyph, also include all layer glyphs + if utf.hasColorGlyphs { + layers := utf.GetColorGlyphLayers(uint16(glyphID)) + for _, layer := range layers { + layerGlyphID := int(layer.GlyphID) + if _, exists := symbolCollection[layerGlyphID]; !exists { + // Add layer glyph with a placeholder rune value + // The layer glyphs don't need to be mapped to Unicode chars + symbolCollection[layerGlyphID] = 0 + } + } + } } - utf.LastRune = max(utf.LastRune, char) + utf.LastRune = max(utf.LastRune, cid) } begin := utf.tableDescriptions["glyf"].position @@ -635,7 +991,7 @@ func (utf *utf8FontFile) generateCMAPTable(cidSymbolPairCollection map[int]int, return cmapstr } -//GenerateCutFont fill utf8FontFile from .utf file, only with runes from usedRunes +// GenerateCutFont fill utf8FontFile from .utf file, only with runes from usedRunes func (utf *utf8FontFile) GenerateCutFont(usedRunes map[int]int) []byte { utf.fileReader.readerPosition = 0 utf.symbolPosition = make([]int, 0) @@ -648,6 +1004,11 @@ func (utf *utf8FontFile) GenerateCutFont(usedRunes map[int]int) []byte { utf.LastRune = 0 utf.generateTableDescriptions() + // Parse color tables if present (needed for including layer glyphs in subsetting) + utf.parseCOLRTable() + utf.parseCPALTable() + utf.hasColorGlyphs = utf.colrTable != nil && utf.cpalTable != nil + utf.SeekTable("head") utf.skip(50) LocaFormat := utf.readUint16() @@ -836,7 +1197,7 @@ func (utf *utf8FontFile) parseHMTXTable(numberOfHMetrics, numSymbols int, symbol start := utf.SeekTable("hmtx") arrayWidths := 0 var arr []int - utf.CharWidths = make([]int, 256*256) + utf.CharWidths = make(map[int]int) charCount := 0 arr = unpackUint16Array(utf.getRange(start, numberOfHMetrics*4)) for symbol := 0; symbol < numberOfHMetrics; symbol++ { @@ -856,10 +1217,8 @@ func (utf *utf8FontFile) parseHMTXTable(numberOfHMetrics, numSymbols int, symbol if widths == 0 { widths = 65535 } - if char < 196608 { - utf.CharWidths[char] = widths - charCount++ - } + utf.CharWidths[char] = widths + charCount++ } } } @@ -874,10 +1233,8 @@ func (utf *utf8FontFile) parseHMTXTable(numberOfHMetrics, numSymbols int, symbol if widths == 0 { widths = 65535 } - if char < 196608 { - utf.CharWidths[char] = widths - charCount++ - } + utf.CharWidths[char] = widths + charCount++ } } } @@ -922,6 +1279,29 @@ func (utf *utf8FontFile) parseLOCATable(format, numSymbols int) { } func (utf *utf8FontFile) generateSCCSDictionaries(runeCmapPosition int, symbolCharDictionary map[int][]int, charSymbolDictionary map[int]int) { + utf.seek(runeCmapPosition) + format := utf.readUint16() + + if format == 12 { + utf.skip(2) // reserved + _ = utf.readUint32() // length + utf.skip(4) // language + nGroups := utf.readUint32() + + for i := 0; i < int(nGroups); i++ { + startCharCode := int(utf.readUint32()) + endCharCode := int(utf.readUint32()) + startGlyphID := int(utf.readUint32()) + + for char := startCharCode; char <= endCharCode; char++ { + symbol := startGlyphID + (char - startCharCode) + charSymbolDictionary[char] = symbol + symbolCharDictionary[symbol] = append(symbolCharDictionary[symbol], char) + } + } + return + } + maxRune := 0 utf.seek(runeCmapPosition + 2) size := utf.readUint16() @@ -967,9 +1347,7 @@ func (utf *utf8FontFile) generateSCCSDictionaries(runeCmapPosition int, symbolCh } } charSymbolDictionary[char] = symbol - if char < 196608 { - maxRune = max(char, maxRune) - } + maxRune = max(char, maxRune) symbolCharDictionary[symbol] = append(symbolCharDictionary[symbol], char) } } diff --git a/util.go b/util.go index 351d3192..ea5e1c4c 100644 --- a/util.go +++ b/util.go @@ -115,29 +115,16 @@ func utf8toutf16(s string, withBOM ...bool) string { if bom { res = append(res, 0xFE, 0xFF) } - nb := len(s) - i := 0 - for i < nb { - c1 := byte(s[i]) - i++ - switch { - case c1 >= 224: - // 3-byte character - c2 := byte(s[i]) - i++ - c3 := byte(s[i]) - i++ - res = append(res, ((c1&0x0F)<<4)+((c2&0x3C)>>2), - ((c2&0x03)<<6)+(c3&0x3F)) - case c1 >= 192: - // 2-byte character - c2 := byte(s[i]) - i++ - res = append(res, ((c1 & 0x1C) >> 2), - ((c1&0x03)<<6)+(c2&0x3F)) - default: - // Single-byte character - res = append(res, 0, c1) + for _, r := range s { + if r < 0x10000 { + // BMP character + res = append(res, byte(r>>8), byte(r)) + } else { + // Supplementary character (needs surrogate pair) + r -= 0x10000 + high := 0xD800 | (r >> 10) + low := 0xDC00 | (r & 0x3FF) + res = append(res, byte(high>>8), byte(high), byte(low>>8), byte(low)) } } return string(res)