From c21fbfdce9c43fe77f59434224f3a5735ff08d5d Mon Sep 17 00:00:00 2001 From: xf0e Date: Wed, 12 Dec 2018 04:52:00 +0100 Subject: [PATCH] [FEATURE, CGL] added new "sandwich" engine --- cli-httpd/main.go | 3 +- cli-preprocessor/main.go | 1 + cli-worker/main.go | 6 +- convert-pdf.go | 1 - docs/testimage.pdf | Bin 0 -> 44817 bytes ocr_engine.go | 31 ++- ocr_engine_test.go | 2 +- ocr_http_handler.go | 2 +- ocr_http_handler_test.go | 2 +- ocr_http_multipart_handler.go | 2 +- ocr_rpc_client.go | 22 +- ocr_rpc_client_test.go | 2 +- ocr_rpc_worker.go | 24 +- ocr_util.go | 55 +++++ sandwich_engine.go | 403 ++++++++++++++++++++++++++++++++++ sandwich_engine_test.go | 100 +++++++++ tesseract_engine_test.go | 2 +- 17 files changed, 612 insertions(+), 46 deletions(-) create mode 100644 docs/testimage.pdf create mode 100644 sandwich_engine.go create mode 100644 sandwich_engine_test.go diff --git a/cli-httpd/main.go b/cli-httpd/main.go index 0d9122f..5fd3255 100644 --- a/cli-httpd/main.go +++ b/cli-httpd/main.go @@ -7,7 +7,7 @@ import ( "net/http" "github.com/couchbaselabs/logg" - "github.com/tleyden/open-ocr" + "github.com/xf0e/open-ocr" ) // This assumes that there is a worker running @@ -20,6 +20,7 @@ func init() { logg.LogKeys["OCR_WORKER"] = true logg.LogKeys["OCR_HTTP"] = true logg.LogKeys["OCR_TESSERACT"] = true + logg.LogKeys["OCR_SANDWICH"] = true } func main() { diff --git a/cli-preprocessor/main.go b/cli-preprocessor/main.go index be35d8b..d57bdb9 100644 --- a/cli-preprocessor/main.go +++ b/cli-preprocessor/main.go @@ -18,6 +18,7 @@ func init() { logg.LogKeys["PREPROCESSOR_WORKER"] = true logg.LogKeys["OCR_HTTP"] = true logg.LogKeys["OCR_TESSERACT"] = true + logg.LogKeys["OCR_SANDWICH"] = true } func main() { diff --git a/cli-worker/main.go b/cli-worker/main.go index 3664b4c..7f5b430 100644 --- a/cli-worker/main.go +++ b/cli-worker/main.go @@ -2,9 +2,8 @@ package main import ( "fmt" - "github.com/couchbaselabs/logg" - "github.com/tleyden/open-ocr" + "github.com/xf0e/open-ocr" ) // This assumes that there is a rabbit mq running @@ -16,6 +15,7 @@ func init() { logg.LogKeys["OCR_WORKER"] = true logg.LogKeys["OCR_HTTP"] = true logg.LogKeys["OCR_TESSERACT"] = true + logg.LogKeys["OCR_SANDWICH"] = true } func main() { @@ -23,7 +23,7 @@ func main() { noOpFlagFunc := ocrworker.NoOpFlagFunction() rabbitConfig := ocrworker.DefaultConfigFlagsOverride(noOpFlagFunc) - // inifinite loop, since sometimes worker <-> rabbitmq connection + // infinite loop, since sometimes worker <-> rabbitmq connection // gets broken. see https://github.com/tleyden/open-ocr/issues/4 for { logg.LogTo("OCR_WORKER", "Creating new OCR Worker") diff --git a/convert-pdf.go b/convert-pdf.go index b9d0ca7..3e6a465 100644 --- a/convert-pdf.go +++ b/convert-pdf.go @@ -63,7 +63,6 @@ func (c ConvertPdf) preprocess(ocrRequest *OcrRequest) error { if err != nil { logg.LogFatal("Error running command: %s. out: %s", err, out) } - logg.LogTo("PREPROCESSOR_WORKER", "output: %v", string(out)) // read bytes from output file resultBytes, err := ioutil.ReadFile(tmpFileNameOutput) diff --git a/docs/testimage.pdf b/docs/testimage.pdf new file mode 100644 index 0000000000000000000000000000000000000000..24ae1db94710921d98e75d39343842a3f0869146 GIT binary patch literal 44817 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

