unidoc / unipdf

Golang PDF library for creating and processing PDF files (pure go)
https://unidoc.io
Other
2.56k stars 251 forks source link

[BUG] NewImageDevice and NewPdfReaderFromFile #476

Closed polderudo closed 2 years ago

polderudo commented 2 years ago

Description

Using a function in the defer for closing the file returned by model.NewPdfReaderFromFile leads to an error in NewImageDevice when calling RenderToPath/Render

Might be a go related problem, but it took me some hours to find the error, thus i'm posting it here.

Here is some demo code to convert a PDF to an image (taken from the examples):

func main() {
    if len(os.Args) < 3 {
        fmt.Printf("Usage: %s OUTPUT_DIR INPUT.pdf...\n", os.Args[0])
        os.Exit(1)
    }
    outDir := os.Args[1]

    for _, filename := range os.Args[2:] {
        // Create reader.
        reader, f, err := model.NewPdfReaderFromFile(filename, nil)
        if err != nil {
            log.Fatalf("Could not create reader: %v\n", err)
        }
        defer checkError(f.Close())

        // Get total number of pages.
        numPages, err := reader.GetNumPages()
        if err != nil {
            log.Fatalf("Could not retrieve number of pages: %v\n", err)
        }

        // Render pages.
        basename := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))

        device := render.NewImageDevice()
        for i := 1; i <= numPages; i++ {
            // Get page.
            page, err := reader.GetPage(i)
            if err != nil {
                log.Fatalf("Could not retrieve page: %v\n", err)
            }

            // Render page to PNG file.
            // RenderToPath chooses the image format by looking at the extension
            // of the specified filename. Only PNG and JPEG files are supported
            // currently.
            device.OutputWidth = 1000

            outFilename := filepath.Join(outDir, fmt.Sprintf("%s_%d.jpg", basename, i))
            if err = device.RenderToPath(page, outFilename); err != nil {
                log.Fatalf("Image rendering error: %v\n", err)
            }

            // Alternatively, an image.Image instance can be obtained by using
            // the Render method of the image device, which can then be encoded
            // and saved in any format.
            // image, err := device.Render(page)
        }
    }
}

func checkError(err error){
    if err != nil {
        panic(err)      
    }   
}

RenderToPath will return an error: 'Image rendering error: invalid content stream object holder (*core.PdfObjectNull)'

Changeing defer checkError(f.Close()) to defer f.Close() solves the issue.

gunnsth commented 2 years ago

Hi @polderudo thanks. I was playing around with this at https://go.dev/play/p/aOS8IOvMHDE and it does seem like the inner function call is executed immediately and passed as input to the outermost function which is executed immediately. Otherwise need to wrap the entire thing inside a defer func() {...}

We will go through the examples and make sure that this is addressed.

gunnsth commented 2 years ago

@polderudo Where did you find this example? Cannot find anything using defer in this way in our examples repository. Can you specify the source so we can fix it?

arknable commented 2 years ago

Hi @polderudo. It is how defer works, f.Close() will be executed when checkError() evaluated, hence when RenderToPath() called, the file already closed. So the solution is to use deferred anonymous function like this: defer func() { checkError(f.Close()) }()

this way, checkError() will be evaluated later. More about defer can be checked here: https://go.dev/blog/defer-panic-and-recover.

polderudo commented 2 years ago

@arknable : Thanks. Working for years now with go but never read about this ... evaluation vs. execution ... Makes sense now. @gunnsth : This is not from your code. I just wanted to get rid of those yellow warnings goland gives you, because you don't handle the return (error) in the defer.