From 0d6a38d49d631da305a6f17704f06f35c36cbddc Mon Sep 17 00:00:00 2001 From: DarkFreedman Date: Fri, 19 Apr 2019 17:00:39 +0300 Subject: Added full support for UTF-8 font. With MIT license. --- README.md | 16 +- def.go | 34 +- font.go | 2 + font/DejaVuSansCondensed-Bold.ttf | Bin 0 -> 665028 bytes font/DejaVuSansCondensed-BoldOblique.ttf | Bin 0 -> 611836 bytes font/DejaVuSansCondensed-Oblique.ttf | Bin 0 -> 599292 bytes font/DejaVuSansCondensed.json | 1 + font/DejaVuSansCondensed.ttf | Bin 0 -> 680264 bytes fpdf.go | 784 +++++++++++++++++---- fpdf_test.go | 33 + pdf/reference/Fpdf_AddUTF8Font.pdf | Bin 0 -> 67549 bytes text/utf-8test.txt | 38 + text/utf-8test2.txt | 1 + utf8fontfile.go | 1124 ++++++++++++++++++++++++++++++ util.go | 141 +++- 15 files changed, 2020 insertions(+), 154 deletions(-) create mode 100644 font/DejaVuSansCondensed-Bold.ttf create mode 100644 font/DejaVuSansCondensed-BoldOblique.ttf create mode 100644 font/DejaVuSansCondensed-Oblique.ttf create mode 100644 font/DejaVuSansCondensed.json create mode 100644 font/DejaVuSansCondensed.ttf create mode 100644 pdf/reference/Fpdf_AddUTF8Font.pdf create mode 100644 text/utf-8test.txt create mode 100644 text/utf-8test2.txt create mode 100644 utf8fontfile.go diff --git a/README.md b/README.md index 54c4d55..860d524 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,14 @@ text, drawing and images. * Templates * Barcodes * Charting facility +* UTF-8 support gofpdf has no dependencies other than the Go standard library. All tests pass on Linux, Mac and Windows platforms. -Like FPDF version 1.7, from which gofpdf is derived, this package does not yet -support UTF-8 fonts. In particular, languages that require more than one code -page such as Chinese, Japanese, and Arabic are not currently supported. This is -explained in [issue 109](https://github.com/jung-kurt/gofpdf/issues/109). However, support is provided to automatically translate +gofpdf supports UTF-8 fonts. + +Also, support is provided to automatically translate UTF-8 runes to code page encodings for languages that have fewer than 256 glyphs. @@ -135,7 +135,9 @@ for all examples. Nothing special is required to use the standard PDF fonts (courier, helvetica, times, zapfdingbats) in your documents other than calling SetFont(). -In order to use a different TrueType or Type1 font, you will need to generate a +You should use AddUTF8Font or AddUTF8FontFromBytes to add UTF-8 TTF font. + +In order to use a different non-UTF-8 TrueType or Type1 font, you will need to generate a font definition file and, if the font will be embedded into PDFs, a compressed version of the font file. This is done by calling the MakeFont function or using the included makefont command line utility. To create the utility, cd @@ -225,12 +227,10 @@ which the internal catalogs were not sorted stably. Paul Montag added encoding and decoding functionality for templates, including images that are embedded in templates; this allows templates to be stored independently of gofpdf. Paul also added support for page boxes used in printing PDF documents. Wojciech -Matusiak added supported for word spacing. +Matusiak added supported for word spacing. Artem Korotkiy added support of UTF-8 fonts. ## Roadmap -* Handle UTF-8 source text natively. Until then, automatic translation of -UTF-8 runes to code page bytes is provided. * Improve test coverage as reported by the coverage tool. diff --git a/def.go b/def.go index fcc9237..4c46d03 100644 --- a/def.go +++ b/def.go @@ -266,6 +266,7 @@ type fontFileType struct { n int embedded bool content []byte + fontType string } type linkType struct { @@ -495,6 +496,7 @@ type PageBox struct { // Fpdf is the principal structure for creating a single PDF document type Fpdf struct { + isCurrentUTF8 bool // is current font used in utf-8 mode page int // current page number n int // current object number offsets []int // array of object offsets @@ -682,20 +684,22 @@ 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 [256]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 + 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 } // generateFontID generates a font Id from the font definition @@ -715,7 +719,7 @@ type fontInfoType struct { IsFixedPitch bool UnderlineThickness int UnderlinePosition int - Widths [256]int + Widths []int Size1, Size2 uint32 Desc FontDescType } diff --git a/font.go b/font.go index 7443a30..29417bb 100644 --- a/font.go +++ b/font.go @@ -83,6 +83,7 @@ func loadMap(encodingFileStr string) (encList encListType, err error) { // getInfoFromTrueType returns information from a TrueType font func getInfoFromTrueType(fileStr string, msgWriter io.Writer, embed bool, encList encListType) (info fontInfoType, err error) { + info.Widths = make([]int, 256) var ttf TtfType ttf, err = TtfParse(fileStr) if err != nil { @@ -168,6 +169,7 @@ func segmentRead(r io.Reader) (s segmentType, err error) { // getInfoFromType1 return information from a Type1 font func getInfoFromType1(fileStr string, msgWriter io.Writer, embed bool, encList encListType) (info fontInfoType, err error) { + info.Widths = make([]int, 256) if embed { var f *os.File f, err = os.Open(fileStr) diff --git a/font/DejaVuSansCondensed-Bold.ttf b/font/DejaVuSansCondensed-Bold.ttf new file mode 100644 index 0000000..22987c6 Binary files /dev/null and b/font/DejaVuSansCondensed-Bold.ttf differ diff --git a/font/DejaVuSansCondensed-BoldOblique.ttf b/font/DejaVuSansCondensed-BoldOblique.ttf new file mode 100644 index 0000000..f5fa0ca Binary files /dev/null and b/font/DejaVuSansCondensed-BoldOblique.ttf differ diff --git a/font/DejaVuSansCondensed-Oblique.ttf b/font/DejaVuSansCondensed-Oblique.ttf new file mode 100644 index 0000000..7fde907 Binary files /dev/null and b/font/DejaVuSansCondensed-Oblique.ttf differ diff --git a/font/DejaVuSansCondensed.json b/font/DejaVuSansCondensed.json new file mode 100644 index 0000000..f833dff --- /dev/null +++ b/font/DejaVuSansCondensed.json @@ -0,0 +1 @@ +{"Tp":"TrueType","Name":"DejaVuSansCondensed","Desc":{"Ascent":760,"Descent":-240,"CapHeight":760,"Flags":32,"FontBBox":{"Xmin":-918,"Ymin":-463,"Xmax":1614,"Ymax":1232},"ItalicAngle":0,"StemV":70,"MissingWidth":540},"Up":-63,"Ut":44,"Cw":[540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,540,286,360,414,754,572,855,702,247,351,351,450,754,286,325,286,303,572,572,572,572,572,572,572,572,572,572,303,303,754,754,754,478,900,615,617,628,693,568,518,697,677,265,265,590,501,776,673,708,542,708,625,571,549,659,615,890,616,549,616,351,303,351,754,450,450,551,571,495,571,554,316,571,570,250,250,521,250,876,570,550,571,571,370,469,353,570,532,736,532,532,472,572,303,572,754,540,708,549,286,473,466,900,450,450,572,1208,984,360,940,639,708,677,563,286,286,466,466,531,450,900,540,900,812,360,809,543,586,588,286,548,532,265,572,549,303,450,568,900,628,550,754,325,900,265,450,754,265,250,473,572,572,286,554,936,494,550,250,571,469,250,615,617,617,549,703,568,969,577,673,673,639,677,776,677,708,677,542,628,549,548,774,616,699,617,962,984,749,794,617,628,971,625,551,555,530,473,622,554,811,479,584,584,543,575,679,588,550,588,571,495,524,532,769,532,612,532,823,848,636,710,530,494,757,541],"Enc":"cp1251","Diff":"128 /afii10051 /afii10052 131 /afii10100 136 /Euro 138 /afii10058 140 /afii10059 /afii10061 /afii10060 /afii10145 /afii10099 152 /.notdef 154 /afii10106 156 /afii10107 /afii10109 /afii10108 /afii10193 161 /afii10062 /afii10110 /afii10057 165 /afii10050 168 /afii10023 170 /afii10053 175 /afii10056 178 /afii10055 /afii10103 /afii10098 184 /afii10071 /afii61352 /afii10101 188 /afii10105 /afii10054 /afii10102 /afii10104 /afii10017 /afii10018 /afii10019 /afii10020 /afii10021 /afii10022 /afii10024 /afii10025 /afii10026 /afii10027 /afii10028 /afii10029 /afii10030 /afii10031 /afii10032 /afii10033 /afii10034 /afii10035 /afii10036 /afii10037 /afii10038 /afii10039 /afii10040 /afii10041 /afii10042 /afii10043 /afii10044 /afii10045 /afii10046 /afii10047 /afii10048 /afii10049 /afii10065 /afii10066 /afii10067 /afii10068 /afii10069 /afii10070 /afii10072 /afii10073 /afii10074 /afii10075 /afii10076 /afii10077 /afii10078 /afii10079 /afii10080 /afii10081 /afii10082 /afii10083 /afii10084 /afii10085 /afii10086 /afii10087 /afii10088 /afii10089 /afii10090 /afii10091 /afii10092 /afii10093 /afii10094 /afii10095 /afii10096 /afii10097","File":"","Size1":0,"Size2":0,"OriginalSize":0,"N":0,"DiffN":0} \ No newline at end of file diff --git a/font/DejaVuSansCondensed.ttf b/font/DejaVuSansCondensed.ttf new file mode 100644 index 0000000..3259bc2 Binary files /dev/null and b/font/DejaVuSansCondensed.ttf differ diff --git a/fpdf.go b/fpdf.go index bd4aa4e..1400a85 100644 --- a/fpdf.go +++ b/fpdf.go @@ -912,17 +912,43 @@ func (f *Fpdf) GetTextColor() (int, int, int) { // GetStringWidth returns the length of a string in user units. A font must be // currently selected. func (f *Fpdf) GetStringWidth(s string) float64 { + if f.err != nil { + return 0 + } + w := f.GetStringSymbolWidth(s) + return float64(w) * f.fontSize / 1000 +} + +// GetStringSymbolWidth returns the length of a string in glyf units. A font must be +// currently selected. +func (f *Fpdf) GetStringSymbolWidth(s string) int { if f.err != nil { return 0 } w := 0 - for _, ch := range []byte(s) { - if ch == 0 { - break + if f.isCurrentUTF8 { + 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] + } + } else if f.currentFont.Desc.MissingWidth != 0 { + w += f.currentFont.Desc.MissingWidth + } else { + w += 500 + } + } + } else { + for _, ch := range []byte(s) { + if ch == 0 { + break + } + w += f.currentFont.Cw[ch] } - w += f.currentFont.Cw[ch] } - return float64(w) * f.fontSize / 1000 + return w } // SetLineWidth defines the line width. By default, the value equals 0.2 mm. @@ -1522,30 +1548,128 @@ func (f *Fpdf) ClipEnd() { // definition file to be added. The file will be loaded from the font directory // specified in the call to New() or SetFontLocation(). func (f *Fpdf) AddFont(familyStr, styleStr, fileStr string) { + f.addFont(familyStr, styleStr, fileStr, false) +} + +// AddUTF8Font imports a TrueType font with utf-8 symbols and makes it available. +// It is necessary to generate a font definition file first with the makefont +// utility. It is not necessary to call this function for the core PDF fonts +// (courier, helvetica, times, zapfdingbats). +// +// The JSON definition file (and the font file itself when embedding) must be +// present in the font directory. If it is not found, the error "Could not +// include font definition file" is set. +// +// family specifies the font family. The name can be chosen arbitrarily. If it +// is a standard family name, it will override the corresponding font. This +// string is used to subsequently set the font with the SetFont method. +// +// style specifies the font style. Acceptable values are (case insensitive) the +// empty string for regular style, "B" for bold, "I" for italic, or "BI" or +// "IB" for bold and italic combined. +// +// fileStr specifies the base name with ".json" extension of the font +// definition file to be added. The file will be loaded from the font directory +// specified in the call to New() or SetFontLocation(). +func (f *Fpdf) AddUTF8Font(familyStr, styleStr, fileStr string) { + f.addFont(familyStr, styleStr, fileStr, true) +} + +func (f *Fpdf) addFont(familyStr, styleStr, fileStr string, isUTF8 bool) { if fileStr == "" { - fileStr = strings.Replace(familyStr, " ", "", -1) + strings.ToLower(styleStr) + ".json" + if isUTF8 { + fileStr = strings.Replace(familyStr, " ", "", -1) + strings.ToLower(styleStr) + ".ttf" + } else { + fileStr = strings.Replace(familyStr, " ", "", -1) + strings.ToLower(styleStr) + ".json" + } } + if isUTF8 { + fontKey := getFontKey(familyStr, styleStr) + _, ok := f.fonts[fontKey] + if ok { + return + } + ttfStat, _ := os.Stat(fileStr) + originalSize := ttfStat.Size() + Type := "UTF8" - if f.fontLoader != nil { - reader, err := f.fontLoader.Open(fileStr) - if err == nil { - f.AddFontFromReader(familyStr, styleStr, reader) - if closer, ok := reader.(io.Closer); ok { - closer.Close() + utf8Bytes, _ := ioutil.ReadFile(fileStr) + reader := fileReader{readerPosition: 0, array: utf8Bytes} + utf8File := newUTF8Font(&reader) + + err := utf8File.parseFile() + if err != nil { + fmt.Printf("get metrics Error: %e\n", err) + return + } + + desc := FontDescType{ + Ascent: int(utf8File.Ascent), + Descent: int(utf8File.Descent), + CapHeight: utf8File.CapHeight, + Flags: utf8File.Flags, + FontBBox: utf8File.Bbox, + ItalicAngle: utf8File.ItalicAngle, + StemV: utf8File.StemV, + MissingWidth: round(utf8File.DefaultWidth), + } + + var sbarr map[int]int + if f.aliasNbPagesStr == "" { + sbarr = makeSubsetRange(57) + } else { + 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, + } + def.i, _ = generateFontID(def) + f.fonts[fontKey] = def + f.fontFiles[fontKey] = fontFileType{ + length1: originalSize, + fontType: "UTF8", + } + f.fontFiles[fileStr] = fontFileType{ + fontType: "UTF8", + } + } else { + if f.fontLoader != nil { + reader, err := f.fontLoader.Open(fileStr) + if err == nil { + f.AddFontFromReader(familyStr, styleStr, reader) + if closer, ok := reader.(io.Closer); ok { + closer.Close() + } + return } + } + + fileStr = path.Join(f.fontpath, fileStr) + file, err := os.Open(fileStr) + if err != nil { + f.err = err return } - } + defer file.Close() - fileStr = path.Join(f.fontpath, fileStr) - file, err := os.Open(fileStr) - if err != nil { - f.err = err - return + f.AddFontFromReader(familyStr, styleStr, file) } - defer file.Close() +} - f.AddFontFromReader(familyStr, styleStr, file) +func makeSubsetRange(end int) map[int]int { + answer := make(map[int]int) + for i := 0; i < end; i++ { + answer[i] = 0 + } + return answer } // AddFontFromBytes imports a TrueType, OpenType or Type1 font from static @@ -1564,6 +1688,29 @@ func (f *Fpdf) AddFont(familyStr, styleStr, fileStr string) { // // zFileBytes contain all bytes of Z file. func (f *Fpdf) AddFontFromBytes(familyStr, styleStr string, jsonFileBytes, zFileBytes []byte) { + f.addFontFromBytes(familyStr, styleStr, jsonFileBytes, zFileBytes, nil) +} + +// AddUTF8FontFromBytes imports a TrueType font with utf-8 symbols from static +// bytes within the executable and makes it available for use in the generated +// document. +// +// family specifies the font family. The name can be chosen arbitrarily. If it +// is a standard family name, it will override the corresponding font. This +// string is used to subsequently set the font with the SetFont method. +// +// style specifies the font style. Acceptable values are (case insensitive) the +// empty string for regular style, "B" for bold, "I" for italic, or "BI" or +// "IB" for bold and italic combined. +// +// jsonFileBytes contain all bytes of JSON file. +// +// zFileBytes contain all bytes of Z file. +func (f *Fpdf) AddUTF8FontFromBytes(familyStr, styleStr string, utf8Bytes []byte) { + f.addFontFromBytes(familyStr, styleStr, nil, nil, utf8Bytes) +} + +func (f *Fpdf) addFontFromBytes(familyStr, styleStr string, jsonFileBytes, zFileBytes, utf8Bytes []byte) { if f.err != nil { return } @@ -1577,61 +1724,108 @@ func (f *Fpdf) AddFontFromBytes(familyStr, styleStr string, jsonFileBytes, zFile return } - // load font definitions - var info fontDefType - err := json.Unmarshal(jsonFileBytes, &info) + if utf8Bytes != nil { - if err != nil { - f.err = err - } + if styleStr == "IB" { + styleStr = "BI" + } - if f.err != nil { - return - } + Type := "UTF8" + reader := fileReader{readerPosition: 0, array: utf8Bytes} - if info.i, err = generateFontID(info); err != nil { - f.err = err - return - } + utf8File := newUTF8Font(&reader) - // search existing encodings - if len(info.Diff) > 0 { - n := -1 + err := utf8File.parseFile() + if err != nil { + fmt.Printf("get metrics Error: %e\n", err) + return + } + desc := FontDescType{ + Ascent: int(utf8File.Ascent), + Descent: int(utf8File.Descent), + CapHeight: utf8File.CapHeight, + Flags: utf8File.Flags, + FontBBox: utf8File.Bbox, + ItalicAngle: utf8File.ItalicAngle, + StemV: utf8File.StemV, + MissingWidth: round(utf8File.DefaultWidth), + } - for j, str := range f.diffs { - if str == info.Diff { - n = j + 1 - break - } + var sbarr map[int]int + if f.aliasNbPagesStr == "" { + sbarr = makeSubsetRange(57) + } else { + 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, + } + def.i, _ = generateFontID(def) + f.fonts[fontkey] = def + } else { + // load font definitions + var info fontDefType + err := json.Unmarshal(jsonFileBytes, &info) - if n < 0 { - f.diffs = append(f.diffs, info.Diff) - n = len(f.diffs) + if err != nil { + f.err = err } - info.DiffN = n - } + if f.err != nil { + return + } - // embed font - if len(info.File) > 0 { - if info.Tp == "TrueType" { - f.fontFiles[info.File] = fontFileType{ - length1: int64(info.OriginalSize), - embedded: true, - content: zFileBytes, + if info.i, err = generateFontID(info); err != nil { + f.err = err + return + } + + // search existing encodings + if len(info.Diff) > 0 { + n := -1 + + for j, str := range f.diffs { + if str == info.Diff { + n = j + 1 + break + } } - } else { - f.fontFiles[info.File] = fontFileType{ - length1: int64(info.Size1), - length2: int64(info.Size2), - embedded: true, - content: zFileBytes, + + if n < 0 { + f.diffs = append(f.diffs, info.Diff) + n = len(f.diffs) + } + + info.DiffN = n + } + + // embed font + if len(info.File) > 0 { + if info.Tp == "TrueType" { + f.fontFiles[info.File] = fontFileType{ + length1: int64(info.OriginalSize), + embedded: true, + content: zFileBytes, + } + } else { + f.fontFiles[info.File] = fontFileType{ + length1: int64(info.Size1), + length2: int64(info.Size2), + embedded: true, + content: zFileBytes, + } } } - } - f.fonts[fontkey] = info + f.fonts[fontkey] = info + } } // getFontKey is used by AddFontFromReader and GetFontDesc @@ -1756,8 +1950,8 @@ func (f *Fpdf) SetFont(familyStr, styleStr string, size float64) { } // Test if font is already loaded - fontkey := familyStr + styleStr - _, ok = f.fonts[fontkey] + fontKey := familyStr + styleStr + _, ok = f.fonts[fontKey] if !ok { // Test if one of the core fonts if familyStr == "arial" { @@ -1771,8 +1965,8 @@ func (f *Fpdf) SetFont(familyStr, styleStr string, size float64) { if familyStr == "zapfdingbats" { styleStr = "" } - fontkey = familyStr + styleStr - _, ok = f.fonts[fontkey] + fontKey = familyStr + styleStr + _, ok = f.fonts[fontKey] if !ok { rdr := f.coreFontReader(familyStr, styleStr) if f.err == nil { @@ -1792,7 +1986,12 @@ func (f *Fpdf) SetFont(familyStr, styleStr string, size float64) { f.fontStyle = styleStr f.fontSizePt = size f.fontSize = size / f.k - f.currentFont = f.fonts[fontkey] + f.currentFont = f.fonts[fontKey] + if f.currentFont.Tp == "UTF8" { + f.isCurrentUTF8 = true + } else { + f.isCurrentUTF8 = false + } if f.page > 0 { f.outf("BT /F%s %.2f Tf ET", f.currentFont.i, f.fontSizePt) } @@ -1890,7 +2089,16 @@ func (f *Fpdf) Bookmark(txtStr string, level int, y float64) { // 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) { - s := sprintf("BT %.2f %.2f Td (%s) Tj ET", x*f.k, (f.h-y)*f.k, f.escape(txtStr)) + var txt2 string + if f.isCurrentUTF8 { + txt2 = f.escape(utf8toutf16(txtStr, false)) + for _, uni := range []rune(txtStr) { + f.currentFont.usedRunes[int(uni)] = int(uni) + } + } else { + txt2 = f.escape(txtStr) + } + s := sprintf("BT %.2f %.2f Td (%s) Tj ET", x*f.k, (f.h-y)*f.k, txt2) if f.underline && txtStr != "" { s += " " + f.dounderline(x, y, txtStr) } @@ -2069,14 +2277,46 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, if f.colorFlag { s.printf("q %s ", f.color.text.str) } - txt2 := strings.Replace(txtStr, "\\", "\\\\", -1) - txt2 = strings.Replace(txt2, "(", "\\(", -1) - txt2 = strings.Replace(txt2, ")", "\\)", -1) - // if strings.Contains(txt2, "end of excerpt") { - // dbg("f.h %.2f, f.y %.2f, h %.2f, f.fontSize %.2f, k %.2f", f.h, f.y, h, f.fontSize, k) - // } - s.printf("BT %.2f %.2f Td (%s) Tj ET", (f.x+dx)*k, (f.h-(f.y+dy+.5*h+.3*f.fontSize))*k, txt2) - //BT %.2F %.2F Td (%s) Tj ET',($this->x+$dx)*$k,($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k,$txt2); + //If multibyte, Tw has no effect - do word spacing using an adjustment before each space + if (f.ws != 0 || alignStr == "J") && f.isCurrentUTF8 { // && f.ws != 0 + 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)) + 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, " ") + shift := float64((wmax - strSize)) / float64(len(t)-1) + numt := len(t) + for i := 0; i < numt; i++ { + tx := t[i] + tx = "(" + f.escape(utf8toutf16(tx, false)) + ")" + s.printf("%s ", tx) + if (i + 1) < numt { + s.printf("%.3f(%s) ", -shift, space) + } + } + s.printf("] TJ ET") + } else { + var txt2 string + if f.isCurrentUTF8 { + txt2 = f.escape(utf8toutf16(txtStr, false)) + for _, uni := range []rune(txtStr) { + f.currentFont.usedRunes[int(uni)] = int(uni) + } + } else { + + txt2 = strings.Replace(txtStr, "\\", "\\\\", -1) + txt2 = strings.Replace(txt2, "(", "\\(", -1) + txt2 = strings.Replace(txt2, ")", "\\)", -1) + } + bt := (f.x + dx) * k + td := (f.h - (f.y + dy + .5*h + .3*f.fontSize)) * k + s.printf("BT %.2f %.2f Td (%s)Tj ET", bt, td, txt2) + //BT %.2F %.2F Td (%s) Tj ET',(f.x+dx)*k,(f.h-(f.y+.5*h+.3*f.FontSize))*k,txt2); + } + if f.underline { s.printf(" %s", f.dounderline(f.x+dx, f.y+dy+.5*h+.3*f.fontSize, txtStr)) } @@ -2127,7 +2367,7 @@ func (f *Fpdf) Cellf(w, h float64, fmtStr string, args ...interface{}) { func (f *Fpdf) SplitLines(txt []byte, w float64) [][]byte { // Function contributed by Bruno Michel lines := [][]byte{} - cw := &f.currentFont.Cw + cw := f.currentFont.Cw wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) s := bytes.Replace(txt, []byte("\r"), []byte{}, -1) nb := len(s) @@ -2187,17 +2427,26 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill if alignStr == "" { alignStr = "J" } - cw := &f.currentFont.Cw + cw := f.currentFont.Cw if w == 0 { w = f.w - f.rMargin - f.x } wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) s := strings.Replace(txtStr, "\r", "", -1) - nb := len(s) - // if nb > 0 && s[nb-1:nb] == "\n" { - if nb > 0 && []byte(s)[nb-1] == '\n' { - nb-- - s = s[0:nb] + + var nb int + if f.isCurrentUTF8 { + nb = len([]rune(s)) + for nb > 0 && []rune(s)[nb-1] == '\n' { + nb-- + s = string([]rune(s)[0:nb]) + } + } else { + nb = len(s) + if nb > 0 && []byte(s)[nb-1] == '\n' { + nb-- + s = s[0:nb] + } } // dbg("[%s]\n", s) var b, b2 string @@ -2231,14 +2480,28 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill nl := 1 for i < nb { // Get next character - c := []byte(s)[i] + var c rune + if f.isCurrentUTF8 { + c = []rune(s)[i] + } else { + c = rune([]byte(s)[i]) + } if c == '\n' { // Explicit line break if f.ws > 0 { f.ws = 0 f.out("0 Tw") } - f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") + + if f.isCurrentUTF8 { + newAlignStr := alignStr + if newAlignStr == "J" { + newAlignStr = "L" + } + f.CellFormat(w, h, string([]rune(s)[j:i]), b, 2, newAlignStr, fill, 0, "") + } else { + f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") + } i++ sep = -1 j = i @@ -2255,7 +2518,11 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill ls = l ns++ } - l += cw[c] + if cw[int(c)] == 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)] + } if l > wmax { // Automatic line break if sep == -1 { @@ -2266,7 +2533,11 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill f.ws = 0 f.out("0 Tw") } - f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") + if f.isCurrentUTF8 { + f.CellFormat(w, h, string([]rune(s)[j:i]), b, 2, alignStr, fill, 0, "") + } else { + f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") + } } else { if alignStr == "J" { if ns > 1 { @@ -2276,7 +2547,11 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill } f.outf("%.3f Tw", f.ws*f.k) } - f.CellFormat(w, h, s[j:sep], b, 2, alignStr, fill, 0, "") + if f.isCurrentUTF8 { + f.CellFormat(w, h, string([]rune(s)[j:sep]), b, 2, alignStr, fill, 0, "") + } else { + f.CellFormat(w, h, s[j:sep], b, 2, alignStr, fill, 0, "") + } i = sep + 1 } sep = -1 @@ -2299,18 +2574,34 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill if len(borderStr) > 0 && strings.Contains(borderStr, "B") { b += "B" } - f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") + if f.isCurrentUTF8 { + if alignStr == "J" { + alignStr = "" + } + f.CellFormat(w, h, string([]rune(s)[j:i]), b, 2, alignStr, fill, 0, "") + } else { + f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") + } f.x = f.lMargin } // write outputs text in flowing mode func (f *Fpdf) write(h float64, txtStr string, link int, linkStr string) { // dbg("Write") - cw := &f.currentFont.Cw + cw := f.currentFont.Cw w := f.w - f.rMargin - f.x wmax := (w - 2*f.cMargin) * 1000 / f.fontSize s := strings.Replace(txtStr, "\r", "", -1) - nb := len(s) + var nb int + if f.isCurrentUTF8 { + nb = len([]rune(s)) + if nb == 1 && s == " " { + f.x += f.GetStringWidth(s) + return + } + } else { + nb = len(s) + } sep := -1 i := 0 j := 0 @@ -2318,10 +2609,19 @@ func (f *Fpdf) write(h float64, txtStr string, link int, linkStr string) { nl := 1 for i < nb { // Get next character - c := []byte(s)[i] + var c rune + if f.isCurrentUTF8 { + c = []rune(s)[i] + } else { + c = rune([]byte(s)[i]) + } if c == '\n' { // Explicit line break - f.CellFormat(w, h, s[j:i], "", 2, "", false, link, linkStr) + if f.isCurrentUTF8 { + f.CellFormat(w, h, string([]rune(s)[j:i]), "", 2, "", false, link, linkStr) + } else { + f.CellFormat(w, h, s[j:i], "", 2, "", false, link, linkStr) + } i++ sep = -1 j = i @@ -2337,7 +2637,7 @@ func (f *Fpdf) write(h float64, txtStr string, link int, linkStr string) { if c == ' ' { sep = i } - l += float64(cw[c]) + l += float64(cw[int(c)]) if l > wmax { // Automatic line break if sep == -1 { @@ -2354,9 +2654,17 @@ func (f *Fpdf) write(h float64, txtStr string, link int, linkStr string) { if i == j { i++ } - f.CellFormat(w, h, s[j:i], "", 2, "", false, link, linkStr) + if f.isCurrentUTF8 { + f.CellFormat(w, h, string([]rune(s)[j:i]), "", 2, "", false, link, linkStr) + } else { + f.CellFormat(w, h, s[j:i], "", 2, "", false, link, linkStr) + } } else { - f.CellFormat(w, h, s[j:sep], "", 2, "", false, link, linkStr) + if f.isCurrentUTF8 { + f.CellFormat(w, h, string([]rune(s)[j:sep]), "", 2, "", false, link, linkStr) + } else { + f.CellFormat(w, h, s[j:sep], "", 2, "", false, link, linkStr) + } i = sep + 1 } sep = -1 @@ -2374,7 +2682,11 @@ func (f *Fpdf) write(h float64, txtStr string, link int, linkStr string) { } // Last chunk if i != j { - f.CellFormat(l/1000*f.fontSize, h, s[j:], "", 0, "", false, link, linkStr) + if f.isCurrentUTF8 { + f.CellFormat(l/1000*f.fontSize, h, string([]rune(s)[j:]), "", 0, "", false, link, linkStr) + } else { + f.CellFormat(l/1000*f.fontSize, h, s[j:], "", 0, "", false, link, linkStr) + } } } @@ -3239,6 +3551,9 @@ func (f *Fpdf) putpages() { nb := f.page if len(f.aliasNbPagesStr) > 0 { // Replace number of pages + alias := utf8toutf16(f.aliasNbPagesStr, false) + r := utf8toutf16(sprintf("%d", nb), false) + f.RegisterAlias(alias, r) f.RegisterAlias(f.aliasNbPagesStr, sprintf("%d", nb)) } f.replaceAliases() @@ -3345,42 +3660,41 @@ func (f *Fpdf) putfonts() { } for _, file = range fileList { info = f.fontFiles[file] - // Font file embedding - f.newobj() - info.n = f.n - f.fontFiles[file] = info + if info.fontType != "UTF8" { + f.newobj() + info.n = f.n + f.fontFiles[file] = info - var font []byte + var font []byte - if info.embedded { - font = info.content - } else { - var err error - font, err = f.loadFontFile(file) - if err != nil { - f.err = err - return + if info.embedded { + font = info.content + } else { + var err error + font, err = f.loadFontFile(file) + if err != nil { + f.err = err + return + } } + compressed := file[len(file)-2:] == ".z" + if !compressed && info.length2 > 0 { + buf := font[6:info.length1] + buf = append(buf, font[6+info.length1+6:info.length2]...) + font = buf + } + f.outf("< 0 { + f.outf("/Length2 %d /Length3 0", info.length2) + } + f.out(">>") + f.putstream(font) + f.out("endobj") } - - // dbg("font file [%s], ext [%s]", file, file[len(file)-2:]) - compressed := file[len(file)-2:] == ".z" - if !compressed && info.length2 > 0 { - buf := font[6:info.length1] - buf = append(buf, font[6+info.length1+6:info.length2]...) - font = buf - } - f.outf("< 0 { - f.outf("/Length2 %d /Length3 0", info.length2) - } - f.out(">>") - f.putstream(font) - f.out("endobj") } } { @@ -3460,6 +3774,81 @@ func (f *Fpdf) putfonts() { s.printf("/FontFile%s %d 0 R>>", suffix, f.fontFiles[font.File].n) f.out(s.String()) f.out("endobj") + case "UTF8": + fontName := "utf8" + font.Name + usedRunes := font.usedRunes + delete(usedRunes, 0) + utf8FontStream := font.utf8File.GenerateСutFont(usedRunes) + utf8FontSize := len(utf8FontStream) + compressedFontStream := sliceCompress(utf8FontStream) + CodeSignDictionary := font.utf8File.CodeSymbolDictionary + delete(CodeSignDictionary, 0) + + f.newobj() + f.out(fmt.Sprintf("<>\n"+"endobj", fontName, f.n+1, f.n+2)) + + f.newobj() + f.out("<>") + f.out("endobj") + + f.newobj() + f.out("<>") + f.putstream([]byte(toUnicode)) + f.out("endobj") + + // CIDInfo + f.newobj() + f.out("<>") + f.out("endobj") + + // Font descriptor + f.newobj() + var s fmtBuffer + s.printf("<>") + f.out(s.String()) + f.out("endobj") + + // Embed CIDToGIDMap + cidToGidMap := make([]byte, 256*256*2) + + for cc, glyph := range CodeSignDictionary { + cidToGidMap[cc*2] = byte(glyph >> 8) + cidToGidMap[cc*2+1] = byte(glyph & 0xFF) + } + + cidToGidMap = sliceCompress(cidToGidMap) + f.newobj() + f.out("<>") + f.putstream(cidToGidMap) + f.out("endobj") + + //Font file + f.newobj() + f.out("<>") + f.putstream(compressedFontStream) + f.out("endobj") default: f.err = fmt.Errorf("unsupported font type: %s", tp) return @@ -3469,6 +3858,145 @@ func (f *Fpdf) putfonts() { return } +func (f *Fpdf) generateCIDFontMap(font *fontDefType, LastRune int) { + rangeID := 0 + cidArray := make(map[int]*untypedKeyMap) + cidArrayKeys := make([]int, 0) + prevCid := -2 + prevWidth := -1 + interval := false + startCid := 1 + cwLen := LastRune + 1 + + // for each character + for cid := startCid; cid < cwLen; cid++ { + if font.Cw[cid] == 0x00 { + continue + } + width := font.Cw[cid] + if width == 65535 { + width = 0 + } + if numb, OK := font.usedRunes[cid]; cid > 255 && (!OK || numb == 0) { + continue + } + + if cid == prevCid+1 { + if width == prevWidth { + + if width == cidArray[rangeID].get(0) { + cidArray[rangeID].put(nil, width) + } else { + cidArray[rangeID].pop() + rangeID = prevCid + r := untypedKeyMap{ + valueSet: make([]int, 0), + keySet: make([]interface{}, 0), + } + cidArray[rangeID] = &r + cidArrayKeys = append(cidArrayKeys, rangeID) + cidArray[rangeID].put(nil, prevWidth) + cidArray[rangeID].put(nil, width) + } + interval = true + cidArray[rangeID].put("interval", 1) + ui := 0 + ui = ui + 1 + } else { + if interval { + // new range + rangeID = cid + r := untypedKeyMap{ + valueSet: make([]int, 0), + keySet: make([]interface{}, 0), + } + cidArray[rangeID] = &r + cidArrayKeys = append(cidArrayKeys, rangeID) + cidArray[rangeID].put(nil, width) + } else { + cidArray[rangeID].put(nil, width) + } + interval = false + } + } else { + rangeID = cid + r := untypedKeyMap{ + valueSet: make([]int, 0), + keySet: make([]interface{}, 0), + } + cidArray[rangeID] = &r + cidArrayKeys = append(cidArrayKeys, rangeID) + cidArray[rangeID].put(nil, width) + interval = false + } + prevCid = cid + prevWidth = width + + } + previousKey := -1 + nextKey := -1 + isInterval := false + for g := 0; g < len(cidArrayKeys); { + key := cidArrayKeys[g] + ws := *cidArray[key] + cws := len(ws.keySet) + if (key == nextKey) && (!isInterval) && (ws.getIndex("interval") < 0 || cws < 4) { + if cidArray[key].getIndex("interval") >= 0 { + cidArray[key].delete("interval") + } + cidArray[previousKey] = arrayMerge(cidArray[previousKey], cidArray[key]) + cidArrayKeys = remove(cidArrayKeys, key) + } else { + g++ + previousKey = key + } + nextKey = key + cws + ui := ws.getIndex("interval") + ui = ui + 1 + if ws.getIndex("interval") >= 0 { + if cws > 3 { + isInterval = true + } else { + isInterval = false + } + cidArray[key].delete("interval") + nextKey-- + } else { + isInterval = false + } + } + var w fmtBuffer + for _, k := range cidArrayKeys { + ws := cidArray[k] + if len(arrayCountValues(ws.valueSet)) == 1 { + w.printf(" %d %d %d", k, k+len(ws.valueSet)-1, ws.get(0)) + } else { + w.printf(" %d [ %s ]\n", k, implode(" ", ws.valueSet)) + } + } + f.out("/W [" + w.String() + " ]") +} + +func implode(sep string, arr []int) string { + var s fmtBuffer + for i := 0; i < len(arr)-1; i++ { + s.printf("%v", arr[i]) + s.printf(sep) + } + if len(arr) > 0 { + s.printf("%v", arr[len(arr)-1]) + } + return s.String() +} + +func arrayCountValues(mp []int) map[int]int { + answer := make(map[int]int) + for _, v := range mp { + answer[v] = answer[v] + 1 + } + return answer +} + func (f *Fpdf) loadFontFile(name string) ([]byte, error) { if f.fontLoader != nil { reader, err := f.fontLoader.Open(name) diff --git a/fpdf_test.go b/fpdf_test.go index 7d956ce..6042fab 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -2519,3 +2519,36 @@ func ExampleFpdf_TransformRotate() { // Output: // Successfully generated pdf/Fpdf_RotateText.pdf } + +// ExampleFpdf_AddUTF8Font demonstrates how use the font +// with utf-8 mode +func ExampleFpdf_AddUTF8Font() { + pdf := gofpdf.New("P", "mm", "A4", "") + + pdf.AddPage() + + pdf.AddUTF8Font("dejavu", "", example.FontFile("DejaVuSansCondensed.ttf")) + pdf.AddUTF8Font("dejavu", "B", example.FontFile("DejaVuSansCondensed-Bold.ttf")) + pdf.AddUTF8Font("dejavu", "I", example.FontFile("DejaVuSansCondensed-Oblique.ttf")) + pdf.AddUTF8Font("dejavu", "BI", example.FontFile("DejaVuSansCondensed-BoldOblique.ttf")) + + txtStr, _ := ioutil.ReadFile(example.TextFile("utf-8test.txt")) + + pdf.SetFont("dejavu", "B", 17) + pdf.MultiCell(100, 8, "Text in different languages :", "", "C", false) + pdf.SetFont("dejavu", "", 14) + pdf.MultiCell(100, 5, string(txtStr), "", "C", false) + pdf.Ln(15) + + txtStr, _ = ioutil.ReadFile(example.TextFile("utf-8test2.txt")) + pdf.SetFont("dejavu", "BI", 17) + pdf.MultiCell(100, 8, "Greek text with alignStr = \"J\":", "", "C", false) + pdf.SetFont("dejavu", "I", 14) + pdf.MultiCell(100, 5, string(txtStr), "", "J", false) + + fileStr := example.Filename("Fpdf_AddUTF8Font") + err := pdf.OutputFileAndClose(fileStr) + example.Summary(err, fileStr) + // Output: + // Successfully generated pdf/Fpdf_AddUTF8Font.pdf +} diff --git a/pdf/reference/Fpdf_AddUTF8Font.pdf b/pdf/reference/Fpdf_AddUTF8Font.pdf new file mode 100644 index 0000000..cb2b6e2 Binary files /dev/null and b/pdf/reference/Fpdf_AddUTF8Font.pdf differ diff --git a/text/utf-8test.txt b/text/utf-8test.txt new file mode 100644 index 0000000..c9596a3 --- /dev/null +++ b/text/utf-8test.txt @@ -0,0 +1,38 @@ +English: +Eat some more of these soft French buns and drink some tea. + +Greek: +Φάτε μερικά από αυτά τα μαλακά γαλλικά κουλουράκια και πίνετε λίγο τσάι. + +Polish: +Zjedz trochę tych miękkich francuskich bułeczek i wypij herbatę. + +Portuguese: +Coma um pouco mais desses delicados pãezinhos franceses e beba um pouco de chá. + +Russian: +Ешьте еще эти мягкие французские булочки и пейте чай. + +Vietnamese: +Ăn thêm một số bánh Pháp mềm và uống một ít trà. + +Arabic: +أكل بعض أكثر من هذه الكعك الفرنسي لينة وشرب بعض الشاي. + +Armenian: +Կերեք այս փափուկ ֆրանսիական բանջարեղեններից մի քանիսը եւ մի քանի թեյ խմեք: + +Georgian: +ჭამე კიდევ უფრო ამ რბილი ფრანგული buns და ვსვავ გარკვეული ჩაი. + +Hebrew: +לאכול קצת יותר אלה לחמניות צרפתית רכה לשתות תה. + +Icelandic: +Borða meira af þessum mjúka franska bollum og drekkaðu te. + +Igbo: +Rie ụfọdụ n'ime ndị na-asụ French bun ma ṅụọ ụfọdụ tii. + +Kazakh: +Осы жұмсақ француз көкөністерінің кейбіреулерін жеп, кейбір шай ішіңіз. diff --git a/text/utf-8test2.txt b/text/utf-8test2.txt new file mode 100644 index 0000000..cc83e14 --- /dev/null +++ b/text/utf-8test2.txt @@ -0,0 +1 @@ +Η μορφή φορητού εγγράφου (PDF) είναι μια ανοικτή ηλεκτρονική μορφή εγγράφων, η οποία αναπτύχθηκε αρχικά από την Adobe Systems, χρησιμοποιώντας μια ποικιλία λειτουργιών γλώσσας PostScript. Πρώτα απ 'όλα προορίζεται για την παρουσίαση έντυπων προϊόντων σε ηλεκτρονική μορφή. Για να δείτε υπάρχουν πολλά προγράμματα, καθώς και το επίσημο δωρεάν πρόγραμμα Adobe Reader. Ένα σημαντικό μέρος του σύγχρονου επαγγελματικού εξοπλισμού εκτύπωσης έχει υποστήριξη υλικού για τη μορφή PDF, η οποία επιτρέπει την εκτύπωση εγγράφων σε αυτή τη μορφή χωρίς τη χρήση λογισμικού. Ο παραδοσιακός τρόπος δημιουργίας εγγράφων PDF είναι ένας εικονικός εκτυπωτής, δηλαδή το ίδιο το έγγραφο είναι έτοιμο στο εξειδικευμένο του πρόγραμμα - ένα γραφικό πρόγραμμα ή ένα πρόγραμμα επεξεργασίας κειμένου. diff --git a/utf8fontfile.go b/utf8fontfile.go new file mode 100644 index 0000000..ad9f924 --- /dev/null +++ b/utf8fontfile.go @@ -0,0 +1,1124 @@ +package gofpdf + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "sort" +) + +// flags +const symbolWords = 1 << 0 +const symbolScale = 1 << 3 +const symbolContinue = 1 << 5 +const symbolAllScale = 1 << 6 +const symbol2x2 = 1 << 7 +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" + +type utf8FontFile struct { + fileReader *fileReader + LastRune int + tableDescriptions map[string]*tableDescription + outTablesData map[string][]byte + symbolPosition []int + charSymbolDictionary map[int]int + Ascent int + Descent int + fontElementSize int + Bbox fontBoxType + CapHeight int + StemV int + ItalicAngle int + Flags int + UnderlinePosition float64 + UnderlineThickness float64 + CharWidths []int + DefaultWidth float64 + symbolData map[int]map[string][]int + CodeSymbolDictionary map[int]int +} + +type tableDescription struct { + name string + checksum []int + position int + size int +} + +type fileReader struct { + readerPosition int64 + array []byte +} + +func (fr *fileReader) Read(s int) []byte { + b := fr.array[fr.readerPosition : fr.readerPosition+int64(s)] + fr.readerPosition += int64(s) + return b +} + +func (fr *fileReader) seek(shift int64, flag int) (int64, error) { + if flag == 0 { + fr.readerPosition = shift + } else if flag == 1 { + fr.readerPosition += shift + } else if flag == 2 { + fr.readerPosition = int64(len(fr.array)) - shift + } + return int64(fr.readerPosition), nil +} + +func newUTF8Font(reader *fileReader) *utf8FontFile { + utf := utf8FontFile{ + fileReader: reader, + } + return &utf +} + +func (utf *utf8FontFile) parseFile() error { + utf.fileReader.readerPosition = 0 + utf.symbolPosition = make([]int, 0) + utf.charSymbolDictionary = make(map[int]int) + utf.tableDescriptions = make(map[string]*tableDescription) + utf.outTablesData = make(map[string][]byte) + utf.Ascent = 0 + utf.Descent = 0 + codeType := uint32(utf.readUint32()) + if codeType == 0x4F54544F { + return fmt.Errorf("not supported\n ") + } + if codeType == 0x74746366 { + return fmt.Errorf("not supported\n ") + } + if codeType != 0x00010000 && codeType != 0x74727565 { + return fmt.Errorf("Not a TrueType font: codeType=%v\n ", codeType) + } + utf.generateTableDescriptions() + utf.parseTables() + return nil +} + +func (utf *utf8FontFile) generateTableDescriptions() { + + tablesCount := utf.readUint16() + _ = utf.readUint16() + _ = utf.readUint16() + _ = utf.readUint16() + utf.tableDescriptions = make(map[string]*tableDescription) + + for i := 0; i < tablesCount; i++ { + record := tableDescription{ + name: utf.readTableName(), + checksum: []int{utf.readUint16(), utf.readUint16()}, + position: utf.readUint32(), + size: utf.readUint32(), + } + utf.tableDescriptions[record.name] = &record + } +} + +func (utf *utf8FontFile) readTableName() string { + return string(utf.fileReader.Read(4)) +} + +func (utf *utf8FontFile) readUint16() int { + s := utf.fileReader.Read(2) + return (int(s[0]) << 8) + int(s[1]) +} + +func (utf *utf8FontFile) readUint32() int { + s := utf.fileReader.Read(4) + return (int(s[0]) * 16777216) + (int(s[1]) << 16) + (int(s[2]) << 8) + int(s[3]) // 16777216 = 1<<24 +} + +func (utf *utf8FontFile) calcInt32(x, y []int) []int { + answer := make([]int, 2) + if y[1] > x[1] { + x[1] += 1 << 16 + x[0]++ + } + answer[1] = x[1] - y[1] + if y[0] > x[0] { + x[0] += 1 << 16 + } + answer[0] = x[0] - y[0] + answer[0] = answer[0] & 0xFFFF + return answer +} + +func (utf *utf8FontFile) generateChecksum(data []byte) []int { + if (len(data) % 4) != 0 { + for i := 0; (len(data) % 4) != 0; i++ { + data = append(data, 0) + } + } + answer := []int{0x0000, 0x0000} + for i := 0; i < len(data); i += 4 { + answer[0] += (int(data[i]) << 8) + int(data[i+1]) + answer[1] += (int(data[i+2]) << 8) + int(data[i+3]) + answer[0] += answer[1] >> 16 + answer[1] = answer[1] & 0xFFFF + answer[0] = answer[0] & 0xFFFF + } + return answer +} + +func (utf *utf8FontFile) seek(shift int) { + _, _ = utf.fileReader.seek(int64(shift), 0) +} + +func (utf *utf8FontFile) skip(delta int) { + _, _ = utf.fileReader.seek(int64(delta), 1) +} + +//SeekTable position +func (utf *utf8FontFile) SeekTable(name string) int { + return utf.seekTable(name, 0) +} + +func (utf *utf8FontFile) seekTable(name string, offsetInTable int) int { + _, _ = utf.fileReader.seek(int64(utf.tableDescriptions[name].position+offsetInTable), 0) + return int(utf.fileReader.readerPosition) +} + +func (utf *utf8FontFile) readInt16() int16 { + s := utf.fileReader.Read(2) + a := (int16(s[0]) << 8) + int16(s[1]) + if (int(a) & (1 << 15)) == 0 { + a = int16(int(a) - (1 << 16)) + } + return a +} + +func (utf *utf8FontFile) getUint16(pos int) int { + _, _ = utf.fileReader.seek(int64(pos), 0) + s := utf.fileReader.Read(2) + return (int(s[0]) << 8) + int(s[1]) +} + +func (utf *utf8FontFile) splice(stream []byte, offset int, value []byte) []byte { + return append(append(stream[:offset], value...), stream[offset+len(value):]...) +} + +func (utf *utf8FontFile) insertUint16(stream []byte, offset int, value int) []byte { + up := make([]byte, 2) + binary.BigEndian.PutUint16(up, uint16(value)) + return utf.splice(stream, offset, up) +} + +func (utf *utf8FontFile) getRange(pos, length int) []byte { + utf.fileReader.seek(int64(pos), 0) + if length < 1 { + return make([]byte, 0) + } + s := utf.fileReader.Read(length) + return s +} + +func (utf *utf8FontFile) getTableData(name string) []byte { + desckrip := utf.tableDescriptions[name] + if desckrip == nil { + return nil + } + if desckrip.size == 0 { + return nil + } + utf.fileReader.seek(int64(desckrip.position), 0) + s := utf.fileReader.Read(desckrip.size) + return s +} + +func (utf *utf8FontFile) setOutTable(name string, data []byte) { + if data == nil { + return + } + if name == "head" { + data = utf.splice(data, 8, []byte{0, 0, 0, 0}) + } + utf.outTablesData[name] = data +} + +func arrayKeys(arr map[int]string) []int { + answer := make([]int, len(arr)) + i := 0 + for key := range arr { + answer[i] = key + i++ + } + return answer +} + +func inArray(s int, arr []int) bool { + for _, i := range arr { + if s == i { + return true + } + } + return false +} + +func (utf *utf8FontFile) parseNAMETable() int { + namePosition := utf.SeekTable("name") + format := utf.readUint16() + if format != 0 { + fmt.Printf("Illegal format %d\n", format) + return format + } + nameCount := utf.readUint16() + stringDataPosition := namePosition + utf.readUint16() + names := map[int]string{1: "", 2: "", 3: "", 4: "", 6: ""} + keys := arrayKeys(names) + counter := len(names) + for i := 0; i < nameCount; i++ { + system := utf.readUint16() + code := utf.readUint16() + local := utf.readUint16() + nameID := utf.readUint16() + size := utf.readUint16() + position := utf.readUint16() + if !inArray(nameID, keys) { + continue + } + currentName := "" + if system == 3 && code == 1 && local == 0x409 { + oldPos := utf.fileReader.readerPosition + utf.seek(stringDataPosition + position) + if size%2 != 0 { + fmt.Printf("name is not binar byte format\n") + return format + } + size /= 2 + currentName = "" + for size > 0 { + char := utf.readUint16() + currentName += string(rune(char)) + size-- + } + utf.fileReader.readerPosition = oldPos + utf.seek(int(oldPos)) + } else if system == 1 && code == 0 && local == 0 { + oldPos := utf.fileReader.readerPosition + currentName = string(utf.getRange(stringDataPosition+position, size)) + utf.fileReader.readerPosition = oldPos + utf.seek(int(oldPos)) + } + if currentName != "" && names[nameID] == "" { + names[nameID] = currentName + counter-- + if counter == 0 { + break + } + } + } + return format +} + +func (utf *utf8FontFile) parseHEADTable() { + utf.SeekTable("head") + utf.skip(18) + utf.fontElementSize = utf.readUint16() + scale := 1000.0 / float64(utf.fontElementSize) + utf.skip(16) + xMin := utf.readInt16() + yMin := utf.readInt16() + xMax := utf.readInt16() + 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() + symbolDataFormat := utf.readUint16() + if symbolDataFormat != 0 { + fmt.Printf("Unknown symbol data format %d\n", symbolDataFormat) + return + } +} + +func (utf *utf8FontFile) parseHHEATable() int { + metricsCount := 0 + if _, OK := utf.tableDescriptions["hhea"]; OK { + scale := 1000.0 / float64(utf.fontElementSize) + utf.SeekTable("hhea") + utf.skip(4) + hheaAscender := utf.readInt16() + hheaDescender := utf.readInt16() + utf.Ascent = int(float64(hheaAscender) * scale) + utf.Descent = int(float64(hheaDescender) * scale) + utf.skip(24) + metricDataFormat := utf.readUint16() + if metricDataFormat != 0 { + fmt.Printf("Unknown horizontal metric data format %d\n", metricDataFormat) + return 0 + } + metricsCount = utf.readUint16() + if metricsCount == 0 { + fmt.Printf("Number of horizontal metrics is 0\n") + return 0 + } + } + return metricsCount +} + +func (utf *utf8FontFile) parseOS2Table() int { + weightType := 0 + scale := 1000.0 / float64(utf.fontElementSize) + if _, OK := utf.tableDescriptions["OS/2"]; OK { + utf.SeekTable("OS/2") + version := utf.readUint16() + utf.skip(2) + weightType = utf.readUint16() + utf.skip(2) + fsType := utf.readUint16() + if fsType == 0x0002 || (fsType&0x0300) != 0 { + fmt.Printf("ERROR - copyright restrictions.\n") + return 0 + } + utf.skip(20) + _ = utf.readInt16() + + utf.skip(36) + sTypoAscender := utf.readInt16() + sTypoDescender := utf.readInt16() + if utf.Ascent == 0 { + utf.Ascent = int(float64(sTypoAscender) * scale) + } + if utf.Descent == 0 { + utf.Descent = int(float64(sTypoDescender) * scale) + } + if version > 1 { + utf.skip(16) + sCapHeight := utf.readInt16() + utf.CapHeight = int(float64(sCapHeight) * scale) + } else { + utf.CapHeight = utf.Ascent + } + } else { + weightType = 500 + if utf.Ascent == 0 { + utf.Ascent = int(float64(utf.Bbox.Ymax) * scale) + } + if utf.Descent == 0 { + utf.Descent = int(float64(utf.Bbox.Ymin) * scale) + } + utf.CapHeight = utf.Ascent + } + utf.StemV = 50 + int(math.Pow(float64(weightType)/65.0, 2)) + return weightType +} + +func (utf *utf8FontFile) parsePOSTTable(weight int) { + utf.SeekTable("post") + utf.skip(4) + utf.ItalicAngle = int(utf.readInt16()) + utf.readUint16()/65536.0 + scale := 1000.0 / float64(utf.fontElementSize) + utf.UnderlinePosition = float64(utf.readInt16()) * scale + utf.UnderlineThickness = float64(utf.readInt16()) * scale + fixed := utf.readUint32() + + utf.Flags = 4 + + if utf.ItalicAngle != 0 { + utf.Flags = utf.Flags | 64 + } + if weight >= 600 { + utf.Flags = utf.Flags | 262144 + } + if fixed != 0 { + utf.Flags = utf.Flags | 1 + } +} + +func (utf *utf8FontFile) parseCMAPTable(format int) int { + cmapPosition := utf.SeekTable("cmap") + utf.skip(2) + cmapTableCount := utf.readUint16() + cidCMAPPosition := 0 + for i := 0; i < cmapTableCount; i++ { + system := utf.readUint16() + coded := utf.readUint16() + position := utf.readUint32() + oldReaderPosition := utf.fileReader.readerPosition + if (system == 3 && coded == 1) || system == 0 { // Microsoft, Unicode + format = utf.getUint16(cmapPosition + position) + if format == 4 { + if cidCMAPPosition == 0 { + cidCMAPPosition = cmapPosition + position + } + break + } + } + utf.seek(int(oldReaderPosition)) + } + if cidCMAPPosition == 0 { + fmt.Printf("Font does not have cmap for Unicode\n") + return cidCMAPPosition + } + return cidCMAPPosition +} + +func (utf *utf8FontFile) parseTables() { + f := utf.parseNAMETable() + utf.parseHEADTable() + n := utf.parseHHEATable() + w := utf.parseOS2Table() + utf.parsePOSTTable(w) + runeCMAPPosition := utf.parseCMAPTable(f) + + utf.SeekTable("maxp") + utf.skip(4) + numSymbols := utf.readUint16() + + symbolCharDictionary := make(map[int][]int) + charSymbolDictionary := make(map[int]int) + utf.generateSCCSDictionaries(runeCMAPPosition, symbolCharDictionary, charSymbolDictionary) + + scale := 1000.0 / float64(utf.fontElementSize) + utf.parseHMTXTable(n, numSymbols, symbolCharDictionary, scale) +} + +func (utf *utf8FontFile) generateCMAP() map[int][]int { + cmapPosition := utf.SeekTable("cmap") + utf.skip(2) + cmapTableCount := utf.readUint16() + runeCmapPosition := 0 + for i := 0; i < cmapTableCount; i++ { + system := utf.readUint16() + coder := utf.readUint16() + position := utf.readUint32() + oldPosition := utf.fileReader.readerPosition + if (system == 3 && coder == 1) || system == 0 { + format := utf.getUint16(cmapPosition + position) + if format == 4 { + runeCmapPosition = cmapPosition + position + break + } + } + utf.seek(int(oldPosition)) + } + + if runeCmapPosition == 0 { + fmt.Printf("Font does not have cmap for Unicode\n") + return nil + } + + symbolCharDictionary := make(map[int][]int) + charSymbolDictionary := make(map[int]int) + utf.generateSCCSDictionaries(runeCmapPosition, symbolCharDictionary, charSymbolDictionary) + + utf.charSymbolDictionary = charSymbolDictionary + + return symbolCharDictionary +} + +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 { + if _, OK := utf.charSymbolDictionary[char]; OK { + symbolCollection[utf.charSymbolDictionary[char]] = char + charSymbolPairCollection[char] = utf.charSymbolDictionary[char] + + } + utf.LastRune = max(utf.LastRune, char) + } + + begin := utf.tableDescriptions["glyf"].position + + symbolArray := make(map[int]int) + symbolCollectionKeys := keySortInt(symbolCollection) + + symbolCounter := 0 + maxRune := 0 + for _, oldSymbolIndex := range symbolCollectionKeys { + maxRune = max(maxRune, symbolCollection[oldSymbolIndex]) + symbolArray[oldSymbolIndex] = symbolCounter + symbolCounter++ + } + charSymbolPairCollectionKeys := keySortInt(charSymbolPairCollection) + runeSymbolPairCollection := make(map[int]int) + for _, runa := range charSymbolPairCollectionKeys { + runeSymbolPairCollection[runa] = symbolArray[charSymbolPairCollection[runa]] + } + utf.CodeSymbolDictionary = runeSymbolPairCollection + + symbolCollectionKeys = keySortInt(symbolCollection) + for _, oldSymbolIndex := range symbolCollectionKeys { + _, symbolArray, symbolCollection, symbolCollectionKeys = utf.getSymbols(oldSymbolIndex, &begin, symbolArray, symbolCollection, symbolCollectionKeys) + } + + return runeSymbolPairCollection, symbolArray, symbolCollection, symbolCollectionKeys +} + +func (utf *utf8FontFile) generateCMAPTable(cidSymbolPairCollection map[int]int, numSymbols int) []byte { + cidSymbolPairCollectionKeys := keySortInt(cidSymbolPairCollection) + cidID := 0 + cidArray := make(map[int][]int) + prevCid := -2 + prevSymbol := -1 + for _, cid := range cidSymbolPairCollectionKeys { + if cid == (prevCid+1) && cidSymbolPairCollection[cid] == (prevSymbol+1) { + if n, OK := cidArray[cidID]; !OK || n == nil { + cidArray[cidID] = make([]int, 0) + } + cidArray[cidID] = append(cidArray[cidID], cidSymbolPairCollection[cid]) + } else { + cidID = cid + cidArray[cidID] = make([]int, 0) + cidArray[cidID] = append(cidArray[cidID], cidSymbolPairCollection[cid]) + } + prevCid = cid + prevSymbol = cidSymbolPairCollection[cid] + } + cidArrayKeys := keySortArrayRangeMap(cidArray) + segCount := len(cidArray) + 1 + + searchRange := 1 + entrySelector := 0 + for searchRange*2 <= segCount { + searchRange = searchRange * 2 + entrySelector = entrySelector + 1 + } + searchRange = searchRange * 2 + rangeShift := segCount*2 - searchRange + length := 16 + (8 * segCount) + (numSymbols + 1) + cmap := []int{0, 1, 3, 1, 0, 12, 4, length, 0, segCount * 2, searchRange, entrySelector, rangeShift} + + for _, start := range cidArrayKeys { + endCode := start + (len(cidArray[start]) - 1) + cmap = append(cmap, endCode) + } + cmap = append(cmap, 0xFFFF) + cmap = append(cmap, 0) + + for _, cidKey := range cidArrayKeys { + cmap = append(cmap, cidKey) + } + cmap = append(cmap, 0xFFFF) + for _, cidKey := range cidArrayKeys { + idDelta := -(cidKey - cidArray[cidKey][0]) + cmap = append(cmap, idDelta) + } + cmap = append(cmap, 1) + for range cidArray { + cmap = append(cmap, 0) + + } + cmap = append(cmap, 0) + for _, start := range cidArrayKeys { + for _, glidx := range cidArray[start] { + cmap = append(cmap, glidx) + } + } + cmap = append(cmap, 0) + cmapstr := make([]byte, 0) + for _, cm := range cmap { + cmapstr = append(cmapstr, packUint16(cm)...) + } + return cmapstr +} + +//GenerateСutFont fill utf8FontFile from .utf file, only with runes from usedRunes +func (utf *utf8FontFile) GenerateСutFont(usedRunes map[int]int) []byte { + utf.fileReader.readerPosition = 0 + utf.symbolPosition = make([]int, 0) + utf.charSymbolDictionary = make(map[int]int) + utf.tableDescriptions = make(map[string]*tableDescription) + utf.outTablesData = make(map[string][]byte) + utf.Ascent = 0 + utf.Descent = 0 + utf.skip(4) + utf.LastRune = 0 + utf.generateTableDescriptions() + + utf.SeekTable("head") + utf.skip(50) + LocaFormat := utf.readUint16() + + utf.SeekTable("hhea") + utf.skip(34) + metricsCount := utf.readUint16() + oldMetrics := metricsCount + + utf.SeekTable("maxp") + utf.skip(4) + numSymbols := utf.readUint16() + + symbolCharDictionary := utf.generateCMAP() + if symbolCharDictionary == nil { + return nil + } + + utf.parseHMTXTable(metricsCount, numSymbols, symbolCharDictionary, 1.0) + + utf.parseLOCATable(LocaFormat, numSymbols) + + cidSymbolPairCollection, symbolArray, symbolCollection, symbolCollectionKeys := utf.parseSymbols(usedRunes) + + metricsCount = len(symbolCollection) + numSymbols = metricsCount + + utf.setOutTable("name", utf.getTableData("name")) + utf.setOutTable("cvt ", utf.getTableData("cvt ")) + utf.setOutTable("fpgm", utf.getTableData("fpgm")) + utf.setOutTable("prep", utf.getTableData("prep")) + utf.setOutTable("gasp", utf.getTableData("gasp")) + + postTable := utf.getTableData("post") + postTable = append(append([]byte{0x00, 0x03, 0x00, 0x00}, postTable[4:16]...), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}...) + utf.setOutTable("post", postTable) + + delete(cidSymbolPairCollection, 0) + + utf.setOutTable("cmap", utf.generateCMAPTable(cidSymbolPairCollection, numSymbols)) + + symbolData := utf.getTableData("glyf") + + offsets := make([]int, 0) + glyfData := make([]byte, 0) + pos := 0 + hmtxData := make([]byte, 0) + utf.symbolData = make(map[int]map[string][]int, 0) + + for _, originalSymbolIdx := range symbolCollectionKeys { + hm := utf.getMetrics(oldMetrics, originalSymbolIdx) + hmtxData = append(hmtxData, hm...) + + offsets = append(offsets, pos) + symbolPos := utf.symbolPosition[originalSymbolIdx] + symbolLen := utf.symbolPosition[originalSymbolIdx+1] - symbolPos + data := symbolData[symbolPos : symbolPos+symbolLen] + var up int + if symbolLen > 0 { + up = unpackUint16(data[0:2]) + } + + if symbolLen > 2 && (up&(1<<15)) != 0 { + posInSymbol := 10 + flags := symbolContinue + nComponentElements := 0 + for (flags & symbolContinue) != 0 { + nComponentElements++ + up = unpackUint16(data[posInSymbol : posInSymbol+2]) + flags = up + up = unpackUint16(data[posInSymbol+2 : posInSymbol+4]) + symbolIdx := up + if _, OK := utf.symbolData[originalSymbolIdx]; !OK { + utf.symbolData[originalSymbolIdx] = make(map[string][]int) + } + if _, OK := utf.symbolData[originalSymbolIdx]["compSymbols"]; !OK { + utf.symbolData[originalSymbolIdx]["compSymbols"] = make([]int, 0) + } + utf.symbolData[originalSymbolIdx]["compSymbols"] = append(utf.symbolData[originalSymbolIdx]["compSymbols"], symbolIdx) + data = utf.insertUint16(data, posInSymbol+2, symbolArray[symbolIdx]) + posInSymbol += 4 + if (flags & symbolWords) != 0 { + posInSymbol += 4 + } else { + posInSymbol += 2 + } + if (flags & symbolScale) != 0 { + posInSymbol += 2 + } else if (flags & symbolAllScale) != 0 { + posInSymbol += 4 + } else if (flags & symbol2x2) != 0 { + posInSymbol += 8 + } + } + } + + glyfData = append(glyfData, data...) + pos += symbolLen + if pos%4 != 0 { + padding := 4 - (pos % 4) + glyfData = append(glyfData, make([]byte, padding)...) + pos += padding + } + } + + offsets = append(offsets, pos) + utf.setOutTable("glyf", glyfData) + + utf.setOutTable("hmtx", hmtxData) + + locaData := make([]byte, 0) + if ((pos + 1) >> 1) > 0xFFFF { + LocaFormat = 1 + for _, offset := range offsets { + locaData = append(locaData, packUint32(offset)...) + } + } else { + LocaFormat = 0 + for _, offset := range offsets { + locaData = append(locaData, packUint16(offset/2)...) + } + } + utf.setOutTable("loca", locaData) + + headData := utf.getTableData("head") + headData = utf.insertUint16(headData, 50, LocaFormat) + utf.setOutTable("head", headData) + + hheaData := utf.getTableData("hhea") + hheaData = utf.insertUint16(hheaData, 34, metricsCount) + utf.setOutTable("hhea", hheaData) + + maxp := utf.getTableData("maxp") + maxp = utf.insertUint16(maxp, 4, numSymbols) + utf.setOutTable("maxp", maxp) + + os2Data := utf.getTableData("OS/2") + utf.setOutTable("OS/2", os2Data) + + return utf.assembleTables() +} + +func (utf *utf8FontFile) getSymbols(originalSymbolIdx int, start *int, symbolSet map[int]int, SymbolsCollection map[int]int, SymbolsCollectionKeys []int) (*int, map[int]int, map[int]int, []int) { + symbolPos := utf.symbolPosition[originalSymbolIdx] + symbolSize := utf.symbolPosition[originalSymbolIdx+1] - symbolPos + if symbolSize == 0 { + return start, symbolSet, SymbolsCollection, SymbolsCollectionKeys + } + utf.seek(*start + symbolPos) + + lineCount := utf.readInt16() + + if lineCount < 0 { + utf.skip(8) + flags := symbolContinue + for flags&symbolContinue != 0 { + flags = utf.readUint16() + symbolIndex := utf.readUint16() + if _, OK := symbolSet[symbolIndex]; !OK { + symbolSet[symbolIndex] = len(SymbolsCollection) + SymbolsCollection[symbolIndex] = 1 + SymbolsCollectionKeys = append(SymbolsCollectionKeys, symbolIndex) + } + oldPosition, _ := utf.fileReader.seek(0, 1) + _, _, _, SymbolsCollectionKeys = utf.getSymbols(symbolIndex, start, symbolSet, SymbolsCollection, SymbolsCollectionKeys) + utf.seek(int(oldPosition)) + if flags&symbolWords != 0 { + utf.skip(4) + } else { + utf.skip(2) + } + if flags&symbolScale != 0 { + utf.skip(2) + } else if flags&symbolAllScale != 0 { + utf.skip(4) + } else if flags&symbol2x2 != 0 { + utf.skip(8) + } + } + } + return start, symbolSet, SymbolsCollection, SymbolsCollectionKeys +} + +func (utf *utf8FontFile) parseHMTXTable(numberOfHMetrics, numSymbols int, symbolToChar map[int][]int, scale float64) { + start := utf.SeekTable("hmtx") + arrayWidths := 0 + widths := 0 + var arr []int + utf.CharWidths = make([]int, 256*256) + charCount := 0 + arr = unpackUint16Array(utf.getRange(start, numberOfHMetrics*4)) + for symbol := 0; symbol < numberOfHMetrics; symbol++ { + arrayWidths = arr[(symbol*2)+1] + if _, OK := symbolToChar[symbol]; OK || symbol == 0 { + + if arrayWidths >= (1 << 15) { + arrayWidths = 0 + } + if symbol == 0 { + utf.DefaultWidth = scale * float64(arrayWidths) + continue + } + for _, char := range symbolToChar[symbol] { + if char != 0 && char != 65535 { + widths = int(math.Round(scale * float64(arrayWidths))) + if widths == 0 { + widths = 65535 + } + if char < 196608 { + utf.CharWidths[char] = widths + charCount++ + } + } + } + } + } + data := utf.getRange(start+numberOfHMetrics*4, numSymbols*2) + arr = unpackUint16Array(data) + diff := numSymbols - numberOfHMetrics + for pos := 0; pos < diff; pos++ { + symbol := pos + numberOfHMetrics + if _, OK := symbolToChar[symbol]; OK { + for _, char := range symbolToChar[symbol] { + if char != 0 && char != 65535 { + widths = int(math.Round(scale * float64(arrayWidths))) + if widths == 0 { + widths = 65535 + } + if char < 196608 { + utf.CharWidths[char] = widths + charCount++ + } + } + } + } + } + utf.CharWidths[0] = charCount +} + +func (utf *utf8FontFile) getMetrics(metricCount, gid int) []byte { + start := utf.SeekTable("hmtx") + var metrics []byte + if gid < metricCount { + utf.seek(start + (gid * 4)) + metrics = utf.fileReader.Read(4) + } else { + utf.seek(start + ((metricCount - 1) * 4)) + metrics = utf.fileReader.Read(2) + utf.seek(start + (metricCount * 2) + (gid * 2)) + metrics = append(metrics, utf.fileReader.Read(2)...) + } + return metrics +} + +func (utf *utf8FontFile) parseLOCATable(format, numSymbols int) { + start := utf.SeekTable("loca") + utf.symbolPosition = make([]int, 0) + if format == 0 { + data := utf.getRange(start, (numSymbols*2)+2) + arr := unpackUint16Array(data) + for n := 0; n <= numSymbols; n++ { + utf.symbolPosition = append(utf.symbolPosition, arr[n+1]*2) + } + } else if format == 1 { + data := utf.getRange(start, (numSymbols*4)+4) + arr := unpackUint32Array(data) + for n := 0; n <= numSymbols; n++ { + utf.symbolPosition = append(utf.symbolPosition, arr[n+1]) + } + } else { + fmt.Printf("Unknown loca table format %d\n", format) + return + } +} + +func (utf *utf8FontFile) generateSCCSDictionaries(runeCmapPosition int, symbolCharDictionary map[int][]int, charSymbolDictionary map[int]int) { + maxRune := 0 + utf.seek(runeCmapPosition + 2) + size := utf.readUint16() + rim := runeCmapPosition + size + utf.skip(2) + + segmentSize := utf.readUint16() / 2 + utf.skip(6) + completers := make([]int, 0) + for i := 0; i < segmentSize; i++ { + completers = append(completers, utf.readUint16()) + } + utf.skip(2) + beginners := make([]int, 0) + for i := 0; i < segmentSize; i++ { + beginners = append(beginners, utf.readUint16()) + } + sizes := make([]int, 0) + for i := 0; i < segmentSize; i++ { + sizes = append(sizes, int(utf.readInt16())) + } + readerPositionStart := utf.fileReader.readerPosition + positions := make([]int, 0) + for i := 0; i < segmentSize; i++ { + positions = append(positions, utf.readUint16()) + } + symbol := 0 + for n := 0; n < segmentSize; n++ { + completePosition := completers[n] + 1 + for char := beginners[n]; char < completePosition; char++ { + if positions[n] == 0 { + symbol = (char + sizes[n]) & 0xFFFF + } else { + position := (char-beginners[n])*2 + positions[n] + position = int(readerPositionStart) + 2*n + position + if position >= rim { + symbol = 0 + } else { + symbol = utf.getUint16(position) + if symbol != 0 { + symbol = (symbol + sizes[n]) & 0xFFFF + } + } + } + charSymbolDictionary[char] = symbol + if char < 196608 { + maxRune = max(char, maxRune) + } + symbolCharDictionary[symbol] = append(symbolCharDictionary[symbol], char) + } + } +} + +func max(i, n int) int { + if n > i { + return n + } + return i +} + +func (utf *utf8FontFile) assembleTables() []byte { + answer := make([]byte, 0) + tablesCount := len(utf.outTablesData) + findSize := 1 + writer := 0 + for findSize*2 <= tablesCount { + findSize = findSize * 2 + writer = writer + 1 + } + findSize = findSize * 16 + rOffset := tablesCount*16 - findSize + + answer = append(answer, packHeader(0x00010000, tablesCount, findSize, writer, rOffset)...) + + tables := utf.outTablesData + tablesNames := keySortStrings(tables) + + offset := 12 + tablesCount*16 + begin := 0 + + for _, name := range tablesNames { + if name == "head" { + begin = offset + } + answer = append(answer, []byte(name)...) + checksum := utf.generateChecksum(tables[name]) + answer = append(answer, pack2Uint16(checksum[0], checksum[1])...) + answer = append(answer, pack2Uint32(offset, len(tables[name]))...) + paddedLength := (len(tables[name]) + 3) &^ 3 + offset = offset + paddedLength + } + + for _, key := range tablesNames { + data := tables[key] + data = append(data, []byte{0, 0, 0}...) + answer = append(answer, data[:(len(data)&^3)]...) + } + + checksum := utf.generateChecksum([]byte(answer)) + checksum = utf.calcInt32([]int{0xB1B0, 0xAFBA}, checksum) + answer = utf.splice(answer, (begin + 8), pack2Uint16(checksum[0], checksum[1])) + return answer +} + +func unpackUint16Array(data []byte) []int { + answer := make([]int, 1) + r := bytes.NewReader(data) + bs := make([]byte, 2) + var e error + var c int + c, e = r.Read(bs) + for e == nil && c > 0 { + answer = append(answer, int(binary.BigEndian.Uint16(bs))) + c, e = r.Read(bs) + } + return answer +} + +func unpackUint32Array(data []byte) []int { + answer := make([]int, 1) + r := bytes.NewReader(data) + bs := make([]byte, 4) + var e error + var c int + c, e = r.Read(bs) + for e == nil && c > 0 { + answer = append(answer, int(binary.BigEndian.Uint32(bs))) + c, e = r.Read(bs) + } + return answer +} + +func unpackUint16(data []byte) int { + return int(binary.BigEndian.Uint16(data)) +} + +func packHeader(N uint32, n1, n2, n3, n4 int) []byte { + answer := make([]byte, 0) + bs4 := make([]byte, 4) + binary.BigEndian.PutUint32(bs4, N) + answer = append(answer, bs4...) + bs := make([]byte, 2) + binary.BigEndian.PutUint16(bs, uint16(n1)) + answer = append(answer, bs...) + binary.BigEndian.PutUint16(bs, uint16(n2)) + answer = append(answer, bs...) + binary.BigEndian.PutUint16(bs, uint16(n3)) + answer = append(answer, bs...) + binary.BigEndian.PutUint16(bs, uint16(n4)) + answer = append(answer, bs...) + return answer +} + +func pack2Uint16(n1, n2 int) []byte { + answer := make([]byte, 0) + bs := make([]byte, 2) + binary.BigEndian.PutUint16(bs, uint16(n1)) + answer = append(answer, bs...) + binary.BigEndian.PutUint16(bs, uint16(n2)) + answer = append(answer, bs...) + return answer +} + +func pack2Uint32(n1, n2 int) []byte { + answer := make([]byte, 0) + bs := make([]byte, 4) + binary.BigEndian.PutUint32(bs, uint32(n1)) + answer = append(answer, bs...) + binary.BigEndian.PutUint32(bs, uint32(n2)) + answer = append(answer, bs...) + return answer +} + +func packUint32(n1 int) []byte { + bs := make([]byte, 4) + binary.BigEndian.PutUint32(bs, uint32(n1)) + return bs +} + +func packUint16(n1 int) []byte { + bs := make([]byte, 2) + binary.BigEndian.PutUint16(bs, uint16(n1)) + return bs +} + +func keySortStrings(s map[string][]byte) []string { + keys := make([]string, len(s)) + i := 0 + for key := range s { + keys[i] = key + i++ + } + sort.Strings(keys) + return keys +} + +func keySortInt(s map[int]int) []int { + keys := make([]int, len(s)) + i := 0 + for key := range s { + keys[i] = key + i++ + } + sort.Ints(keys) + return keys +} + +func keySortArrayRangeMap(s map[int][]int) []int { + keys := make([]int, len(s)) + i := 0 + for key := range s { + keys[i] = key + i++ + } + sort.Ints(keys) + return keys +} diff --git a/util.go b/util.go index 1dad60e..1a776e8 100644 --- a/util.go +++ b/util.go @@ -105,10 +105,16 @@ func sliceUncompress(data []byte) (outData []byte, err error) { return } -// utf8toutf16 converts UTF-8 to UTF-16BE with BOM; from http://www.fpdf.org/ -func utf8toutf16(s string) string { +// utf8toutf16 converts UTF-8 to UTF-16BE; from http://www.fpdf.org/ +func utf8toutf16(s string, withBOM ...bool) string { + bom := true + if len(withBOM) > 0 { + bom = withBOM[0] + } res := make([]byte, 0, 8) - res = append(res, 0xFE, 0xFF) + if bom { + res = append(res, 0xFE, 0xFF) + } nb := len(s) i := 0 for i < nb { @@ -314,3 +320,132 @@ func (s *SizeType) ScaleToHeight(height float64) SizeType { width := s.Wd * height / s.Ht return SizeType{width, height} } + +//Imitation of untyped Map Array +type untypedKeyMap struct { + keySet []interface{} + valueSet []int +} + +//Get position of key=>value in PHP Array +func (pa *untypedKeyMap) getIndex(key interface{}) int { + if key != nil { + for i, mKey := range pa.keySet { + if mKey == key { + return i + } + } + return -1 + } + return -1 +} + +//Put key=>value in PHP Array +func (pa *untypedKeyMap) put(key interface{}, value int) { + if key == nil { + i := 0 + for n := 0; ; n++ { + i = pa.getIndex(n) + if i < 0 { + key = n + break + } + } + pa.keySet = append(pa.keySet, key) + pa.valueSet = append(pa.valueSet, value) + } else { + i := pa.getIndex(key) + if i < 0 { + pa.keySet = append(pa.keySet, key) + pa.valueSet = append(pa.valueSet, value) + } else { + pa.valueSet[i] = value + } + } +} + +//Delete value in PHP Array +func (pa *untypedKeyMap) delete(key interface{}) { + if pa == nil || pa.keySet == nil || pa.valueSet == nil { + return + } + i := pa.getIndex(key) + if i >= 0 { + if i == 0 { + pa.keySet = pa.keySet[1:] + pa.valueSet = pa.valueSet[1:] + } else if i == len(pa.keySet)-1 { + pa.keySet = pa.keySet[:len(pa.keySet)-1] + pa.valueSet = pa.valueSet[:len(pa.valueSet)-1] + } else { + pa.keySet = append(pa.keySet[:i], pa.keySet[i+1:]...) + pa.valueSet = append(pa.valueSet[:i], pa.valueSet[i+1:]...) + } + } +} + +//Get value from PHP Array +func (pa *untypedKeyMap) get(key interface{}) int { + i := pa.getIndex(key) + if i >= 0 { + return pa.valueSet[i] + } + return 0 +} + +//Imitation of PHP function pop() +func (pa *untypedKeyMap) pop() { + pa.keySet = pa.keySet[:len(pa.keySet)-1] + pa.valueSet = pa.valueSet[:len(pa.valueSet)-1] +} + +//Imitation of PHP function array_merge() +func arrayMerge(arr1, arr2 *untypedKeyMap) *untypedKeyMap { + answer := untypedKeyMap{} + if arr1 == nil && arr2 == nil { + answer = untypedKeyMap{ + make([]interface{}, 0), + make([]int, 0), + } + } else if arr2 == nil { + answer.keySet = arr1.keySet[:] + answer.valueSet = arr1.valueSet[:] + } else if arr1 == nil { + answer.keySet = arr2.keySet[:] + answer.valueSet = arr2.valueSet[:] + } else { + answer.keySet = arr1.keySet[:] + answer.valueSet = arr1.valueSet[:] + for i := 0; i < len(arr2.keySet); i++ { + if arr2.keySet[i] == "interval" { + u := 0 + u = u + 1 + if arr1.getIndex("interval") < 0 { + answer.put("interval", arr2.valueSet[i]) + } else { + + u := 0 + u = u + 1 + } + } else { + answer.put(nil, arr2.valueSet[i]) + } + } + } + return &answer +} + +func remove(arr []int, key int) []int { + n := 0 + for i, mKey := range arr { + if mKey == key { + n = i + } + } + if n == 0 { + return arr[1:] + } else if n == len(arr)-1 { + return arr[:len(arr)-1] + } + return append(arr[:n], arr[n+1:]...) +} -- cgit v1.2.1-24-ge1ad From 8022e9b91aea9cdbf7f7c561fc59a6e291cbfa59 Mon Sep 17 00:00:00 2001 From: DarkFreedman Date: Mon, 6 May 2019 13:23:42 +0300 Subject: Added copyrights. And "right to left" languages support. --- def.go | 1 + fpdf.go | 43 +++++++++++++++++++++++++++++++++++++++++-- utf8fontfile.go | 18 ++++++++++++++++++ util.go | 1 + 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/def.go b/def.go index 4c46d03..205389b 100644 --- a/def.go +++ b/def.go @@ -497,6 +497,7 @@ type PageBox struct { // Fpdf is the principal structure for creating a single PDF document type Fpdf struct { isCurrentUTF8 bool // is current font used in utf-8 mode + isRTL bool // is is right to left mode enabled page int // current page number n int // current object number offsets []int // array of object offsets diff --git a/fpdf.go b/fpdf.go index 1400a85..fa79033 100644 --- a/fpdf.go +++ b/fpdf.go @@ -618,6 +618,16 @@ func (f *Fpdf) AliasNbPages(aliasStr string) { f.aliasNbPagesStr = aliasStr } +// enable right to left mode +func (f *Fpdf) RTL() { + f.isRTL = true +} + +// disable right to left mode +func (f *Fpdf) LTR() { + f.isRTL = false +} + // open begins a document func (f *Fpdf) open() { f.state = 1 @@ -2091,6 +2101,10 @@ func (f *Fpdf) Bookmark(txtStr string, level int, y float64) { func (f *Fpdf) Text(x, y float64, txtStr string) { var txt2 string if f.isCurrentUTF8 { + if f.isRTL { + txtStr = revertText(txtStr) + x -= f.GetStringWidth(txtStr) + } txt2 = f.escape(utf8toutf16(txtStr, false)) for _, uni := range []rune(txtStr) { f.currentFont.usedRunes[int(uni)] = int(uni) @@ -2279,6 +2293,9 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, } //If multibyte, Tw has no effect - do word spacing using an adjustment before each space if (f.ws != 0 || alignStr == "J") && f.isCurrentUTF8 { // && f.ws != 0 + if f.isRTL { + txtStr = revertText(txtStr) + } wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) for _, uni := range []rune(txtStr) { f.currentFont.usedRunes[int(uni)] = int(uni) @@ -2301,6 +2318,9 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, } else { var txt2 string if f.isCurrentUTF8 { + if f.isRTL { + txtStr = revertText(txtStr) + } txt2 = f.escape(utf8toutf16(txtStr, false)) for _, uni := range []rune(txtStr) { f.currentFont.usedRunes[int(uni)] = int(uni) @@ -2344,6 +2364,17 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, return } +// Revert string to use in RTL languages +func revertText(text string) string { + oldText := []rune(text) + newText := make([]rune, len(oldText)) + lenght := len(oldText) - 1 + for i, r := range oldText { + newText[lenght-i] = r + } + return string(newText) +} + // Cell is a simpler version of CellFormat with no fill, border, links or // special alignment. func (f *Fpdf) Cell(w, h float64, txtStr string) { @@ -2496,7 +2527,11 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill if f.isCurrentUTF8 { newAlignStr := alignStr if newAlignStr == "J" { - newAlignStr = "L" + if f.isRTL { + newAlignStr = "R" + } else { + newAlignStr = "L" + } } f.CellFormat(w, h, string([]rune(s)[j:i]), b, 2, newAlignStr, fill, 0, "") } else { @@ -2576,7 +2611,11 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill } if f.isCurrentUTF8 { if alignStr == "J" { - alignStr = "" + if f.isRTL { + alignStr = "R" + } else { + alignStr = "" + } } f.CellFormat(w, h, string([]rune(s)[j:i]), b, 2, alignStr, fill, 0, "") } else { diff --git a/utf8fontfile.go b/utf8fontfile.go index ad9f924..964e919 100644 --- a/utf8fontfile.go +++ b/utf8fontfile.go @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019 Arteom Korotkiy (Gmail: arteomkorotkiy) + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + package gofpdf import ( @@ -14,6 +30,8 @@ const symbolScale = 1 << 3 const symbolContinue = 1 << 5 const symbolAllScale = 1 << 6 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" type utf8FontFile struct { diff --git a/util.go b/util.go index 1a776e8..e96cdd8 100644 --- a/util.go +++ b/util.go @@ -321,6 +321,7 @@ func (s *SizeType) ScaleToHeight(height float64) SizeType { return SizeType{width, height} } +//The untypedKeyMap structure and its methods are copyrighted 2019 by Arteom Korotkiy (Gmail: arteomkorotkiy). //Imitation of untyped Map Array type untypedKeyMap struct { keySet []interface{} -- cgit v1.2.1-24-ge1ad From 4ca1a43173413971a6bb92c78be31a71b29b416b Mon Sep 17 00:00:00 2001 From: DarkFreedman Date: Mon, 6 May 2019 13:30:19 +0300 Subject: Added "right to left" languages support. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 860d524..9f0ef59 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ text, drawing and images. gofpdf has no dependencies other than the Go standard library. All tests pass on Linux, Mac and Windows platforms. -gofpdf supports UTF-8 fonts. +gofpdf supports UTF-8 fonts and "right to left" languages. Also, support is provided to automatically translate UTF-8 runes to code page encodings for languages that have fewer than 256 @@ -136,6 +136,7 @@ Nothing special is required to use the standard PDF fonts (courier, helvetica, times, zapfdingbats) in your documents other than calling SetFont(). You should use AddUTF8Font or AddUTF8FontFromBytes to add UTF-8 TTF font. +RTL() and LTR() methods switch between "right to left" and "left to right" mode. In order to use a different non-UTF-8 TrueType or Type1 font, you will need to generate a font definition file and, if the font will be embedded into PDFs, a compressed -- cgit v1.2.1-24-ge1ad