返回 DeepSeek-Reasonix
session_export_files.go
根目录 / desktop / session_export_files.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "path/filepath"
11 "strings"
12 "unicode/utf16"
13 )
14
15 type pageDimensions struct {
16 Width int `json:"width"`
17 Height int `json:"height"`
18 }
19 type exportPDFWriter struct {
20 dst io.Writer
21 offset int64
22 err error
23 }
24
25 func (w *exportPDFWriter) write(value string) {
26 if w.err != nil {
27 return
28 }
29 var n int
30 n, w.err = io.WriteString(w.dst, value)
31 w.offset += int64(n)
32 }
33 func (w *exportPDFWriter) object(id int, body string, offsets []int64) {
34 offsets[id] = w.offset
35 w.write(fmt.Sprintf("%d 0 obj\n%s\nendobj\n", id, body))
36 }
37 func writeExportPDF(ctx context.Context, dst io.Writer, dir string, count int, title string) error {
38 if count == 0 {
39 return errors.New("no export pages")
40 }
41 w := &exportPDFWriter{dst: dst}
42 offsets := make([]int64, 4+count*3)
43 w.write("%PDF-1.4\n%\xff\xff\xff\xff\n")
44 w.object(1, "<< /Type /Catalog /Pages 2 0 R >>", offsets)
45 var kids strings.Builder
46 for i := range count {
47 if err := ctx.Err(); err != nil {
48 return err
49 }
50 pageID := 3 + i*3
51 fmt.Fprintf(&kids, "%d 0 R ", pageID)
52 path := filepath.Join(dir, fmt.Sprintf("page-%06d", i))
53 meta, err := os.ReadFile(path + ".json")
54 if err != nil {
55 return err
56 }
57 var dimensions pageDimensions
58 if err = json.Unmarshal(meta, &dimensions); err != nil {
59 return err
60 }
61 file, err := os.Open(path)
62 if err != nil {
63 return err
64 }
65 info, err := file.Stat()
66 if err != nil {
67 file.Close()
68 return err
69 }
70 height := min(769.89, float64(dimensions.Height)*523.28/float64(dimensions.Width))
71 y := 841.89 - 36 - height
72 w.object(pageID, fmt.Sprintf("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Resources << /XObject << /Im0 %d 0 R >> >> /Contents %d 0 R >>", pageID+2, pageID+1), offsets)
73 commands := fmt.Sprintf("q\n523.28 0 0 %.3f 36 %.3f cm\n/Im0 Do\nQ\n", height, y)
74 w.object(pageID+1, fmt.Sprintf("<< /Length %d >>\nstream\n%sendstream", len(commands), commands), offsets)
75 offsets[pageID+2] = w.offset
76 w.write(fmt.Sprintf("%d 0 obj\n<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n", pageID+2, dimensions.Width, dimensions.Height, info.Size()))
77 if w.err != nil {
78 file.Close()
79 return w.err
80 }
81 n, err := copyExportContext(ctx, dst, file)
82 file.Close()
83 if err != nil {
84 return err
85 }
86 w.offset += n
87 w.write("\nendstream\nendobj\n")
88 }
89 w.object(2, fmt.Sprintf("<< /Type /Pages /Kids [%s] /Count %d >>", kids.String(), count), offsets)
90 // UTF-16BE PDF strings preserve non-ASCII titles.
91 encoded := utf16.Encode([]rune(title))
92 var hexTitle strings.Builder
93 hexTitle.WriteString("FEFF")
94 for _, unit := range encoded {
95 fmt.Fprintf(&hexTitle, "%04X", unit)
96 }
97 w.object(3+count*3, "<< /Title <"+hexTitle.String()+"> /Producer (Reasonix) >>", offsets)
98 xref := w.offset
99 w.write(fmt.Sprintf("xref\n0 %d\n0000000000 65535 f \n", len(offsets)))
100 for _, offset := range offsets[1:] {
101 w.write(fmt.Sprintf("%010d 00000 n \n", offset))
102 }
103 w.write(fmt.Sprintf("trailer\n<< /Size %d /Root 1 0 R /Info %d 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(offsets), 3+count*3, xref))
104 return w.err
105 }
106
107 func publishExportImages(ctx context.Context, dir, path string, count int) ([]string, error) {
108 if count == 0 {
109 return nil, errors.New("no export pages")
110 }
111 targets := make([]string, count)
112 for i := range targets {
113 targets[i] = numberedExportPath(path, i, count)
114 }
115 if count == 1 {
116 err := writeStreamingExport(path, func(dst io.Writer) error {
117 src, err := os.Open(filepath.Join(dir, "page-000000"))
118 if err != nil {
119 return err
120 }
121 defer src.Close()
122 _, err = copyExportContext(ctx, dst, src)
123 return err
124 })
125 return targets, err
126 }
127 err := publishJournaledImages(ctx, dir, targets)
128 return targets, err
129 }
130
131 func numberedExportPath(path string, partIndex, partCount int) string {
132 if partCount <= 1 {
133 return path
134 }
135 ext := filepath.Ext(path)
136 stem := strings.TrimSuffix(path, ext)
137 return fmt.Sprintf("%s-%d-of-%d%s", stem, partIndex+1, partCount, ext)
138 }
139
139 lines GO