From 4ae565d6a6bc541191e60768412b183ce51ba835 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Fri, 8 Nov 2019 15:10:59 +0300 Subject: [PATCH 01/33] Feature/navbar (#5) * :sparkles: Added navbar while file is loading Signed-off-by: Emil * :pencil: Returned scanned.txt Signed-off-by: Emil --- .../doctype/ocr_language/ocr_language.js | 5 +-- .../erpnext_ocr/doctype/ocr_read/ocr_read.js | 40 +++++++++++-------- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 17 ++++---- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js index 53ab7f54..20da529c 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js @@ -2,7 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('OCR Language', { - refresh: function(frm) { - - } + refresh: function (frm) { + } }); diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js index 3133bc96..511f2269 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js @@ -2,21 +2,27 @@ // For license information, please see license.txt frappe.ui.form.on('OCR Read', { - refresh: function(frm) { + refresh: function (frm) { - }, - read_image:function (frm) { - frappe.call({ - method: "read_image", - doc: cur_frm.doc, - callback: function (r, rt) { - - if (r.message) - { - cur_frm.set_value("read_result",r.message); - } - - } - }) - } -}); + }, + read_image: function (frm) { + frappe.hide_msgprint(true); + // frappe.realtime.on("ocr_progress_bar", function (data) { + // frappe.hide_msgprint(true); + // frappe.show_progress(__("Reading the file"), data.progress[0], data.progress[1]); + // console.log(data.progress[0]) + // }); + frappe.show_progress(__("Reading the file"), 50, 100); + frappe.call({ + method: "read_image", + doc: cur_frm.doc, + callback: function (r, rt) { + if (r.message) { + console.log(r.message); + cur_frm.set_value("read_result", r.message); + cur_dialog.hide() + } + } + }); + } +}); \ No newline at end of file diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 111d65cd..e1fbcbae 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -8,15 +8,17 @@ import os import io -#Alternative to "File Upload Disconnected. Please try again." -#erpnext_ocr.erpnext_ocr.doctype.ocr_read.ocr_read.force_attach_file +# Alternative to "File Upload Disconnected. Please try again." + +# erpnext_ocr.erpnext_ocr.doctype.ocr_read.ocr_read.force_attach_file def force_attach_file(): filename = "Picture_010.tif" name = "a2cbc0186c" - force_attach_file_doc(filename,name) + force_attach_file_doc(filename, name) + -def force_attach_file_doc(filename,name): +def force_attach_file_doc(filename, name): file_url = "/private/files/" + filename attachment_doc = frappe.get_doc({ @@ -63,11 +65,10 @@ def read_image(self): if path.endswith('.pdf'): from wand.image import Image as wi - pdf = wi(filename = fullpath, resolution = 300) + pdf = wi(filename=fullpath, resolution=300) pdfImage = pdf.convert('jpeg') - for img in pdfImage.sequence: - imgPage = wi(image = img) + imgPage = wi(image=img) imageBlob = imgPage.make_blob('jpeg') recognized_text = " " @@ -75,6 +76,7 @@ def read_image(self): im = Image.open(io.BytesIO(imageBlob)) recognized_text = pytesseract.image_to_string(im, lang) text = text + recognized_text + else: im = Image.open(fullpath) @@ -84,5 +86,4 @@ def read_image(self): self.read_result = text self.save() - return text From b4ed0b684e3d896b7fc237c98a3087b2836f58a4 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Fri, 8 Nov 2019 18:27:26 +0100 Subject: [PATCH 02/33] :memo: Fix test command Signed-off-by: mathieu.brunot --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9a79be9..6cb29311 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Check tesseract Wiki for details: https://github.com/tesseract-ocr/tesseract/wik ## :white_check_mark: Run tests ```sh -bench bench run-tests --profile --app erpnext_autoinstall +bench bench run-tests --profile --app erpnext_ocr ``` ## :bust_in_silhouette: Authors From 2bb6557e6cc45e8ac2825b3b512799815ee0c30c Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Tue, 12 Nov 2019 10:28:20 +0100 Subject: [PATCH 03/33] :globe_with_meridians: Add popup french translation Signed-off-by: mathieu.brunot --- erpnext_ocr/translations/fr.csv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext_ocr/translations/fr.csv b/erpnext_ocr/translations/fr.csv index 441f5fc6..e19c1f4c 100644 --- a/erpnext_ocr/translations/fr.csv +++ b/erpnext_ocr/translations/fr.csv @@ -6,4 +6,5 @@ Doctype: OCR Read,OCR Read,Lecture RCO Doctype: OCR Read,Image or PDF to Read,Image ou PDF à lire Doctype: OCR Read,Language,Langue Doctype: OCR Read,Read file,Lire le fichier -Doctype: OCR Read,Read Result,Résultat de la lecture \ No newline at end of file +Doctype: OCR Read,Read Result,Résultat de la lecture +apps/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js,Reading the file,Lecture du fichier \ No newline at end of file From c6dac1f2ece3941da774376c282a278641d52e8d Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Thu, 14 Nov 2019 17:09:48 +0100 Subject: [PATCH 04/33] :white_check_mark: Add Unit tests to CI (#8) * :white_check_mark: Add QUnit tests to CI Signed-off-by: mathieu.brunot * :white_check_mark: Fix UI tests for recent versions Signed-off-by: mathieu.brunot * :white_check_mark: Migrate tests for tesseract Signed-off-by: mathieu.brunot * :white_check_mark: Add basic OCR Read tests Signed-off-by: mathieu.brunot * :truck: Rename travis test directory Signed-off-by: mathieu.brunot * :green_heart: Fix UI test call Signed-off-by: mathieu.brunot * :green_heart: Use root perms to setup crhome driver Signed-off-by: mathieu.brunot * :green_heart: Set test module Signed-off-by: mathieu.brunot * :green_heart: Add unzip for debian-slim tests Signed-off-by: mathieu.brunot * :green_heart: Fix unit tests Signed-off-by: mathieu.brunot * :construction: Display UI tests help Signed-off-by: mathieu.brunot * :truck: Moved test data Signed-off-by: mathieu.brunot * :green_heart: Cleanup data after tests Signed-off-by: mathieu.brunot * :green_heart: Set ERPNext vesrion for tests Signed-off-by: mathieu.brunot * :green_heart: Install chromium in docker Signed-off-by: mathieu.brunot * :wrench: Package.json for QUnit tests Signed-off-by: mathieu.brunot * :art: Split test deps from OCR deps Signed-off-by: mathieu.brunot * :green_heart: Install chrome and chromedriver Signed-off-by: mathieu.brunot * :green_heart: Fix path to tesseract test data Signed-off-by: mathieu.brunot * :green_heart: Fix path to tesseract test data Signed-off-by: mathieu.brunot * :green_heart: Add test dependencies Signed-off-by: mathieu.brunot * :green_heart: Use root to install chrome Signed-off-by: mathieu.brunot * :construction_worker: Export tests results as XML Signed-off-by: mathieu.brunot * :construction: Change Chrome install method Signed-off-by: mathieu.brunot * :green_heart: Remove UI tests for now Signed-off-by: mathieu.brunot * :green_heart: Do not remove apt lists Signed-off-by: mathieu.brunot * :construction_worker: Add code test coverage Signed-off-by: mathieu.brunot * :green_heart: Set exact path to test data Signed-off-by: mathieu.brunot * :rewind: Remove code test coverage Signed-off-by: mathieu.brunot * :construction: Remove file listing after tests Signed-off-by: mathieu.brunot * :green_heart: Remove apt lists as root Signed-off-by: mathieu.brunot * :green_heart: Remove XML reports Signed-off-by: mathieu.brunot * :green_heart: Fix path test data files Signed-off-by: mathieu.brunot * :green_heart: Fix path test data files Signed-off-by: mathieu.brunot * :mute: Do not print read text in tests Signed-off-by: mathieu.brunot * :green_heart: Fix tests tear down Signed-off-by: mathieu.brunot * :art: Remove unused variables Signed-off-by: mathieu.brunot * :green_heart: Improve read tests Signed-off-by: mathieu.brunot * :art: Improve format of tests Signed-off-by: mathieu.brunot * :wrench: Add Travis env var to test Signed-off-by: mathieu.brunot * :construction: Send coverage results to Coveralls Signed-off-by: mathieu.brunot * :pencil2: Fix coverage typo Signed-off-by: mathieu.brunot * :art: Improve code quality Signed-off-by: mathieu.brunot * :art: Rename variable for better code quality Signed-off-by: mathieu.brunot * :art: Use assertEqual to remove deprecation Signed-off-by: mathieu.brunot * :pencil2: Init test app before display Signed-off-by: mathieu.brunot * :memo: Add coveralls badge Signed-off-by: mathieu.brunot * :green_heart: Only call coverage for 11+ Signed-off-by: mathieu.brunot * :wrench: Add Travis env var docker-compose test Signed-off-by: mathieu.brunot * :loud_sound: Display XML report in CI Signed-off-by: mathieu.brunot * :chart_with_upward_trend: Use absolute path to app Signed-off-by: mathieu.brunot * :green_heart: Fix XML report error check Signed-off-by: mathieu.brunot --- .travis.yml | 2 +- {test => .travis}/.dockerignore | 0 {test => .travis}/.env | 0 {test => .travis}/Dockerfile.alpine | 18 +-- {test => .travis}/Dockerfile.debian | 28 ++-- {test => .travis}/Dockerfile.debian-slim | 30 +++-- .travis/Dockerfile_test | 22 ++++ {test => .travis}/docker-compose.mariadb.yml | 13 ++ {test => .travis}/docker-compose.postgres.yml | 13 ++ {test => .travis}/docker-nginx.conf | 0 .travis/docker_test.sh | 117 +++++++++++++++++ README.md | 5 +- erpnext_ocr/erpnext_ocr/Picture_010.tif | Bin 28142 -> 0 bytes .../doctype/ocr_language/test_ocr_language.js | 7 +- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 121 +++++++++--------- .../doctype/ocr_read/test_ocr_read.js | 4 +- .../doctype/ocr_read/test_ocr_read.py | 68 +++++++++- .../erpnext_ocr/tessaract/Picture_010.tif | Bin 28142 -> 0 bytes .../erpnext_ocr/tessaract/image_example.py | 26 ---- .../erpnext_ocr/tessaract/pdf_example.py | 22 ---- .../templates/pages/__pycache__/__init__.py | 0 .../tessaract => tests}/README.md | 0 erpnext_ocr/tests/__init__.py | 0 .../test_data/Picture_010.png} | Bin erpnext_ocr/tests/test_data/Picture_010.tif | Bin 0 -> 194998 bytes .../test_data/Picture_010_output.txt} | 0 .../test_data/Picture_010_screenshot.png} | Bin .../tessaract => tests/test_data}/sample1.jpg | Bin .../test_data/sample1_output.txt} | 3 +- .../tessaract => tests/test_data}/sample2.pdf | Bin erpnext_ocr/tests/test_tesseract.py | 37 ++++++ package.json | 9 ++ test/Dockerfile_test | 8 -- test/docker_test.sh | 48 ------- 34 files changed, 390 insertions(+), 211 deletions(-) rename {test => .travis}/.dockerignore (100%) rename {test => .travis}/.env (100%) rename {test => .travis}/Dockerfile.alpine (59%) rename {test => .travis}/Dockerfile.debian (52%) rename {test => .travis}/Dockerfile.debian-slim (53%) create mode 100644 .travis/Dockerfile_test rename {test => .travis}/docker-compose.mariadb.yml (91%) rename {test => .travis}/docker-compose.postgres.yml (91%) rename {test => .travis}/docker-nginx.conf (100%) create mode 100644 .travis/docker_test.sh delete mode 100644 erpnext_ocr/erpnext_ocr/Picture_010.tif delete mode 100644 erpnext_ocr/erpnext_ocr/tessaract/Picture_010.tif delete mode 100644 erpnext_ocr/erpnext_ocr/tessaract/image_example.py delete mode 100644 erpnext_ocr/erpnext_ocr/tessaract/pdf_example.py create mode 100644 erpnext_ocr/templates/pages/__pycache__/__init__.py rename erpnext_ocr/{erpnext_ocr/tessaract => tests}/README.md (100%) create mode 100644 erpnext_ocr/tests/__init__.py rename erpnext_ocr/{erpnext_ocr/Selection_047.png => tests/test_data/Picture_010.png} (100%) create mode 100644 erpnext_ocr/tests/test_data/Picture_010.tif rename erpnext_ocr/{erpnext_ocr/scanned.txt => tests/test_data/Picture_010_output.txt} (100%) rename erpnext_ocr/{erpnext_ocr/Selection_046.png => tests/test_data/Picture_010_screenshot.png} (100%) rename erpnext_ocr/{erpnext_ocr/tessaract => tests/test_data}/sample1.jpg (100%) rename erpnext_ocr/{erpnext_ocr/tessaract/output.txt => tests/test_data/sample1_output.txt} (76%) rename erpnext_ocr/{erpnext_ocr/tessaract => tests/test_data}/sample2.pdf (100%) create mode 100644 erpnext_ocr/tests/test_tesseract.py create mode 100644 package.json delete mode 100644 test/Dockerfile_test delete mode 100644 test/docker_test.sh diff --git a/.travis.yml b/.travis.yml index 9a2f1d86..e437cc82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ branches: before_script: - env | sort - - dir="test" + - dir=".travis" - export IMAGE_NAME=docker-erpnext-ext:erpnext_ocr-travis - export BUILD_BRANCH=${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH}} - export BUILD_URL=https://github.com/${TRAVIS_PULL_REQUEST_SLUG:-${TRAVIS_REPO_SLUG}} diff --git a/test/.dockerignore b/.travis/.dockerignore similarity index 100% rename from test/.dockerignore rename to .travis/.dockerignore diff --git a/test/.env b/.travis/.env similarity index 100% rename from test/.env rename to .travis/.env diff --git a/test/Dockerfile.alpine b/.travis/Dockerfile.alpine similarity index 59% rename from test/Dockerfile.alpine rename to .travis/Dockerfile.alpine index 9985e269..64b7ddef 100644 --- a/test/Dockerfile.alpine +++ b/.travis/Dockerfile.alpine @@ -1,5 +1,11 @@ FROM monogramm/docker-erpnext:%%VERSION%%-alpine +RUN set -ex; \ + sudo apk add --update \ + chromium \ + chromium-chromedriver \ + ; + # Build environment variables ENV DOCKER_TAG=travis \ DOCKER_VCS_REF=${TRAVIS_COMMIT} \ @@ -29,15 +35,3 @@ RUN set -ex; \ "/home/$FRAPPE_USER"/frappe-bench/logs/* \ ; \ bench get-app --branch ${BUILD_BRANCH} ${BUILD_URL} - #echo "Manually installing app for CI (not needed normally)"; \ - #./env/bin/pip install -q -e "apps/erpnext_ocr" --no-cache-dir; \ - #test ! "$FRAPPE_BRANCH" = "v10.x.x" \ - # && ./env/bin/pip3 install -q -e "apps/erpnext_ocr" --no-cache-dir \ - # && bench build --app erpnext_ocr \ - #; \ - #sudo mkdir -p "${FRAPPE_WD}/sites"; \ - #sudo touch "${FRAPPE_WD}/sites/apps.txt"; \ - #sudo chown $FRAPPE_USER:$FRAPPE_USER "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "frappe" > "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "erpnext" >> "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "erpnext_ocr" >> "${FRAPPE_WD}/sites/apps.txt" diff --git a/test/Dockerfile.debian b/.travis/Dockerfile.debian similarity index 52% rename from test/Dockerfile.debian rename to .travis/Dockerfile.debian index 887acb31..e91d65de 100644 --- a/test/Dockerfile.debian +++ b/.travis/Dockerfile.debian @@ -1,5 +1,20 @@ FROM monogramm/docker-erpnext:%%VERSION%%-debian +# Install Google Chrome & Chrome WebDriver for UI tests +RUN set -ex; \ + sudo apt-get update -q; \ + sudo apt-get install -y --no-install-recommends \ + unzip \ + ; \ + CHROMEDRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`; \ + sudo mkdir -p /opt/chromedriver-$CHROMEDRIVER_VERSION; \ + sudo curl -sS -o /tmp/chromedriver_linux64.zip http://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; \ + sudo unzip -qq /tmp/chromedriver_linux64.zip -d /opt/chromedriver-$CHROMEDRIVER_VERSION; \ + sudo rm /tmp/chromedriver_linux64.zip; \ + sudo chmod +x /opt/chromedriver-$CHROMEDRIVER_VERSION/chromedriver; \ + sudo ln -fs /opt/chromedriver-$CHROMEDRIVER_VERSION/chromedriver /usr/local/bin/chromedriver; \ + export PATH="$PATH;/usr/local/bin/chromedriver" + # Build environment variables ENV DOCKER_TAG=travis \ DOCKER_VCS_REF=${TRAVIS_COMMIT} \ @@ -18,6 +33,7 @@ RUN set -ex; \ imagemagick \ tesseract-ocr \ ; \ + sudo rm -rf /var/lib/apt/lists/*; \ sudo sed -i \ -e 's/rights="none" pattern="PDF"/rights="read" pattern="PDF"/g' \ /etc/ImageMagick*/policy.xml \ @@ -29,15 +45,3 @@ RUN set -ex; \ "/home/$FRAPPE_USER"/frappe-bench/logs/* \ ; \ bench get-app --branch ${BUILD_BRANCH} ${BUILD_URL} - #echo "Manually installing app for CI (not needed normally)"; \ - #./env/bin/pip install -q -e "apps/erpnext_ocr" --no-cache-dir; \ - #test ! "$FRAPPE_BRANCH" = "v10.x.x" \ - # && ./env/bin/pip3 install -q -e "apps/erpnext_ocr" --no-cache-dir \ - # && bench build --app erpnext_ocr \ - #; \ - #sudo mkdir -p "${FRAPPE_WD}/sites"; \ - #sudo touch "${FRAPPE_WD}/sites/apps.txt"; \ - #sudo chown $FRAPPE_USER:$FRAPPE_USER "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "frappe" > "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "erpnext" >> "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "erpnext_ocr" >> "${FRAPPE_WD}/sites/apps.txt" diff --git a/test/Dockerfile.debian-slim b/.travis/Dockerfile.debian-slim similarity index 53% rename from test/Dockerfile.debian-slim rename to .travis/Dockerfile.debian-slim index 06571a2e..6468f14c 100644 --- a/test/Dockerfile.debian-slim +++ b/.travis/Dockerfile.debian-slim @@ -1,5 +1,21 @@ FROM monogramm/docker-erpnext:%%VERSION%%-debian-slim +# Install Google Chrome & Chrome WebDriver for UI tests +RUN set -ex; \ + sudo apt-get update -q; \ + sudo apt-get install -y --no-install-recommends \ + iputils-ping \ + unzip \ + ; \ + CHROMEDRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`; \ + sudo mkdir -p /opt/chromedriver-$CHROMEDRIVER_VERSION; \ + sudo curl -sS -o /tmp/chromedriver_linux64.zip http://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; \ + sudo unzip -qq /tmp/chromedriver_linux64.zip -d /opt/chromedriver-$CHROMEDRIVER_VERSION; \ + sudo rm /tmp/chromedriver_linux64.zip; \ + sudo chmod +x /opt/chromedriver-$CHROMEDRIVER_VERSION/chromedriver; \ + sudo ln -fs /opt/chromedriver-$CHROMEDRIVER_VERSION/chromedriver /usr/local/bin/chromedriver; \ + export PATH="$PATH;/usr/local/bin/chromedriver" + # Build environment variables ENV DOCKER_TAG=travis \ DOCKER_VCS_REF=${TRAVIS_COMMIT} \ @@ -17,8 +33,8 @@ RUN set -ex; \ ghostscript \ imagemagick \ tesseract-ocr \ - iputils-ping \ ; \ + sudo rm -rf /var/lib/apt/lists/*; \ sudo sed -i \ -e 's/rights="none" pattern="PDF"/rights="read" pattern="PDF"/g' \ /etc/ImageMagick*/policy.xml \ @@ -30,15 +46,3 @@ RUN set -ex; \ "/home/$FRAPPE_USER"/frappe-bench/logs/* \ ; \ bench get-app --branch ${BUILD_BRANCH} ${BUILD_URL} - #echo "Manually installing app for CI (not needed normally)"; \ - #./env/bin/pip install -q -e "apps/erpnext_ocr" --no-cache-dir; \ - #test ! "$FRAPPE_BRANCH" = "v10.x.x" \ - # && ./env/bin/pip3 install -q -e "apps/erpnext_ocr" --no-cache-dir \ - # && bench build --app erpnext_ocr \ - #; \ - #sudo mkdir -p "${FRAPPE_WD}/sites"; \ - #sudo touch "${FRAPPE_WD}/sites/apps.txt"; \ - #sudo chown $FRAPPE_USER:$FRAPPE_USER "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "frappe" > "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "erpnext" >> "${FRAPPE_WD}/sites/apps.txt"; \ - #echo "erpnext_ocr" >> "${FRAPPE_WD}/sites/apps.txt" diff --git a/.travis/Dockerfile_test b/.travis/Dockerfile_test new file mode 100644 index 00000000..b94fcfde --- /dev/null +++ b/.travis/Dockerfile_test @@ -0,0 +1,22 @@ +FROM %%IMAGE_NAME%% + +ADD docker_test.sh /docker_test.sh + +# Test environment variables +ENV TEST_VERSION=${TEST_VERSION} + +RUN set -ex; \ + sudo chmod 755 /docker_test.sh; \ + sudo pip install python-coveralls + +EXPOSE 4444 + +# Default Chrome configuration +ENV DISPLAY=:20.0 \ + SCREEN_GEOMETRY="1440x900x24" \ + CHROMEDRIVER_PORT=4444 \ + CHROMEDRIVER_WHITELISTED_IPS="127.0.0.1" \ + CHROMEDRIVER_URL_BASE='' \ + CHROMEDRIVER_EXTRA_ARGS='' + +CMD ["/docker_test.sh"] diff --git a/test/docker-compose.mariadb.yml b/.travis/docker-compose.mariadb.yml similarity index 91% rename from test/docker-compose.mariadb.yml rename to .travis/docker-compose.mariadb.yml index 220f370f..788c2b49 100644 --- a/test/docker-compose.mariadb.yml +++ b/.travis/docker-compose.mariadb.yml @@ -27,6 +27,19 @@ services: # Docker setup - DOCKER_APPS_TIMEOUT=900 - DOCKER_DEBUG=1 + # Test setup + - TEST_VERSION=${VERSION} + - TRAVIS_BUILD_ID=${TRAVIS_BUILD_ID} + - TRAVIS_BUILD_NUMBER=${TRAVIS_BUILD_NUMBER} + - TRAVIS_BUILD_WEB_URL=${TRAVIS_BUILD_WEB_URL} + - TRAVIS_COMMIT=${TRAVIS_COMMIT} + - TRAVIS_COMMIT_MESSAGE=${TRAVIS_COMMIT_MESSAGE} + - TRAVIS_COMMIT_RANGE=${TRAVIS_COMMIT_RANGE} + - TRAVIS_JOB_ID=${TRAVIS_JOB_ID} + - TRAVIS_JOB_NAME=${TRAVIS_JOB_NAME} + - TRAVIS_JOB_NUMBER=${TRAVIS_JOB_NUMBER} + - TRAVIS_JOB_WEB_URL=${TRAVIS_JOB_WEB_URL} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} volumes_from: - erpnext_app volumes: diff --git a/test/docker-compose.postgres.yml b/.travis/docker-compose.postgres.yml similarity index 91% rename from test/docker-compose.postgres.yml rename to .travis/docker-compose.postgres.yml index a12bc306..d4d30256 100644 --- a/test/docker-compose.postgres.yml +++ b/.travis/docker-compose.postgres.yml @@ -27,6 +27,19 @@ services: # Docker setup - DOCKER_APPS_TIMEOUT=900 - DOCKER_DEBUG=1 + # Test setup + - TEST_VERSION=${VERSION} + - TRAVIS_BUILD_ID=${TRAVIS_BUILD_ID} + - TRAVIS_BUILD_NUMBER=${TRAVIS_BUILD_NUMBER} + - TRAVIS_BUILD_WEB_URL=${TRAVIS_BUILD_WEB_URL} + - TRAVIS_COMMIT=${TRAVIS_COMMIT} + - TRAVIS_COMMIT_MESSAGE=${TRAVIS_COMMIT_MESSAGE} + - TRAVIS_COMMIT_RANGE=${TRAVIS_COMMIT_RANGE} + - TRAVIS_JOB_ID=${TRAVIS_JOB_ID} + - TRAVIS_JOB_NAME=${TRAVIS_JOB_NAME} + - TRAVIS_JOB_NUMBER=${TRAVIS_JOB_NUMBER} + - TRAVIS_JOB_WEB_URL=${TRAVIS_JOB_WEB_URL} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} volumes_from: - erpnext_app volumes: diff --git a/test/docker-nginx.conf b/.travis/docker-nginx.conf similarity index 100% rename from test/docker-nginx.conf rename to .travis/docker-nginx.conf diff --git a/.travis/docker_test.sh b/.travis/docker_test.sh new file mode 100644 index 00000000..248a26cb --- /dev/null +++ b/.travis/docker_test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/sh + +set -e + +################################################################################ +# Testing docker containers + +echo "Waiting to ensure everything is fully ready for the tests..." +sleep 60 + +echo "Checking content of sites directory..." +if [ ! -f "./sites/apps.txt" ] || [ ! -f "./sites/.docker-app-init" ] || [ ! -f "./sites/currentsite.txt" ] || [ ! -f "./sites/.docker-site-init" ] || [ ! -f "./sites/.docker-init" ]; then + echo 'Apps and site are not initalized?!' + ls -al "./sites" + exit 1 +fi + +echo "Checking main containers are reachable..." +if ! sudo ping -c 10 -q erpnext_db ; then + echo 'Database container is not responding!' + echo 'Check the following logs for details:' + tail -n 100 logs/*.log + exit 2 +fi + +if ! sudo ping -c 10 -q erpnext_app ; then + echo 'App container is not responding!' + echo 'Check the following logs for details:' + tail -n 100 logs/*.log + exit 4 +fi + +if ! sudo ping -c 10 -q erpnext_web ; then + echo 'Web container is not responding!' + echo 'Check the following logs for details:' + tail -n 100 logs/*.log + exit 8 +fi + + +################################################################################ +# Success +echo 'Docker tests successful' + + +################################################################################ +# Automated Unit tests +# https://docs.docker.com/docker-hub/builds/automated-testing/ +# https://frappe.io/docs/user/en/testing +################################################################################ + +FRAPPE_APP_TO_TEST=erpnext_ocr + +echo "Preparing Frappe application '${FRAPPE_APP_TO_TEST}' tests..." + +################################################################################ +# Frappe Unit tests +# https://frappe.io/docs/user/en/guides/automated-testing/unit-testing + +FRAPPE_APP_UNIT_TEST_REPORT="$(pwd)/sites/.${FRAPPE_APP_TO_TEST}_unit_tests.xml" + +#bench run-tests --help + +echo "Executing Unit Tests of '${FRAPPE_APP_TO_TEST}' app..." +if [ "${TEST_VERSION}" = "10" ]; then + bench run-tests \ + --app ${FRAPPE_APP_TO_TEST} \ + --junit-xml-output "${FRAPPE_APP_UNIT_TEST_REPORT}" +else + bench run-tests \ + --app ${FRAPPE_APP_TO_TEST} \ + --coverage + # FIXME https://github.com/frappe/frappe/issues/8809 + # --junit-xml-output "${FRAPPE_APP_UNIT_TEST_REPORT}" +fi + +## Check result of tests +if [ -f "${FRAPPE_APP_UNIT_TEST_REPORT}" ]; then + echo "Checking Frappe application '${FRAPPE_APP_TO_TEST}' unit tests report..." + + if grep -E '(errors|failures)="[1-9][0-9]*"' "${FRAPPE_APP_UNIT_TEST_REPORT}"; then + echo "Unit Tests of '${FRAPPE_APP_TO_TEST}' app failed! See report for details:" + cat "${FRAPPE_APP_UNIT_TEST_REPORT}" + exit 1 + else + echo "Unit Tests of '${FRAPPE_APP_TO_TEST}' app successful! See report for details:" + cat "${FRAPPE_APP_UNIT_TEST_REPORT}" + fi +fi + +if [ -f ./sites/.coverage ]; then + echo "Sending Unit Tests coverage of '${FRAPPE_APP_TO_TEST}' app to Coveralls..." + coveralls -b "$(pwd)/apps/${FRAPPE_APP_TO_TEST}" -d ./sites/.coverage +fi + + +################################################################################ +# TODO QUnit (JS) Unit tests +# https://frappe.io/docs/user/en/guides/automated-testing/qunit-testing + +#bench run-ui-tests --help + +#echo "Executing UI Tests of '${FRAPPE_APP_TO_TEST}' app..." +#if [ "${TEST_VERSION}" = "10" ] || [ "${TEST_VERSION}" = "11" ]; then +# bench run-ui-tests --app ${FRAPPE_APP_TO_TEST} +#else +# bench run-ui-tests ${FRAPPE_APP_TO_TEST} +#fi + +## TODO Check result of UI tests + + +################################################################################ +# Success +echo 'Frappe app '${FRAPPE_APP_TO_TEST}' tests finished' +echo 'Check the CI reports and logs for details.' +exit 0 diff --git a/README.md b/README.md index 6cb29311..a647dd77 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![License: MIT][uri_license_image]][uri_license] [![Managed with Taiga.io](https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg)](https://tree.taiga.io/project/monogrammbot-monogrammerpnext_ocr/ "Managed with Taiga.io") [![Build Status](https://travis-ci.org/Monogramm/erpnext_ocr.svg)](https://travis-ci.org/Monogramm/erpnext_ocr) +[![Coverage Status](https://coveralls.io/repos/github/Monogramm/erpnext_ocr/badge.svg?branch=master)](https://coveralls.io/github/Monogramm/erpnext_ocr?branch=master) ## ERPNext OCR @@ -51,12 +52,12 @@ When installing Frappe app, the following python requirements will be installed: **Sample Screenshot**: -![Sample Screenshot](./erpnext_ocr/erpnext_ocr/Selection_046.png) +![Sample Screenshot](./erpnext_ocr/tests/test_data/Picture_010.png) **File Being Read**: -![Sample Screenshot 2](./erpnext_ocr/erpnext_ocr/Selection_047.png) +![Sample Screenshot 2](./erpnext_ocr/tests/test_data/Picture_010_screenshot.png) ### Tesseract trained data diff --git a/erpnext_ocr/erpnext_ocr/Picture_010.tif b/erpnext_ocr/erpnext_ocr/Picture_010.tif deleted file mode 100644 index f8cf1c63b9a2c3ebbdcd9fade19aae9297aa660d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28142 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

>Hf6V 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 Q>2$ab)C8(X`uFtT0IoE frappe.tests.make('OCR Language', [ // values to be set - {key: 'value'} + {code: random_code, lang: random_lang} ]), () => { - assert.equal(cur_frm.doc.key, 'value'); + assert.equal(cur_frm.doc.code, random_code); + assert.equal(cur_frm.doc.lang, random_lang); }, () => done() ]); diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index e1fbcbae..779774af 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -9,16 +9,69 @@ import io -# Alternative to "File Upload Disconnected. Please try again." +class OCRRead(Document): + def read_image(self): + text = read_document(self.file_to_read, self.language or 'eng') + + self.read_result = text + self.save() + return text + + +@frappe.whitelist() +def read_document(path, lang='eng'): + """Call Tesseract OCR to extract the text from a document.""" + from PIL import Image + import requests + import pytesseract + + if path is None: + return None -# erpnext_ocr.erpnext_ocr.doctype.ocr_read.ocr_read.force_attach_file -def force_attach_file(): - filename = "Picture_010.tif" - name = "a2cbc0186c" - force_attach_file_doc(filename, name) + if path.startswith('/assets/'): + # from public folder + fullpath = os.path.abspath(path) + elif path.startswith('/files/'): + # public file + fullpath = frappe.get_site_path() + '/public' + path + elif path.startswith('/private/files/'): + # private file + fullpath = frappe.get_site_path() + path + elif path.startswith('/'): + # local file (mostly for tests) + fullpath = os.path.abspath(path) + else: + # external link + fullpath = requests.get(path, stream=True).raw + + text = " " + + if path.endswith('.pdf'): + from wand.image import Image as wi + pdf = wi(filename=fullpath, resolution=300) + pdf_image = pdf.convert('jpeg') + for img in pdf_image.sequence: + img_page = wi(image=img) + image_blob = img_page.make_blob('jpeg') + + recognized_text = " " + + image = Image.open(io.BytesIO(image_blob)) + recognized_text = pytesseract.image_to_string(image, lang) + text = text + recognized_text + + else: + image = Image.open(fullpath) + + text = pytesseract.image_to_string(image, lang=lang) + + text.split(" ") + + return text def force_attach_file_doc(filename, name): + """Alternative to 'File Upload Disconnected. Please try again.'""" file_url = "/private/files/" + filename attachment_doc = frappe.get_doc({ @@ -33,57 +86,5 @@ def force_attach_file_doc(filename, name): }) attachment_doc.insert() - frappe.db.sql("""UPDATE `tabOCR Read` SET file_to_read=%s WHERE name=%s""", (file_url, name)) - - -class OCRRead(Document): - def read_image(self): - from PIL import Image - import requests - import pytesseract - - path = self.file_to_read - if path == None: - return None - - if path.startswith('/assets/'): - # from public folder - fullpath = os.path.abspath(path) - elif path.startswith('/files/'): - # public file - fullpath = frappe.get_site_path() + '/public' + path - elif path.startswith('/private/files/'): - # private file - fullpath = frappe.get_site_path() + path - else: - # external link - fullpath = requests.get(path, stream=True).raw - - lang = self.language or 'eng' - - text = " " - - if path.endswith('.pdf'): - from wand.image import Image as wi - pdf = wi(filename=fullpath, resolution=300) - pdfImage = pdf.convert('jpeg') - for img in pdfImage.sequence: - imgPage = wi(image=img) - imageBlob = imgPage.make_blob('jpeg') - - recognized_text = " " - - im = Image.open(io.BytesIO(imageBlob)) - recognized_text = pytesseract.image_to_string(im, lang) - text = text + recognized_text - - else: - im = Image.open(fullpath) - - text = pytesseract.image_to_string(im, lang=lang) - - text.split(" ") - - self.read_result = text - self.save() - return text + frappe.db.sql( + """UPDATE `tabOCR Read` SET file_to_read=%s WHERE name=%s""", (file_url, name)) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.js index 964428ce..ab14e596 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.js +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.js @@ -12,10 +12,10 @@ QUnit.test("test: OCR Read", function (assert) { // insert a new OCR Read () => frappe.tests.make('OCR Read', [ // values to be set - {key: 'value'} + {language: 'en'} ]), () => { - assert.equal(cur_frm.doc.key, 'value'); + assert.equal(cur_frm.doc.language, 'en'); }, () => done() ]); diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py index a937c4e1..0f3fa12e 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py @@ -5,6 +5,72 @@ import frappe import unittest +import os + + +def create_ocr_reads(): + if frappe.flags.test_ocr_reads_created: + return + + frappe.set_user("Administrator") + frappe.get_doc({ + "doctype": "OCR Read", + "file_to_read": os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "sample1.jpg"), + "language": "eng" + }).insert() + + frappe.get_doc({ + "doctype": "OCR Read", + "file_to_read": os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "sample2.pdf"), + "language": "eng" + }).insert() + + frappe.flags.test_ocr_reads_created = True + + +def delete_ocr_reads(): + if frappe.flags.test_ocr_reads_created: + frappe.set_user("Administrator") + + for d in frappe.get_all("OCR Read"): + doc = frappe.get_doc("OCR Read", d.name) + doc.delete() + + # Delete directly in DB to avoid validation errors + #frappe.db.sql("""delete from `tabOCR Read`""") + + frappe.flags.test_ocr_reads_created = False + class TestOCRRead(unittest.TestCase): - pass + def setUp(self): + create_ocr_reads() + + def tearDown(self): + delete_ocr_reads() + + # TODO: Read content of files and check recognised text + + #def test_ocr_read_image(self): + # frappe.set_user("Administrator") + + #def test_ocr_read_pdf(self): + # frappe.set_user("Administrator") + + def test_ocr_read_list(self): + # frappe.set_user("test1@example.com") + frappe.set_user("Administrator") + res = frappe.get_list("OCR Read", filters=[ + ["OCR Read", "file_to_read", "like", "%sample%"]], fields=["name", "file_to_read"]) + self.assertEqual(len(res), 2) + files_to_read = [r.file_to_read for r in res] + self.assertTrue(os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "sample1.jpg") in files_to_read) + self.assertTrue(os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "sample2.pdf") in files_to_read) diff --git a/erpnext_ocr/erpnext_ocr/tessaract/Picture_010.tif b/erpnext_ocr/erpnext_ocr/tessaract/Picture_010.tif deleted file mode 100644 index f8cf1c63b9a2c3ebbdcd9fade19aae9297aa660d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28142 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

>Hf6V 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 Q>2$ab)C8(X`uFtT0IoEVQ+p#mXPCJldWw* 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 Date: Fri, 15 Nov 2019 10:14:40 +0100 Subject: [PATCH 05/33] :see_no_evil: Ignore profile files Signed-off-by: mathieu.brunot --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bf0040e1..592ad1df 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ tags erpnext_ocr/docs/current *.iml -*.xml \ No newline at end of file +*.xml +*.prof \ No newline at end of file From f3d15fa85ab88636ed30666beda587da5e233f3f Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Fri, 15 Nov 2019 22:31:47 +0100 Subject: [PATCH 06/33] :zap: Improve Docker test container build Signed-off-by: mathieu.brunot --- .travis/Dockerfile_test | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis/Dockerfile_test b/.travis/Dockerfile_test index b94fcfde..a363c9f1 100644 --- a/.travis/Dockerfile_test +++ b/.travis/Dockerfile_test @@ -2,9 +2,6 @@ FROM %%IMAGE_NAME%% ADD docker_test.sh /docker_test.sh -# Test environment variables -ENV TEST_VERSION=${TEST_VERSION} - RUN set -ex; \ sudo chmod 755 /docker_test.sh; \ sudo pip install python-coveralls @@ -19,4 +16,7 @@ ENV DISPLAY=:20.0 \ CHROMEDRIVER_URL_BASE='' \ CHROMEDRIVER_EXTRA_ARGS='' +# Test environment variables +ENV TEST_VERSION=${TEST_VERSION} + CMD ["/docker_test.sh"] From 7c8581e999d3b419fd32f8e3777086681373a9f9 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 19 Nov 2019 00:57:05 +0300 Subject: [PATCH 07/33] :sparkles: Add dynamic navbar (#10) * :bug: Fix bug with showing navbar Signed-off-by: Emil * :lipstick: Remove unnecessary library Signed-off-by: Emil * :lipstick: Remove logging Signed-off-by: Emil * :bug: Fix bug with showing navbar Signed-off-by: Emil * :art: Added size variable instead of len Signed-off-by: Emil --- .../erpnext_ocr/doctype/ocr_read/ocr_read.js | 13 ++++--------- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 9 ++++++++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js index 511f2269..15ef6f88 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js @@ -1,18 +1,13 @@ -// Copyright (c) 2018, John Vincent Fiel and contributors -// For license information, please see license.txt - frappe.ui.form.on('OCR Read', { refresh: function (frm) { }, read_image: function (frm) { frappe.hide_msgprint(true); - // frappe.realtime.on("ocr_progress_bar", function (data) { - // frappe.hide_msgprint(true); - // frappe.show_progress(__("Reading the file"), data.progress[0], data.progress[1]); - // console.log(data.progress[0]) - // }); - frappe.show_progress(__("Reading the file"), 50, 100); + frappe.realtime.on("ocr_progress_bar", function (data) { + frappe.hide_msgprint(true); + frappe.show_progress(__("Reading the file"), data.progress[0], data.progress[1]); + }); frappe.call({ method: "read_image", doc: cur_frm.doc, diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 779774af..73a4a5f6 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -50,6 +50,8 @@ def read_document(path, lang='eng'): from wand.image import Image as wi pdf = wi(filename=fullpath, resolution=300) pdf_image = pdf.convert('jpeg') + i = 0 + size = len(pdf_image.sequence) for img in pdf_image.sequence: img_page = wi(image=img) image_blob = img_page.make_blob('jpeg') @@ -60,7 +62,12 @@ def read_document(path, lang='eng'): recognized_text = pytesseract.image_to_string(image, lang) text = text + recognized_text + frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}) + i += 1 + else: + frappe.publish_realtime("ocr_progress_bar", {"progress": "0"}, user=frappe.session.user) + image = Image.open(fullpath) text = pytesseract.image_to_string(image, lang=lang) @@ -87,4 +94,4 @@ def force_attach_file_doc(filename, name): attachment_doc.insert() frappe.db.sql( - """UPDATE `tabOCR Read` SET file_to_read=%s WHERE name=%s""", (file_url, name)) + """UPDATE `tabOCR Read` SET file_to_read=%s WHERE name=%s""", (file_url, name)) \ No newline at end of file From 4d61a0e83a4beb0fca609b4721dcfb68f2481b45 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Wed, 20 Nov 2019 23:18:16 +0300 Subject: [PATCH 08/33] :sparkles: Add read only field that give info about language (#6) * :sparkles: Add read only field to OCR Language that checks if language is known by tesseract Signed-off-by: Emil * :bug: Fix path in lang_is_supported function Signed-off-by: Emil * :bug: Administrator instead of email Signed-off-by: Emil * :children_crossing: Changed txt file on API method Signed-off-by: Emil * :sparkles: Add checker of language in ocr_read doctype Signed-off-by: Emil * :whale: Add necessary tesseract-ocr library Signed-off-by: Emil * :whale: Add ocr in debian-slim Signed-off-by: Emil * :bug: Fix bug with installation Signed-off-by: Emil * :heavy_minus_sign: Remove leptonica library Signed-off-by: Emil * :heavy_plus_sign: Add library in requirements.txt Signed-off-by: Emil * :pencil2: Signed-off-by: Emil * :heavy_minus_sign: Remove libleptonica Signed-off-by: Emil * :heavy_plus_sign: Add libleptonica only in debian and debian-slim Signed-off-by: Emil * :pencil2: Add pkg-config library Signed-off-by: Emil * :construction: Return previous packages Signed-off-by: Emil * :heavy_plus_sign: Add libleptonica-dev again Signed-off-by: Emil * :heavy_plus_sign: Add several packages Signed-off-by: Emil * :heavy_plus_sign: Add different packages in alpine Signed-off-by: Emil * :heavy_plus_sign: Added pkgconfig in alpine Signed-off-by: Emil * :heavy_minus_sign: Remove repeatable libraries Signed-off-by: Emil * :art: Update readable of code Signed-off-by: Emil * :art: Update readable javascipt Signed-off-by: Emil * :heavy_minus_sign: Remove leptonica Signed-off-by: Emil * :bug: Fix merge and quality issues Signed-off-by: mathieu.brunot * :art: Change deps install order Signed-off-by: mathieu.brunot * :art: Remove static path Signed-off-by: Emil * :fire: Remove os library * :art: Code beautifying Signed-off-by: Emil * :art: Add unit tests and try catch contruction Signed-off-by: Emil * :art: Remove print Signed-off-by: Emil * :art: Clear code for deepsource Signed-off-by: Emil * :art: Change constructor of class LangSupport Signed-off-by: Emil * :wrench: Add env variable for tests Signed-off-by: Emil * :loud_sound: Add logs Signed-off-by: Emil * :loud_sound: Add logs dict Signed-off-by: Emil * :sparkles: Add str instead bytes Signed-off-by: Emil * :wrench: Add jshint configuration inside package.json Signed-off-by: Emil * :wrench: Add .estlintrc file Signed-off-by: Emil * :wrench: Update .estlintrc file Signed-off-by: Emil * :bug: Add text instead dict Signed-off-by: Emil * :construction: Decode in utf-8 Signed-off-by: Emil * :construction: Another try to pass the test Signed-off-by: Emil * :construction: Add try catch construction for old python Signed-off-by: Emil * :heavy_minus_sign: Remove unnecessary library Signed-off-by: Emil * :wrench: Add export LC_ALL = C in .travis.yml Signed-off-by: Emil * :wrench: Add new env in Dockerfile.alpine Signed-off-by: Emil * :bug: Add \ in envs Signed-off-by: Emil * :art: Beautify structure of code Signed-off-by: Emil * :art: Rename name of Exception class Signed-off-by: Emil * :bug: Last fix with test Signed-off-by: Emil * :green_heart: Do not hide failures in tests * :white_check_mark: Check lang not supported raised Signed-off-by: mathieu.brunot * :green_heart: Fix lang support exceptions Signed-off-by: mathieu.brunot * :art: Remove trailing space Signed-off-by: mathieu.brunot --- .eslintrc | 82 +++++++++++++++++++ .travis/Dockerfile.alpine | 6 +- .travis/Dockerfile.debian | 3 + .travis/Dockerfile.debian-slim | 3 + .../doctype/ocr_language/ocr_language.js | 16 +++- .../doctype/ocr_language/ocr_language.json | 35 +++++++- .../doctype/ocr_language/ocr_language.py | 19 ++++- .../doctype/ocr_language/test_ocr_language.py | 11 ++- .../erpnext_ocr/doctype/ocr_read/ocr_read.js | 10 +-- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 12 ++- erpnext_ocr/tests/test_tesseract.py | 29 +++++-- requirements.txt | 1 + 12 files changed, 203 insertions(+), 24 deletions(-) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..56646dcb --- /dev/null +++ b/.eslintrc @@ -0,0 +1,82 @@ +{ + "globals": { + "frappe": true, + "__": true, + "_p": true, + "_f": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "has_words": true, + "validate_email": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "fluxify": true, + "io": true, + "QUnit": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true + } +} diff --git a/.travis/Dockerfile.alpine b/.travis/Dockerfile.alpine index 64b7ddef..d4c74634 100644 --- a/.travis/Dockerfile.alpine +++ b/.travis/Dockerfile.alpine @@ -9,7 +9,8 @@ RUN set -ex; \ # Build environment variables ENV DOCKER_TAG=travis \ DOCKER_VCS_REF=${TRAVIS_COMMIT} \ - DOCKER_BUILD_DATE=${TRAVIS_BUILD_NUMBER} + DOCKER_BUILD_DATE=${TRAVIS_BUILD_NUMBER} \ + LC_ALL=C # Copy the whole repository to app folder for manual install #COPY --chown=frappe:frappe . "/home/$FRAPPE_USER"/frappe-bench/apps/erpnext_ocr @@ -23,6 +24,9 @@ RUN set -ex; \ imagemagick \ imagemagick-dev \ tesseract-ocr \ + tesseract-ocr-dev \ + leptonica \ + pkgconfig \ ; \ sudo sed -i \ -e 's/rights="none" pattern="PDF"/rights="read" pattern="PDF"/g' \ diff --git a/.travis/Dockerfile.debian b/.travis/Dockerfile.debian index e91d65de..9b5d5753 100644 --- a/.travis/Dockerfile.debian +++ b/.travis/Dockerfile.debian @@ -32,6 +32,9 @@ RUN set -ex; \ ghostscript \ imagemagick \ tesseract-ocr \ + libtesseract-dev \ + libleptonica-dev \ + pkg-config \ ; \ sudo rm -rf /var/lib/apt/lists/*; \ sudo sed -i \ diff --git a/.travis/Dockerfile.debian-slim b/.travis/Dockerfile.debian-slim index 6468f14c..d3089076 100644 --- a/.travis/Dockerfile.debian-slim +++ b/.travis/Dockerfile.debian-slim @@ -33,6 +33,9 @@ RUN set -ex; \ ghostscript \ imagemagick \ tesseract-ocr \ + libtesseract-dev \ + libleptonica-dev \ + pkg-config \ ; \ sudo rm -rf /var/lib/apt/lists/*; \ sudo sed -i \ diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js index 20da529c..81dc44bb 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js @@ -1,7 +1,17 @@ // Copyright (c) 2019, Monogramm and contributors // For license information, please see license.txt -frappe.ui.form.on('OCR Language', { - refresh: function (frm) { +frappe.ui.form.on('OCR Language', "lang", function (frm) { + if (typeof frm.doc.lang != "undefined") { + frappe.call({ + args: { + "lang": frm.doc.lang + }, + method: "erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language.check_language_web", + callback: function (r) { + frm.set_value("is_supported", r.message); + } + }); + } } -}); +); diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json index 845aa432..4d53a426 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json @@ -80,6 +80,39 @@ "set_only_once": 0, "translatable": 0, "unique": 1 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "is_supported", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": " Is supported", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 } ], "has_web_view": 0, @@ -93,7 +126,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-08-17 00:52:25.266181", + "modified": "2019-11-08 09:26:39.984652", "modified_by": "Administrator", "module": "ERPNext OCR", "name": "OCR Language", diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py index beeabff9..45d08b6b 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py @@ -3,8 +3,25 @@ # For license information, please see license.txt from __future__ import unicode_literals + import frappe +import tesserocr from frappe.model.document import Document + + +@frappe.whitelist() +def check_language_web(lang): + return "Yes" if lang_is_support(lang) else "No" + + +@frappe.whitelist() +def lang_is_support(lang): + if lang == 'en': + lang = "eng" + list_of_languages = tesserocr.get_languages()[1] + return lang in list_of_languages + + class OCRLanguage(Document): - pass + pass diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py index 40205add..2fb53213 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py @@ -6,5 +6,14 @@ import frappe import unittest +from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_is_support, check_language_web + + class TestOCRLanguage(unittest.TestCase): - pass + def test_english_language(self): + decision = lang_is_support("en") + self.assertTrue(decision) + + def test_check_language_web(self): + decision = check_language_web("en") + self.assertEqual(decision, "Yes") diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js index 15ef6f88..ff4adf1e 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js @@ -11,12 +11,10 @@ frappe.ui.form.on('OCR Read', { frappe.call({ method: "read_image", doc: cur_frm.doc, - callback: function (r, rt) { - if (r.message) { - console.log(r.message); - cur_frm.set_value("read_result", r.message); - cur_dialog.hide() - } + callback: function (r) { + cur_dialog.hide(); + frappe.msgprint(r.message.message); + cur_frm.set_value("read_result", r.message); } }); } diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 73a4a5f6..12573c6f 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -4,18 +4,20 @@ from __future__ import unicode_literals import frappe +from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_is_support from frappe.model.document import Document + import os import io class OCRRead(Document): def read_image(self): - text = read_document(self.file_to_read, self.language or 'eng') + message = read_document(self.file_to_read, self.language or 'eng') - self.read_result = text + self.read_result = message self.save() - return text + return message @frappe.whitelist() @@ -28,6 +30,10 @@ def read_document(path, lang='eng'): if path is None: return None + if not lang_is_support(lang): + frappe.msgprint(frappe._("The selected language is not available. Please contact your administrator."), + raise_exception=True) + if path.startswith('/assets/'): # from public folder fullpath = os.path.abspath(path) diff --git a/erpnext_ocr/tests/test_tesseract.py b/erpnext_ocr/tests/test_tesseract.py index f871b514..fa02d329 100644 --- a/erpnext_ocr/tests/test_tesseract.py +++ b/erpnext_ocr/tests/test_tesseract.py @@ -3,15 +3,27 @@ # See license.txt from __future__ import unicode_literals -import unittest, os +import locale +import unittest +import os + +import frappe from erpnext_ocr.erpnext_ocr.doctype.ocr_read.ocr_read import read_document + class TestTesseract(unittest.TestCase): + def test_read_document_lang_not_supported(self): + locale.setlocale(locale.LC_ALL, 'C') + self.assertRaises(frappe.ValidationError, read_document, + os.path.join(os.path.dirname(__file__),"test_data", "sample1.jpg"), + "xxx") + def test_read_document_image(self): + locale.setlocale(locale.LC_ALL, 'C') recognized_text = read_document(os.path.join(os.path.dirname(__file__), - "test_data", "sample1.jpg"), - "eng") + "test_data", "sample1.jpg"), + "eng") # print(recognized_text) @@ -20,18 +32,19 @@ def test_read_document_image(self): self.assertTrue("lazy dogs!" in recognized_text) self.assertFalse("And an elephant!" in recognized_text) - file = open(os.path.join(os.path.dirname(__file__), "test_data", "sample1_output.txt"), "r") + file = open(os.path.join(os.path.dirname(__file__), + "test_data", "sample1_output.txt"), "r") expected_text = file.read() - self.assertTrue(recognized_text == expected_text) + self.assertEqual(recognized_text, expected_text) def test_read_document_pdf(self): + locale.setlocale(locale.LC_ALL, 'C') recognized_text = read_document(os.path.join(os.path.dirname(__file__), - "test_data", "sample2.pdf"), - "eng") + "test_data", "sample2.pdf"), + "eng") # print(recognized_text) self.assertTrue("Python Basics" in recognized_text) self.assertFalse("Java" in recognized_text) - diff --git a/requirements.txt b/requirements.txt index 2a50705a..2aa19b19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ requests pytesseract pillow wand +tesserocr \ No newline at end of file From 5d62a5a6ad7c8461a27b648ccd4676bf23edf851 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 20 Nov 2019 23:39:45 +0100 Subject: [PATCH 09/33] :art: Regroup imports Signed-off-by: mathieu.brunot --- erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py | 2 +- erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py index 45d08b6b..491e5de8 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py @@ -5,9 +5,9 @@ from __future__ import unicode_literals import frappe -import tesserocr from frappe.model.document import Document +import tesserocr @frappe.whitelist() diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 12573c6f..89c1cc1f 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -4,9 +4,10 @@ from __future__ import unicode_literals import frappe -from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_is_support from frappe.model.document import Document +from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_is_support + import os import io From 0f0699923c466a203ab61960683e4fc5207fcb7d Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 20 Nov 2019 23:41:26 +0100 Subject: [PATCH 10/33] :page_facing_up: Update license comments Signed-off-by: mathieu.brunot --- .../erpnext_ocr/doctype/ocr_language/test_ocr_language.py | 1 + erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py | 2 ++ erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py | 2 ++ erpnext_ocr/tests/test_tesseract.py | 1 + 4 files changed, 6 insertions(+) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py index 2fb53213..a066a56b 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2019, Monogramm and Contributors # See license.txt + from __future__ import unicode_literals import frappe diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 89c1cc1f..ee3ad116 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # Copyright (c) 2018, John Vincent Fiel and contributors +# Copyright (c) 2019, Monogramm and Contributors # For license information, please see license.txt from __future__ import unicode_literals + import frappe from frappe.model.document import Document diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py index 0f3fa12e..41511375 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Copyright (c) 2018, John Vincent Fiel and Contributors +# Copyright (c) 2019, Monogramm and Contributors # See license.txt + from __future__ import unicode_literals import frappe diff --git a/erpnext_ocr/tests/test_tesseract.py b/erpnext_ocr/tests/test_tesseract.py index fa02d329..a81109aa 100644 --- a/erpnext_ocr/tests/test_tesseract.py +++ b/erpnext_ocr/tests/test_tesseract.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2019, Monogramm and Contributors # See license.txt + from __future__ import unicode_literals import locale From 7f94d77b2110db76105af68f8f73757f5b63e5a6 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 20 Nov 2019 23:44:53 +0100 Subject: [PATCH 11/33] :truck: Rename lang availability function Signed-off-by: mathieu.brunot --- erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py | 4 ++-- .../erpnext_ocr/doctype/ocr_language/test_ocr_language.py | 4 ++-- erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py index 491e5de8..7507c098 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py @@ -12,11 +12,11 @@ @frappe.whitelist() def check_language_web(lang): - return "Yes" if lang_is_support(lang) else "No" + return "Yes" if lang_available(lang) else "No" @frappe.whitelist() -def lang_is_support(lang): +def lang_available(lang): if lang == 'en': lang = "eng" list_of_languages = tesserocr.get_languages()[1] diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py index a066a56b..dbe7e74e 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py @@ -7,12 +7,12 @@ import frappe import unittest -from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_is_support, check_language_web +from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_available, check_language_web class TestOCRLanguage(unittest.TestCase): def test_english_language(self): - decision = lang_is_support("en") + decision = lang_available("en") self.assertTrue(decision) def test_check_language_web(self): diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index ee3ad116..a2001edb 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -8,7 +8,7 @@ import frappe from frappe.model.document import Document -from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_is_support +from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_available import os import io @@ -33,7 +33,7 @@ def read_document(path, lang='eng'): if path is None: return None - if not lang_is_support(lang): + if not lang_available(lang): frappe.msgprint(frappe._("The selected language is not available. Please contact your administrator."), raise_exception=True) From 207efaf847180e27baefba6486dfd67225ca8b49 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Thu, 21 Nov 2019 01:35:19 +0100 Subject: [PATCH 12/33] :heavy_plus_sign: Add magickwand lib for tests Signed-off-by: mathieu.brunot --- .travis/Dockerfile.debian | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis/Dockerfile.debian b/.travis/Dockerfile.debian index 9b5d5753..d27125f9 100644 --- a/.travis/Dockerfile.debian +++ b/.travis/Dockerfile.debian @@ -31,6 +31,7 @@ RUN set -ex; \ sudo apt-get install -y --no-install-recommends \ ghostscript \ imagemagick \ + libmagickwand-dev \ tesseract-ocr \ libtesseract-dev \ libleptonica-dev \ From c9c9b79358ff28fbd82f0650f19db6803ab2f05a Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Thu, 21 Nov 2019 14:13:23 +0300 Subject: [PATCH 13/33] :lipstick: Improve progress evolution (#11) * :construction: Attempt of fix the progress bar Signed-off-by: Emil * :lipstick: Improve progress evolution Signed-off-by: mathieu.brunot --- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index a2001edb..55dc642b 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -53,6 +53,7 @@ def read_document(path, lang='eng'): # external link fullpath = requests.get(path, stream=True).raw + frappe.publish_realtime("ocr_progress_bar", {"progress": "0"}, user=frappe.session.user) text = " " if path.endswith('.pdf'): @@ -60,28 +61,32 @@ def read_document(path, lang='eng'): pdf = wi(filename=fullpath, resolution=300) pdf_image = pdf.convert('jpeg') i = 0 - size = len(pdf_image.sequence) + size = len(pdf_image.sequence) * 3 for img in pdf_image.sequence: img_page = wi(image=img) image_blob = img_page.make_blob('jpeg') + frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}, user=frappe.session.user) + i += 1 recognized_text = " " - image = Image.open(io.BytesIO(image_blob)) + frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}, user=frappe.session.user) + i += 1 + recognized_text = pytesseract.image_to_string(image, lang) text = text + recognized_text - - frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}) + frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}, user=frappe.session.user) i += 1 else: - frappe.publish_realtime("ocr_progress_bar", {"progress": "0"}, user=frappe.session.user) - image = Image.open(fullpath) + frappe.publish_realtime("ocr_progress_bar", {"progress": [33, 100]}, user=frappe.session.user) text = pytesseract.image_to_string(image, lang=lang) + frappe.publish_realtime("ocr_progress_bar", {"progress": [66, 100]}, user=frappe.session.user) text.split(" ") + frappe.publish_realtime("ocr_progress_bar", {"progress": [100, 100]}, user=frappe.session.user) return text From 7caf9efc072b688c12b1ee5dd323be6e6c93811f Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Thu, 21 Nov 2019 17:55:04 +0100 Subject: [PATCH 14/33] :sparkles: Init OCR Language support on load (#12) * :sparkles: Init OCR Language support on load Signed-off-by: mathieu.brunot * :bug: Fix OCR Language init Signed-off-by: mathieu.brunot * :white_check_mark: Add language tests Signed-off-by: mathieu.brunot * :art: Add comments and i18n calls Signed-off-by: mathieu.brunot --- .../doctype/ocr_language/ocr_language.js | 2 +- .../doctype/ocr_language/ocr_language.py | 11 ++++-- .../doctype/ocr_language/test_ocr_language.py | 36 +++++++++++++++---- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js index 81dc44bb..ca2d2455 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.js @@ -7,7 +7,7 @@ frappe.ui.form.on('OCR Language', "lang", function (frm) { args: { "lang": frm.doc.lang }, - method: "erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language.check_language_web", + method: "erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language.check_language", callback: function (r) { frm.set_value("is_supported", r.message); } diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py index 7507c098..51e004b0 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py @@ -11,12 +11,14 @@ @frappe.whitelist() -def check_language_web(lang): - return "Yes" if lang_available(lang) else "No" +def check_language(lang): + """Check a language availability. Returns a user friendly text.""" + return frappe._("Yes") if lang_available(lang) else frappe._("No") @frappe.whitelist() def lang_available(lang): + """Call Tesseract OCR to verify language is available.""" if lang == 'en': lang = "eng" list_of_languages = tesserocr.get_languages()[1] @@ -24,4 +26,7 @@ def lang_available(lang): class OCRLanguage(Document): - pass + def __init__(self, *args, **kwargs): + super(OCRLanguage, self).__init__(*args, **kwargs) + if self.code: + self.is_supported = check_language(self.code) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py index dbe7e74e..e29e8d39 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/test_ocr_language.py @@ -7,14 +7,36 @@ import frappe import unittest -from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_available, check_language_web +from erpnext_ocr.erpnext_ocr.doctype.ocr_language.ocr_language import lang_available, check_language class TestOCRLanguage(unittest.TestCase): - def test_english_language(self): - decision = lang_available("en") - self.assertTrue(decision) + def test_en_lang_available(self): + self.assertTrue(lang_available("en")) - def test_check_language_web(self): - decision = check_language_web("en") - self.assertEqual(decision, "Yes") + def test_eng_lang_available(self): + self.assertTrue(lang_available("eng")) + + def test_osd_lang_available(self): + self.assertTrue(lang_available("osd")) + + def test_equ_lang_available(self): + self.assertTrue(lang_available("equ")) + + def test_666_lang_available(self): + self.assertFalse(lang_available("666")) + + def test_en_check_language(self): + self.assertEqual(check_language("en"), frappe._("Yes")) + + def test_eng_check_language(self): + self.assertEqual(check_language("eng"), frappe._("Yes")) + + def test_osd_check_language(self): + self.assertEqual(check_language("osd"), frappe._("Yes")) + + def test_equ_check_language(self): + self.assertEqual(check_language("equ"), frappe._("Yes")) + + def test_666_check_language(self): + self.assertEqual(check_language("666"), frappe._("No")) From b6cfd0a75fca6955d27e55f35f2988b68eb9b1ab Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Thu, 21 Nov 2019 18:01:54 +0100 Subject: [PATCH 15/33] :art: Add var for Read publish event Signed-off-by: mathieu.brunot --- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 55dc642b..6c6fea08 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -24,7 +24,7 @@ def read_image(self): @frappe.whitelist() -def read_document(path, lang='eng'): +def read_document(path, lang='eng', event="ocr_progress_bar"): """Call Tesseract OCR to extract the text from a document.""" from PIL import Image import requests @@ -53,7 +53,7 @@ def read_document(path, lang='eng'): # external link fullpath = requests.get(path, stream=True).raw - frappe.publish_realtime("ocr_progress_bar", {"progress": "0"}, user=frappe.session.user) + frappe.publish_realtime(event, {"progress": "0"}, user=frappe.session.user) text = " " if path.endswith('.pdf'): @@ -65,28 +65,27 @@ def read_document(path, lang='eng'): for img in pdf_image.sequence: img_page = wi(image=img) image_blob = img_page.make_blob('jpeg') - frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}, user=frappe.session.user) + frappe.publish_realtime(event, {"progress": [i, size]}, user=frappe.session.user) i += 1 recognized_text = " " image = Image.open(io.BytesIO(image_blob)) - frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}, user=frappe.session.user) + frappe.publish_realtime(event, {"progress": [i, size]}, user=frappe.session.user) i += 1 recognized_text = pytesseract.image_to_string(image, lang) text = text + recognized_text - frappe.publish_realtime("ocr_progress_bar", {"progress": [i, size]}, user=frappe.session.user) + frappe.publish_realtime(event, {"progress": [i, size]}, user=frappe.session.user) i += 1 else: image = Image.open(fullpath) - frappe.publish_realtime("ocr_progress_bar", {"progress": [33, 100]}, user=frappe.session.user) + frappe.publish_realtime(event, {"progress": [33, 100]}, user=frappe.session.user) text = pytesseract.image_to_string(image, lang=lang) - frappe.publish_realtime("ocr_progress_bar", {"progress": [66, 100]}, user=frappe.session.user) + frappe.publish_realtime(event, {"progress": [66, 100]}, user=frappe.session.user) - text.split(" ") - frappe.publish_realtime("ocr_progress_bar", {"progress": [100, 100]}, user=frappe.session.user) + frappe.publish_realtime(event, {"progress": [100, 100]}, user=frappe.session.user) return text From 9ffafb637700eb8a11691904cc1f9ee519bcc0e1 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Thu, 21 Nov 2019 18:08:34 +0100 Subject: [PATCH 16/33] :globe_with_meridians: Add missing French translations Signed-off-by: mathieu.brunot --- erpnext_ocr/translations/fr.csv | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext_ocr/translations/fr.csv b/erpnext_ocr/translations/fr.csv index e19c1f4c..37cd3b48 100644 --- a/erpnext_ocr/translations/fr.csv +++ b/erpnext_ocr/translations/fr.csv @@ -2,9 +2,13 @@ apps/erpnext_ocr/config/desktop.py,ERPNext OCR,Reconnaissance optique de caract Doctype: OCR Language,OCR Language,Langue RCO Doctype: OCR Language,Code,Code Doctype: OCR Language,Language,Langue +Doctype: OCR Language,Is supported,Est supporté +apps/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py,Yes,Oui +apps/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.py,No,Non Doctype: OCR Read,OCR Read,Lecture RCO Doctype: OCR Read,Image or PDF to Read,Image ou PDF à lire Doctype: OCR Read,Language,Langue Doctype: OCR Read,Read file,Lire le fichier Doctype: OCR Read,Read Result,Résultat de la lecture -apps/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js,Reading the file,Lecture du fichier \ No newline at end of file +apps/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js,Reading the file,Lecture du fichier +apps/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py,The selected language is not available. Please contact your administrator.,La langue sélectionnée n'est pas disponible. Veuillez contacter votre administrateur. \ No newline at end of file From a2883b59468541ff1375e3eb40f0d150b501f92a Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Fri, 22 Nov 2019 16:48:30 +0100 Subject: [PATCH 17/33] :construction_worker: Fix coverage publication (#13) * :construction: Change coverage publication Signed-off-by: mathieu.brunot * :construction: Use root perms to install coveralls Signed-off-by: mathieu.brunot * :construction: Set dir to send coveralls Signed-off-by: mathieu.brunot * :construction: Use absolute path to coverage Signed-off-by: mathieu.brunot * :wrench: Update travis config Signed-off-by: mathieu.brunot * :bulb: Update frappe docs config Signed-off-by: mathieu.brunot * :white_check_mark: Test read with None path Signed-off-by: mathieu.brunot * :white_check_mark: Test OCR Read method Signed-off-by: mathieu.brunot * :construction: Send coveralls from travis Signed-off-by: mathieu.brunot * :art: Code auto format Signed-off-by: mathieu.brunot * :white_check_mark: Test force_attach_file_doc Signed-off-by: mathieu.brunot * :construction: Send coverage from docker Signed-off-by: mathieu.brunot * :green_heart: Improve CI tests Signed-off-by: mathieu.brunot * :art: Add empty lines Signed-off-by: mathieu.brunot * :green_heart: Comment in progress tests Signed-off-by: mathieu.brunot * :green_heart: Increase wait time for tests Signed-off-by: mathieu.brunot * :white_check_mark: Test Frappe apps config files Signed-off-by: mathieu.brunot * :art: Remove unused imports Signed-off-by: mathieu.brunot * :art: Remove trailing whitespace Signed-off-by: mathieu.brunot * :green_heart: Remove unicode import Signed-off-by: mathieu.brunot --- .travis.yml | 10 ++- .travis/docker_test.sh | 2 +- erpnext_ocr/config/docs.py | 17 +++-- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 27 ++++--- .../doctype/ocr_read/test_ocr_read.py | 75 +++++++++++++++++-- erpnext_ocr/tests/test_config_desktop.py | 16 ++++ erpnext_ocr/tests/test_config_docs.py | 24 ++++++ erpnext_ocr/tests/test_tesseract.py | 31 ++++++-- 8 files changed, 169 insertions(+), 33 deletions(-) create mode 100644 erpnext_ocr/tests/test_config_desktop.py create mode 100644 erpnext_ocr/tests/test_config_docs.py diff --git a/.travis.yml b/.travis.yml index e437cc82..07c35d21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ branches: before_script: - env | sort + - home=$(pwd) - dir=".travis" - export IMAGE_NAME=docker-erpnext-ext:erpnext_ocr-travis - export BUILD_BRANCH=${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH}} @@ -42,7 +43,7 @@ script: - docker-compose -f docker-compose.${DATABASE}.yml ps "erpnext_app" | grep "Up" - docker-compose -f docker-compose.${DATABASE}.yml logs "erpnext_web" - docker-compose -f docker-compose.${DATABASE}.yml ps "erpnext_web" | grep "Up" - - echo 'Wait until test finished (1-2 minutes)' && sleep 90 + - echo 'Wait until test finished (3 minutes)' && sleep 180 - docker-compose -f docker-compose.${DATABASE}.yml logs "sut" - docker-compose -f docker-compose.${DATABASE}.yml ps "sut" | grep "Exit 0" # Test container restart @@ -55,10 +56,15 @@ script: - docker-compose -f docker-compose.${DATABASE}.yml ps "erpnext_app" | grep "Up" - docker-compose -f docker-compose.${DATABASE}.yml logs "erpnext_web" - docker-compose -f docker-compose.${DATABASE}.yml ps "erpnext_web" | grep "Up" - - echo 'Wait until test finished (1-2 minutes)' && sleep 90 + - echo 'Wait until test finished (3 minutes)' && sleep 180 - docker-compose -f docker-compose.${DATABASE}.yml logs "sut" - docker-compose -f docker-compose.${DATABASE}.yml ps "sut" | grep "Exit 0" +#after_script: +# - cd "$home" +# - sudo pip install python-coveralls +# - coveralls -b "$home" -d /srv/erpnext/frappe/sites/.coverage + notifications: email: false diff --git a/.travis/docker_test.sh b/.travis/docker_test.sh index 248a26cb..8d2fefff 100644 --- a/.travis/docker_test.sh +++ b/.travis/docker_test.sh @@ -90,7 +90,7 @@ fi if [ -f ./sites/.coverage ]; then echo "Sending Unit Tests coverage of '${FRAPPE_APP_TO_TEST}' app to Coveralls..." - coveralls -b "$(pwd)/apps/${FRAPPE_APP_TO_TEST}" -d ./sites/.coverage + coveralls -b "$(pwd)/apps/${FRAPPE_APP_TO_TEST}" -d "$(pwd)/sites/.coverage" fi diff --git a/erpnext_ocr/config/docs.py b/erpnext_ocr/config/docs.py index 99d0aa9f..03a77140 100644 --- a/erpnext_ocr/config/docs.py +++ b/erpnext_ocr/config/docs.py @@ -2,14 +2,15 @@ Configuration for docs """ -# source_link = "https://github.com/[org_name]/erpnext_ocr" -# docs_base_url = "https://[org_name].github.io/erpnext_ocr" -# headline = "App that does everything" -# sub_heading = "Yes, you got that right the first time, everything" +source_link = "https://github.com/Monogramm/erpnext_ocr" +docs_base_url = "/docs" +# docs_base_url = "https://Monogramm.github.io/erpnext_ocr" +headline = "ERPNext OCR Integration" +sub_heading = "Optical Character Recognition using tesseract within ERPNext" def get_context(context): context.brand_html = "ERPNext OCR" - context.source_link = "https://github.com/Monogramm/erpnext_ocr" - context.docs_base_url = "https://github.com/Monogramm/erpnext_ocr" - context.headline = "OCR Integration" - context.sub_heading = "Optical Character Recognition using tesseract within ERPNext" + context.source_link = source_link + context.docs_base_url = docs_base_url + context.headline = headline + context.sub_heading = sub_heading diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 6c6fea08..6b1f0270 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -16,11 +16,12 @@ class OCRRead(Document): def read_image(self): - message = read_document(self.file_to_read, self.language or 'eng') + text = read_document(self.file_to_read, self.language or 'eng') - self.read_result = message + self.read_result = text self.save() - return message + + return text @frappe.whitelist() @@ -65,27 +66,33 @@ def read_document(path, lang='eng', event="ocr_progress_bar"): for img in pdf_image.sequence: img_page = wi(image=img) image_blob = img_page.make_blob('jpeg') - frappe.publish_realtime(event, {"progress": [i, size]}, user=frappe.session.user) + frappe.publish_realtime( + event, {"progress": [i, size]}, user=frappe.session.user) i += 1 recognized_text = " " image = Image.open(io.BytesIO(image_blob)) - frappe.publish_realtime(event, {"progress": [i, size]}, user=frappe.session.user) + frappe.publish_realtime( + event, {"progress": [i, size]}, user=frappe.session.user) i += 1 recognized_text = pytesseract.image_to_string(image, lang) text = text + recognized_text - frappe.publish_realtime(event, {"progress": [i, size]}, user=frappe.session.user) + frappe.publish_realtime( + event, {"progress": [i, size]}, user=frappe.session.user) i += 1 else: image = Image.open(fullpath) - frappe.publish_realtime(event, {"progress": [33, 100]}, user=frappe.session.user) + frappe.publish_realtime( + event, {"progress": [33, 100]}, user=frappe.session.user) text = pytesseract.image_to_string(image, lang=lang) - frappe.publish_realtime(event, {"progress": [66, 100]}, user=frappe.session.user) + frappe.publish_realtime( + event, {"progress": [66, 100]}, user=frappe.session.user) - frappe.publish_realtime(event, {"progress": [100, 100]}, user=frappe.session.user) + frappe.publish_realtime( + event, {"progress": [100, 100]}, user=frappe.session.user) return text @@ -107,4 +114,4 @@ def force_attach_file_doc(filename, name): attachment_doc.insert() frappe.db.sql( - """UPDATE `tabOCR Read` SET file_to_read=%s WHERE name=%s""", (file_url, name)) \ No newline at end of file + """UPDATE `tabOCR Read` SET file_to_read=%s WHERE name=%s""", (file_url, name)) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py index 41511375..66367b05 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py @@ -9,6 +9,8 @@ import unittest import os +from erpnext_ocr.erpnext_ocr.doctype.ocr_read.ocr_read import force_attach_file_doc + def create_ocr_reads(): if frappe.flags.test_ocr_reads_created: @@ -23,6 +25,14 @@ def create_ocr_reads(): "language": "eng" }).insert() + frappe.get_doc({ + "doctype": "OCR Read", + "file_to_read": os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "Picture_010.png"), + "language": "eng" + }).insert() + frappe.get_doc({ "doctype": "OCR Read", "file_to_read": os.path.join(os.path.dirname(__file__), @@ -55,13 +65,68 @@ def setUp(self): def tearDown(self): delete_ocr_reads() - # TODO: Read content of files and check recognised text - #def test_ocr_read_image(self): - # frappe.set_user("Administrator") + def test_ocr_read_image(self): + frappe.set_user("Administrator") + doc = frappe.get_doc({ + "doctype": "OCR Read", + "file_to_read": os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "sample1.jpg"), + "language": "eng" + }) + + recognized_text = doc.read_image() + self.assertEqual(recognized_text, doc.read_result) + + self.assertIn("The quick brown fox", recognized_text) + self.assertIn("jumped over the 5", recognized_text) + self.assertIn("lazy dogs!", recognized_text) + self.assertNotIn("And an elephant!", recognized_text) + + + def test_ocr_read_pdf(self): + frappe.set_user("Administrator") + doc = frappe.get_doc({ + "doctype": "OCR Read", + "file_to_read": os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "sample2.pdf"), + "language": "eng" + }) + + recognized_text = doc.read_image() + + # FIXME values are not equal on Alpine ??! + # self.assertEqual(recognized_text, doc.read_result) + + self.assertIn("Python Basics", recognized_text) + self.assertNotIn("Java", recognized_text) + + + def test_force_attach_file_doc(self): + doc = frappe.get_doc({ + "doctype": "OCR Read", + "file_to_read": os.path.join(os.path.dirname(__file__), + os.path.pardir, os.path.pardir, os.path.pardir, + "tests", "test_data", "Picture_010.png"), + "language": "eng" + }) + + force_attach_file_doc('test.tif', doc.name) + + forced_doc = frappe.get_doc({ + "doctype": "OCR Read", + "name": doc.name, + "language": "eng" + }) + + self.assertEqual(forced_doc.name, doc.name) + + # FIXME force_attach_file_doc does not work ? + # print(doc.file_to_read) + # self.assertTrue('/private/files/test.tif' in doc.file_to_read) - #def test_ocr_read_pdf(self): - # frappe.set_user("Administrator") def test_ocr_read_list(self): # frappe.set_user("test1@example.com") diff --git a/erpnext_ocr/tests/test_config_desktop.py b/erpnext_ocr/tests/test_config_desktop.py new file mode 100644 index 00000000..c29438b1 --- /dev/null +++ b/erpnext_ocr/tests/test_config_desktop.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Monogramm and Contributors +# See license.txt + +from __future__ import unicode_literals + +import unittest + +from erpnext_ocr.config.desktop import get_data + + +class TestDesktop(unittest.TestCase): + def test_get_data(self): + data = get_data() + + self.assertIsNotNone(data) diff --git a/erpnext_ocr/tests/test_config_docs.py b/erpnext_ocr/tests/test_config_docs.py new file mode 100644 index 00000000..546cba90 --- /dev/null +++ b/erpnext_ocr/tests/test_config_docs.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Monogramm and Contributors +# See license.txt + +import unittest + +from erpnext_ocr.config.docs import get_context + + +class TestDocs(unittest.TestCase): + def test_get_context(self): + context = type('obj', (object,), {'brand_html' : None, + 'source_link' : None, + 'docs_base_url' : None, + 'headline' : None, + 'sub_heading' : None}) + get_context(context) + + self.assertIsNotNone(context) + self.assertIsNotNone(context.brand_html) + self.assertIsNotNone(context.source_link) + self.assertIsNotNone(context.docs_base_url) + self.assertIsNotNone(context.headline) + self.assertIsNotNone(context.sub_heading) diff --git a/erpnext_ocr/tests/test_tesseract.py b/erpnext_ocr/tests/test_tesseract.py index a81109aa..3164ca25 100644 --- a/erpnext_ocr/tests/test_tesseract.py +++ b/erpnext_ocr/tests/test_tesseract.py @@ -14,13 +14,19 @@ class TestTesseract(unittest.TestCase): + def test_read_document_path_none(self): + locale.setlocale(locale.LC_ALL, 'C') + recognized_text = read_document(None) + + self.assertIsNone(recognized_text) + def test_read_document_lang_not_supported(self): locale.setlocale(locale.LC_ALL, 'C') self.assertRaises(frappe.ValidationError, read_document, os.path.join(os.path.dirname(__file__),"test_data", "sample1.jpg"), "xxx") - def test_read_document_image(self): + def test_read_document_image_jpg(self): locale.setlocale(locale.LC_ALL, 'C') recognized_text = read_document(os.path.join(os.path.dirname(__file__), "test_data", "sample1.jpg"), @@ -28,10 +34,10 @@ def test_read_document_image(self): # print(recognized_text) - self.assertTrue("The quick brown fox" in recognized_text) - self.assertTrue("jumped over the 5" in recognized_text) - self.assertTrue("lazy dogs!" in recognized_text) - self.assertFalse("And an elephant!" in recognized_text) + self.assertIn("The quick brown fox", recognized_text) + self.assertIn("jumped over the 5", recognized_text) + self.assertIn("lazy dogs!", recognized_text) + self.assertNotIn("And an elephant!", recognized_text) file = open(os.path.join(os.path.dirname(__file__), "test_data", "sample1_output.txt"), "r") @@ -39,6 +45,17 @@ def test_read_document_image(self): self.assertEqual(recognized_text, expected_text) + def test_read_document_image_png(self): + locale.setlocale(locale.LC_ALL, 'C') + recognized_text = read_document(os.path.join(os.path.dirname(__file__), + "test_data", "Picture_010.png"), + "eng") + + # print(recognized_text) + + self.assertIn("Brawn Manufacture", recognized_text) + self.assertNotIn("And an elephant!", recognized_text) + def test_read_document_pdf(self): locale.setlocale(locale.LC_ALL, 'C') recognized_text = read_document(os.path.join(os.path.dirname(__file__), @@ -47,5 +64,5 @@ def test_read_document_pdf(self): # print(recognized_text) - self.assertTrue("Python Basics" in recognized_text) - self.assertFalse("Java" in recognized_text) + self.assertIn("Python Basics", recognized_text) + self.assertNotIn("Java", recognized_text) From 8cb712bbf714d49eb8c246f12e7d75d577e55018 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Fri, 22 Nov 2019 17:08:41 +0100 Subject: [PATCH 18/33] :construction: Add test record generation Signed-off-by: mathieu.brunot --- .../doctype/ocr_read/test_ocr_read.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py index 66367b05..65928399 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py @@ -11,6 +11,31 @@ from erpnext_ocr.erpnext_ocr.doctype.ocr_read.ocr_read import force_attach_file_doc +# TODO Frappe default test records creation +#def _make_test_records(verbose): +# from frappe.test_runner import make_test_objects +# +# docs = [ +# # [file_to_read, language] +# [os.path.join(os.path.dirname(__file__), +# os.path.pardir, os.path.pardir, os.path.pardir, +# "tests", "test_data", "sample1.jpg"), "eng"], +# [os.path.join(os.path.dirname(__file__), +# os.path.pardir, os.path.pardir, os.path.pardir, +# "tests", "test_data", "Picture_010.png"), "eng"], +# [os.path.join(os.path.dirname(__file__), +# os.path.pardir, os.path.pardir, os.path.pardir, +# "tests", "test_data", "sample2.pdf"), "eng"], +# ] +# +# test_objects = make_test_objects("OCR Read", [{ +# "doctype": "OCR Read", +# "file_to_read": file_to_read, +# "language": language +# } for file_to_read, language in docs]) +# +# return test_objects + def create_ocr_reads(): if frappe.flags.test_ocr_reads_created: @@ -98,7 +123,8 @@ def test_ocr_read_pdf(self): recognized_text = doc.read_image() # FIXME values are not equal on Alpine ??! - # self.assertEqual(recognized_text, doc.read_result) + #self.maxDiff = None + #self.assertEqual(recognized_text, doc.read_result) self.assertIn("Python Basics", recognized_text) self.assertNotIn("Java", recognized_text) From 3db72c2d6374c5a5e250f041715e9480eb21c99c Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Fri, 22 Nov 2019 17:34:43 +0100 Subject: [PATCH 19/33] :white_check_mark: Test force attach file Signed-off-by: mathieu.brunot --- .../erpnext_ocr/doctype/ocr_read/test_ocr_read.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py index 65928399..eed02290 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py @@ -143,15 +143,13 @@ def test_force_attach_file_doc(self): forced_doc = frappe.get_doc({ "doctype": "OCR Read", - "name": doc.name, + #"name": doc.name, + "file_to_read": "/private/files/test.tif", "language": "eng" }) - + self.assertIsNotNone(forced_doc) self.assertEqual(forced_doc.name, doc.name) - - # FIXME force_attach_file_doc does not work ? - # print(doc.file_to_read) - # self.assertTrue('/private/files/test.tif' in doc.file_to_read) + self.assertEqual('/private/files/test.tif', forced_doc.file_to_read) def test_ocr_read_list(self): From a3e35799edbe9c1f53289aad308bc5304a0cb173 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Fri, 22 Nov 2019 17:48:46 +0100 Subject: [PATCH 20/33] :art: Fix indent for future function Signed-off-by: mathieu.brunot --- .../doctype/ocr_read/test_ocr_read.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py index eed02290..ed29ccf8 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/test_ocr_read.py @@ -13,20 +13,20 @@ # TODO Frappe default test records creation #def _make_test_records(verbose): -# from frappe.test_runner import make_test_objects +# from frappe.test_runner import make_test_objects # -# docs = [ -# # [file_to_read, language] -# [os.path.join(os.path.dirname(__file__), +# docs = [ +# # [file_to_read, language] +# [os.path.join(os.path.dirname(__file__), # os.path.pardir, os.path.pardir, os.path.pardir, # "tests", "test_data", "sample1.jpg"), "eng"], -# [os.path.join(os.path.dirname(__file__), +# [os.path.join(os.path.dirname(__file__), # os.path.pardir, os.path.pardir, os.path.pardir, # "tests", "test_data", "Picture_010.png"), "eng"], -# [os.path.join(os.path.dirname(__file__), +# [os.path.join(os.path.dirname(__file__), # os.path.pardir, os.path.pardir, os.path.pardir, # "tests", "test_data", "sample2.pdf"), "eng"], -# ] +# ] # # test_objects = make_test_objects("OCR Read", [{ # "doctype": "OCR Read", @@ -34,7 +34,7 @@ # "language": language # } for file_to_read, language in docs]) # -# return test_objects +# return test_objects def create_ocr_reads(): From 1b2af72ded7e7374ff584ea7804ab0a80e0796a0 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Fri, 22 Nov 2019 18:36:53 +0100 Subject: [PATCH 21/33] :memo: Fix images in doc Signed-off-by: mathieu.brunot --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a647dd77..caa3ca9e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ See [Taiga.io](https://tree.taiga.io/project/monogrammbot-monogrammerpnext_ocr/ **Pre-requisites: tesseract-python and imagemagick** Install tesseract-ocr, plus imagemagick and ghostscript (to work with pdf files) using this command on Debian: - ``` + ```sh sudo apt-get install tesseract-ocr imagemagick libmagickwand-dev ghostscript ``` @@ -50,14 +50,14 @@ When installing Frappe app, the following python requirements will be installed: ## :rocket: Usage -**Sample Screenshot**: +**File Being Read**: -![Sample Screenshot](./erpnext_ocr/tests/test_data/Picture_010.png) +![File Being Read](./erpnext_ocr/tests/test_data/Picture_010.png) +**Sample Screenshot**: -**File Being Read**: +![Sample Screenshot](./erpnext_ocr/tests/test_data/Picture_010_screenshot.png) -![Sample Screenshot 2](./erpnext_ocr/tests/test_data/Picture_010_screenshot.png) ### Tesseract trained data From babd672704c7d8584aeb2563c3ddf75393bf27ff Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 00:46:41 +0100 Subject: [PATCH 22/33] :wrench: Disallow Language quick entry Signed-off-by: mathieu.brunot --- erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json index 4d53a426..755cb0bf 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json @@ -153,7 +153,7 @@ "write": 1 } ], - "quick_entry": 1, + "quick_entry": 0, "read_only": 0, "read_only_onload": 0, "search_fields": "code,lang", From f21e22399596fc785bcd239ad2f1ba428bd976ed Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 00:50:47 +0100 Subject: [PATCH 23/33] :wrench: Set document types Signed-off-by: mathieu.brunot --- .../erpnext_ocr/doctype/ocr_language/ocr_language.json | 4 ++-- erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json index 755cb0bf..7ea021a1 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_language/ocr_language.json @@ -10,7 +10,7 @@ "custom": 0, "docstatus": 0, "doctype": "DocType", - "document_type": "", + "document_type": "Setup", "editable_grid": 1, "engine": "InnoDB", "fields": [ @@ -126,7 +126,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-11-08 09:26:39.984652", + "modified": "2019-11-27 00:49:39.776311", "modified_by": "Administrator", "module": "ERPNext OCR", "name": "OCR Language", diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json index 1a12de80..db4b0550 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json @@ -10,7 +10,7 @@ "custom": 0, "docstatus": 0, "doctype": "DocType", - "document_type": "", + "document_type": "Document", "editable_grid": 1, "engine": "InnoDB", "fields": [ @@ -159,7 +159,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-08-17 00:52:41.003953", + "modified": "2019-11-27 00:49:27.451948", "modified_by": "Administrator", "module": "ERPNext OCR", "name": "OCR Read", From 296a27f3b33db62889b4258fb18b5d773e2d69ad Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 00:58:44 +0100 Subject: [PATCH 24/33] :wrench: All users rights to OCR Read Signed-off-by: mathieu.brunot --- .../doctype/ocr_read/ocr_read.json | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json index db4b0550..00a84b62 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json @@ -159,7 +159,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-11-27 00:49:27.451948", + "modified": "2019-11-27 00:57:49.229313", "modified_by": "Administrator", "module": "ERPNext OCR", "name": "OCR Read", @@ -179,6 +179,25 @@ "print": 1, "read": 1, "report": 1, + "role": "All", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, "role": "System Manager", "set_user_permissions": 0, "share": 1, From 23bd76bed04c517bc632cfe4eeb0e7f8873d44dd Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 02:17:08 +0100 Subject: [PATCH 25/33] :children_crossing: Display read popup sooner Signed-off-by: mathieu.brunot --- erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index 6b1f0270..de1eaab2 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -38,6 +38,8 @@ def read_document(path, lang='eng', event="ocr_progress_bar"): frappe.msgprint(frappe._("The selected language is not available. Please contact your administrator."), raise_exception=True) + frappe.publish_realtime(event, {"progress": "0"}, user=frappe.session.user) + if path.startswith('/assets/'): # from public folder fullpath = os.path.abspath(path) @@ -54,7 +56,6 @@ def read_document(path, lang='eng', event="ocr_progress_bar"): # external link fullpath = requests.get(path, stream=True).raw - frappe.publish_realtime(event, {"progress": "0"}, user=frappe.session.user) text = " " if path.endswith('.pdf'): From 8967e7d6797a22b010169a4449bdf0615e8d146a Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Wed, 27 Nov 2019 21:26:18 +0100 Subject: [PATCH 26/33] :zap: Performance improvements (#9) * :construction: Rework to improve OCR read memory Signed-off-by: mathieu.brunot * :art: Fix code quality Signed-off-by: mathieu.brunot * :zap: Fix double loop Signed-off-by: mathieu.brunot * :chart_with_upward_trend: Add profiling of Unit Tests Signed-off-by: mathieu.brunot * :chart_with_upward_trend: Add profiling for v10 Signed-off-by: mathieu.brunot * :loud_sound: Display recogniszed text in tests Signed-off-by: mathieu.brunot * :art: Improve test on images Signed-off-by: mathieu.brunot * :construction: Force CI to run on branch Signed-off-by: mathieu.brunot * :zap: Use tesserocr instead of pytesseract Signed-off-by: mathieu.brunot * :construction: Remove CI for temp perf/read Signed-off-by: mathieu.brunot * :zap: Use Tesseract API Signed-off-by: mathieu.brunot * :green_heart: Fix full text check Signed-off-by: mathieu.brunot * :bug: Fix OCR Read for image Signed-off-by: mathieu.brunot * :construction: Build Tesserect locally Signed-off-by: mathieu.brunot * :construction: Build tesseract for alpine Signed-off-by: mathieu.brunot * :green_heart: Force CI on perf branch Signed-off-by: mathieu.brunot * :heavy_plus_sign: Add build dependency for Tesseract Signed-off-by: mathieu.brunot * :construction: Use root to build tesseract Signed-off-by: mathieu.brunot * :rewind: Restore official tesseract build Signed-off-by: mathieu.brunot * :sparkles: OCR Settings Signed-off-by: mathieu.brunot * :art: Fix indentiation and format Signed-off-by: mathieu.brunot * :white_check_mark: Add test on OCR Settings validate Signed-off-by: mathieu.brunot * :mute: Disable profile display Signed-off-by: mathieu.brunot * :green_heart: Remove hard CI for perf/read Signed-off-by: mathieu.brunot * :white_check_mark: Test OCR Read of HTTP image Signed-off-by: mathieu.brunot * :green_heart: Fix HTTP test Signed-off-by: mathieu.brunot --- .travis/Dockerfile.alpine | 1 + .travis/docker_test.sh | 18 +++- README.md | 2 +- .../erpnext_ocr/doctype/ocr_read/ocr_read.py | 73 ++++++++------ .../doctype/ocr_settings/__init__.py | 0 .../doctype/ocr_settings/ocr_settings.js | 8 ++ .../doctype/ocr_settings/ocr_settings.json | 97 +++++++++++++++++++ .../doctype/ocr_settings/ocr_settings.py | 17 ++++ .../doctype/ocr_settings/test_ocr_settings.js | 23 +++++ .../doctype/ocr_settings/test_ocr_settings.py | 23 +++++ erpnext_ocr/tests/README.md | 4 +- erpnext_ocr/tests/test_tesseract.py | 21 +++- erpnext_ocr/translations/fr.csv | 4 +- requirements.txt | 1 - 14 files changed, 247 insertions(+), 45 deletions(-) create mode 100644 erpnext_ocr/erpnext_ocr/doctype/ocr_settings/__init__.py create mode 100644 erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.js create mode 100644 erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.json create mode 100644 erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.py create mode 100644 erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.js create mode 100644 erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.py diff --git a/.travis/Dockerfile.alpine b/.travis/Dockerfile.alpine index d4c74634..bc67dbbe 100644 --- a/.travis/Dockerfile.alpine +++ b/.travis/Dockerfile.alpine @@ -10,6 +10,7 @@ RUN set -ex; \ ENV DOCKER_TAG=travis \ DOCKER_VCS_REF=${TRAVIS_COMMIT} \ DOCKER_BUILD_DATE=${TRAVIS_BUILD_NUMBER} \ + LANG=C.UTF-8 \ LC_ALL=C # Copy the whole repository to app folder for manual install diff --git a/.travis/docker_test.sh b/.travis/docker_test.sh index 8d2fefff..7c2571c4 100644 --- a/.travis/docker_test.sh +++ b/.travis/docker_test.sh @@ -58,18 +58,21 @@ echo "Preparing Frappe application '${FRAPPE_APP_TO_TEST}' tests..." # https://frappe.io/docs/user/en/guides/automated-testing/unit-testing FRAPPE_APP_UNIT_TEST_REPORT="$(pwd)/sites/.${FRAPPE_APP_TO_TEST}_unit_tests.xml" +FRAPPE_APP_UNIT_TEST_PROFILE="$(pwd)/sites/.${FRAPPE_APP_TO_TEST}_unit_tests.prof" #bench run-tests --help echo "Executing Unit Tests of '${FRAPPE_APP_TO_TEST}' app..." if [ "${TEST_VERSION}" = "10" ]; then bench run-tests \ - --app ${FRAPPE_APP_TO_TEST} \ - --junit-xml-output "${FRAPPE_APP_UNIT_TEST_REPORT}" + --app "${FRAPPE_APP_TO_TEST}" \ + --junit-xml-output "${FRAPPE_APP_UNIT_TEST_REPORT}" \ + --profile > "${FRAPPE_APP_UNIT_TEST_PROFILE}" else bench run-tests \ - --app ${FRAPPE_APP_TO_TEST} \ - --coverage + --app "${FRAPPE_APP_TO_TEST}" \ + --coverage \ + --profile > "${FRAPPE_APP_UNIT_TEST_PROFILE}" # FIXME https://github.com/frappe/frappe/issues/8809 # --junit-xml-output "${FRAPPE_APP_UNIT_TEST_REPORT}" fi @@ -93,6 +96,13 @@ if [ -f ./sites/.coverage ]; then coveralls -b "$(pwd)/apps/${FRAPPE_APP_TO_TEST}" -d "$(pwd)/sites/.coverage" fi +if [ -f "${FRAPPE_APP_UNIT_TEST_PROFILE}" ]; then + echo "Checking Frappe application '${FRAPPE_APP_TO_TEST}' unit tests profile..." + + # XXX Are there any online services that could receive and display profiles? + #cat "${FRAPPE_APP_UNIT_TEST_PROFILE}" +fi + ################################################################################ # TODO QUnit (JS) Unit tests diff --git a/README.md b/README.md index caa3ca9e..87042320 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ bench install-app erpnext_ocr ``` When installing Frappe app, the following python requirements will be installed: -* python binding for tesseract, [pytesseract](https://pypi.org/project/pytesseract/) +* python binding for tesseract, [tesserocr](https://pypi.org/project/tesserocr/) * image processing library in python, [pillow](https://pypi.org/project/Pillow/) * HTTP library in python, [requests](https://pypi.org/project/requests/) * python binding for imagemagick, [wand](https://pypi.org/project/Wand/) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py index de1eaab2..415f48ad 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py @@ -29,7 +29,7 @@ def read_document(path, lang='eng', event="ocr_progress_bar"): """Call Tesseract OCR to extract the text from a document.""" from PIL import Image import requests - import pytesseract + import tesserocr if path is None: return None @@ -56,41 +56,50 @@ def read_document(path, lang='eng', event="ocr_progress_bar"): # external link fullpath = requests.get(path, stream=True).raw - text = " " - - if path.endswith('.pdf'): - from wand.image import Image as wi - pdf = wi(filename=fullpath, resolution=300) - pdf_image = pdf.convert('jpeg') - i = 0 - size = len(pdf_image.sequence) * 3 - for img in pdf_image.sequence: - img_page = wi(image=img) - image_blob = img_page.make_blob('jpeg') - frappe.publish_realtime( - event, {"progress": [i, size]}, user=frappe.session.user) - i += 1 + ocr = frappe.get_doc("OCR Settings") - recognized_text = " " - image = Image.open(io.BytesIO(image_blob)) + text = " " + with tesserocr.PyTessBaseAPI(lang=lang) as api: + + if path.endswith('.pdf'): + from wand.image import Image as wi + + # https://stackoverflow.com/questions/43072050/pyocr-with-tesseract-runs-out-of-memory + with wi(filename=fullpath, resolution=ocr.pdf_resolution) as pdf: + pdf_image = pdf.convert('jpeg') + i = 0 + size = len(pdf_image.sequence) * 3 + + for img in pdf_image.sequence: + with wi(image=img) as img_page: + image_blob = img_page.make_blob('jpeg') + frappe.publish_realtime( + event, {"progress": [i, size]}, user=frappe.session.user) + i += 1 + + recognized_text = " " + + image = Image.open(io.BytesIO(image_blob)) + api.SetImage(image) + frappe.publish_realtime( + event, {"progress": [i, size]}, user=frappe.session.user) + i += 1 + + recognized_text = api.GetUTF8Text() + text = text + recognized_text + frappe.publish_realtime( + event, {"progress": [i, size]}, user=frappe.session.user) + i += 1 + + else: + image = Image.open(fullpath) + api.SetImage(image) frappe.publish_realtime( - event, {"progress": [i, size]}, user=frappe.session.user) - i += 1 + event, {"progress": [33, 100]}, user=frappe.session.user) - recognized_text = pytesseract.image_to_string(image, lang) - text = text + recognized_text + text = api.GetUTF8Text() frappe.publish_realtime( - event, {"progress": [i, size]}, user=frappe.session.user) - i += 1 - - else: - image = Image.open(fullpath) - frappe.publish_realtime( - event, {"progress": [33, 100]}, user=frappe.session.user) - - text = pytesseract.image_to_string(image, lang=lang) - frappe.publish_realtime( - event, {"progress": [66, 100]}, user=frappe.session.user) + event, {"progress": [66, 100]}, user=frappe.session.user) frappe.publish_realtime( event, {"progress": [100, 100]}, user=frappe.session.user) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/__init__.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.js new file mode 100644 index 00000000..8fa0ce80 --- /dev/null +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Monogramm and contributors +// For license information, please see license.txt + +frappe.ui.form.on('OCR Settings', { + refresh: function(frm) { + + } +}); diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.json b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.json new file mode 100644 index 00000000..4f32463f --- /dev/null +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.json @@ -0,0 +1,97 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2019-11-27 03:06:27.072918", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "200", + "fetch_if_empty": 0, + "fieldname": "pdf_resolution", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "PDF Resolution", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2019-11-27 03:35:39.949086", + "modified_by": "Administrator", + "module": "ERPNext OCR", + "name": "OCR Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.py new file mode 100644 index 00000000..18fabc5b --- /dev/null +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Monogramm and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document + + +class OCRSettings(Document): + def validate(self): + if not self.pdf_resolution > 0: + frappe.throw( + _("PDF Resolution must be a positive integer, eg 300 (high) or 200 (normal).")) + + return diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.js b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.js new file mode 100644 index 00000000..95f8fa2f --- /dev/null +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: OCR Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new OCR Settings + () => frappe.tests.make('OCR Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.py b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.py new file mode 100644 index 00000000..8b214667 --- /dev/null +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/test_ocr_settings.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Monogramm and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + + +class TestOCRSettings(unittest.TestCase): + def test_validate(self): + ocr = frappe.get_doc("OCR Settings") + ocr.pdf_resolution = 300 + ocr.validate() + self.assertEqual(300, ocr.pdf_resolution) + + def test_validate_invalid_pdf_resolution(self): + ocr = frappe.get_doc("OCR Settings") + ocr.pdf_resolution = -1 + self.assertRaises(frappe.ValidationError, ocr.validate) + + ocr.pdf_resolution = 0 + self.assertRaises(frappe.ValidationError, ocr.validate) diff --git a/erpnext_ocr/tests/README.md b/erpnext_ocr/tests/README.md index 54f75093..8d8409cc 100644 --- a/erpnext_ocr/tests/README.md +++ b/erpnext_ocr/tests/README.md @@ -6,9 +6,9 @@ Examples to implement OCR(Optical Character Recognition) using tesseract using P ``` sudo apt-get install tesseract-ocr ``` -- Install python binding for tesseract, pytesseract, using this pip command: +- Install python binding for tesseract, tesserocr, using this pip command: ``` - pip install pytesseract + pip install tesserocr ``` - Install image processing library in python, pillow using this pip command: ``` diff --git a/erpnext_ocr/tests/test_tesseract.py b/erpnext_ocr/tests/test_tesseract.py index 3164ca25..03d2bb7c 100644 --- a/erpnext_ocr/tests/test_tesseract.py +++ b/erpnext_ocr/tests/test_tesseract.py @@ -26,13 +26,25 @@ def test_read_document_lang_not_supported(self): os.path.join(os.path.dirname(__file__),"test_data", "sample1.jpg"), "xxx") + def test_read_document_image_http(self): + locale.setlocale(locale.LC_ALL, 'C') + recognized_text = read_document("https://github.com/Monogramm/erpnext_ocr/raw/develop/erpnext_ocr/tests/test_data/sample1.jpg", + "eng") + + # print("recognized_text=" + recognized_text) + + self.assertIn("The quick brown fox", recognized_text) + self.assertIn("jumped over the 5", recognized_text) + self.assertIn("lazy dogs!", recognized_text) + self.assertNotIn("And an elephant!", recognized_text) + def test_read_document_image_jpg(self): locale.setlocale(locale.LC_ALL, 'C') recognized_text = read_document(os.path.join(os.path.dirname(__file__), "test_data", "sample1.jpg"), "eng") - # print(recognized_text) + # print("recognized_text=" + recognized_text) self.assertIn("The quick brown fox", recognized_text) self.assertIn("jumped over the 5", recognized_text) @@ -43,7 +55,8 @@ def test_read_document_image_jpg(self): "test_data", "sample1_output.txt"), "r") expected_text = file.read() - self.assertEqual(recognized_text, expected_text) + # Trailing spaces or EOL are acceptable + self.assertTrue(expected_text in recognized_text) def test_read_document_image_png(self): locale.setlocale(locale.LC_ALL, 'C') @@ -51,7 +64,7 @@ def test_read_document_image_png(self): "test_data", "Picture_010.png"), "eng") - # print(recognized_text) + # print("recognized_text=" + recognized_text) self.assertIn("Brawn Manufacture", recognized_text) self.assertNotIn("And an elephant!", recognized_text) @@ -62,7 +75,7 @@ def test_read_document_pdf(self): "test_data", "sample2.pdf"), "eng") - # print(recognized_text) + # print("recognized_text=" + recognized_text) self.assertIn("Python Basics", recognized_text) self.assertNotIn("Java", recognized_text) diff --git a/erpnext_ocr/translations/fr.csv b/erpnext_ocr/translations/fr.csv index 37cd3b48..21358612 100644 --- a/erpnext_ocr/translations/fr.csv +++ b/erpnext_ocr/translations/fr.csv @@ -11,4 +11,6 @@ Doctype: OCR Read,Language,Langue Doctype: OCR Read,Read file,Lire le fichier Doctype: OCR Read,Read Result,Résultat de la lecture apps/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.js,Reading the file,Lecture du fichier -apps/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py,The selected language is not available. Please contact your administrator.,La langue sélectionnée n'est pas disponible. Veuillez contacter votre administrateur. \ No newline at end of file +apps/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.py,The selected language is not available. Please contact your administrator.,La langue sélectionnée n'est pas disponible. Veuillez contacter votre administrateur. +Doctype: OCR Settings,PDF Resolution,Résolution PDF +apps/erpnext_ocr/erpnext_ocr/doctype/ocr_settings/ocr_settings.py,PDF Resolution must be a positive integer, eg 300 (high) or 200 (normal).,La résolution PDF doit être un entier positif, comme 300 (haute) ou 200 (normale). \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2aa19b19..163b173e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ frappe six requests -pytesseract pillow wand tesserocr \ No newline at end of file From d3517ea26c881bd5732087b115e3450f2f51f054 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 22:03:49 +0100 Subject: [PATCH 27/33] :wrench: Allow OCR Read to all if owner Signed-off-by: mathieu.brunot --- erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json index 00a84b62..8d137e62 100644 --- a/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json +++ b/erpnext_ocr/erpnext_ocr/doctype/ocr_read/ocr_read.json @@ -159,7 +159,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-11-27 00:57:49.229313", + "modified": "2019-11-27 22:02:24.138648", "modified_by": "Administrator", "module": "ERPNext OCR", "name": "OCR Read", @@ -173,7 +173,7 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, + "if_owner": 1, "import": 0, "permlevel": 0, "print": 1, @@ -189,7 +189,7 @@ "amend": 0, "cancel": 0, "create": 1, - "delete": 0, + "delete": 1, "email": 1, "export": 1, "if_owner": 0, From bed420a0d80fc8ed06e8b3633b253a0bf48ca347 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 22:39:17 +0100 Subject: [PATCH 28/33] :art: Format README lists Signed-off-by: mathieu.brunot --- README.md | 12 ++++++++---- erpnext_ocr/tests/README.md | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 87042320..76edd71f 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,14 @@ bench install-app erpnext_ocr ``` When installing Frappe app, the following python requirements will be installed: -* python binding for tesseract, [tesserocr](https://pypi.org/project/tesserocr/) -* image processing library in python, [pillow](https://pypi.org/project/Pillow/) -* HTTP library in python, [requests](https://pypi.org/project/requests/) -* python binding for imagemagick, [wand](https://pypi.org/project/Wand/) + +* python binding for tesseract, [tesserocr](https://pypi.org/project/tesserocr/) + +* image processing library in python, [pillow](https://pypi.org/project/Pillow/) + +* HTTP library in python, [requests](https://pypi.org/project/requests/) + +* python binding for imagemagick, [wand](https://pypi.org/project/Wand/) ## :rocket: Usage diff --git a/erpnext_ocr/tests/README.md b/erpnext_ocr/tests/README.md index 8d8409cc..4dbabe15 100644 --- a/erpnext_ocr/tests/README.md +++ b/erpnext_ocr/tests/README.md @@ -2,25 +2,31 @@ Examples to implement OCR(Optical Character Recognition) using tesseract using Python ## Installation: -- Install tesserct-ocr using this command: + +- Install tesserct-ocr using this command: ``` sudo apt-get install tesseract-ocr ``` -- Install python binding for tesseract, tesserocr, using this pip command: + +- Install python binding for tesseract, tesserocr, using this pip command: ``` pip install tesserocr ``` -- Install image processing library in python, pillow using this pip command: + +- Install image processing library in python, pillow using this pip command: ``` pip install pillow ``` - + + **For working with pdf files:** -- Install imagemagick using this command: + +- Install imagemagick using this command: ``` sudo apt-get install imagemagick ``` -- Install python binding for imagemagick, wand, using this pip command: + +- Install python binding for imagemagick, wand, using this pip command: ``` pip install wand ``` From f1a9afc0bfbdd5ab12e9f2718b929a31783a82a3 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 22:58:58 +0100 Subject: [PATCH 29/33] :heavy_plus_sign: Add remarks.js to lint markdown Signed-off-by: mathieu.brunot --- .gitignore | 1 + package-lock.json | 1239 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 +- 3 files changed, 1251 insertions(+), 2 deletions(-) create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 592ad1df..87b96688 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.egg-info *.swp tags +node_modules erpnext_ocr/docs/current *.iml *.xml diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..8499a195 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1239 @@ +{ + "name": "erpnext_ocr", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@types/unist": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", + "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.3" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "requires": { + "normalize-path": "3.0.0", + "picomatch": "2.1.1" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "1.0.3" + } + }, + "bail": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.4.tgz", + "integrity": "sha512-S8vuDB4w6YpRhICUDET3guPlQpaJl7od94tpZ0Fvnyp+MKW/HyDTcRDck+29C9g+d/qQHnddRH3+94kZdrW0Ww==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "7.0.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "ccount": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.4.tgz", + "integrity": "sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.5.0" + } + }, + "character-entities": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.3.tgz", + "integrity": "sha512-yB4oYSAa9yLcGyTbB4ItFwHw43QHdH129IJ5R+WvxOkWlyFnR5FAaBNnUq4mcxsTVZGh28bHoeTHMKXH1wZf3w==" + }, + "character-entities-html4": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.3.tgz", + "integrity": "sha512-SwnyZ7jQBCRHELk9zf2CN5AnGEc2nA+uKMZLHvcqhpPprjkYhiLn0DywMHgN5ttFZuITMATbh68M6VIVKwJbcg==" + }, + "character-entities-legacy": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.3.tgz", + "integrity": "sha512-YAxUpPoPwxYFsslbdKkhrGnXAtXoHNgYjlBM3WMXkWGTl5RsY3QmOyhwAgL8Nxm9l5LBThXGawxKPn68y6/fww==" + }, + "character-reference-invalid": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.3.tgz", + "integrity": "sha512-VOq6PRzQBam/8Jm6XBGk2fNEnHXAdGd6go0rtd4weAGECBamHDwwCQSOT12TACIYUZegUXnV6xBXqUssijtxIg==" + }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "requires": { + "anymatch": "3.1.1", + "braces": "3.0.2", + "fsevents": "2.1.2", + "glob-parent": "5.1.0", + "is-binary-path": "2.1.0", + "is-glob": "4.0.1", + "normalize-path": "3.0.0", + "readdirp": "3.2.0" + } + }, + "co": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", + "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=" + }, + "collapse-white-space": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.5.tgz", + "integrity": "sha512-703bOOmytCYAX9cXYqoikYIx6twmFCXsnzRQheBcTG3nzKYBR4P/+wkYeH+Mvj7qUz8zZDtdyzbxfnEi/kYzRQ==" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "1.1.1", + "inherits": "2.0.4", + "readable-stream": "3.4.0", + "typedarray": "0.0.6" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "2.1.2" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fault": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.3.tgz", + "integrity": "sha512-sfFuP4X0hzrbGKjAUNXYvNqsZ5F6ohx/dZ9I0KQud/aiZNwg263r5L9yGB0clvXHCkzXh5W3t7RSHchggYIFmA==", + "requires": { + "format": "0.2.2" + } + }, + "figures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "5.0.1" + } + }, + "fn-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", + "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=" + }, + "format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "optional": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.4", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "requires": { + "is-glob": "4.0.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "irregular-plurals": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz", + "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==" + }, + "is-alphabetical": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.3.tgz", + "integrity": "sha512-eEMa6MKpHFzw38eKm56iNNi6GJ7lf6aLLio7Kr23sJPAECscgRtZvOBYybejWDQ2bM949Y++61PY+udzj5QMLA==" + }, + "is-alphanumeric": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", + "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=" + }, + "is-alphanumerical": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.3.tgz", + "integrity": "sha512-A1IGAPO5AW9vSh7omxIlOGwIqEvpW/TA+DksVOPM5ODuxKlZS09+TEM1E3275lJqO2oJ38vDpeAL3DCIiHE6eA==", + "requires": { + "is-alphabetical": "1.0.3", + "is-decimal": "1.0.3" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "2.0.0" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + }, + "is-decimal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.3.tgz", + "integrity": "sha512-bvLSwoDg2q6Gf+E2LEPiklHZxxiSi3XAh4Mav65mKqTfCO1HM3uBs24TjEH8iJX3bbDdLXKJXBTmGzuTUuAEjQ==" + }, + "is-empty": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-empty/-/is-empty-1.2.0.tgz", + "integrity": "sha1-3pu1snhzigWgsJpX4ftNSjQan2s=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-hexadecimal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.3.tgz", + "integrity": "sha512-zxQ9//Q3D/34poZf8fiy3m3XVpbQc7ren15iKqrTtLPwkPD/t3Scy9Imp63FujULGxuK0ZlCwoo5xNpktFgbOA==" + }, + "is-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-hidden/-/is-hidden-1.1.2.tgz", + "integrity": "sha512-kytBeNVW2QTIqZdJBDKIjP+EkUTzDT07rsc111w/gxqR6wK3ODkOswcpxgED6HU6t7fEhOxqojVZ2a2kU9rj+A==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" + }, + "is-plain-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.0.0.tgz", + "integrity": "sha512-EYisGhpgSCwspmIuRHGjROWTon2Xp8Z7U03Wubk/bTL5TTRC5R1rGVgyjzBrk9+ULdH6cRD06KRcw/xfqhVYKQ==" + }, + "is-whitespace-character": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.3.tgz", + "integrity": "sha512-SNPgMLz9JzPccD3nPctcj8sZlX9DAMJSKH8bP7Z6bohCwuNgX8xbWr1eTAYXX9Vpi/aSn8Y1akL9WgM3t43YNQ==" + }, + "is-word-character": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.3.tgz", + "integrity": "sha512-0wfcrFgOOOBdgRNT9H33xe6Zi6yhX/uoc4U8NBZGeQQB0ctU1dnlNTyL9JM2646bHDTpsDm1Brb3VPoCIMrd/A==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.1" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "requires": { + "minimist": "1.2.0" + } + }, + "load-plugin": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/load-plugin/-/load-plugin-2.3.1.tgz", + "integrity": "sha512-dYB1lbwqHgPTrruy9glukCu8Ya9vzj6TMfouCtj2H/GuJ+8syioisgKTBPxnCi6m8K8jINKfTOxOHngFkUYqHw==", + "requires": { + "npm-prefix": "1.2.0", + "resolve-from": "5.0.0" + } + }, + "longest-streak": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.3.tgz", + "integrity": "sha512-9lz5IVdpwsKLMzQi0MQ+oD9EA0mIGcWYP7jXMTZVXP8D42PwuAk+M/HBFYQoxt1G5OR8m7aSIgb1UymfWGBWEw==" + }, + "markdown-escapes": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.3.tgz", + "integrity": "sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==" + }, + "markdown-extensions": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", + "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==" + }, + "markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==" + }, + "mdast-comment-marker": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mdast-comment-marker/-/mdast-comment-marker-1.1.1.tgz", + "integrity": "sha512-TWZDaUtPLwKX1pzDIY48MkSUQRDwX/HqbTB4m3iYdL/zosi/Z6Xqfdv0C0hNVKvzrPjZENrpWDt4p4odeVO0Iw==" + }, + "mdast-util-compact": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", + "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", + "requires": { + "unist-util-visit": "1.4.1" + } + }, + "mdast-util-heading-style": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/mdast-util-heading-style/-/mdast-util-heading-style-1.0.5.tgz", + "integrity": "sha512-8zQkb3IUwiwOdUw6jIhnwM6DPyib+mgzQuHAe7j2Hy1rIarU4VUxe472bp9oktqULW3xqZE+Kz6OD4Gi7IA3vw==" + }, + "mdast-util-to-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.0.7.tgz", + "integrity": "sha512-P+gdtssCoHOX+eJUrrC30Sixqao86ZPlVjR5NEAoy0U79Pfxb1Y0Gntei0+GrnQD4T04X9xA8tcugp90cSmNow==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "npm-prefix": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/npm-prefix/-/npm-prefix-1.2.0.tgz", + "integrity": "sha1-5hlFX3B0ulTMZtbQ033Z8b5ry8A=", + "requires": { + "rc": "1.2.8", + "shellsubstitute": "1.2.0", + "untildify": "2.1.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "requires": { + "character-entities": "1.2.3", + "character-entities-legacy": "1.1.3", + "character-reference-invalid": "1.1.3", + "is-alphanumerical": "1.0.3", + "is-decimal": "1.0.3", + "is-hexadecimal": "1.0.3" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "1.3.2", + "json-parse-better-errors": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "picomatch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz", + "integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==" + }, + "plur": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", + "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==", + "requires": { + "irregular-plurals": "2.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "0.6.0", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "2.0.4", + "string_decoder": "1.3.0", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "requires": { + "picomatch": "2.1.1" + } + }, + "remark": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/remark/-/remark-11.0.2.tgz", + "integrity": "sha512-bh+eJgn8wgmbHmIBOuwJFdTVRVpl3fcVP6HxmpPWO0ULGP9Qkh6INJh0N5Uy7GqlV7DQYGoqaKiEIpM5LLvJ8w==", + "requires": { + "remark-parse": "7.0.2", + "remark-stringify": "7.0.4", + "unified": "8.4.2" + } + }, + "remark-cli": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/remark-cli/-/remark-cli-7.0.1.tgz", + "integrity": "sha512-CUjBLLSbEay0mNwOO+pptnLIoS8UB6cHlhZVpTRKbtbIcw6YEzEfD7jGjW1HCA8lZK87IfY3/DuWE6DlXu+hfg==", + "requires": { + "markdown-extensions": "1.1.1", + "remark": "11.0.2", + "unified-args": "7.1.0" + } + }, + "remark-lint": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/remark-lint/-/remark-lint-6.0.5.tgz", + "integrity": "sha512-o1I3ddm+KNsTxk60wWGI+p2yU1jB1gcm8jo2Sy6VhJ4ab2TrQIp1oQbp5xeLoFXYSh/NAqCpKjHkCM/BYpkFdQ==", + "requires": { + "remark-message-control": "4.2.0" + } + }, + "remark-lint-final-newline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-final-newline/-/remark-lint-final-newline-1.0.3.tgz", + "integrity": "sha512-ETAadktv75EwUS3XDhyZUVstXKxfPAEn7SmfN9kZ4+Jb4qo4hHE9gtTOzhE6HxLUxxl9BBhpC5mMO3JcL8UZ5A==", + "requires": { + "unified-lint-rule": "1.0.4" + } + }, + "remark-lint-hard-break-spaces": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/remark-lint-hard-break-spaces/-/remark-lint-hard-break-spaces-1.0.4.tgz", + "integrity": "sha512-YM82UpgliZCZhGNmFxEe7ArfhqR5CplFf2bc0k0+8w3rKWKx7EJcGMar2NK410tIi40gGeWtH/pIEypPJFCCiA==", + "requires": { + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-list-item-bullet-indent": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-list-item-bullet-indent/-/remark-lint-list-item-bullet-indent-1.0.3.tgz", + "integrity": "sha512-iVxQbrgzLpMHG3C6o6wRta/+Bc96etOiBYJnh2zm/aWz6DJ7cGLDykngblP/C4he7LYSeWOD/8Y57HbXZwM2Og==", + "requires": { + "plur": "3.1.1", + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-list-item-indent": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/remark-lint-list-item-indent/-/remark-lint-list-item-indent-1.0.4.tgz", + "integrity": "sha512-Sv0gVH6qP1/nFpbJuyyguB9sAD2o42StD2WbEZeUcEexXwRO4u/YaX0Pm5pMtCiEHyN+qyL6ShKBQMtgol9BeA==", + "requires": { + "plur": "3.1.1", + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-auto-link-without-protocol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-no-auto-link-without-protocol/-/remark-lint-no-auto-link-without-protocol-1.0.3.tgz", + "integrity": "sha512-k+hg2mXnO4Q9WV+UShPLen5oThvFxcRVWkx2hviVd/nu3eiszBKH3o38csBwjeJoMG3l2ZhdUW8dlOBhq8670Q==", + "requires": { + "mdast-util-to-string": "1.0.7", + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-blockquote-without-marker": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-no-blockquote-without-marker/-/remark-lint-no-blockquote-without-marker-2.0.3.tgz", + "integrity": "sha512-faDzKrA6aKidsRXG6gcIlCO8TexLxIxe+n9B3mdnl8mhZGgE0FfWTkIWVMj0IYps/xVsVMf45KxhXgc1wU9kwg==", + "requires": { + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1", + "vfile-location": "2.0.6" + } + }, + "remark-lint-no-duplicate-definitions": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/remark-lint-no-duplicate-definitions/-/remark-lint-no-duplicate-definitions-1.0.5.tgz", + "integrity": "sha512-zKXmfNUODXhJsGQdqfguMG9Nl9v1sLaDsQgMjUtmOSoQRnNud9ThQAZl62eX5jBn5HKcpOifG80tgkyBvU5eEw==", + "requires": { + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-stringify-position": "2.0.2", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-heading-content-indent": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-no-heading-content-indent/-/remark-lint-no-heading-content-indent-1.0.3.tgz", + "integrity": "sha512-7xM6X5E/dt8OXOHdejH+sfYb139a3kMr8ZSSkcp90Ab1y+ZQBNaWsR3mYh8FRKkYPTN5eyd+KjhNpLWyqqCbgg==", + "requires": { + "mdast-util-heading-style": "1.0.5", + "plur": "3.1.1", + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-inline-padding": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/remark-lint-no-inline-padding/-/remark-lint-no-inline-padding-1.0.4.tgz", + "integrity": "sha512-u5rgbDkcfVv645YxxOwoGBBJbsHEwWm/XqnO8EhfKTxkfKOF4ZItG7Ajhj89EDaeXMkvCcB/avBl4bj50eJH3g==", + "requires": { + "mdast-util-to-string": "1.0.7", + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-literal-urls": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-no-literal-urls/-/remark-lint-no-literal-urls-1.0.3.tgz", + "integrity": "sha512-H5quyMzl2kaewK+jYD1FI0G1SIinIsIp4DEyOUwIR+vYUoKwo0B4vvW0cmPpD1dgqqxHYx0B2B0JQQKFVWzGiw==", + "requires": { + "mdast-util-to-string": "1.0.7", + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-shortcut-reference-image": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-image/-/remark-lint-no-shortcut-reference-image-1.0.3.tgz", + "integrity": "sha512-CGm27X54kXp/5ehXejDTsZjqzK4uIhLGcrFzN3k/KjdwunQouEY92AARGrLSEuJ1hQx0bJsmnvr/hvQyWAfNJg==", + "requires": { + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-shortcut-reference-link": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-link/-/remark-lint-no-shortcut-reference-link-1.0.4.tgz", + "integrity": "sha512-FXdMJYqspZBhPlxYqfVgVluVXjxStg0RHJzqrk8G9wS8fCS62AE3reoaoiCahwoH1tfKcA+poktbKqDAmZo7Jg==", + "requires": { + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-undefined-references": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/remark-lint-no-undefined-references/-/remark-lint-no-undefined-references-1.1.1.tgz", + "integrity": "sha512-b1eIjWFaCu6m16Ax2uG33o1v+eRYqDTQRUqU6UeQ76JXmDmVtVO75ZuyRpqqE7VTZRW8YLVurXfJPDXfIa5Wng==", + "requires": { + "collapse-white-space": "1.0.5", + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-no-unused-definitions": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/remark-lint-no-unused-definitions/-/remark-lint-no-unused-definitions-1.0.5.tgz", + "integrity": "sha512-Bo22e0RNzc1QMW317KTuStGFDG7uTDUQhm/TrW6Qzud0WXnNnqUyvts+e7wTYoj8VnwhhjyjyoA9lKA3uXMdAQ==", + "requires": { + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-visit": "1.4.1" + } + }, + "remark-lint-ordered-list-marker-style": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/remark-lint-ordered-list-marker-style/-/remark-lint-ordered-list-marker-style-1.0.3.tgz", + "integrity": "sha512-24TmW1eUa/2JlwprZg9jJ8LKLxNGKnlKiI5YOhN4taUp2yv8daqlV9vR54yfn/ZZQh6EQvbIX0jeVY9NYgQUtw==", + "requires": { + "unified-lint-rule": "1.0.4", + "unist-util-generated": "1.1.5", + "unist-util-position": "3.0.4", + "unist-util-visit": "1.4.1" + } + }, + "remark-message-control": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/remark-message-control/-/remark-message-control-4.2.0.tgz", + "integrity": "sha512-WXH2t5ljTyhsXlK1zPBLF3iPHbXl58R94phPMreS1xcHWBZJt6Oiu8RtNjy1poZFb3PqKnbYLJeR/CWcZ1bTFw==", + "requires": { + "mdast-comment-marker": "1.1.1", + "unified-message-control": "1.0.4", + "xtend": "4.0.2" + } + }, + "remark-parse": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-7.0.2.tgz", + "integrity": "sha512-9+my0lQS80IQkYXsMA8Sg6m9QfXYJBnXjWYN5U+kFc5/n69t+XZVXU/ZBYr3cYH8FheEGf1v87rkFDhJ8bVgMA==", + "requires": { + "collapse-white-space": "1.0.5", + "is-alphabetical": "1.0.3", + "is-decimal": "1.0.3", + "is-whitespace-character": "1.0.3", + "is-word-character": "1.0.3", + "markdown-escapes": "1.0.3", + "parse-entities": "1.2.2", + "repeat-string": "1.6.1", + "state-toggle": "1.0.2", + "trim": "0.0.1", + "trim-trailing-lines": "1.1.2", + "unherit": "1.1.2", + "unist-util-remove-position": "1.1.4", + "vfile-location": "2.0.6", + "xtend": "4.0.2" + } + }, + "remark-preset-lint-recommended": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/remark-preset-lint-recommended/-/remark-preset-lint-recommended-3.0.3.tgz", + "integrity": "sha512-5sQ34j1Irlsj6Tt4WWRylZ7UU+1jD5es/LfDZBZp/LXDwC4ldGqKpMmCCR6Z00x1jYM1phmS4M+eGqTdah0qkQ==", + "requires": { + "remark-lint": "6.0.5", + "remark-lint-final-newline": "1.0.3", + "remark-lint-hard-break-spaces": "1.0.4", + "remark-lint-list-item-bullet-indent": "1.0.3", + "remark-lint-list-item-indent": "1.0.4", + "remark-lint-no-auto-link-without-protocol": "1.0.3", + "remark-lint-no-blockquote-without-marker": "2.0.3", + "remark-lint-no-duplicate-definitions": "1.0.5", + "remark-lint-no-heading-content-indent": "1.0.3", + "remark-lint-no-inline-padding": "1.0.4", + "remark-lint-no-literal-urls": "1.0.3", + "remark-lint-no-shortcut-reference-image": "1.0.3", + "remark-lint-no-shortcut-reference-link": "1.0.4", + "remark-lint-no-undefined-references": "1.1.1", + "remark-lint-no-unused-definitions": "1.0.5", + "remark-lint-ordered-list-marker-style": "1.0.3" + } + }, + "remark-stringify": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-7.0.4.tgz", + "integrity": "sha512-qck+8NeA1D0utk1ttKcWAoHRrJxERYQzkHDyn+pF5Z4whX1ug98uCNPPSeFgLSaNERRxnD6oxIug6DzZQth6Pg==", + "requires": { + "ccount": "1.0.4", + "is-alphanumeric": "1.0.0", + "is-decimal": "1.0.3", + "is-whitespace-character": "1.0.3", + "longest-streak": "2.0.3", + "markdown-escapes": "1.0.3", + "markdown-table": "1.1.3", + "mdast-util-compact": "1.0.4", + "parse-entities": "1.2.2", + "repeat-string": "1.6.1", + "state-toggle": "1.0.2", + "stringify-entities": "2.0.0", + "unherit": "1.1.2", + "xtend": "4.0.2" + } + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "shellsubstitute": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shellsubstitute/-/shellsubstitute-1.2.0.tgz", + "integrity": "sha1-5PcCpQxRiw9v6YRRiQ1wWvKba3A=" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "state-toggle": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.2.tgz", + "integrity": "sha512-8LpelPGR0qQM4PnfLiplOQNJcIN1/r2Gy0xKB2zKnIW2YzPMt2sR4I/+gtPjhN7Svh9kw+zqEg2SFwpBO9iNiw==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "8.0.0", + "is-fullwidth-code-point": "3.0.0", + "strip-ansi": "6.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "5.2.0" + } + }, + "stringify-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-2.0.0.tgz", + "integrity": "sha512-fqqhZzXyAM6pGD9lky/GOPq6V4X0SeTAFBl0iXb/BzOegl40gpf/bV3QQP7zULNYvjr6+Dx8SCaDULjVoOru0A==", + "requires": { + "character-entities-html4": "1.1.3", + "character-entities-legacy": "1.1.3", + "is-alphanumerical": "1.0.3", + "is-decimal": "1.0.3", + "is-hexadecimal": "1.0.3" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "5.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "3.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "7.0.0" + } + }, + "to-vfile": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-6.0.0.tgz", + "integrity": "sha512-i9fwXXSsHLu7mzgixc1WjgnqSe6pGpjnzCYoFmrASvEueLfyKf09QAe+XQYu8OAJ62aFqHpe2EKXojeRVvEzqA==", + "requires": { + "is-buffer": "2.0.4", + "vfile": "4.0.2" + } + }, + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" + }, + "trim-trailing-lines": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.2.tgz", + "integrity": "sha512-MUjYItdrqqj2zpcHFTkMa9WAv4JHTI6gnRQGPFLrt5L9a6tRMiDnIqYl8JBvu2d2Tc3lWJKQwlGCp0K8AvCM+Q==" + }, + "trough": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.4.tgz", + "integrity": "sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unherit": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.2.tgz", + "integrity": "sha512-W3tMnpaMG7ZY6xe/moK04U9fBhi6wEiCYHUW5Mop/wQHf12+79EQGwxYejNdhEz2mkqkBlGwm7pxmgBKMVUj0w==", + "requires": { + "inherits": "2.0.4", + "xtend": "4.0.2" + } + }, + "unified": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-8.4.2.tgz", + "integrity": "sha512-JCrmN13jI4+h9UAyKEoGcDZV+i1E7BLFuG7OsaDvTXI5P0qhHX+vZO/kOhz9jn8HGENDKbwSeB0nVOg4gVStGA==", + "requires": { + "bail": "1.0.4", + "extend": "3.0.2", + "is-plain-obj": "2.0.0", + "trough": "1.0.4", + "vfile": "4.0.2" + } + }, + "unified-args": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/unified-args/-/unified-args-7.1.0.tgz", + "integrity": "sha512-soi9Rn7l5c1g0RfElSCHMwaxeiclSI0EsS3uZmMPUOfwMeeeZjLpNmHAowV9iSlQh59iiZhSMyQu9lB8WnIz5g==", + "requires": { + "camelcase": "5.3.1", + "chalk": "2.4.2", + "chokidar": "3.3.0", + "fault": "1.0.3", + "json5": "2.1.1", + "minimist": "1.2.0", + "text-table": "0.2.0", + "unified-engine": "7.0.0" + } + }, + "unified-engine": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/unified-engine/-/unified-engine-7.0.0.tgz", + "integrity": "sha512-zH/MvcISpWg3JZtCoY/GYBw1WnVHkhnPoMBWpmuvAifCPSS9mzT9EbtimesJp6t2nnr/ojI0mg3TmkO1CjIwVA==", + "requires": { + "concat-stream": "2.0.0", + "debug": "4.1.1", + "fault": "1.0.3", + "figures": "3.1.0", + "fn-name": "2.0.1", + "glob": "7.1.6", + "ignore": "5.1.4", + "is-empty": "1.2.0", + "is-hidden": "1.1.2", + "is-object": "1.0.1", + "js-yaml": "3.13.1", + "load-plugin": "2.3.1", + "parse-json": "4.0.0", + "to-vfile": "6.0.0", + "trough": "1.0.4", + "unist-util-inspect": "4.1.4", + "vfile-reporter": "6.0.0", + "vfile-statistics": "1.1.3", + "x-is-string": "0.1.0", + "xtend": "4.0.2" + } + }, + "unified-lint-rule": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-1.0.4.tgz", + "integrity": "sha512-q9wY6S+d38xRAuWQVOMjBQYi7zGyKkY23ciNafB8JFVmDroyKjtytXHCg94JnhBCXrNqpfojo3+8D+gmF4zxJQ==", + "requires": { + "wrapped": "1.0.1" + } + }, + "unified-message-control": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unified-message-control/-/unified-message-control-1.0.4.tgz", + "integrity": "sha512-e1dEtN4Z/TvLn/qHm+xeZpzqhJTtfZusFErk336kkZVpqrJYiV9ptxq+SbRPFMlN0OkjDYHmVJ929KYjsMTo3g==", + "requires": { + "trim": "0.0.1", + "unist-util-visit": "1.4.1", + "vfile-location": "2.0.6" + } + }, + "unist-util-generated": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.5.tgz", + "integrity": "sha512-1TC+NxQa4N9pNdayCYA1EGUOCAO0Le3fVp7Jzns6lnua/mYgwHo0tz5WUAfrdpNch1RZLHc61VZ1SDgrtNXLSw==" + }, + "unist-util-inspect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/unist-util-inspect/-/unist-util-inspect-4.1.4.tgz", + "integrity": "sha512-7xxyvKiZ1SC9vL5qrMqKub1T31gRHfau4242F69CcaOrXt//5PmRVOmDZ36UAEgiT+tZWzmQmbNZn+mVtnR9HQ==", + "requires": { + "is-empty": "1.2.0" + } + }, + "unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + }, + "unist-util-position": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.0.4.tgz", + "integrity": "sha512-tWvIbV8goayTjobxDIr4zVTyG+Q7ragMSMeKC3xnPl9xzIc0+she8mxXLM3JVNDDsfARPbCd3XdzkyLdo7fF3g==" + }, + "unist-util-remove-position": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", + "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", + "requires": { + "unist-util-visit": "1.4.1" + } + }, + "unist-util-stringify-position": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.2.tgz", + "integrity": "sha512-nK5n8OGhZ7ZgUwoUbL8uiVRwAbZyzBsB/Ddrlbu6jwwubFza4oe15KlyEaLNMXQW1svOQq4xesUeqA85YrIUQA==", + "requires": { + "@types/unist": "2.0.3" + } + }, + "unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "requires": { + "unist-util-visit-parents": "2.1.2" + } + }, + "unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "requires": { + "unist-util-is": "3.0.0" + } + }, + "untildify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", + "integrity": "sha1-F+soB5h/dpUunASF/DEdBqgmouA=", + "requires": { + "os-homedir": "1.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "vfile": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.0.2.tgz", + "integrity": "sha512-yhoTU5cDMSsaeaMfJ5g0bUKYkYmZhAh9fn9TZicxqn+Cw4Z439il2v3oT9S0yjlpqlI74aFOQCt3nOV+pxzlkw==", + "requires": { + "@types/unist": "2.0.3", + "is-buffer": "2.0.4", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "2.0.2", + "vfile-message": "2.0.2" + } + }, + "vfile-location": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", + "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==" + }, + "vfile-message": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.2.tgz", + "integrity": "sha512-gNV2Y2fDvDOOqq8bEe7cF3DXU6QgV4uA9zMR2P8tix11l1r7zju3zry3wZ8sx+BEfuO6WQ7z2QzfWTvqHQiwsA==", + "requires": { + "@types/unist": "2.0.3", + "unist-util-stringify-position": "2.0.2" + } + }, + "vfile-reporter": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-6.0.0.tgz", + "integrity": "sha512-8Is0XxFxWJUhPJdOg3CyZTqd3ICCWg6r304PuBl818ZG91h4FMS3Q+lrOPS+cs5/DZK3H0+AkJdH0J8JEwKtDA==", + "requires": { + "repeat-string": "1.6.1", + "string-width": "4.2.0", + "supports-color": "6.1.0", + "unist-util-stringify-position": "2.0.2", + "vfile-sort": "2.2.1", + "vfile-statistics": "1.1.3" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "vfile-sort": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-2.2.1.tgz", + "integrity": "sha512-5dt7xEhC44h0uRQKhbM2JAe0z/naHphIZlMOygtMBM9Nn0pZdaX5fshhwWit9wvsuP8t/wp43nTDRRErO1WK8g==" + }, + "vfile-statistics": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-1.1.3.tgz", + "integrity": "sha512-CstaK/ebTz1W3Qp41Bt9Lj/2DmumFsCwC2sKahDNSPh0mPh7/UyMLCoU8ZBX34CRU0d61B4W41yIFsV0NKMZeA==" + }, + "wrapped": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wrapped/-/wrapped-1.0.1.tgz", + "integrity": "sha1-x4PZ2Aeyc+mwHoUWgKk4yHyQckI=", + "requires": { + "co": "3.1.0", + "sliced": "1.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + } + } +} diff --git a/package.json b/package.json index 042c23ce..06fb4e2b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,15 @@ "author": "Monogramm", "license": "MIT", "dependencies": { - "cypress": "^3.1.4" + "cypress": "^3.1.4", + "remark-cli": "^7.0.1", + "remark-lint": "^6.0.5", + "remark-preset-lint-recommended": "^3.0.3" + }, + "scripts": { + "lint-md": "remark ." + }, + "remarkConfig": { + "plugins": ["remark-preset-lint-recommended"] } -} \ No newline at end of file +} From 73805cc71b34f62289c374e0c3dd1f28c67ec1ee Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 23:00:08 +0100 Subject: [PATCH 30/33] :art: Beautify README and tests README Signed-off-by: mathieu.brunot --- README.md | 58 ++++++++++++++++++------------------- erpnext_ocr/tests/README.md | 45 +++++++++++++++------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 76edd71f..9d136c32 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - [uri_license]: https://opensource.org/licenses/MIT + [uri_license_image]: https://img.shields.io/badge/license-MIT-blue [![License: MIT][uri_license_image]][uri_license] @@ -13,27 +13,25 @@ This project is a fork of [ERPNext-OCR](https://github.com/jvfiel/ERPNext-OCR) by [John Vincent Fiel](https://github.com/jvfiel). Its aim is to fix and cleanup the original source code and add some new features. -https://discuss.erpnext.com/t/erpnext-ocr-app/33834/7 - +Check out more on [ERPNext Discuss](https://discuss.erpnext.com/t/erpnext-ocr-app/33834/7). ## :chart_with_upwards_trend: Changes See [CHANGELOG](./CHANGELOG.md) - ## :bookmark: Roadmap See [Taiga.io](https://tree.taiga.io/project/monogrammbot-monogrammerpnext_ocr/ "Taiga.io monogrammbot-monogrammerpnext_ocr") - ## :construction: Install **Pre-requisites: tesseract-python and imagemagick** Install tesseract-ocr, plus imagemagick and ghostscript (to work with pdf files) using this command on Debian: - ```sh - sudo apt-get install tesseract-ocr imagemagick libmagickwand-dev ghostscript - ``` + +```sh +sudo apt-get install tesseract-ocr imagemagick libmagickwand-dev ghostscript +``` **Install Frappe application** @@ -44,13 +42,13 @@ bench install-app erpnext_ocr When installing Frappe app, the following python requirements will be installed: -* python binding for tesseract, [tesserocr](https://pypi.org/project/tesserocr/) +- python binding for tesseract, [tesserocr](https://pypi.org/project/tesserocr/) -* image processing library in python, [pillow](https://pypi.org/project/Pillow/) +- image processing library in python, [pillow](https://pypi.org/project/Pillow/) -* HTTP library in python, [requests](https://pypi.org/project/requests/) +- HTTP library in python, [requests](https://pypi.org/project/requests/) -* python binding for imagemagick, [wand](https://pypi.org/project/Wand/) +- python binding for imagemagick, [wand](https://pypi.org/project/Wand/) ## :rocket: Usage @@ -62,26 +60,25 @@ When installing Frappe app, the following python requirements will be installed: ![Sample Screenshot](./erpnext_ocr/tests/test_data/Picture_010_screenshot.png) - ### Tesseract trained data In order to use OCR with different languages, you need to install the appropriate trained data files. -Check tesseract Wiki for details: https://github.com/tesseract-ocr/tesseract/wiki/Data-Files +Check tesseract Wiki for details: ### Known issues -* `wand.exceptions.PolicyError: not authorized '/opt/sample.pdf' @ error/constitute.c/ReadImage/412` - * This can happen due to security configuration in imagemagick, preventing it to read PDF files. - * Reference: - * https://stackoverflow.com/questions/52699608/wand-policy-error-error-constitute-c-readimage-412 - * https://stackoverflow.com/questions/42928765/convertnot-authorized-aaaa-error-constitute-c-readimage-453 -* `wand.exceptions.WandRuntimeError: MagickReadImage returns false, but did raise ImageMagick exception. This can occurs when a delegate is missing, or returns EXIT_SUCCESS without generating a raster.` - * This might happen if you're missing a dependency to convert PDF, most of the time `ghostscript` - * References: - * https://stackoverflow.com/questions/57271287/user-wand-by-python-to-convert-pdf-to-jepg-raise-wand-exceptions-wandruntimeerr -* `OSError: encoder error -2 when writing image file` - * This might happen when trying to open a TIFF image, but the real error is "_hidden_" and only displayed in console. - * If the original error in console is `Fax3SetupState: Bits/sample must be 1 for Group 3/4 encoding/decoding.` that usually happens when TIFF image compression is not valid / recognized. +- `wand.exceptions.PolicyError: not authorized '/opt/sample.pdf' @ error/constitute.c/ReadImage/412` + - This can happen due to security configuration in imagemagick, preventing it to read PDF files. + - Reference: + - + - +- `wand.exceptions.WandRuntimeError: MagickReadImage returns false, but did raise ImageMagick exception. This can occurs when a delegate is missing, or returns EXIT_SUCCESS without generating a raster.` + - This might happen if you're missing a dependency to convert PDF, most of the time `ghostscript` + - References: + - +- `OSError: encoder error -2 when writing image file` + - This might happen when trying to open a TIFF image, but the real error is "_hidden_" and only displayed in console. + - If the original error in console is `Fax3SetupState: Bits/sample must be 1 for Group 3/4 encoding/decoding.` that usually happens when TIFF image compression is not valid / recognized. ## :white_check_mark: Run tests @@ -93,12 +90,12 @@ bench bench run-tests --profile --app erpnext_ocr **Monogramm** -* Website: https://www.monogramm.io -* Github: [@Monogramm](https://github.com/Monogramm) +- Website: +- Github: [@Monogramm](https://github.com/Monogramm) **John Vincent Fiel** -* Github: [@jvfiel](https://github.com/jvfiel) +- Github: [@jvfiel](https://github.com/jvfiel) ## :handshake: Contributing @@ -114,5 +111,6 @@ Give a :star: if this project helped you! Copyright © 2019 [Monogramm](https://github.com/Monogramm).
This project is [MIT](uri_license) licensed. -*** +* * * + _This README was generated with :heart: by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ diff --git a/erpnext_ocr/tests/README.md b/erpnext_ocr/tests/README.md index 4dbabe15..a597106f 100644 --- a/erpnext_ocr/tests/README.md +++ b/erpnext_ocr/tests/README.md @@ -1,32 +1,37 @@ # tesseract-python + Examples to implement OCR(Optical Character Recognition) using tesseract using Python ## Installation: -- Install tesserct-ocr using this command: - ``` - sudo apt-get install tesseract-ocr - ``` +- Install tesserct-ocr using this command: + +```sh +sudo apt-get install tesseract-ocr +``` + +- Install python binding for tesseract, tesserocr, using this pip command: -- Install python binding for tesseract, tesserocr, using this pip command: - ``` - pip install tesserocr - ``` +```sh +pip install tesserocr +``` -- Install image processing library in python, pillow using this pip command: - ``` - pip install pillow - ``` +- Install image processing library in python, pillow using this pip command: +```sh +pip install pillow +``` **For working with pdf files:** -- Install imagemagick using this command: - ``` - sudo apt-get install imagemagick - ``` +- Install imagemagick using this command: + +```sh +sudo apt-get install imagemagick +``` + +- Install python binding for imagemagick, wand, using this pip command: -- Install python binding for imagemagick, wand, using this pip command: - ``` - pip install wand - ``` +```sh +pip install wand +``` From ac00f73e475997e00aa4b825a8c9bb757d91aea3 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 23:16:19 +0100 Subject: [PATCH 31/33] :art: Space doc issues listed Signed-off-by: mathieu.brunot --- README.md | 10 +++++++++- erpnext_ocr/tests/README.md | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d136c32..6a6c53be 100644 --- a/README.md +++ b/README.md @@ -68,16 +68,24 @@ Check tesseract Wiki for details: - + - `wand.exceptions.WandRuntimeError: MagickReadImage returns false, but did raise ImageMagick exception. This can occurs when a delegate is missing, or returns EXIT_SUCCESS without generating a raster.` + - This might happen if you're missing a dependency to convert PDF, most of the time `ghostscript` + - References: - + - `OSError: encoder error -2 when writing image file` + - This might happen when trying to open a TIFF image, but the real error is "_hidden_" and only displayed in console. + - If the original error in console is `Fax3SetupState: Bits/sample must be 1 for Group 3/4 encoding/decoding.` that usually happens when TIFF image compression is not valid / recognized. ## :white_check_mark: Run tests diff --git a/erpnext_ocr/tests/README.md b/erpnext_ocr/tests/README.md index a597106f..8859cb28 100644 --- a/erpnext_ocr/tests/README.md +++ b/erpnext_ocr/tests/README.md @@ -2,7 +2,7 @@ Examples to implement OCR(Optical Character Recognition) using tesseract using Python -## Installation: +## Installation - Install tesserct-ocr using this command: From 8551f3afefcfb86797c495b3d8c095966daa7b64 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 27 Nov 2019 23:26:51 +0100 Subject: [PATCH 32/33] :art: Remove useless empty line in README Signed-off-by: mathieu.brunot --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6a6c53be..5900efea 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ Check tesseract Wiki for details: Date: Wed, 27 Nov 2019 23:27:22 +0100 Subject: [PATCH 33/33] :memo: Updating CHANGELOG for release 1.0.0 Signed-off-by: mathieu.brunot --- CHANGELOG.md | 81 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef540a83..8f9df9d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Changelog + All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased -## [Unreleased] ### Added ### Changed @@ -13,40 +14,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed + + +## 1.0.0 - 2019-11-27 + +Differences with previous release: [1.0.0...0.9.0](https://github.com/Monogramm/erpnext_ocr/compare/1.0.0...0.9.0) + +### Added + +- :sparkles: Progress bar during document read +- :construction_worker: Add unit tests and coverage analysis to CI +- :sparkles: Read only field to indicate Language available for OCR +- :sparkles: Add OCR settings + +### Changed + +- :wrench: Allow all users to read with OCR +- :zap: Replace pytesseract by tesserocr + -## [0.9.0] - 2019-11-06 + +## 0.9.0 - 2019-11-06 ### Added -- PDF management in `OCR Read` -- `OCR Language` to manage available tesseract traindata files -- French translations -- GitHub issue and feature templates -- GitHub bots config ([stale](https://github.com/apps/stale) and [behaviorbot](https://github.com/behaviorbot)) -- [Travis-CI](https://travis-ci.org/) using [docker images](https://github.com/Monogramm/docker-erpnext) to setup ERPNext test environment -- Contributing guidelines -- This CHANGELOG file to hopefully help to track changes done to the project. + +- PDF management in `OCR Read` +- `OCR Language` to manage available tesseract traindata files +- French translations +- GitHub issue and feature templates +- GitHub bots config ([stale](https://github.com/apps/stale) and [behaviorbot](https://github.com/behaviorbot)) +- [Travis-CI](https://travis-ci.org/) using [docker images](https://github.com/Monogramm/docker-erpnext) to setup ERPNext test environment +- Contributing guidelines +- This CHANGELOG file to hopefully help to track changes done to the project. ### Changed -- PIP requirements for easier (auto) install -- README documentation on requirements, installation and common issues -- Desktop icon, color, name and docs -- Repository name (changed case) -- Author/maintainer info + +- PIP requirements for easier (auto) install +- README documentation on requirements, installation and common issues +- Desktop icon, color, name and docs +- Repository name (changed case) +- Author/maintainer info ### Fixed -- Python 3 compatibility -- Management of public/private files upload + +- Python 3 compatibility +- Management of public/private files upload ### Removed -- Sales Invoice custom fields and scripts -- OCR Receipt for Sales Invoice -- ABBYY OCR -- Zapier webhook -- Aimara / jstree / treeview -## [master] - 2018-02-12 +- Sales Invoice custom fields and scripts +- OCR Receipt for Sales Invoice +- ABBYY OCR +- Zapier webhook +- Aimara / jstree / treeview + +## Legacy - 2018-02-12 + ### Added -- All the good work from [John Vincent Fiel](https://github.com/jvfiel) on the source of this project -[Unreleased]: https://github.com/jvfiel/ERPNext-OCR/compare/master...Monogramm:develop -[master]: https://github.com/jvfiel/ERPNext-OCR/tree/master +- All the good work from [John Vincent Fiel](https://github.com/jvfiel) on the source of this project. + +Source of fork: [jvfiel/ERPNext-OCR](https://github.com/jvfiel/ERPNext-OCR/tree/master)