Todas as publicações

Por que meu PDF mostra retângulos (tofu) no lugar de japonês?

Retângulos vazios em vez de caracteres japoneses significam que o PDF não encontrou glifos para esses code points. Quatro causas e como corrigir.

por gpdf team

A pergunta, em outras palavras

Escrevi texto japonês com gpdf e o PDF resultante mostra retângulos vazios onde os caracteres deveriam estar. O que é isso e como faço para que os glifos japoneses reais apareçam no arquivo?

A resposta rápida

Isso é tofu — o visualizador de PDF desenha um retângulo de marcador porque a fonte embutida no PDF não tem glifo para o code point Unicode que você pediu. Quatro coisas causam isso, e uma é muito mais comum que o restante.

Por ordem de frequência:

  1. Nenhuma fonte CJK registrada. gpdf.NewDocument não tem nenhuma chamada a WithFont, então o documento recai nas fontes Base-14 do PDF (Helvetica, Times, Courier). Nenhuma cobre U+3040–U+9FFF.
  2. Fonte CJK registrada, mas o nome da família em c.Text está errado. WithFont("NotoSansJP", ...) está configurado, mas template.FontFamily("Arial") no texto força o gpdf a procurar japonês em uma fonte latina.
  3. O arquivo de fonte não contém glifos CJK. O TTF em disco é um subset latino (NotoSans-Regular.ttf em vez de NotoSansJP-Regular.ttf). O nome parece certo, a cobertura está vazia.
  4. Os bytes foram corrompidos antes do gpdf recebê-los. A string foi decodificada como Shift-JIS ou Latin-1 em algum ponto anterior, e os code points já não são japoneses. Se você vê 縺ゅ→縺 em vez de retângulos, é essa.

A correção canônica para a causa #1

Nove em cada dez vezes é isto:

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    font, err := os.ReadFile("NotoSansJP-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("NotoSansJP", font),
        gpdf.WithDefaultFont("NotoSansJP", 12),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("こんにちは、世界。")
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("hello.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

Duas linhas registram a fonte e a definem como padrão. Sem CGO. Sem a burocracia do AddUTF8Font. Se você estava vendo □□□□□、□□。 e rodar este programa com um NotoSansJP-Regular.ttf real ao lado, os glifos reais aparecem.

Baixe NotoSansJP-Regular.ttf no Google Fonts.

Como saber qual causa é a sua

A maior parte é olhar três lugares: onde você constrói o documento, onde você escreve o texto e o próprio arquivo TTF.

Se a saída são □□□ (retângulos idênticos), é causa 1, 2 ou 3. O PDF embutiu uma fonte, mas ela não tem os glifos. Abra o PDF no Acrobat, vá em Arquivo → Propriedades → Fontes e veja quais fontes foram realmente embutidas. Se a lista só tem Helvetica / Times / Courier, causa 1. Se NotoSansJP está listada e ainda há retângulos, causa 2 ou 3.

Se a saída é 縺ゅ→縺 ou ã"ã‚"ã«ã¡ã¯ (latim embaralhado), é causa 4. Sua string japonesa foi recodificada antes de chegar ao gpdf. Culpado mais comum: um CSV salvo como Shift-JIS pelo Excel e lido com os.ReadFile como se fosse UTF-8, ou um endpoint HTTP que não declarou charset=utf-8. Conserte o decodificador, não o PDF.

Saída mista — alguns caracteres renderizam, outros viram retângulos — significa cobertura parcial da fonte. Uma fonte rotulada como "japonesa" pode incluir hiragana e katakana mas pular kanjis incomuns como 鬱 ou 龠. Troque para Noto Sans JP (cobre JIS X 0213) ou Source Han Sans JP se isso acontecer.

Causa 2 em detalhe: fonte certa, nome de família errado

Essa é traiçoeira porque a fonte está embutida — simplesmente não é usada. Reprodução mínima:

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", font),
    // Sem WithDefaultFont.
)

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Text("こんにちは") // Usa a fonte padrão: Helvetica.
    })
})

Correção: adicione gpdf.WithDefaultFont("NotoSansJP", 12) ao NewDocument, ou passe template.FontFamily("NotoSansJP") em cada c.Text que precisa de japonês. O nome de família em WithFont e o em c.Text devem bater exatamente, incluindo caixa. Para o gpdf, NotoSansJP e notosansjp são duas fontes diferentes.

Causa 3 em detalhe: o arquivo TTF errado

NotoSans-Regular.ttf e NotoSansJP-Regular.ttf são arquivos diferentes. O primeiro é uma fonte latina sem nenhuma cobertura CJK. O segundo é a versão japonesa, com cerca de 17.000 glifos. Eles ficam quase idênticos em um ls, e o autocomplete do editor pega o errado com facilidade.

O gpdf não valida cobertura de glifos no registro. Se você entrega bytes, ele confia. A falha só aparece como tofu no momento do render.

Maneira rápida de conferir:

  • macOS: Font Book → duplo-clique no arquivo → a prévia mostra uma grade de glifos
  • Linux: otfinfo -u NotoSans-Regular.ttf lista a cobertura Unicode
  • Multiplataforma: fontToolsttx -t cmap NotoSans-Regular.ttf despeja a tabela cmap como XML

Se U+3042 (あ) não está na lista, você está com o subset latino.

Causa 4 em detalhe: corrupção de encoding

Essa na verdade não envolve o gpdf. A string entregue ao c.Text já tinha os bytes errados. Imprima antes de renderizar:

text := loadLabelFromSomewhere()
fmt.Printf("%q\n", text) // Mostra as runas reais
c.Text(text)

Se fmt.Printf("%q\n", text) imprime "縺ゅ→縺" em vez de "あいうえ", a corrupção aconteceu antes. O gpdf não pode consertar — ache o ponto onde o UTF-8 foi decodificado errado.

Culpados habituais lá na frente:

  • Ler um CSV exportado do Excel (Windows Shift-JIS) com os.ReadFile e converter direto em string
  • Uma coluna de banco declarada latin1 ou utf8mb3 (não utf8mb4) já guardando mojibake
  • Uma resposta HTTP sem Content-Type: application/json; charset=utf-8 e um cliente que chutou Latin-1

Um caso de borda que vale mencionar

O gpdf faz subset silenciosamente. O subset congela no instante de Generate(). Se durante a construção do documento você renderiza こんにちは e depois 鬱陶しい, o segundo também entra no subset corretamente. Mas se você gerar o PDF, abrir no Acrobat e digitar um kanji que não estava no texto original, aquele caractere virá como tofu — aquele glifo nunca entrou no subset. Não edite o PDF depois; rode o programa Go de novo e chame Generate().

Receitas relacionadas

Experimente o gpdf

gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, suporte nativo a CJK.

go get github.com/gpdf-dev/gpdf

⭐ Star no GitHub · Ler a documentação