feat: warn when a PDF export replaces characters it cannot draw
The hand-written PDF writer uses the standard Courier fonts (WinAnsiEncoding), so any character outside CP1252 - a Polish, Greek, Cyrillic or CJK script - was silently written as '?'. Render now collects the characters it had to replace and returns them, and the export dialog lists them in a warning once the file is written. Embedding a TrueType font (e.g. Courier Prime) with a CID/ToUnicode map would fix the root cause; the warning keeps the zero-dependency trade-off honest in the meantime.
This commit is contained in:
1 parent
5e7ae97b57
commit
e80ad634ef
7 files changed
+125
-35
No files matched your search
@@ -7,6 +7,12 @@ version is in the `VERSION` file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The PDF export warns when the script contains characters the standard PDF fonts
|
||||||
|
cannot draw: they are written as `?` in the file, and are now listed in a dialog
|
||||||
|
instead of being silently lost.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- The unsaved-changes marker is now updated atomically: an edit landing while a save
|
- The unsaved-changes marker is now updated atomically: an edit landing while a save
|
||||||
|
|||||||
@@ -151,11 +151,19 @@ func (a *App) SaveAs(doc screenplay.Document, current string) (string, error) {
|
|||||||
return a.Save(doc, path)
|
return a.Save(doc, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ExportPDF(doc screenplay.Document, opts ExportOptions, current string) (string, error) {
|
// ExportResult describes a written PDF export.
|
||||||
|
type ExportResult struct {
|
||||||
|
Path string `json:"path"` // "" when the user cancelled the dialog
|
||||||
|
// Replaced are the characters the standard PDF fonts cannot draw:
|
||||||
|
// each one is written as "?" in the file.
|
||||||
|
Replaced []string `json:"replaced,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ExportPDF(doc screenplay.Document, opts ExportOptions, current string) (ExportResult, error) {
|
||||||
path, err := a.saveDialog("Export PDF", current, ".pdf",
|
path, err := a.saveDialog("Export PDF", current, ".pdf",
|
||||||
[]runtime.FileFilter{{DisplayName: "PDF (*.pdf)", Pattern: "*.pdf"}})
|
[]runtime.FileFilter{{DisplayName: "PDF (*.pdf)", Pattern: "*.pdf"}})
|
||||||
if err != nil || path == "" {
|
if err != nil || path == "" {
|
||||||
return "", err
|
return ExportResult{}, err
|
||||||
}
|
}
|
||||||
var pages []layout.Page
|
var pages []layout.Page
|
||||||
if opts.TitlePage && !doc.TitlePage.IsEmpty() {
|
if opts.TitlePage && !doc.TitlePage.IsEmpty() {
|
||||||
@@ -167,11 +175,18 @@ func (a *App) ExportPDF(doc screenplay.Document, opts ExportOptions, current str
|
|||||||
pdfOpts.Revisions = doc.Settings.Revisions
|
pdfOpts.Revisions = doc.Settings.Revisions
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = pdf.Render(&buf, pages, pdfOpts)
|
replaced, err := pdf.Render(&buf, pages, pdfOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return ExportResult{}, err
|
||||||
}
|
}
|
||||||
return path, writeAtomic(path, buf.Bytes())
|
if err := writeAtomic(path, buf.Bytes()); err != nil {
|
||||||
|
return ExportResult{}, err
|
||||||
|
}
|
||||||
|
res := ExportResult{Path: path}
|
||||||
|
for _, r := range replaced {
|
||||||
|
res.Replaced = append(res.Replaced, string(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ExportFDX(doc screenplay.Document, current string) (string, error) {
|
func (a *App) ExportFDX(doc screenplay.Document, current string) (string, error) {
|
||||||
|
|||||||
@@ -144,13 +144,21 @@ export interface ExportOptions {
|
|||||||
revisions: boolean
|
revisions: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A written PDF export. */
|
||||||
|
export interface ExportResult {
|
||||||
|
/** Empty when the user cancelled the dialog. */
|
||||||
|
path: string
|
||||||
|
/** Characters the standard PDF fonts cannot draw, written as "?" in the file. */
|
||||||
|
replaced?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
interface GoApp {
|
interface GoApp {
|
||||||
NewDocument(): Promise<ScriptDocument>
|
NewDocument(): Promise<ScriptDocument>
|
||||||
InitialFile(): Promise<OpenResult | null>
|
InitialFile(): Promise<OpenResult | null>
|
||||||
Open(): Promise<OpenResult | null>
|
Open(): Promise<OpenResult | null>
|
||||||
Save(doc: ScriptDocument, path: string): Promise<string>
|
Save(doc: ScriptDocument, path: string): Promise<string>
|
||||||
SaveAs(doc: ScriptDocument, current: string): Promise<string>
|
SaveAs(doc: ScriptDocument, current: string): Promise<string>
|
||||||
ExportPDF(doc: ScriptDocument, opts: ExportOptions, current: string): Promise<string>
|
ExportPDF(doc: ScriptDocument, opts: ExportOptions, current: string): Promise<ExportResult>
|
||||||
ExportFDX(doc: ScriptDocument, current: string): Promise<string>
|
ExportFDX(doc: ScriptDocument, current: string): Promise<string>
|
||||||
Paginate(doc: ScriptDocument): Promise<LayoutInfo>
|
Paginate(doc: ScriptDocument): Promise<LayoutInfo>
|
||||||
ParseFountain(text: string): Promise<Element[] | null>
|
ParseFountain(text: string): Promise<Element[] | null>
|
||||||
|
|||||||
@@ -947,7 +947,15 @@ const actions: Record<string, () => unknown> = {
|
|||||||
renderPaper()
|
renderPaper()
|
||||||
settingsChanged()
|
settingsChanged()
|
||||||
}
|
}
|
||||||
await api.ExportPDF(script(), chosen, app.path).catch(reportError)
|
const res = await api.ExportPDF(script(), chosen, app.path).catch(reportError)
|
||||||
|
if (res?.replaced?.length) {
|
||||||
|
const list = res.replaced.map((c) => `"${c}"`).join(' ')
|
||||||
|
await messageBox({
|
||||||
|
title: 'Characters replaced in the PDF',
|
||||||
|
text: `The standard PDF fonts cannot draw these characters: ${list}. Each one is written as "?" in the exported file.`,
|
||||||
|
ok: 'OK',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dialog.showModal()
|
dialog.showModal()
|
||||||
},
|
},
|
||||||
|
|||||||
+44
-24
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
|
|
||||||
@@ -34,8 +35,11 @@ const (
|
|||||||
var fontNames = []string{"Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique"}
|
var fontNames = []string{"Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique"}
|
||||||
|
|
||||||
// Render writes pages to w. The title page, if any, must be the first page
|
// Render writes pages to w. The title page, if any, must be the first page
|
||||||
// and have Number 0 so that it is not numbered.
|
// and have Number 0 so that it is not numbered. It returns, sorted, the
|
||||||
func Render(w io.Writer, pages []layout.Page, opt Options) error {
|
// characters the standard Courier fonts cannot draw: each one is written
|
||||||
|
// as "?" in the output, so the caller can warn instead of corrupting the
|
||||||
|
// script silently.
|
||||||
|
func Render(w io.Writer, pages []layout.Page, opt Options) ([]rune, error) {
|
||||||
geo := layout.PaperGeometry(opt.Paper)
|
geo := layout.PaperGeometry(opt.Paper)
|
||||||
var objs [][]byte // objs[i] is object number i+1
|
var objs [][]byte // objs[i] is object number i+1
|
||||||
add := func(s string) int {
|
add := func(s string) int {
|
||||||
@@ -50,11 +54,12 @@ func Render(w io.Writer, pages []layout.Page, opt Options) error {
|
|||||||
}
|
}
|
||||||
add(fmt.Sprintf("<< /Title %s /Author %s /Creator (Plotzilla) /Producer (Plotzilla) >>", textString(opt.Title), textString(opt.Author)))
|
add(fmt.Sprintf("<< /Title %s /Author %s /Creator (Plotzilla) /Producer (Plotzilla) >>", textString(opt.Title), textString(opt.Author)))
|
||||||
|
|
||||||
|
missing := map[rune]bool{}
|
||||||
var kids []string
|
var kids []string
|
||||||
for _, pg := range pages {
|
for _, pg := range pages {
|
||||||
stream, err := deflate(pageContent(pg, geo, opt.Revisions))
|
stream, err := deflate(pageContent(pg, geo, opt.Revisions, missing))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
content := add(fmt.Sprintf("<< /Length %d /Filter /FlateDecode >>\nstream\n%s\nendstream", len(stream), stream))
|
content := add(fmt.Sprintf("<< /Length %d /Filter /FlateDecode >>\nstream\n%s\nendstream", len(stream), stream))
|
||||||
page := add(fmt.Sprintf("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %g %g] "+
|
page := add(fmt.Sprintf("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %g %g] "+
|
||||||
@@ -80,7 +85,16 @@ func Render(w io.Writer, pages []layout.Page, opt Options) error {
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root 1 0 R /Info 7 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(objs)+1, xref)
|
fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root 1 0 R /Info 7 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(objs)+1, xref)
|
||||||
_, err := w.Write(buf.Bytes())
|
_, err := w.Write(buf.Bytes())
|
||||||
return err
|
return sortedRunes(missing), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedRunes(set map[rune]bool) []rune {
|
||||||
|
out := make([]rune, 0, len(set))
|
||||||
|
for r := range set {
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
slices.Sort(out)
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// points converts inches to PDF points, rounded to 1/100 pt.
|
// points converts inches to PDF points, rounded to 1/100 pt.
|
||||||
@@ -92,16 +106,16 @@ func baseline(geo layout.Geometry, row int) float64 {
|
|||||||
return top - float64(row)*fontSize - 9
|
return top - float64(row)*fontSize - 9
|
||||||
}
|
}
|
||||||
|
|
||||||
func text(b *bytes.Buffer, x, y float64, s string) {
|
func text(b *bytes.Buffer, x, y float64, s string, missing map[rune]bool) {
|
||||||
fmt.Fprintf(b, "BT /F1 12 Tf %.2f %.2f Td %s Tj ET\n", x, y, pdfString(s))
|
fmt.Fprintf(b, "BT /F1 12 Tf %.2f %.2f Td %s Tj ET\n", x, y, pdfString(s, missing))
|
||||||
}
|
}
|
||||||
|
|
||||||
func pageContent(pg layout.Page, geo layout.Geometry, revisions []screenplay.Revision) []byte {
|
func pageContent(pg layout.Page, geo layout.Geometry, revisions []screenplay.Revision, missing map[rune]bool) []byte {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
header := (geo.Height-0.5)*ptPerIn - 9
|
header := (geo.Height-0.5)*ptPerIn - 9
|
||||||
if pg.Number > 0 && pg.Label != "" && pg.Label != "1" {
|
if pg.Number > 0 && pg.Label != "" && pg.Label != "1" {
|
||||||
num := pg.Label + "."
|
num := pg.Label + "."
|
||||||
text(&b, layout.RightEdge*ptPerIn-float64(len([]rune(num)))*fontSize*0.6, header, num)
|
text(&b, layout.RightEdge*ptPerIn-float64(len([]rune(num)))*fontSize*0.6, header, num, missing)
|
||||||
}
|
}
|
||||||
// A revised page names its revision; its latest changes are starred.
|
// A revised page names its revision; its latest changes are starred.
|
||||||
rev := pg.Rev()
|
rev := pg.Rev()
|
||||||
@@ -114,17 +128,17 @@ func pageContent(pg layout.Page, geo layout.Geometry, revisions []screenplay.Rev
|
|||||||
if r.Date != "" {
|
if r.Date != "" {
|
||||||
label += " - " + r.Date
|
label += " - " + r.Date
|
||||||
}
|
}
|
||||||
text(&b, 1.5*ptPerIn, header, label)
|
text(&b, 1.5*ptPerIn, header, label, missing)
|
||||||
}
|
}
|
||||||
for _, ln := range pg.Lines {
|
for _, ln := range pg.Lines {
|
||||||
y := baseline(geo, ln.Row)
|
y := baseline(geo, ln.Row)
|
||||||
x := ln.X * ptPerIn
|
x := ln.X * ptPerIn
|
||||||
if ln.SceneNumber != "" {
|
if ln.SceneNumber != "" {
|
||||||
text(&b, 0.75*ptPerIn, y, ln.SceneNumber)
|
text(&b, 0.75*ptPerIn, y, ln.SceneNumber, missing)
|
||||||
text(&b, geo.SceneNumber*ptPerIn, y, ln.SceneNumber)
|
text(&b, geo.SceneNumber*ptPerIn, y, ln.SceneNumber, missing)
|
||||||
}
|
}
|
||||||
if rev > 0 && ln.Rev == rev {
|
if rev > 0 && ln.Rev == rev {
|
||||||
text(&b, geo.RevisionMark*ptPerIn, y, "*")
|
text(&b, geo.RevisionMark*ptPerIn, y, "*", missing)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, "BT %.2f %.2f Td ", x, y)
|
fmt.Fprintf(&b, "BT %.2f %.2f Td ", x, y)
|
||||||
cursor := x
|
cursor := x
|
||||||
@@ -142,7 +156,7 @@ func pageContent(pg layout.Page, geo layout.Geometry, revisions []screenplay.Rev
|
|||||||
underlines = append(underlines, [2]float64{cursor, cursor + width})
|
underlines = append(underlines, [2]float64{cursor, cursor + width})
|
||||||
}
|
}
|
||||||
cursor += width
|
cursor += width
|
||||||
fmt.Fprintf(&b, "/F%d 12 Tf %s Tj ", font, pdfString(s.Text))
|
fmt.Fprintf(&b, "/F%d 12 Tf %s Tj ", font, pdfString(s.Text, missing))
|
||||||
}
|
}
|
||||||
b.WriteString("ET\n")
|
b.WriteString("ET\n")
|
||||||
for _, u := range underlines {
|
for _, u := range underlines {
|
||||||
@@ -164,12 +178,16 @@ func deflate(data []byte) ([]byte, error) {
|
|||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfString encodes s as a WinAnsi literal string for the Courier fonts.
|
// pdfString encodes s as a WinAnsi literal string for the Courier fonts,
|
||||||
func pdfString(s string) string {
|
// recording in missing the characters they cannot draw.
|
||||||
|
func pdfString(s string, missing map[rune]bool) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteByte('(')
|
b.WriteByte('(')
|
||||||
for _, r := range s {
|
for _, r := range s {
|
||||||
c := winAnsi(r)
|
c, ok := winAnsi(r)
|
||||||
|
if !ok {
|
||||||
|
missing[r] = true
|
||||||
|
}
|
||||||
if c == '(' || c == ')' || c == '\\' {
|
if c == '(' || c == ')' || c == '\\' {
|
||||||
b.WriteByte('\\')
|
b.WriteByte('\\')
|
||||||
}
|
}
|
||||||
@@ -189,7 +207,7 @@ func textString(s string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ascii {
|
if ascii {
|
||||||
return pdfString(s)
|
return pdfString(s, map[rune]bool{}) // ASCII only: nothing to record
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("<FEFF")
|
b.WriteString("<FEFF")
|
||||||
@@ -207,18 +225,20 @@ var cp1252 = map[rune]byte{
|
|||||||
'˜': 0x98, '™': 0x99, 'š': 0x9a, '›': 0x9b, 'œ': 0x9c, 'ž': 0x9e, 'Ÿ': 0x9f,
|
'˜': 0x98, '™': 0x99, 'š': 0x9a, '›': 0x9b, 'œ': 0x9c, 'ž': 0x9e, 'Ÿ': 0x9f,
|
||||||
}
|
}
|
||||||
|
|
||||||
func winAnsi(r rune) byte {
|
// winAnsi encodes r as a WinAnsi byte; ok is false when the standard
|
||||||
|
// fonts cannot draw r and '?' is written instead.
|
||||||
|
func winAnsi(r rune) (c byte, ok bool) {
|
||||||
switch {
|
switch {
|
||||||
case r >= 0x20 && r < 0x7f:
|
case r >= 0x20 && r < 0x7f:
|
||||||
return byte(r)
|
return byte(r), true
|
||||||
case r >= 0xa0 && r <= 0xff:
|
case r >= 0xa0 && r <= 0xff:
|
||||||
return byte(r)
|
return byte(r), true
|
||||||
}
|
}
|
||||||
if c, ok := cp1252[r]; ok {
|
if c, ok := cp1252[r]; ok {
|
||||||
return c
|
return c, true
|
||||||
}
|
}
|
||||||
if r == ' ' || r == ' ' {
|
if r == ' ' || r == ' ' {
|
||||||
return ' ' // narrow spaces used before French punctuation
|
return ' ', true // narrow spaces used before French punctuation
|
||||||
}
|
}
|
||||||
return '?'
|
return '?', false
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"compress/zlib"
|
"compress/zlib"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -39,9 +40,13 @@ func TestRender(t *testing.T) {
|
|||||||
pages := []layout.Page{layout.TitlePage(doc.TitlePage, doc.Settings.Paper)}
|
pages := []layout.Page{layout.TitlePage(doc.TitlePage, doc.Settings.Paper)}
|
||||||
pages = append(pages, layout.Paginate(doc, layout.Options{SceneNumbers: true}).Pages...)
|
pages = append(pages, layout.Paginate(doc, layout.Options{SceneNumbers: true}).Pages...)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := Render(&buf, pages, Options{Title: doc.TitlePage.Title, Author: doc.TitlePage.Author}); err != nil {
|
replaced, err := Render(&buf, pages, Options{Title: doc.TitlePage.Title, Author: doc.TitlePage.Author})
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if len(replaced) != 0 {
|
||||||
|
t.Errorf("the French sample is fully representable, replaced = %q", string(replaced))
|
||||||
|
}
|
||||||
out := buf.String()
|
out := buf.String()
|
||||||
if !strings.HasPrefix(out, "%PDF-1.4") || !strings.HasSuffix(out, "%%EOF\n") || !strings.Contains(out, "/Count 2") {
|
if !strings.HasPrefix(out, "%PDF-1.4") || !strings.HasSuffix(out, "%%EOF\n") || !strings.Contains(out, "/Count 2") {
|
||||||
t.Fatalf("malformed PDF:\n%.300s", out)
|
t.Fatalf("malformed PDF:\n%.300s", out)
|
||||||
@@ -61,7 +66,7 @@ func TestRevisionsAndA4(t *testing.T) {
|
|||||||
doc.Elements[1].Runs[0].Rev = 1
|
doc.Elements[1].Runs[0].Rev = 1
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
pages := layout.Paginate(doc, layout.Options{}).Pages
|
pages := layout.Paginate(doc, layout.Options{}).Pages
|
||||||
if err := Render(&buf, pages, Options{Paper: doc.Settings.Paper, Revisions: doc.Settings.Revisions}); err != nil {
|
if _, err := Render(&buf, pages, Options{Paper: doc.Settings.Paper, Revisions: doc.Settings.Revisions}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
stream := uncompressed(t, buf.Bytes())
|
stream := uncompressed(t, buf.Bytes())
|
||||||
@@ -74,7 +79,7 @@ func TestRevisionsAndA4(t *testing.T) {
|
|||||||
|
|
||||||
// Without revisions in the options, nothing is marked.
|
// Without revisions in the options, nothing is marked.
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if err := Render(&buf, pages, Options{Paper: doc.Settings.Paper}); err != nil {
|
if _, err := Render(&buf, pages, Options{Paper: doc.Settings.Paper}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if stream := uncompressed(t, buf.Bytes()); strings.Contains(stream, "Revision") || strings.Contains(stream, "(*)") {
|
if stream := uncompressed(t, buf.Bytes()); strings.Contains(stream, "Revision") || strings.Contains(stream, "(*)") {
|
||||||
@@ -82,6 +87,34 @@ func TestRevisionsAndA4(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReplacedChars: characters outside WinAnsi are written as '?' and
|
||||||
|
// reported, so an export can warn instead of corrupting the text silently.
|
||||||
|
func TestReplacedChars(t *testing.T) {
|
||||||
|
src := "INT. ΚΑΦΕΝΕΙΟ - ΜΕΡΑ\n\nΚαλημέρα… Plain ASCII.\n"
|
||||||
|
doc := fountain.Parse(src)
|
||||||
|
pages := layout.Paginate(doc, layout.Options{}).Pages
|
||||||
|
var buf bytes.Buffer
|
||||||
|
replaced, err := Render(&buf, pages, Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := string(replaced)
|
||||||
|
for _, r := range src { // every Greek letter is reported
|
||||||
|
if r > 0x7f && r != '…' && !strings.ContainsRune(got, r) {
|
||||||
|
t.Errorf("%q not reported in %q", r, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.ContainsRune(got, '…') { // WinAnsi has "…"
|
||||||
|
t.Errorf("representable … reported in %q", got)
|
||||||
|
}
|
||||||
|
if !slices.IsSorted(replaced) {
|
||||||
|
t.Errorf("replaced not sorted: %q", got)
|
||||||
|
}
|
||||||
|
if stream := uncompressed(t, buf.Bytes()); !strings.Contains(stream, "????????") {
|
||||||
|
t.Errorf("Greek not written as '?':\n%s", stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// uncompressed returns the content streams of a PDF written by Render.
|
// uncompressed returns the content streams of a PDF written by Render.
|
||||||
func uncompressed(t *testing.T, pdf []byte) string {
|
func uncompressed(t *testing.T, pdf []byte) string {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ func pdfOutput(src, tmp, path string) error {
|
|||||||
pages := append([]layout.Page{layout.TitlePage(doc.TitlePage, doc.Settings.Paper)},
|
pages := append([]layout.Page{layout.TitlePage(doc.TitlePage, doc.Settings.Paper)},
|
||||||
layout.Paginate(doc, layout.Options{SceneNumbers: true}).Pages...)
|
layout.Paginate(doc, layout.Options{SceneNumbers: true}).Pages...)
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
if err := pdf.Render(&b, pages, pdf.Options{Title: doc.TitlePage.Title, Author: doc.TitlePage.Author}); err != nil {
|
if _, err := pdf.Render(&b, pages, pdf.Options{Title: doc.TitlePage.Title, Author: doc.TitlePage.Author}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
file := filepath.Join(tmp, "sample.pdf")
|
file := filepath.Join(tmp, "sample.pdf")
|
||||||
|
|||||||
Reference in new issue
Block a user