mirror of https://gitee.com/godoos/godoos.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
5.5 KiB
258 lines
5.5 KiB
/*
|
|
* GodoOS - A lightweight cloud desktop
|
|
* Copyright (C) 2024 https://godoos.com
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package doc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"unicode/utf16"
|
|
|
|
"github.com/mattetti/filebuffer"
|
|
"github.com/richardlehane/mscfb"
|
|
)
|
|
|
|
var (
|
|
errTable = errors.New("cannot find table stream")
|
|
errDocEmpty = errors.New("WordDocument not found")
|
|
errDocShort = errors.New("wordDoc block too short")
|
|
errInvalidArgument = errors.New("invalid table and/or fib")
|
|
)
|
|
|
|
type allReader interface {
|
|
io.Closer
|
|
io.ReaderAt
|
|
io.ReadSeeker
|
|
}
|
|
|
|
func wrapError(e error) error {
|
|
return errors.New("Error processing file: " + e.Error())
|
|
}
|
|
|
|
// ParseDoc converts a standard io.Reader from a Microsoft Word
|
|
// .doc binary file and returns a reader (actually a bytes.Buffer)
|
|
// which will output the plain text found in the .doc file
|
|
func ParseDoc(r io.Reader) (io.Reader, error) {
|
|
ra, ok := r.(io.ReaderAt)
|
|
if !ok {
|
|
ra, _, err := toMemoryBuffer(r)
|
|
if err != nil {
|
|
return nil, wrapError(err)
|
|
}
|
|
defer ra.Close()
|
|
}
|
|
|
|
d, err := mscfb.New(ra)
|
|
if err != nil {
|
|
return nil, wrapError(err)
|
|
}
|
|
|
|
wordDoc, table0, table1 := getWordDocAndTables(d)
|
|
fib, err := getFib(wordDoc)
|
|
if err != nil {
|
|
return nil, wrapError(err)
|
|
}
|
|
|
|
table := getActiveTable(table0, table1, fib)
|
|
if table == nil {
|
|
return nil, wrapError(errTable)
|
|
}
|
|
|
|
clx, err := getClx(table, fib)
|
|
if err != nil {
|
|
return nil, wrapError(err)
|
|
}
|
|
|
|
return getText(wordDoc, clx)
|
|
}
|
|
|
|
func toMemoryBuffer(r io.Reader) (allReader, int64, error) {
|
|
var b bytes.Buffer
|
|
size, err := b.ReadFrom(r)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
fb := filebuffer.New(b.Bytes())
|
|
return fb, size, nil
|
|
}
|
|
|
|
func getText(wordDoc *mscfb.File, clx *clx) (io.Reader, error) {
|
|
//var buf bytes.Buffer
|
|
var buf utf16Buffer
|
|
for i := 0; i < len(clx.pcdt.PlcPcd.aPcd); i++ {
|
|
pcd := clx.pcdt.PlcPcd.aPcd[i]
|
|
cp := clx.pcdt.PlcPcd.aCP[i]
|
|
cpNext := clx.pcdt.PlcPcd.aCP[i+1]
|
|
|
|
//var start, end, size int
|
|
var start, end int
|
|
if pcd.fc.fCompressed {
|
|
//size = 1
|
|
start = pcd.fc.fc / 2
|
|
end = start + cpNext - cp
|
|
} else {
|
|
//size = 2
|
|
start = pcd.fc.fc
|
|
end = start + 2*(cpNext-cp)
|
|
}
|
|
|
|
b := make([]byte, end-start)
|
|
//_, err := wordDoc.ReadAt(b, int64(start/size)) // read all the characters
|
|
_, err := wordDoc.ReadAt(b, int64(start))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
translateText(b, &buf, pcd.fc.fCompressed)
|
|
}
|
|
//return &buf, nil
|
|
runes := utf16.Decode(buf.Chars())
|
|
|
|
var out bytes.Buffer
|
|
out.Grow(len(runes))
|
|
for _, r := range runes {
|
|
if r == 7 { // table column separator
|
|
r = ' '
|
|
} else if r < 32 && r != 9 && r != 10 && r != 13 { // skip non-printable ASCII characters
|
|
continue
|
|
}
|
|
out.WriteRune(r)
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func translateText(b []byte, buf *utf16Buffer, fCompressed bool) {
|
|
fieldLevel := 0
|
|
var isFieldChar bool
|
|
for cIndex := range b {
|
|
// Handle special field characters (section 2.8.25)
|
|
if b[cIndex] == 0x13 {
|
|
isFieldChar = true
|
|
fieldLevel++
|
|
continue
|
|
} else if b[cIndex] == 0x14 {
|
|
isFieldChar = false
|
|
continue
|
|
} else if b[cIndex] == 0x15 {
|
|
isFieldChar = false
|
|
continue
|
|
} else if isFieldChar {
|
|
continue
|
|
}
|
|
|
|
// if b[cIndex] == 7 { // table column separator
|
|
// buf.WriteByte(' ')
|
|
// continue
|
|
// } else if b[cIndex] < 32 && b[cIndex] != 9 && b[cIndex] != 10 && b[cIndex] != 13 { // skip non-printable ASCII characters
|
|
// //buf.Write([]byte(fmt.Sprintf("|%#x|", b[cIndex])))
|
|
// continue
|
|
// }
|
|
|
|
if fCompressed { // compressed, so replace compressed characters
|
|
buf.Write(replaceCompressed(b[cIndex]))
|
|
} else {
|
|
//buf.Write(b)
|
|
buf.WriteByte(b[cIndex])
|
|
}
|
|
}
|
|
}
|
|
|
|
func replaceCompressed(char byte) []byte {
|
|
var v uint16
|
|
switch char {
|
|
case 0x82:
|
|
v = 0x201A
|
|
case 0x83:
|
|
v = 0x0192
|
|
case 0x84:
|
|
v = 0x201E
|
|
case 0x85:
|
|
v = 0x2026
|
|
case 0x86:
|
|
v = 0x2020
|
|
case 0x87:
|
|
v = 0x2021
|
|
case 0x88:
|
|
v = 0x02C6
|
|
case 0x89:
|
|
v = 0x2030
|
|
case 0x8A:
|
|
v = 0x0160
|
|
case 0x8B:
|
|
v = 0x2039
|
|
case 0x8C:
|
|
v = 0x0152
|
|
case 0x91:
|
|
v = 0x2018
|
|
case 0x92:
|
|
v = 0x2019
|
|
case 0x93:
|
|
v = 0x201C
|
|
case 0x94:
|
|
v = 0x201D
|
|
case 0x95:
|
|
v = 0x2022
|
|
case 0x96:
|
|
v = 0x2013
|
|
case 0x97:
|
|
v = 0x2014
|
|
case 0x98:
|
|
v = 0x02DC
|
|
case 0x99:
|
|
v = 0x2122
|
|
case 0x9A:
|
|
v = 0x0161
|
|
case 0x9B:
|
|
v = 0x203A
|
|
case 0x9C:
|
|
v = 0x0153
|
|
case 0x9F:
|
|
v = 0x0178
|
|
default:
|
|
//return []byte{char}
|
|
return []byte{char, 0x00}
|
|
}
|
|
out := make([]byte, 2)
|
|
binary.LittleEndian.PutUint16(out, v)
|
|
return out
|
|
}
|
|
|
|
func getWordDocAndTables(r *mscfb.Reader) (*mscfb.File, *mscfb.File, *mscfb.File) {
|
|
var wordDoc, table0, table1 *mscfb.File
|
|
for i := 0; i < len(r.File); i++ {
|
|
stream := r.File[i]
|
|
|
|
switch stream.Name {
|
|
case "WordDocument":
|
|
wordDoc = stream
|
|
case "0Table":
|
|
table0 = stream
|
|
case "1Table":
|
|
table1 = stream
|
|
}
|
|
}
|
|
return wordDoc, table0, table1
|
|
}
|
|
|
|
func getActiveTable(table0 *mscfb.File, table1 *mscfb.File, f *fib) *mscfb.File {
|
|
if f.base.fWhichTblStm == 0 {
|
|
return table0
|
|
}
|
|
return table1
|
|
}
|
|
|