Skip to content

Commit ae19084

Browse files
donislawdevclaude
andauthored
rtf: pictures in \pict and links in HYPERLINK fields (#192)
* rtf: pictures in \pict and links in HYPERLINK fields inline, inline_format and inline_size put real png or jpg files into the document, with the names, ranges and defaults a page and a mail message have, each in a paragraph that starts with its name. picture_data writes their bytes in hex lines of 128 or binary after \bin. links adds up to a million HYPERLINK fields, 17 kinds in turn, inside a bookmark the first kind points at. Default settings write the bytes they always wrote. The structural checker cuts every \pict group out of the bytes before reading the rest as text, and is told the five new settings. The pandoc guard reads the pictures back in hex and holds the binary canary: pandoc breaks every png written binary. The stop guard of containers finds every format carrying pictures from the registry instead of a list by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * rtf: say which keys the manifest adds, and make pandoc the subject of the Polish sentence Review of #192: the changelog read as if picture_data and links were the keys of the pictures, and the Polish detail of picture_data read as if the text lost the document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5524f23 commit ae19084

42 files changed

Lines changed: 1820 additions & 36 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ because it turns other people's test suites red.
6363
code page the header declares. A large document is a long one to read:
6464
at 100 MiB pandoc took 95 s and 9.6 GB of memory, and LibreOffice had not
6565
finished after 20 minutes.
66+
- **Documents: pictures and links in `rtf`.** `inline`, `inline_format` and
67+
`inline_size` put real `png` or `jpg` files into the document, the settings
68+
a page and a mail message have, each in a paragraph that starts with its
69+
name, such as `inline_0001.png`, the one description all six readers
70+
measured return. Any other format is refused, since RTF names only those
71+
two. `picture_data` says how their bytes are written: `hex`, twice the size
72+
of the picture, the way LibreOffice writes them, or `binary`, byte for
73+
byte, so the document is no longer text and a program that counts braces
74+
or reads it as text loses its place. LibreOffice, pandoc and Tika give back
75+
each picture in hex as the very file it is, and RichEdit shows the same
76+
picture and writes it out again smaller. pandoc changes the line ends
77+
inside a binary picture, so no `png` comes out whole, and it can lose the
78+
text after the picture or refuse the whole document. `links` adds up to 1 000 000 links, 17
79+
kinds in turn, each saying which kind it is - a bookmark in the document,
80+
relative, absolute, without a scheme, `mailto:`, `tel:`, `data:`, a new
81+
window, a screen tip and more - and every reader makes something different
82+
of them: LibreOffice turns the one without a scheme into a `file://`
83+
address, pandoc 3.1 and 3.7 drop every link with its text, and RichEdit
84+
returns each address in front of its text. The manifest records the
85+
pictures under the keys a page and a message use, and adds `picture_data`
86+
and `links`.
6687
- **Films: the `webm` format.** A WebM video with an AV1 picture and no
6788
sound, at the exact size you ask for, like every other format. Its length
6889
is a setting of its own, independent of the size. The picture moves: every

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ recipe. `tfg formats <id>` prints the allowed range or list for each:
627627
| `docx` | `paragraphs` |
628628
| `xlsx` | `rows`, `columns` |
629629
| `pptx` | `slides` |
630-
| `rtf` | `characters`, `text_encoding`, `elements`, `nesting` |
630+
| `rtf` | `characters`, `text_encoding`, `elements`, `nesting`, `links`, `inline`, `inline_format`, `inline_size`, `picture_data` |
631631
| `csv` | `delimiter`, `line_ending`, `header`, `quote_style`, `columns` |
632632
| `log` | `entry_format`, `timestamps`, `rate`, `methods`, `status_mix`, `level_mix`, `ip_version`, `line_ending` |
633633
| `txt`, `md`, `xml` | `encoding`, `bom` |

‎internal/format/rtf/links.go‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package rtf
2+
3+
import (
4+
"context"
5+
"io"
6+
7+
"github.com/donislawdev/TestingFilesGenerator/internal/core"
8+
)
9+
10+
// The links a document carries when links is above 0: HYPERLINK fields, one to
11+
// a paragraph, after what the settings open the document with and before the
12+
// pictures and the blocks - the order a page has them in.
13+
//
14+
// Seventeen kinds, in turn, each saying in its text what kind it is, so what a
15+
// program pulls out of the document reads without this file beside it. Read on
16+
// 2026-10-15 by six readers, and each made something different of them
17+
// (docs/RTF-2-2026-10-15.md section 1.2), which is what the detail below says.
18+
// A switch of the field is written with its backslash escaped, the way RTF
19+
// writes a backslash. A page has a download link and a nofollow link, and a
20+
// field has nothing that says either, so they are not here. A path on the disk
21+
// or a share and a field that fetches something belong to the security set
22+
// (decision D3).
23+
//
24+
// Every address is on a domain RFC 2606 keeps for examples, and the number is
25+
// in the range NANPA keeps for fiction, so a link followed by mistake reaches
26+
// nobody. All of them are ASCII, which keeps links and characters independent.
27+
var linkKinds = []struct{ instruction, text string }{
28+
{`HYPERLINK \\l "tfg_links"`, "link to a bookmark in this document"},
29+
{`HYPERLINK "page-2.rtf"`, "relative link"},
30+
{`HYPERLINK "../folder/page-3.rtf"`, "link to the folder above"},
31+
{`HYPERLINK "/root/page-4.rtf"`, "link from the root"},
32+
{`HYPERLINK "https://example.com/path/page.html?a=1&b=2#part"`, "absolute link with a query and a fragment"},
33+
{`HYPERLINK "https://example.com/path/page.html" \\l "part"`, "absolute link with a location"},
34+
{`HYPERLINK "http://example.org/"`, "http link"},
35+
{`HYPERLINK "//example.net/scheme-relative"`, "link without a scheme"},
36+
{`HYPERLINK "https://example.com/caf%C3%A9?q=two%20words"`, "percent encoded link"},
37+
{`HYPERLINK "https://xn--bcher-kva.example/"`, "international domain link"},
38+
{`HYPERLINK "https://example.com:8443/port"`, "link with a port"},
39+
{`HYPERLINK "mailto:someone@example.com?subject=Hello%20there"`, "mail link"},
40+
{`HYPERLINK "tel:+1-555-0100"`, "phone link"},
41+
{`HYPERLINK "ftp://example.com/file.txt"`, "ftp link"},
42+
{`HYPERLINK "https://example.org/new" \\t "_blank"`, "link to a new window"},
43+
{`HYPERLINK "https://example.net/tip" \\o "This sentence is the screen tip of a link."`, "link with a screen tip"},
44+
{`HYPERLINK "data:text/plain,hello"`, "data link"},
45+
}
46+
47+
// The pieces of a link's paragraph around its kind, and the bookmark the first
48+
// kind points at, which spans the whole list the way the id of the list does
49+
// in a page.
50+
const (
51+
fieldOpen = `{\field{\*\fldinst `
52+
fieldResult = `}{\fldrslt `
53+
fieldClose = `}}`
54+
bookmarkStart = `{\*\bkmkstart tfg_links}`
55+
bookmarkEnd = `{\*\bkmkend tfg_links}`
56+
)
57+
58+
// linksDetail is what both surfaces say about the setting.
59+
const linksDetail = "How many links the document carries, each a HYPERLINK field in a paragraph of its own, above the pictures and the blocks. " +
60+
"They go through 17 kinds in turn and then repeat - a link to a bookmark in the document, relative, to the folder above, from the root, " +
61+
"absolute with a query and a fragment, absolute with a location, http, without a scheme, percent encoded, an international domain, a port, " +
62+
"mailto:, tel:, ftp:, a new window, a screen tip and data: - and the text of each says which it is. " +
63+
"Every address is on a domain kept for examples. " +
64+
"LibreOffice turns the link without a scheme into a file:// address and rewrites the relative ones from where it saves the document. " +
65+
"pandoc 3.12 leaves the bookmark link without an address, pandoc 3.1 and 3.7 drop every link with its text, " +
66+
"and RichEdit returns the address of each link in front of its text. " +
67+
"LibreOffice took over 11 minutes over a million links. " +
68+
"The catalogue of every part has one link of its own, which this does not count."
69+
70+
// linksBytes is what the list takes, worked out without building it.
71+
func linksBytes(n int) int64 {
72+
if n == 0 {
73+
return 0
74+
}
75+
var cycle, part int64
76+
for i, k := range linkKinds {
77+
size := int64(len(k.instruction) + len(k.text))
78+
cycle += size
79+
if i < n%len(linkKinds) {
80+
part += size
81+
}
82+
}
83+
each := int64(len(fieldOpen) + len(fieldResult) + len(fieldClose) + len(endBlock))
84+
count := int64(n)
85+
return int64(len(bookmarkStart)+len(bookmarkEnd)) + count*each + count/int64(len(linkKinds))*cycle + part
86+
}
87+
88+
// writeLinks writes the list a chunk at a time, so a million links never sit in
89+
// memory and an interrupted run stops between two chunks.
90+
func writeLinks(ctx context.Context, w io.Writer, n int) error {
91+
out := chunks{ctx: ctx, w: w}
92+
for i := 0; i < n && out.err == nil; i++ {
93+
k := linkKinds[i%len(linkKinds)]
94+
if i == 0 {
95+
out.add(bookmarkStart)
96+
}
97+
out.add(fieldOpen, k.instruction, fieldResult, k.text, fieldClose)
98+
if i == n-1 {
99+
out.add(bookmarkEnd)
100+
}
101+
out.add(endBlock)
102+
}
103+
return out.close()
104+
}
105+
106+
// writeChunk is how much is gathered before it is written, and so how often a
107+
// stop is noticed.
108+
const writeChunk = 32 * 1024
109+
110+
// chunks gathers pieces and writes them a chunk at a time, after asking
111+
// whether the run was stopped.
112+
type chunks struct {
113+
ctx context.Context
114+
w io.Writer
115+
buf []byte
116+
err error
117+
}
118+
119+
func (c *chunks) add(pieces ...string) {
120+
if c.err != nil {
121+
return
122+
}
123+
for _, p := range pieces {
124+
c.buf = append(c.buf, p...)
125+
}
126+
if len(c.buf) < writeChunk {
127+
return
128+
}
129+
if c.err = c.ctx.Err(); c.err == nil {
130+
c.err = core.WriteAll(c.w, c.buf)
131+
}
132+
c.buf = c.buf[:0]
133+
}
134+
135+
// close writes what is left, or says why nothing more was written.
136+
func (c *chunks) close() error {
137+
if c.err != nil {
138+
return c.err
139+
}
140+
return core.WriteAll(c.w, c.buf)
141+
}

0 commit comments

Comments
 (0)