+esmN1LFc?N0U3MJ>vbSy$2WmE)wU~n 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 PgF)r8VMwIHq=fxnZme^R literal 0 HcmV?d00001 diff --git a/ocr_engine.go b/ocr_engine.go index b29c62a..2242534 100644 --- a/ocr_engine.go +++ b/ocr_engine.go @@ -10,9 +10,10 @@ import ( type OcrEngineType int const ( - ENGINE_TESSERACT = OcrEngineType(iota) - ENGINE_GO_TESSERACT - ENGINE_MOCK + EngineTesseract = OcrEngineType(iota) + EngineGoTesseract + EngineSandwichTesseract + EngineMock ) type OcrEngine interface { @@ -21,22 +22,26 @@ type OcrEngine interface { func NewOcrEngine(engineType OcrEngineType) OcrEngine { switch engineType { - case ENGINE_MOCK: + case EngineMock: return &MockEngine{} - case ENGINE_TESSERACT: + case EngineTesseract: return &TesseractEngine{} + case EngineSandwichTesseract: + return &SandwichEngine{} } return nil } func (e OcrEngineType) String() string { switch e { - case ENGINE_MOCK: + case EngineMock: return "ENGINE_MOCK" - case ENGINE_TESSERACT: + case EngineTesseract: return "ENGINE_TESSERACT" - case ENGINE_GO_TESSERACT: + case EngineGoTesseract: return "ENGINE_GO_TESSERACT" + case EngineSandwichTesseract: + return "ENGINE_SANDWICH_TESSERACT" } return "" @@ -50,14 +55,16 @@ func (e *OcrEngineType) UnmarshalJSON(b []byte) (err error) { engineString := strings.ToUpper(engineTypeStr) switch engineString { case "TESSERACT": - *e = ENGINE_TESSERACT + *e = EngineTesseract case "GO_TESSERACT": - *e = ENGINE_GO_TESSERACT + *e = EngineGoTesseract + case "SANDWICH": + *e = EngineSandwichTesseract case "MOCK": - *e = ENGINE_MOCK + *e = EngineMock default: logg.LogWarn("Unexpected OcrEngineType json: %v", engineString) - *e = ENGINE_MOCK + *e = EngineMock } return nil } diff --git a/ocr_engine_test.go b/ocr_engine_test.go index 6295715..f60d990 100644 --- a/ocr_engine_test.go +++ b/ocr_engine_test.go @@ -17,7 +17,7 @@ func TestOcrEngineTypeJson(t *testing.T) { logg.LogError(err) } assert.True(t, err == nil) - assert.Equals(t, ocrRequest.EngineType, ENGINE_TESSERACT) + assert.Equals(t, ocrRequest.EngineType, EngineTesseract) logg.LogTo("TEST", "ocrRequest: %v", ocrRequest) } diff --git a/ocr_http_handler.go b/ocr_http_handler.go index 27f501d..9033d88 100644 --- a/ocr_http_handler.go +++ b/ocr_http_handler.go @@ -42,7 +42,7 @@ func (s *OcrHttpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - logg.LogTo("OCR_HTTP", "ocrResult: %v", ocrResult) + // logg.LogTo("OCR_HTTP", "ocrResult: %v", ocrResult) fmt.Fprintf(w, ocrResult.Text) diff --git a/ocr_http_handler_test.go b/ocr_http_handler_test.go index db7bee9..84aaaba 100644 --- a/ocr_http_handler_test.go +++ b/ocr_http_handler_test.go @@ -34,7 +34,7 @@ func DisabledTestOcrHttpHandlerIntegration(t *testing.T) { ocrRequest := OcrRequest{ ImgUrl: "http://localhost:8081/img", - EngineType: ENGINE_MOCK, + EngineType: EngineMock, } jsonBytes, err := json.Marshal(ocrRequest) if err != nil { diff --git a/ocr_http_multipart_handler.go b/ocr_http_multipart_handler.go index a390245..462c7f6 100644 --- a/ocr_http_multipart_handler.go +++ b/ocr_http_multipart_handler.go @@ -111,7 +111,7 @@ func (s *OcrHttpMultipartHandler) ServeHTTP(w http.ResponseWriter, req *http.Req return } - logg.LogTo("OCR_HTTP", "ocrResult: %v", ocrResult) + // logg.LogTo("OCR_HTTP", "ocrResult: %v", ocrResult) fmt.Fprintf(w, ocrResult.Text) diff --git a/ocr_rpc_client.go b/ocr_rpc_client.go index 40bf035..0336825 100644 --- a/ocr_rpc_client.go +++ b/ocr_rpc_client.go @@ -3,10 +3,10 @@ package ocrworker import ( "encoding/json" "fmt" - "time" - "github.com/nu7hatch/gouuid" "github.com/couchbaselabs/logg" + "github.com/nu7hatch/gouuid" "github.com/streadway/amqp" + "time" ) const ( @@ -54,11 +54,11 @@ func (c *OcrRpcClient) DecodeImage(ocrRequest OcrRequest) (OcrResult, error) { if err := c.channel.ExchangeDeclare( c.rabbitConfig.Exchange, // name c.rabbitConfig.ExchangeType, // type - true, // durable - false, // auto-deleted - false, // internal - false, // noWait - nil, // arguments + true, // durable + false, // auto-deleted + false, // internal + false, // noWait + nil, // arguments ); err != nil { return OcrResult{}, err } @@ -167,8 +167,8 @@ func (c OcrRpcClient) subscribeCallbackQueue(correlationUuid string, rpcResponse callbackQueue.Name, // name of the queue callbackQueue.Name, // bindingKey c.rabbitConfig.Exchange, // sourceExchange - false, // noWait - nil, // arguments + false, // noWait + nil, // arguments ); err != nil { return amqp.Queue{}, err } @@ -200,10 +200,10 @@ func (c OcrRpcClient) handleRpcResponse(deliveries <-chan amqp.Delivery, correla if d.CorrelationId == correlationUuid { logg.LogTo( "OCR_CLIENT", - "got %dB delivery: [%v] %q. Reply to: %v", + "got %dB delivery(first 64 Bytes): [%v] %q. Reply to: %v", len(d.Body), d.DeliveryTag, - d.Body, + d.Body[0:64], d.ReplyTo, ) diff --git a/ocr_rpc_client_test.go b/ocr_rpc_client_test.go index d9165ce..9daa145 100644 --- a/ocr_rpc_client_test.go +++ b/ocr_rpc_client_test.go @@ -47,7 +47,7 @@ func DisabledTestOcrRpcClientIntegration(t *testing.T) { for i := 0; i < 50; i++ { - ocrRequest := OcrRequest{ImgUrl: testImageUrl, EngineType: ENGINE_MOCK} + ocrRequest := OcrRequest{ImgUrl: testImageUrl, EngineType: EngineMock} decodeResult, err := ocrClient.DecodeImage(ocrRequest) if err != nil { logg.LogTo("TEST", "err: %v", err) diff --git a/ocr_rpc_worker.go b/ocr_rpc_worker.go index a93e8f4..67135bf 100644 --- a/ocr_rpc_worker.go +++ b/ocr_rpc_worker.go @@ -56,11 +56,11 @@ func (w OcrRpcWorker) Run() error { if err = w.channel.ExchangeDeclare( w.rabbitConfig.Exchange, // name of the exchange w.rabbitConfig.ExchangeType, // type - true, // durable - false, // delete when complete - false, // internal - false, // noWait - nil, // arguments + true, // durable + false, // delete when complete + false, // internal + false, // noWait + nil, // arguments ); err != nil { return err } @@ -72,7 +72,7 @@ func (w OcrRpcWorker) Run() error { queue, err := w.channel.QueueDeclare( queueName, // name of the queue true, // durable - false, // delete when usused + false, // delete when unused false, // exclusive false, // noWait nil, // arguments @@ -87,8 +87,8 @@ func (w OcrRpcWorker) Run() error { queue.Name, // name of the queue w.rabbitConfig.RoutingKey, // bindingKey w.rabbitConfig.Exchange, // sourceExchange - false, // noWait - nil, // arguments + false, // noWait + nil, // arguments ); err != nil { return err } @@ -115,7 +115,7 @@ func (w OcrRpcWorker) Run() error { func (w *OcrRpcWorker) Shutdown() error { // will close() the deliveries channel if err := w.channel.Cancel(w.tag, true); err != nil { - return fmt.Errorf("Worker cancel failed: %s", err) + return fmt.Errorf("worker cancel failed: %s", err) } if err := w.conn.Close(); err != nil { @@ -145,7 +145,7 @@ func (w *OcrRpcWorker) handle(deliveries <-chan amqp.Delivery, done chan error) logg.LogError(fmt.Errorf(msg, err)) } - logg.LogTo("OCR_WORKER", "Sending rpc response: %v", ocrResult) + // logg.LogTo("OCR_WORKER", "Sending rpc response: %v", ocrResult) err = w.sendRpcResponse(ocrResult, d.ReplyTo, d.CorrelationId) if err != nil { msg := "Error returning ocr result: %v. Error: %v" @@ -166,7 +166,7 @@ func (w *OcrRpcWorker) resultForDelivery(d amqp.Delivery) (OcrResult, error) { ocrResult := OcrResult{Text: "Error"} err := json.Unmarshal(d.Body, &ocrRequest) if err != nil { - msg := "Error unmarshaling json: %v. Error: %v" + msg := "Error unmarshalling json: %v. Error: %v" errMsg := fmt.Sprintf(msg, string(d.Body), err) logg.LogError(fmt.Errorf(errMsg)) ocrResult.Text = errMsg @@ -237,7 +237,7 @@ func confirmDeliveryWorker(ack, nack chan uint64) { case tag := <-nack: logg.LogTo("OCR_WORKER", "failed to confirm delivery: %v", tag) case <-time.After(RPC_RESPONSE_TIMEOUT): - // this is bad, the worker will probably be dsyfunctional + // this is bad, the worker will probably be dysfunctional // at this point, so panic logg.LogPanic("timeout trying to confirm delivery") } diff --git a/ocr_util.go b/ocr_util.go index 5e9673f..c4cb7a2 100644 --- a/ocr_util.go +++ b/ocr_util.go @@ -1,10 +1,14 @@ package ocrworker import ( + "fmt" + "github.com/couchbaselabs/logg" "io/ioutil" "net/http" "os" + "os/exec" "path/filepath" + "strings" "github.com/nu7hatch/gouuid" ) @@ -59,3 +63,54 @@ func createTempFileName() (string, error) { uuidStr := uuidRaw.String() return filepath.Join(tempDir, uuidStr), nil } + +func readFirstBytes(filePath string, nBytesToRead uint) ([]byte, error) { + + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + buffer := make([]byte, nBytesToRead) + _, err = file.Read(buffer) + if err != nil { + return nil, err + } + return buffer, nil +} + +// detect uploaded file type +func detectFileType(buffer []byte) string { + logg.LogTo("OCR_SANDWICH", "OK, this is buffer: %v", buffer) + fileType := "" + if len(buffer) > 3 && + buffer[0] == 0x25 && buffer[1] == 0x50 && + buffer[2] == 0x44 && buffer[3] == 0x46 { + fileType = strings.ToUpper("PDF") + } else if len(buffer) > 3 && + ((buffer[0] == 0x49 && buffer[1] == 0x49 && buffer[2] == 0x2A && buffer[3] == 0x0) || + (buffer[0] == 0x4D && buffer[1] == 0x4D && buffer[2] == 0x0 && buffer[3] == 0x2A)) { + fileType = strings.ToUpper("TIFF") + } else { + fileType = strings.ToUpper("UNKNOWN") + } + return fileType +} + +// if sandwich engine gets a TIFF image instead of PDF file +// we need to convert the input file to pdf first since pdfsandwich can't handle images +func convertImageToPdf(inputFilename string) string { + logg.LogTo("OCR_SANDWICH", "got image file instead of pdf, trying to convert it...") + + tmpFileImgToPdf := fmt.Sprintf("%s%s", inputFilename, ".pdf") + cmd := exec.Command("convert", inputFilename, tmpFileImgToPdf) + output, err := cmd.CombinedOutput() + if err != nil { + logg.LogWarn("OCR_SANDWICH", "error exec convert for transforming TIFF to PDF: %v %v", err, string(output)) + return "" + } + + return tmpFileImgToPdf + +} diff --git a/sandwich_engine.go b/sandwich_engine.go new file mode 100644 index 0000000..d5570ce --- /dev/null +++ b/sandwich_engine.go @@ -0,0 +1,403 @@ +package ocrworker + +import ( + "encoding/base64" + "fmt" + "github.com/couchbaselabs/logg" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// This variant of the SandwichEngine calls pdfsandwich via exec +// This implementation returns either the pdf with ocr layer only +// or merged variant of pdf plus ocr layer with the ability to +// optimize the output pdf file by calling "gs" tool +type SandwichEngine struct { +} + +type SandwichEngineArgs struct { + configVars map[string]string `json:"config_vars"` + lang string `json:"lang"` + ocrType string `json:"ocr_type"` + ocrOptimize bool `json:"result_optimize"` +} + +func NewSandwichEngineArgs(ocrRequest OcrRequest) (*SandwichEngineArgs, error) { + + engineArgs := &SandwichEngineArgs{} + + if ocrRequest.EngineArgs == nil { + return engineArgs, nil + } + // config vars + configVarsMapInterfaceOrig := ocrRequest.EngineArgs["config_vars"] + + if configVarsMapInterfaceOrig != nil { + + logg.LogTo("OCR_SANDWICH", "got configVarsMap: %v type: %T", configVarsMapInterfaceOrig, configVarsMapInterfaceOrig) + + configVarsMapInterface := configVarsMapInterfaceOrig.(map[string]interface{}) + + configVarsMap := make(map[string]string) + for k, v := range configVarsMapInterface { + v, ok := v.(string) + if !ok { + return nil, fmt.Errorf("could not convert configVar into string: %v", v) + } + configVarsMap[k] = v + } + + engineArgs.configVars = configVarsMap + + } + + // language + lang := ocrRequest.EngineArgs["lang"] + if lang != nil { + langStr, ok := lang.(string) + if !ok { + return nil, fmt.Errorf("could not convert lang into string: %v", lang) + } + engineArgs.lang = langStr + } + + // select from pdf, layer 1:pdf + layer 2:ocr_pdf + ocrType := ocrRequest.EngineArgs["ocr_type"] + if ocrType != nil { + ocrTypeSrt, ok := ocrType.(string) + if !(ok) { + return nil, fmt.Errorf("could not convert into string: %v", ocrType) + } + engineArgs.ocrType = ocrTypeSrt + } + + // set optimize flag + ocrOptimize := ocrRequest.EngineArgs["result_optimize"] + if ocrOptimize != nil { + ocrOptimizeFlag, ok := ocrOptimize.(bool) + if !(ok) { + return nil, fmt.Errorf("could not convert into boolean: %v", ocrOptimize) + } + engineArgs.ocrOptimize = ocrOptimizeFlag + } + + return engineArgs, nil + +} + +// return a slice that can be passed to tesseract binary as command line +// args, eg, ["-c", "tessedit_char_whitelist=0123456789", "-c", "foo=bar"] +func (t SandwichEngineArgs) Export() []string { + var result []string + if t.lang != "" { + result = append(result, "-lang") + result = append(result, t.lang) + } + // pdfsandwich wants the quotes before -c an after the last key e.g. -tesso '"-c arg1=key1"' + result = append(result, "-tesso", "-c textonly_pdf=1") + if t.configVars != nil { + for k, v := range t.configVars { + keyValArg := fmt.Sprintf("%s=%s", k, v) + result = append(result, keyValArg) + } + } + + return result +} + +func (t SandwichEngine) ProcessRequest(ocrRequest OcrRequest) (OcrResult, error) { + + tmpFileName, err := func() (string, error) { + if ocrRequest.ImgBase64 != "" { + return t.tmpFileFromImageBase64(ocrRequest.ImgBase64) + } else if ocrRequest.ImgUrl != "" { + return t.tmpFileFromImageUrl(ocrRequest.ImgUrl) + } else { + return t.tmpFileFromImageBytes(ocrRequest.ImgBytes) + } + + }() + + if err != nil { + logg.LogTo("OCR_SANDWICH", "error getting tmpFileName") + return OcrResult{}, err + } + + defer func() { + logg.LogTo("OCR_SANDWICH", "step 0: deleting input file after convert it to pdf: %s", + tmpFileName) + if err := os.Remove(tmpFileName); err != nil { + logg.LogWarn("OCR_SANDWICH", err) + } + }() + + // detect if file type is supported + buffer, err := readFirstBytes(tmpFileName, 64) + if err != nil { + logg.LogWarn("OCR_SANDWICH", "safety check can not be completed", err) + logg.LogWarn("OCR_SANDWICH", "processing of %s will be aborted", tmpFileName) + return OcrResult{"WARNING: the provided file format is not supported"}, err + } + uplFileType := detectFileType(buffer[:]) + if uplFileType == "UNKNOWN" { + logg.LogWarn("OCR_SANDWICH", "file type is: %s. only support TIFF and PDF input files", uplFileType) + err := fmt.Errorf("file format not understood") + return OcrResult{"WARNING: only support TIFF and PDF input files"}, err + } + logg.LogTo("OCR_SANDWICH", "file type is: %s", uplFileType) + + engineArgs, err := NewSandwichEngineArgs(ocrRequest) + if err != nil { + logg.LogTo("OCR_SANDWICH", "error getting engineArgs") + return OcrResult{}, err + } + + ocrResult, err := t.processImageFile(tmpFileName, uplFileType, *engineArgs) + + return ocrResult, err +} + +func (t SandwichEngine) tmpFileFromImageBytes(imgBytes []byte) (string, error) { + + logg.LogTo("OCR_SANDWICH", "Use pdfsandwich with bytes image") + + tmpFileName, err := createTempFileName() + if err != nil { + return "", err + } + + // we have to write the contents of the image url to a temp + // file, because the leptonica lib can't seem to handle byte arrays + err = saveBytesToFileName(imgBytes, tmpFileName) + if err != nil { + return "", err + } + + return tmpFileName, nil + +} + +func (t SandwichEngine) tmpFileFromImageBase64(base64Image string) (string, error) { + + logg.LogTo("OCR_SANDWICH", "Use pdfsandwich with base 64") + + tmpFileName, err := createTempFileName() + if err != nil { + return "", err + } + + // decoding into bytes the base64 string + decoded, decodeError := base64.StdEncoding.DecodeString(base64Image) + + if decodeError != nil { + return "", err + } + + err = saveBytesToFileName(decoded, tmpFileName) + if err != nil { + return "", err + } + + return tmpFileName, nil + +} + +func (t SandwichEngine) tmpFileFromImageUrl(imgUrl string) (string, error) { + + logg.LogTo("OCR_SANDWICH", "Use pdfsandwich with url") + + tmpFileName, err := createTempFileName() + if err != nil { + return "", err + } + // we have to write the contents of the image url to a temp + // file, because the leptonica lib can't seem to handle byte arrays + err = saveUrlContentToFileName(imgUrl, tmpFileName) + if err != nil { + return "", err + } + + return tmpFileName, nil + +} + +func (t SandwichEngine) buildCmdLineArgs(inputFilename string, engineArgs SandwichEngineArgs) ([]string, string) { + + // sets output file name for pdfsandwich output file + // and builds the argument list for external program + // since pdfsandwich can only return pdf files the will deliver work with pdf intermediates + // for later use we may expand the implementation + // pdfsandwich by default default expands the name of output file wich _ocr + cflags := engineArgs.Export() + tmpFileExtension := "_ocr.pdf" + ocrLayerFile := inputFilename + cmdArgs := []string{} + + ocrLayerFile = fmt.Sprintf("%s%s", ocrLayerFile, tmpFileExtension) + cmdArgs = append(cmdArgs, cflags...) + cmdArgs = append(cmdArgs, inputFilename, "-o", ocrLayerFile) + logg.LogTo("OCR_SANDWICH", "cmdArgs for pdfsandwich: %v", cmdArgs) + + return cmdArgs, ocrLayerFile + +} + +func (t SandwichEngine) processImageFile(inputFilename string, uplFileType string, engineArgs SandwichEngineArgs) (OcrResult, error) { + + fileToDeliver := "temp.file" + cmdArgs := []string{} + ocrLayerFile := "" + + logg.LogTo("OCR_SANDWICH", "input file name: %v", inputFilename) + + if uplFileType == "TIFF" { + inputFilename = convertImageToPdf(inputFilename) + if inputFilename == "" { + err := fmt.Errorf("can not convert input image to intermediate pdf") + logg.LogTo("OCR_SANDWICH", "Error exec pdfsandwich: %v %v", err) + return OcrResult{}, err + } + } + + ocrType := strings.ToUpper(engineArgs.ocrType) + switch ocrType { + case "COMBINEDPDF": + cmdArgs, ocrLayerFile = t.buildCmdLineArgs(inputFilename, engineArgs) + cmd := exec.Command("pdfsandwich", cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + logg.LogTo("OCR_SANDWICH", "Error exec pdfsandwich: %v %v", err, string(output)) + return OcrResult{}, err + } + tmpOutCombinedPdf, err := createTempFileName() + tmpOutCombinedPdf = fmt.Sprintf("%s%s", tmpOutCombinedPdf, "_comb.pdf") + if err != nil { + return OcrResult{}, err + } + defer func() { + logg.LogWarn("OCR_SANDWICH", "step 2: deleting file (pdftk run): %s", + tmpOutCombinedPdf) + if err := os.Remove(tmpOutCombinedPdf); err != nil { + logg.LogWarn("OCR_SANDWICH", err) + } + }() + + var combinedArgs []string + // pdftk FILE_only_TEXT-LAYER.pdf multistamp FILE_ORIGINAL_IMAGE.pdf output FILE_OUTPUT_IMAGE_AND_TEXT_LAYER.pdf + combinedArgs = append(combinedArgs, ocrLayerFile, "multistamp", inputFilename, "output", tmpOutCombinedPdf) + logg.LogTo("OCR_SANDWICH", "combinedArgs: %v", combinedArgs) + outPdftk, errPdftk := exec.Command("pdftk", combinedArgs...).CombinedOutput() + if errPdftk != nil { + logg.LogWarn("Error running command: %s. out: %s", errPdftk, outPdftk) + return OcrResult{}, err + } + logg.LogTo("OCR_TESSERACT", "output: %v", string(outPdftk)) + + if engineArgs.ocrOptimize { + logg.LogTo("OCR_SANDWICH", "%s", "optimizing was requested. perform selected operation") + var compressedArgs []string + tmpOutCompressedPdf, err := createTempFileName() + if err != nil { + return OcrResult{}, err + } + tmpOutCompressedPdf = fmt.Sprintf("%s%s", tmpOutCompressedPdf, "_compr.pdf") + defer func() { + logg.LogWarn("OCR_SANDWICH", "step 3: deleting compressed result file (gs run): %s", + tmpOutCompressedPdf) + if err := os.Remove(tmpOutCompressedPdf); err != nil { + logg.LogWarn("OCR_SANDWICH", err) + } + }() + + compressedArgs = append( + compressedArgs, + "-sDEVICE=pdfwrite", + "-dCompatibilityLevel=1.5", + "-dPDFSETTINGS=/screen", + "-dNOPAUSE", + "-dBATCH", + "-dQUIET", + "-sOutputFile="+tmpOutCompressedPdf, + tmpOutCombinedPdf, + ) + logg.LogTo("OCR_SANDWICH", "tmpOutCompressedPdf: %s", tmpOutCompressedPdf) + logg.LogTo("OCR_SANDWICH", "tmpOutCombinedPdf: %s", tmpOutCombinedPdf) + logg.LogTo("OCR_SANDWICH", "combinedArgs: %v", compressedArgs) + outQpdf, errQpdf := exec.Command("gs", compressedArgs...).CombinedOutput() + if errQpdf != nil { + logg.LogWarn("Error running command: %s. out: %s", errQpdf, outQpdf) + return OcrResult{}, err + } + + fileToDeliver = tmpOutCompressedPdf + } else { + fileToDeliver = tmpOutCombinedPdf + } + case "OCRLAYERONLY": + cmdArgs, ocrLayerFile = t.buildCmdLineArgs(inputFilename, engineArgs) + cmd := exec.Command("pdfsandwich", cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + logg.LogTo("OCR_SANDWICH", "error exec pdfsandwich: %v %v", err, string(output)) + return OcrResult{}, err + } + + fileToDeliver = ocrLayerFile + case "TXT": + cmdArgs, ocrLayerFile = t.buildCmdLineArgs(inputFilename, engineArgs) + cmd := exec.Command("pdfsandwich", cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + logg.LogTo("OCR_SANDWICH", "error exec pdfsandwich: %v %v", err, string(output)) + return OcrResult{}, err + } + logg.LogTo("OCR_SANDWICH", "extracting text from ocr") + textFile := fmt.Sprintf("%s%s", strings.TrimSuffix(ocrLayerFile, filepath.Ext(ocrLayerFile)), ".txt") + cmdArgsPdfToText := exec.Command("pdftotext", ocrLayerFile) + outputPdfToText, err := cmdArgsPdfToText.CombinedOutput() + if err != nil { + logg.LogTo("OCR_SANDWICH", "error exec pdftotext: %v %v", err, string(outputPdfToText)) + } + // pdftotext will create %filename%.txt + defer func() { + logg.LogWarn("OCR_SANDWICH", "step 2: deleting file (pdftotext run): %s", + textFile) + if err := os.Remove(textFile); err != nil { + logg.LogWarn("OCR_SANDWICH", err) + } + }() + + fileToDeliver = textFile + + default: + err := fmt.Errorf("requested format is not supported") + logg.LogTo("OCR_SANDWICH", "error: %v", err) + return OcrResult{}, err + } + + defer func() { + logg.LogTo("OCR_SANDWICH", "step 1: deleting file (pdfsandwich run): %s", + ocrLayerFile) + if err := os.Remove(ocrLayerFile); err != nil { + logg.LogWarn("OCR_SANDWICH", err) + } + logg.LogTo("OCR_SANDWICH", "step 1: deleting file (pdfsandwich run): %s", + inputFilename) + if err := os.Remove(inputFilename); err != nil { + logg.LogWarn("OCR_SANDWICH", err) + } + }() + + logg.LogTo("OCR_SANDWICH", "outfile %s ", fileToDeliver) + outBytes, err := ioutil.ReadFile(fileToDeliver) + if err != nil { + logg.LogTo("OCR_SANDWICH", "Error getting data from result file: %v", err) + return OcrResult{}, err + } + return OcrResult{ + Text: string(base64.StdEncoding.EncodeToString(outBytes)), + }, nil +} diff --git a/sandwich_engine_test.go b/sandwich_engine_test.go new file mode 100644 index 0000000..a619e4d --- /dev/null +++ b/sandwich_engine_test.go @@ -0,0 +1,100 @@ +package ocrworker + +import ( + "encoding/json" + "testing" + + "io/ioutil" + + "github.com/couchbaselabs/go.assert" + "github.com/couchbaselabs/logg" +) + +func TestSandwichEngineWithRequest(t *testing.T) { + + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + engine := SandwichEngine{} + bytes, err := ioutil.ReadFile("docs/ocrimage.pdf") + assert.True(t, err == nil) + + cFlags := make(map[string]interface{}) + cFlags["tessedit_char_whitelist"] = "0123456789" + + ocrRequest := OcrRequest{ + ImgBytes: bytes, + EngineType: EngineSandwichTesseract, + EngineArgs: cFlags, + } + + assert.True(t, err == nil) + result, err := engine.ProcessRequest(ocrRequest) + assert.True(t, err == nil) + logg.LogTo("TEST", "result: %v", result) + +} + +func TestSandwichEngineWithJson(t *testing.T) { + + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + testJsons := []string{} + /*testJsons = append(testJsons, `{"engine":"sandwich"}`) + testJsons = append(testJsons, `{"engine":"sandwich", "engine_args":{}}`) + testJsons = append(testJsons, `{"engine":"sandwich", "engine_args":null}`) + testJsons = append(testJsons, `{"engine":"sandwich", "engine_args":{"config_vars":{"tessedit_char_whitelist":"0123456789"}, "psm":"1"}}`) + testJsons = append(testJsons, `{"engine":"sandwich", "engine_args":{"config_vars":{"tessedit_create_hocr":"1", "tessedit_pageseg_mode":"1"}, "psm":"3"}}`)*/ + testJsons = append(testJsons, `{"engine":"sandwich", "engine_args":{"lang":"deu", "ocr_type":"ocrlayeronly","result_optimize":true}}`) + testJsons = append(testJsons, `{"engine":"sandwich", "engine_args":{"lang":"deu", "ocr_type":"combinedpdf","result_optimize":true}}`) + testJsons = append(testJsons, `{"engine":"sandwich", "engine_args":{"lang":"deu", "ocr_type":"combinedpdf","result_optimize":false}}`) + + for _, testJson := range testJsons { + logg.LogTo("TEST", "testJson: %v", testJson) + ocrRequest := OcrRequest{} + err := json.Unmarshal([]byte(testJson), &ocrRequest) + assert.True(t, err == nil) + bytes, err := ioutil.ReadFile("docs/testimage.pdf") + assert.True(t, err == nil) + ocrRequest.ImgBytes = bytes + engine := NewOcrEngine(ocrRequest.EngineType) + result, err := engine.ProcessRequest(ocrRequest) + logg.LogTo("TEST", "err: %v", err) + assert.True(t, err == nil) + logg.LogTo("TEST", "result: %v", result) + + } + +} + +func TestNewsandwichEngineArgs(t *testing.T) { + testJson := `{"engine":"sandwich", "engine_args":{"config_vars":{"tessedit_char_whitelist":"0123456789"},"ocr_type":"combinedpdf", "psm":"0", "lang":"jpn"}}` + ocrRequest := OcrRequest{} + err := json.Unmarshal([]byte(testJson), &ocrRequest) + assert.True(t, err == nil) + engineArgs, err := NewSandwichEngineArgs(ocrRequest) + assert.True(t, err == nil) + assert.Equals(t, len(engineArgs.configVars), 1) + assert.Equals(t, engineArgs.configVars["tessedit_char_whitelist"], "0123456789") + // assert.Equals(t, engineArgs.pageSegMode, "0") + assert.Equals(t, engineArgs.lang, "jpn") + +} + +func TestSandwichEngineWithFile(t *testing.T) { + + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + engine := SandwichEngine{} + engineArgs := SandwichEngineArgs{} + result, err := engine.processImageFile("docs/ocrimage.pdf", "PDF", engineArgs) + logg.LogWarn("error %v", err) + assert.True(t, err == nil) + logg.LogTo("TEST", "result: %v", result) + +} diff --git a/tesseract_engine_test.go b/tesseract_engine_test.go index d652836..d06326c 100644 --- a/tesseract_engine_test.go +++ b/tesseract_engine_test.go @@ -25,7 +25,7 @@ func TestTesseractEngineWithRequest(t *testing.T) { ocrRequest := OcrRequest{ ImgBytes: bytes, - EngineType: ENGINE_TESSERACT, + EngineType: EngineTesseract, EngineArgs: cFlags, }