diff --git a/.gitignore b/.gitignore index 1fae316..0d2a5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ doc/body.md doc/index.html doc/index.html.ok coverage.html +.vscode # macOS .DS_Store \ No newline at end of file diff --git a/fpdf.go b/fpdf.go index 4765259..36521ca 100644 --- a/fpdf.go +++ b/fpdf.go @@ -38,6 +38,8 @@ import ( "strconv" "strings" "time" + + "github.com/hmmftg/goarabic" ) var gl struct { @@ -2426,6 +2428,11 @@ 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}) } +// rtl: make direction of text rtl +func rtl(str string) string { + return goarabic.FixArabic(str) +} + // 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() @@ -2433,10 +2440,7 @@ 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 = reverseText(txtStr) - x -= f.GetStringWidth(txtStr) - } + x -= f.GetStringWidth(txtStr) txt2 = f.escape(utf8toutf16(txtStr, false)) for _, uni := range txtStr { f.currentFont.usedRunes[int(uni)] = int(uni) @@ -2546,6 +2550,10 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, return } + if f.isRTL { + txtStr = rtl(txtStr) + } + borderStr = strings.ToUpper(borderStr) k := f.k if f.y+h > f.pageBreakTrigger && !f.inHeader && !f.inFooter && f.acceptPageBreak() { @@ -2647,9 +2655,6 @@ 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 = reverseText(txtStr) - } wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) for _, uni := range txtStr { f.currentFont.usedRunes[int(uni)] = int(uni) @@ -2672,9 +2677,6 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, } else { var txt2 string if f.isCurrentUTF8 { - if f.isRTL { - txtStr = reverseText(txtStr) - } txt2 = f.escape(utf8toutf16(txtStr, false)) for _, uni := range txtStr { f.currentFont.usedRunes[int(uni)] = int(uni) @@ -2720,17 +2722,6 @@ func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, } } -// Revert string to use in RTL languages -func reverseText(text string) string { - oldText := []rune(text) - newText := make([]rune, len(oldText)) - length := len(oldText) - 1 - for i, r := range oldText { - newText[length-i] = r - } - return string(newText) -} - // Cell is a simpler version of CellFormat with no fill, border, links or // special alignment. The Cell_strikeout() example demonstrates this method. func (f *Fpdf) Cell(w, h float64, txtStr string) { @@ -2834,6 +2825,9 @@ func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill w = f.w - f.rMargin - f.x } wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) + + f.isRTL = isRTL(txtStr) + s := strings.Replace(txtStr, "\r", "", -1) srune := []rune(s) diff --git a/fpdf_example_test.go b/fpdf_example_test.go index 60a90d3..ac3e8d2 100644 --- a/fpdf_example_test.go +++ b/fpdf_example_test.go @@ -2429,6 +2429,44 @@ func ExampleFpdf_AddUTF8Font() { // Successfully generated pdf/Fpdf_AddUTF8Font.pdf } +// ExampleFpdf_RTL demonstrates how use rtl mode +func ExampleFpdf_RTL() { + var fileStr string + var txtStr []byte + var err error + + pdf := fpdf.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.ttf")) + pdf.AddUTF8Font("dejavu", "BI", example.FontFile("DejaVuSansCondensed-Bold.ttf")) + + fileStr = example.Filename("Fpdf_RTL") + txtStr, err = os.ReadFile(example.TextFile("rtl-test.txt")) + if err == nil { + pdf.SetFont("dejavu", "B", 17) + pdf.MultiCell(100, 8, "تست لغات عربی و فارسی وسط چین", "1", "C", false) + pdf.SetFont("dejavu", "", 14) + pdf.MultiCell(100, 5, string(txtStr), "1", "C", false) + pdf.SetFont("dejavu", "B", 17) + pdf.MultiCell(100, 8, "تست لغات عربی و فارسی راست چین", "1", "R", false) + pdf.SetFont("dejavu", "", 14) + pdf.MultiCell(100, 5, string(txtStr), "1", "R", false) + pdf.SetFont("dejavu", "B", 17) + pdf.MultiCell(100, 8, "تست لغات عربی و فارسی چپ چین", "1", "L", false) + pdf.SetFont("dejavu", "", 14) + pdf.MultiCell(100, 5, string(txtStr), "1", "L", false) + pdf.Ln(15) + pdf.OutputFileAndClose(fileStr) + } + example.SummaryCompare(err, fileStr) + // Output: + // Successfully generated pdf/Fpdf_RTL.pdf +} + // ExampleUTF8CutFont demonstrates how generate a TrueType font subset. func ExampleUTF8CutFont() { var pdfFileStr, fullFontFileStr, subFontFileStr string @@ -2463,7 +2501,7 @@ func ExampleUTF8CutFont() { pdf.AddPage() pdf.AddUTF8Font("calligra", "", subFontFileStr) pdf.SetFont("calligra", "", fontHt) - write("cabbed") + write("cabbed vwxyz") write("vwxyz") pdf.SetFont("courier", "", fontHt) writeSize(fullFontFileStr) diff --git a/go.mod b/go.mod index 7b2d0d9..16f7dae 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,11 @@ retract ( require ( github.com/boombuler/barcode v1.0.1 + github.com/hmmftg/goarabic v0.0.0-20230523174344-63f61ec6e505 github.com/phpdave11/gofpdi v1.0.13 github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 - golang.org/x/image v0.6.0 + golang.org/x/image v0.7.0 + golang.org/x/text v0.9.0 ) require github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 9096cf9..3f1fbcc 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyX github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hmmftg/goarabic v0.0.0-20230523174344-63f61ec6e505 h1:YOMZSJmlAfUryfZvkzq0m7cw1cA0xrV5ORRzUL8N0Vg= +github.com/hmmftg/goarabic v0.0.0-20230523174344-63f61ec6e505/go.mod h1:72JQM/NN64op0MoliuRYr9tsrDAgeyXfYHMneegbeks= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -22,8 +24,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= -golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= +golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -46,7 +48,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/pdf/reference/ExampleFpdf_RTL.pdf b/pdf/reference/ExampleFpdf_RTL.pdf new file mode 100644 index 0000000..bfa34f6 Binary files /dev/null and b/pdf/reference/ExampleFpdf_RTL.pdf differ diff --git a/pdf/reference/Fpdf_AddUTF8Font.pdf b/pdf/reference/Fpdf_AddUTF8Font.pdf index 1a5353b..e3be9db 100644 Binary files a/pdf/reference/Fpdf_AddUTF8Font.pdf and b/pdf/reference/Fpdf_AddUTF8Font.pdf differ diff --git a/pdf/reference/Fpdf_RTL.pdf b/pdf/reference/Fpdf_RTL.pdf new file mode 100644 index 0000000..bfa34f6 Binary files /dev/null and b/pdf/reference/Fpdf_RTL.pdf differ diff --git a/rtl.go b/rtl.go new file mode 100644 index 0000000..2d336d3 --- /dev/null +++ b/rtl.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2013-2014 Kurt Jung (Gmail: kurt.w.jung) + * + * 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 fpdf + +var registeredIsRtl func(string) bool = nil + +func RegisterIsRtl(method func(string) bool) { + registeredIsRtl = method +} + +// IsRtl checks if the text has rtl direction +func isRTL(text string) bool { + if registeredIsRtl != nil { + return registeredIsRtl(text) + } + if len(text) == 0 { + return false + } + r := []rune(text) + //Ranges are taken from : https://stackoverflow.com/questions/12006095/javascript-how-to-check-if-character-is-rtl + if r[0] >= 0x0591 && 0x07FF >= r[0] { + return true + } + if r[0] >= 0xFB1D && 0xFDFD >= r[0] { + return true + } + if r[0] >= 0xFE70 && 0xFEFC >= r[0] { + return true + } + if r[0] == 0x200F || r[0] == 0x202B || r[0] == 0x202E { + return true + } + return false +} diff --git a/text/rtl-test.txt b/text/rtl-test.txt new file mode 100644 index 0000000..64538e9 --- /dev/null +++ b/text/rtl-test.txt @@ -0,0 +1,14 @@ +فارسی با انگلیسی: +سلام abcdefghijklmnopqrstuvwwxyz خوبی xml چرا swf + +العربی: +أكل بعض أكثر من هذه الكعك الفرنسي لينة وشرب بعض الشاي. + +فارسی: +مقداری دیگر از این نان های نرم فرانسوی بخورید و کمی چای بنوشید. + +فارسی با اعداد: +سلام ۱۲۳ خوبی ۲۴۲۳ چرا 123 + +english: +sample english text همراه arabic لغت inside \ No newline at end of file diff --git a/util.go b/util.go index b9867ae..ff79694 100644 --- a/util.go +++ b/util.go @@ -25,6 +25,8 @@ import ( "os" "path/filepath" "strings" + + "golang.org/x/text/encoding/unicode" ) func must(n int, err error) { @@ -78,36 +80,15 @@ func utf8toutf16(s string, withBOM ...bool) string { if len(withBOM) > 0 { bom = withBOM[0] } - res := make([]byte, 0, 8) + + bomState := unicode.IgnoreBOM if bom { - res = append(res, 0xFE, 0xFF) + bomState = unicode.UseBOM } - 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) - } - } - return string(res) + + enc := unicode.UTF16(unicode.BigEndian, bomState).NewEncoder() + u, _ := enc.String(s) + return string(u) } // intIf returns a if cnd is true, otherwise b