Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ func paramsFromDict(d *Dict) filter.Params {
if n, ok := d.Int("BitsPerComponent"); ok {
p.BitsPerComponent = int(n)
}
if n, ok := d.Int("EarlyChange"); ok {
p.EarlyChange = int(n)
if n, ok := d.Int("EarlyChange"); ok && n == 0 {
p.NoEarlyChange = true
}
return p
}
103 changes: 103 additions & 0 deletions filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package pdfdisassembler

import (
"bytes"
"compress/lzw"
"compress/zlib"
"encoding/ascii85"
"fmt"
"testing"
)

// buildStreamObjectPDF wraps stream as indirect object 3 with the given stream
// dict entries (e.g. "/Filter /LZWDecode ..."), reachable via a classical xref.
func buildStreamObjectPDF(t *testing.T, dictEntries string, stream []byte) []byte {
t.Helper()
var buf bytes.Buffer
off := func() int { return buf.Len() }
buf.WriteString("%PDF-1.7\n%\xe2\xe3\xcf\xd3\n")
offsets := make([]int, 4)

offsets[1] = off()
buf.WriteString("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")
offsets[2] = off()
buf.WriteString("2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n")
offsets[3] = off()
fmt.Fprintf(&buf, "3 0 obj\n<< %s /Length %d >>\nstream\n", dictEntries, len(stream))
buf.Write(stream)
buf.WriteString("\nendstream\nendobj\n")

xrefOff := off()
buf.WriteString("xref\n0 4\n")
fmt.Fprintf(&buf, "%010d %05d f \n", 0, 65535)
for i := 1; i <= 3; i++ {
fmt.Fprintf(&buf, "%010d %05d n \n", offsets[i], 0)
}
buf.WriteString("trailer\n<< /Size 4 /Root 1 0 R >>\n")
fmt.Fprintf(&buf, "startxref\n%d\n%%%%EOF\n", xrefOff)
return buf.Bytes()
}

func streamObject3(t *testing.T, data []byte) []byte {
t.Helper()
r, err := Open(bytes.NewReader(data))
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { r.Close() })
v, err := r.Resolve(Reference{Number: 3, Generation: 0})
if err != nil {
t.Fatalf("Resolve: %v", err)
}
stm, ok := v.(*Stream)
if !ok {
t.Fatalf("object 3 is %T, want *Stream", v)
}
got, err := stm.Content()
if err != nil {
t.Fatalf("Content: %v", err)
}
return got
}

// A stream declaring /DecodeParms << /EarlyChange 0 >> must decode with early
// change off. The stdlib LZW writer emits the non-early convention, so honouring
// the parameter reproduces the input; ignoring it garbles a stream past the
// first code-width boundary.
func TestLZWStreamEarlyChangeZero(t *testing.T) {
orig := make([]byte, 4096)
x := uint32(99)
for i := range orig {
x = x*1664525 + 1013904223
orig[i] = byte(x >> 24)
}
var enc bytes.Buffer
w := lzw.NewWriter(&enc, lzw.MSB, 8)
w.Write(orig)
w.Close()

got := streamObject3(t, buildStreamObjectPDF(t,
"/Filter /LZWDecode /DecodeParms << /EarlyChange 0 >>", enc.Bytes()))
if !bytes.Equal(got, orig) {
t.Fatal("LZW stream with /EarlyChange 0 decoded incorrectly")
}
}

// A /Filter array applies filters in order: the raw bytes are ASCII85 wrapping a
// FlateDecode stream, so the chain must un-ASCII85 then inflate.
func TestStreamFilterChainArray(t *testing.T) {
orig := []byte("chained filters: ASCII85 over Flate over the original bytes")
var fl bytes.Buffer
zw := zlib.NewWriter(&fl)
zw.Write(orig)
zw.Close()
a85 := make([]byte, ascii85.MaxEncodedLen(fl.Len()))
n := ascii85.Encode(a85, fl.Bytes())
stream := append(a85[:n:n], '~', '>')

got := streamObject3(t, buildStreamObjectPDF(t,
"/Filter [ /ASCII85Decode /FlateDecode ]", stream))
if !bytes.Equal(got, orig) {
t.Fatalf("chained decode = %q, want %q", got, orig)
}
}
5 changes: 3 additions & 2 deletions internal/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ type Params struct {
Columns int
Colors int
BitsPerComponent int
// LZWDecode early-change flag.
EarlyChange int
// NoEarlyChange honours the rare /EarlyChange 0; the zero value keeps
// LZWDecode's default early code-width change.
NoEarlyChange bool
// MaxOutput caps decoded bytes per filter; <= 0 means unlimited.
MaxOutput int64
}
Expand Down
122 changes: 122 additions & 0 deletions internal/filter/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,133 @@ package filter

import (
"bytes"
"compress/lzw"
"compress/zlib"
"encoding/ascii85"
"fmt"
"testing"
)

// lcg fills b with a deterministic pseudo-random byte stream — enough entropy
// to grow the LZW dictionary through every code width and trigger a reset.
func lcg(b []byte, seed uint32) {
x := seed
for i := range b {
x = x*1664525 + 1013904223
b[i] = byte(x >> 24)
}
}

