diff --git a/internal/document/clean/clean.go b/internal/document/clean/clean.go index 9b46a301..01f70cf7 100644 --- a/internal/document/clean/clean.go +++ b/internal/document/clean/clean.go @@ -29,6 +29,8 @@ const ( InvalidDocumentLargeFile InvalidDocumentReason = "large_file" InvalidDocumentSmallDimensions InvalidDocumentReason = "small_dimensions" InvalidDocumentLargeDimensions InvalidDocumentReason = "large_dimensions" + InvalidDocumentSmallDPI InvalidDocumentReason = "small_dpi" + InvalidDocumentLargeDPI InvalidDocumentReason = "large_dpi" ) type ExecuteCleanResponse struct { diff --git a/internal/document/clean/pdf.go b/internal/document/clean/pdf.go index f5f733e4..cd970c0c 100644 --- a/internal/document/clean/pdf.go +++ b/internal/document/clean/pdf.go @@ -16,6 +16,7 @@ type PDF struct { maxBytes int64 maxDimension int minDimension int + minDPI int } const ( @@ -24,6 +25,8 @@ const ( TEXTRACT_PAGE_LIMIT = 3000 TEXTRACT_MAX_DIMENSION = 10000 TEXTRACT_MIN_DIMENSION = 50 + TEXTRACT_MIN_DPI = 72 + PDF_DEFAULT_DPI = 72 ) func NewPDF(buf io.ReadSeeker) *PDF { @@ -40,6 +43,7 @@ func NewPDF(buf io.ReadSeeker) *PDF { maxBytes: TEXTRACT_SYNC_LIMIT, maxDimension: TEXTRACT_MAX_DIMENSION, minDimension: TEXTRACT_MIN_DIMENSION, + minDPI: TEXTRACT_MIN_DPI, } } @@ -124,6 +128,23 @@ func (s *PDF) isValidDimensions(pdfCtx *model.Context) *InvalidDocumentReason { reason := InvalidDocumentSmallDimensions return &reason } + + dpiErr := s.isValidDPI(mediaBox, dims) + if dpiErr != nil { + return dpiErr + } + } + + return nil +} + +func (s *PDF) isValidDPI(mediaBox *types.Rectangle, dims types.Dim) *InvalidDocumentReason { + widthDPI := mediaBox.Width() / dims.ToInches().Width + heightDPI := mediaBox.Height() / dims.ToInches().Height + + if widthDPI < float64(s.minDPI) || heightDPI < float64(s.minDPI) { + reason := InvalidDocumentSmallDPI + return &reason } return nil diff --git a/internal/document/clean/pdf_test.go b/internal/document/clean/pdf_test.go index 83eaa241..5ec9e376 100644 --- a/internal/document/clean/pdf_test.go +++ b/internal/document/clean/pdf_test.go @@ -2,13 +2,138 @@ package documentclean import ( "context" + "math" "strings" "testing" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" "github.com/stretchr/testify/assert" ) +func TestIsValidDPI(t *testing.T) { + t.Run("no imgs", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfHelloWorld)) + + pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config) + assert.NoError(t, err) + err = pdfCtx.EnsurePageCount() + assert.NoError(t, err) + dims := types.Dim{ + Width: TEXTRACT_MIN_DIMENSION, + Height: TEXTRACT_MIN_DIMENSION, + } + boxes, err := pdfCtx.PageBoundaries(types.IntSet{ + 1: true, + }) + assert.NoError(t, err) + + reason := pdf.isValidDPI(boxes[0].MediaBox(), dims) + assert.Nil(t, reason) + }) + t.Run("with img", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfWithImg)) + + pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config) + assert.NoError(t, err) + err = pdfCtx.EnsurePageCount() + assert.NoError(t, err) + dims := types.Dim{ + Width: TEXTRACT_MIN_DIMENSION, + Height: TEXTRACT_MIN_DIMENSION, + } + boxes, err := pdfCtx.PageBoundaries(types.IntSet{ + 1: true, + }) + assert.NoError(t, err) + + reason := pdf.isValidDPI(boxes[0].MediaBox(), dims) + assert.Nil(t, reason) + }) + t.Run("with tiny dpi", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfWithImg)) + + pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config) + assert.NoError(t, err) + err = pdfCtx.EnsurePageCount() + assert.NoError(t, err) + dims := types.Dim{ + Width: math.MaxFloat64, + Height: math.MaxFloat64, + } + boxes, err := pdfCtx.PageBoundaries(types.IntSet{ + 1: true, + }) + assert.NoError(t, err) + + reason := pdf.isValidDPI(boxes[0].MediaBox(), dims) + assert.Equal(t, InvalidDocumentSmallDPI, *reason) + }) + t.Run("with tiny width dpi", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfWithImg)) + + pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config) + assert.NoError(t, err) + err = pdfCtx.EnsurePageCount() + assert.NoError(t, err) + dims := types.Dim{ + Width: math.MaxFloat64, + Height: 1, + } + boxes, err := pdfCtx.PageBoundaries(types.IntSet{ + 1: true, + }) + assert.NoError(t, err) + + reason := pdf.isValidDPI(boxes[0].MediaBox(), dims) + assert.Equal(t, InvalidDocumentSmallDPI, *reason) + }) + t.Run("with tiny height dpi", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfWithImg)) + + pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config) + assert.NoError(t, err) + err = pdfCtx.EnsurePageCount() + assert.NoError(t, err) + dims := types.Dim{ + Width: 1, + Height: math.MaxFloat64, + } + boxes, err := pdfCtx.PageBoundaries(types.IntSet{ + 1: true, + }) + assert.NoError(t, err) + + reason := pdf.isValidDPI(boxes[0].MediaBox(), dims) + assert.Equal(t, InvalidDocumentSmallDPI, *reason) + }) + t.Run("with huge dpi", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfWithImg)) + + pdfCtx, err := pdfcpu.ReadWithContext(ctx, pdf.pdf, pdf.config) + assert.NoError(t, err) + err = pdfCtx.EnsurePageCount() + assert.NoError(t, err) + dims := types.Dim{ + Width: 1, + Height: 1, + } + boxes, err := pdfCtx.PageBoundaries(types.IntSet{ + 1: true, + }) + assert.NoError(t, err) + + reason := pdf.isValidDPI(boxes[0].MediaBox(), dims) + assert.Nil(t, reason) + }) +} + func TestIsValidDimensions(t *testing.T) { t.Run("valid", func(t *testing.T) { ctx := context.Background() @@ -871,3 +996,61 @@ trailer startxref 612 %%EOF` + +const pdfWithImg = `%PDF-1.7 +1 0 obj +<> +endobj +2 0 obj +<> +endobj +3 0 obj +<>>>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>>/MediaBox[0 0 595 842]/Contents 4 0 R>> +endobj +4 0 obj +<> +stream +BT +/F1 12 Tf +50 750 Td +(This is the first line of a multiline string.) Tj +0 -15 Td +(This is the second line of text.) Tj +0 -15 Td +(Below this text will be an inline image.) Tj +ET + +q +50 650 Td +BI + /W 500 + /H 500 + /BPC 8 + /CS /RGB + /F [/DCTDecode] +ID +[BINARY JPEG IMAGE DATA WOULD BE HERE - 500x500 pixels] +EI +Q + +BT +/F1 12 Tf +50 120 Td +(This text appears after the image in the content stream.) Tj +0 -15 Td +(The entire content is a single multiline string in the PDF.) Tj +ET +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000009 00000 n +0000000056 00000 n +0000000107 00000 n +0000000302 00000 n +trailer +<> +startxref +887 +%%EOF`