// TestLZWRoundTripStdlib decodes streams produced by the standard library's
// MSB LZW writer. That writer uses the non-early code-width change, so decode
// with NoEarlyChange; the varied inputs exercise dictionary reuse, the KwKwK
// case, 9->12-bit width growth, and the dictionary-full reset.
func TestLZWRoundTripStdlib(t *testing.T) {
big := make([]byte, 64<<10)
lcg(big, 1)
for _, orig := range [][]byte{
[]byte("ABABABABABABABABABAB"),
[]byte("TOBEORNOTTOBEORTOBEORNOT"),
bytes.Repeat([]byte("xyz "), 4096),
big,
{},
{42},
} {
var buf bytes.Buffer
w := lzw.NewWriter(&buf, lzw.MSB, 8)
if _, err := w.Write(orig); err != nil {
t.Fatal(err)
}
w.Close()
got, err := Decode("LZWDecode", buf.Bytes(), Params{NoEarlyChange: true})
if err != nil {
t.Fatalf("decode (len %d): %v", len(orig), err)
}
if !bytes.Equal(got, orig) {
t.Fatalf("round-trip mismatch for len %d input", len(orig))
}
}
}

// TestLZWEarlyChangeHonored proves NoEarlyChange actually selects the decode
// convention: the stdlib stream (non-early) round-trips only with NoEarlyChange
// set; under the early-change default the same bytes must not reproduce the input.
func TestLZWEarlyChangeHonored(t *testing.T) {
orig := make([]byte, 4096) // long enough to cross the first width boundary
lcg(orig, 7)
var buf bytes.Buffer
w := lzw.NewWriter(&buf, lzw.MSB, 8)
w.Write(orig)
w.Close()
enc := buf.Bytes()

got, err := Decode("LZWDecode", enc, Params{NoEarlyChange: true})
if err != nil || !bytes.Equal(got, orig) {
t.Fatalf("NoEarlyChange decode of a non-early stream: err=%v equal=%v", err, bytes.Equal(got, orig))
}
if d, err := Decode("LZWDecode", enc, Params{}); err == nil && bytes.Equal(d, orig) {
t.Fatal("early-change default reproduced a non-early stream: the flag is ignored")
}
}

// A stream truncated mid-code must not panic: readBits pads the final partial
// word with zeros and decoding stops cleanly (partial output or error).
func TestLZWTruncatedNoPanic(t *testing.T) {
orig := make([]byte, 600) // long enough to reach 10-bit codes
lcg(orig, 3)
var buf bytes.Buffer
w := lzw.NewWriter(&buf, lzw.MSB, 8)
w.Write(orig)
w.Close()
enc := buf.Bytes()
for cut := 1; cut <= 4 && cut < len(enc); cut++ {
_, _ = Decode("LZWDecode", enc[:len(enc)-cut], Params{NoEarlyChange: true})
}
}

func TestASCII85RoundTripStdlib(t *testing.T) {
for _, orig := range [][]byte{
[]byte("Hello World!"),
{0, 0, 0, 0, 1, 2, 3}, // includes an all-zero group
{1}, // 1-byte partial group
{1, 2}, // 2-byte partial group
{1, 2, 3}, // 3-byte partial group
bytes.Repeat([]byte{255}, 17),
{},
} {
enc := make([]byte, ascii85.MaxEncodedLen(len(orig)))
n := ascii85.Encode(enc, orig)
in := append(enc[:n:n], '~', '>')
out, err := Decode("ASCII85Decode", in, Params{})
if err != nil {
t.Fatalf("decode %v: %v", orig, err)
}
if !bytes.Equal(out, orig) {
t.Fatalf("round-trip mismatch: in=%v out=%v", orig, out)
}
}
}

func TestASCII85EdgeCases(t *testing.T) {
// 'z' is shorthand for a full zero group; <~ is an optional opening marker;
// whitespace between digits is ignored.
out, err := Decode("ASCII85Decode", []byte("<~z 8 7 c U R D ] i , \" E b o 8 0 ~>"), Params{})
if err != nil {
t.Fatalf("decode: %v", err)
}
if want := append([]byte{0, 0, 0, 0}, "Hello World!"...); !bytes.Equal(out, want) {
t.Fatalf("got %q, want %q", out, want)
}

if _, err := Decode("ASCII85Decode", []byte("abc!\x01def~>"), Params{}); err == nil {
t.Fatal("expected an error on a byte outside the ASCII85 alphabet")
}
// 'z' may not appear mid-group (after a partial digit).
if _, err := Decode("ASCII85Decode", []byte("87z~>"), Params{}); err == nil {
t.Fatal("expected an error for 'z' inside a group")
}
}

func TestASCIIHex(t *testing.T) {
cases := map[string]string{
"48656C6C6F>": "Hello",
Expand Down
16 changes: 4 additions & 12 deletions internal/filter/lzw.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package filter

import (
"errors"
"fmt"
)
import "fmt"

// decodeLZW decodes a PDF LZW stream. Code widths grow from 9 to 12 bits;
// the early-change flag (default 1) shrinks the threshold at which each
// width step happens.
func decodeLZW(in []byte, p Params) ([]byte, error) {
early := p.EarlyChange
if early == 0 {
early = 1
early := 1
if p.NoEarlyChange {
early = 0
}
const (
clearCode = 256
Expand Down Expand Up @@ -78,9 +75,6 @@ func decodeLZW(in []byte, p Params) ([]byte, error) {
codeWidth++
}
}
if prev < 0 {
return out, nil
}
return out, nil
}

Expand Down Expand Up @@ -115,5 +109,3 @@ func (b *bitReader) readBits(n int) (uint32, bool) {
b.buf &= (uint64(1) << b.have) - 1
return v, true
}

var _ = errors.New