From ffecf10688f1af030db8f8dd3f70def9a2875595 Mon Sep 17 00:00:00 2001 From: lvxiangxiang Date: Tue, 14 Jul 2020 11:33:54 +0800 Subject: [PATCH] ios ocr demo --- deploy/ios_demo/download_dependencies.sh | 32 + .../ocr_demo.xcodeproj/project.pbxproj | 462 ++ deploy/ios_demo/ocr_demo/AppDelegate.h | 17 + deploy/ios_demo/ocr_demo/AppDelegate.m | 51 + .../AppIcon.appiconset/Contents.json | 98 + .../ocr_demo/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../ocr_demo/Base.lproj/Main.storyboard | 111 + deploy/ios_demo/ocr_demo/BoxLayer.h | 20 + deploy/ios_demo/ocr_demo/BoxLayer.m | 80 + deploy/ios_demo/ocr_demo/Helpers.h | 31 + deploy/ios_demo/ocr_demo/Helpers.m | 17 + deploy/ios_demo/ocr_demo/Info.plist | 49 + deploy/ios_demo/ocr_demo/OcrData.h | 15 + deploy/ios_demo/ocr_demo/OcrData.m | 12 + deploy/ios_demo/ocr_demo/ViewController.h | 16 + deploy/ios_demo/ocr_demo/ViewController.mm | 536 ++ deploy/ios_demo/ocr_demo/label_list.txt | 6623 +++++++++++++++++ deploy/ios_demo/ocr_demo/main.m | 16 + deploy/ios_demo/ocr_demo/ocr.png | Bin 0 -> 62926 bytes .../ios_demo/ocr_demo/pdocr/ocr_clipper.cpp | 4629 ++++++++++++ .../ios_demo/ocr_demo/pdocr/ocr_clipper.hpp | 547 ++ .../ocr_demo/pdocr/ocr_crnn_process.cpp | 141 + .../ocr_demo/pdocr/ocr_crnn_process.h | 19 + .../ocr_demo/pdocr/ocr_db_post_process.cpp | 372 + .../ocr_demo/pdocr/ocr_db_post_process.h | 10 + deploy/ios_demo/ocr_demo/timer.h | 87 + 27 files changed, 14022 insertions(+) create mode 100755 deploy/ios_demo/download_dependencies.sh create mode 100644 deploy/ios_demo/ocr_demo.xcodeproj/project.pbxproj create mode 100644 deploy/ios_demo/ocr_demo/AppDelegate.h create mode 100644 deploy/ios_demo/ocr_demo/AppDelegate.m create mode 100644 deploy/ios_demo/ocr_demo/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 deploy/ios_demo/ocr_demo/Assets.xcassets/Contents.json create mode 100644 deploy/ios_demo/ocr_demo/Base.lproj/LaunchScreen.storyboard create mode 100644 deploy/ios_demo/ocr_demo/Base.lproj/Main.storyboard create mode 100644 deploy/ios_demo/ocr_demo/BoxLayer.h create mode 100644 deploy/ios_demo/ocr_demo/BoxLayer.m create mode 100644 deploy/ios_demo/ocr_demo/Helpers.h create mode 100644 deploy/ios_demo/ocr_demo/Helpers.m create mode 100644 deploy/ios_demo/ocr_demo/Info.plist create mode 100644 deploy/ios_demo/ocr_demo/OcrData.h create mode 100644 deploy/ios_demo/ocr_demo/OcrData.m create mode 100644 deploy/ios_demo/ocr_demo/ViewController.h create mode 100644 deploy/ios_demo/ocr_demo/ViewController.mm create mode 100644 deploy/ios_demo/ocr_demo/label_list.txt create mode 100644 deploy/ios_demo/ocr_demo/main.m create mode 100644 deploy/ios_demo/ocr_demo/ocr.png create mode 100644 deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.cpp create mode 100644 deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.hpp create mode 100644 deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.cpp create mode 100644 deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.h create mode 100644 deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.cpp create mode 100644 deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.h create mode 100644 deploy/ios_demo/ocr_demo/timer.h diff --git a/deploy/ios_demo/download_dependencies.sh b/deploy/ios_demo/download_dependencies.sh new file mode 100755 index 00000000..40a8b3d2 --- /dev/null +++ b/deploy/ios_demo/download_dependencies.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +OCR_MODEL_URL="https://paddleocr.bj.bcebos.com/deploy/lite/ocr_v1_for_cpu.tar.gz" +PADDLE_LITE_LIB_URL="https://paddlelite-demo.bj.bcebos.com/libs/ios/paddle_lite_libs_v2_6_0.tar.gz" +OPENCV3_FRAMEWORK_URL="https://paddlelite-demo.bj.bcebos.com/libs/ios/opencv3.framework.tar.gz" + +download_and_extract() { + local url="$1" + local dst_dir="$2" + local tempdir=$(mktemp -d) + + echo "Downloading ${url} ..." + curl -L ${url} > ${tempdir}/temp.tar.gz + echo "Download ${url} done " + + if [ ! -d ${dst_dir} ];then + mkdir -p ${dst_dir} + fi + + echo "Extracting ..." + tar -zxvf ${tempdir}/temp.tar.gz -C ${dst_dir} + echo "Extract done " + + rm -rf ${tempdir} +} + +echo -e "[Download ios ocr demo denpendancy]\n" +download_and_extract "${OCR_MODEL_URL}" "./ios-demo/ocr_demo/models" +download_and_extract "${PADDLE_LITE_LIB_URL}" "./ios-demo/ocr_demo" +download_and_extract "${OPENCV3_FRAMEWORK_URL}" "./ios-demo/ocr_demo" +echo -e "[done]\n" diff --git a/deploy/ios_demo/ocr_demo.xcodeproj/project.pbxproj b/deploy/ios_demo/ocr_demo.xcodeproj/project.pbxproj new file mode 100644 index 00000000..4a5fc93e --- /dev/null +++ b/deploy/ios_demo/ocr_demo.xcodeproj/project.pbxproj @@ -0,0 +1,462 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A98EA8B624CD8E85B9EFADA1 /* OcrData.m in Sources */ = {isa = PBXBuildFile; fileRef = A98EAD74A71FE136D084F392 /* OcrData.m */; }; + E0A53559219A832A005A6056 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E0A53558219A832A005A6056 /* AppDelegate.m */; }; + E0A5355C219A832A005A6056 /* ViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = E0A5355B219A832A005A6056 /* ViewController.mm */; }; + E0A5355F219A832A005A6056 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E0A5355D219A832A005A6056 /* Main.storyboard */; }; + E0A53561219A832E005A6056 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E0A53560219A832E005A6056 /* Assets.xcassets */; }; + E0A53564219A832E005A6056 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E0A53562219A832E005A6056 /* LaunchScreen.storyboard */; }; + E0A53567219A832E005A6056 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E0A53566219A832E005A6056 /* main.m */; }; + E0A53576219A89FF005A6056 /* lib in Resources */ = {isa = PBXBuildFile; fileRef = E0A53574219A89FF005A6056 /* lib */; }; + E0A5357B219AA3D1005A6056 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0A5357A219AA3D1005A6056 /* AVFoundation.framework */; }; + E0A5357D219AA3DF005A6056 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0A5357C219AA3DE005A6056 /* AssetsLibrary.framework */; }; + E0A5357F219AA3E7005A6056 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0A5357E219AA3E7005A6056 /* CoreMedia.framework */; }; + ED37528F24B88737008DEBDA /* opencv2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED37528E24B88737008DEBDA /* opencv2.framework */; }; + ED3752A424B9AD5C008DEBDA /* ocr_clipper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED37529E24B9AD5C008DEBDA /* ocr_clipper.cpp */; }; + ED3752A524B9AD5C008DEBDA /* ocr_db_post_process.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED3752A024B9AD5C008DEBDA /* ocr_db_post_process.cpp */; }; + ED3752A624B9AD5C008DEBDA /* ocr_crnn_process.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED3752A124B9AD5C008DEBDA /* ocr_crnn_process.cpp */; }; + ED3752A924B9ADB4008DEBDA /* BoxLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = ED3752A824B9ADB4008DEBDA /* BoxLayer.m */; }; + ED3752AC24B9B118008DEBDA /* Helpers.m in Sources */ = {isa = PBXBuildFile; fileRef = ED3752AA24B9B118008DEBDA /* Helpers.m */; }; + ED3752B324B9D4C6008DEBDA /* ch_det_mv3_db_opt.nb in Resources */ = {isa = PBXBuildFile; fileRef = ED3752B124B9D4C6008DEBDA /* ch_det_mv3_db_opt.nb */; }; + ED3752B424B9D4C6008DEBDA /* ch_rec_mv3_crnn_opt.nb in Resources */ = {isa = PBXBuildFile; fileRef = ED3752B224B9D4C6008DEBDA /* ch_rec_mv3_crnn_opt.nb */; }; + ED3752BC24B9DAD7008DEBDA /* ocr.png in Resources */ = {isa = PBXBuildFile; fileRef = ED3752BA24B9DAD6008DEBDA /* ocr.png */; }; + ED3752BD24B9DAD7008DEBDA /* label_list.txt in Resources */ = {isa = PBXBuildFile; fileRef = ED3752BB24B9DAD7008DEBDA /* label_list.txt */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A98EAD74A71FE136D084F392 /* OcrData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OcrData.m; sourceTree = ""; }; + A98EAD76E0669B2BFB628B48 /* OcrData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OcrData.h; sourceTree = ""; }; + E0846DF2230BC93900031405 /* timer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = timer.h; sourceTree = ""; }; + E0A18EDD219C03000015DC15 /* face.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = face.jpg; sourceTree = ""; }; + E0A53554219A8329005A6056 /* paddle-lite-ocr.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "paddle-lite-ocr.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + E0A53557219A832A005A6056 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + E0A53558219A832A005A6056 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + E0A5355A219A832A005A6056 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + E0A5355B219A832A005A6056 /* ViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ViewController.mm; sourceTree = ""; }; + E0A5355E219A832A005A6056 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + E0A53560219A832E005A6056 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E0A53563219A832E005A6056 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + E0A53565219A832E005A6056 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E0A53566219A832E005A6056 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + E0A53574219A89FF005A6056 /* lib */ = {isa = PBXFileReference; lastKnownFileType = folder; path = lib; sourceTree = ""; }; + E0A5357A219AA3D1005A6056 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + E0A5357C219AA3DE005A6056 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + E0A5357E219AA3E7005A6056 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + ED37528E24B88737008DEBDA /* opencv2.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = opencv2.framework; path = ocr_demo/opencv2.framework; sourceTree = ""; }; + ED37529E24B9AD5C008DEBDA /* ocr_clipper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ocr_clipper.cpp; sourceTree = ""; }; + ED37529F24B9AD5C008DEBDA /* ocr_db_post_process.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ocr_db_post_process.h; sourceTree = ""; }; + ED3752A024B9AD5C008DEBDA /* ocr_db_post_process.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ocr_db_post_process.cpp; sourceTree = ""; }; + ED3752A124B9AD5C008DEBDA /* ocr_crnn_process.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ocr_crnn_process.cpp; sourceTree = ""; }; + ED3752A224B9AD5C008DEBDA /* ocr_clipper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ocr_clipper.hpp; sourceTree = ""; }; + ED3752A324B9AD5C008DEBDA /* ocr_crnn_process.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ocr_crnn_process.h; sourceTree = ""; }; + ED3752A724B9ADB4008DEBDA /* BoxLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BoxLayer.h; sourceTree = ""; }; + ED3752A824B9ADB4008DEBDA /* BoxLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BoxLayer.m; sourceTree = ""; }; + ED3752AA24B9B118008DEBDA /* Helpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Helpers.m; sourceTree = ""; }; + ED3752AB24B9B118008DEBDA /* Helpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Helpers.h; sourceTree = ""; }; + ED3752B124B9D4C6008DEBDA /* ch_det_mv3_db_opt.nb */ = {isa = PBXFileReference; lastKnownFileType = file; path = ch_det_mv3_db_opt.nb; sourceTree = ""; }; + ED3752B224B9D4C6008DEBDA /* ch_rec_mv3_crnn_opt.nb */ = {isa = PBXFileReference; lastKnownFileType = file; path = ch_rec_mv3_crnn_opt.nb; sourceTree = ""; }; + ED3752BA24B9DAD6008DEBDA /* ocr.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ocr.png; sourceTree = ""; }; + ED3752BB24B9DAD7008DEBDA /* label_list.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = label_list.txt; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E0A53551219A8329005A6056 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ED37528F24B88737008DEBDA /* opencv2.framework in Frameworks */, + E0A5357F219AA3E7005A6056 /* CoreMedia.framework in Frameworks */, + E0A5357D219AA3DF005A6056 /* AssetsLibrary.framework in Frameworks */, + E0A5357B219AA3D1005A6056 /* AVFoundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E0846DE8230658DC00031405 /* models */ = { + isa = PBXGroup; + children = ( + ED3752B124B9D4C6008DEBDA /* ch_det_mv3_db_opt.nb */, + ED3752B224B9D4C6008DEBDA /* ch_rec_mv3_crnn_opt.nb */, + ); + path = models; + sourceTree = ""; + }; + E0A5354B219A8329005A6056 = { + isa = PBXGroup; + children = ( + E0A53556219A8329005A6056 /* ocr_demo */, + E0A53555219A8329005A6056 /* Products */, + E0A53570219A8945005A6056 /* Frameworks */, + ); + sourceTree = ""; + }; + E0A53555219A8329005A6056 /* Products */ = { + isa = PBXGroup; + children = ( + E0A53554219A8329005A6056 /* paddle-lite-ocr.app */, + ); + name = Products; + sourceTree = ""; + }; + E0A53556219A8329005A6056 /* ocr_demo */ = { + isa = PBXGroup; + children = ( + ED3752BB24B9DAD7008DEBDA /* label_list.txt */, + ED3752BA24B9DAD6008DEBDA /* ocr.png */, + ED3752AB24B9B118008DEBDA /* Helpers.h */, + ED3752AA24B9B118008DEBDA /* Helpers.m */, + ED3752A724B9ADB4008DEBDA /* BoxLayer.h */, + ED3752A824B9ADB4008DEBDA /* BoxLayer.m */, + ED37529D24B9AD5C008DEBDA /* pdocr */, + E0846DE8230658DC00031405 /* models */, + E0A18EDD219C03000015DC15 /* face.jpg */, + E0A53574219A89FF005A6056 /* lib */, + E0A53557219A832A005A6056 /* AppDelegate.h */, + E0A53558219A832A005A6056 /* AppDelegate.m */, + E0846DF2230BC93900031405 /* timer.h */, + E0A5355A219A832A005A6056 /* ViewController.h */, + E0A5355B219A832A005A6056 /* ViewController.mm */, + E0A5355D219A832A005A6056 /* Main.storyboard */, + E0A53560219A832E005A6056 /* Assets.xcassets */, + E0A53562219A832E005A6056 /* LaunchScreen.storyboard */, + E0A53565219A832E005A6056 /* Info.plist */, + E0A53566219A832E005A6056 /* main.m */, + A98EAD74A71FE136D084F392 /* OcrData.m */, + A98EAD76E0669B2BFB628B48 /* OcrData.h */, + ); + path = ocr_demo; + sourceTree = ""; + }; + E0A53570219A8945005A6056 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED37528E24B88737008DEBDA /* opencv2.framework */, + E0A5357E219AA3E7005A6056 /* CoreMedia.framework */, + E0A5357C219AA3DE005A6056 /* AssetsLibrary.framework */, + E0A5357A219AA3D1005A6056 /* AVFoundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + ED37529D24B9AD5C008DEBDA /* pdocr */ = { + isa = PBXGroup; + children = ( + ED37529E24B9AD5C008DEBDA /* ocr_clipper.cpp */, + ED37529F24B9AD5C008DEBDA /* ocr_db_post_process.h */, + ED3752A024B9AD5C008DEBDA /* ocr_db_post_process.cpp */, + ED3752A124B9AD5C008DEBDA /* ocr_crnn_process.cpp */, + ED3752A224B9AD5C008DEBDA /* ocr_clipper.hpp */, + ED3752A324B9AD5C008DEBDA /* ocr_crnn_process.h */, + ); + path = pdocr; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E0A53553219A8329005A6056 /* ocr_demo */ = { + isa = PBXNativeTarget; + buildConfigurationList = E0A5356A219A832E005A6056 /* Build configuration list for PBXNativeTarget "ocr_demo" */; + buildPhases = ( + E0A53550219A8329005A6056 /* Sources */, + E0A53551219A8329005A6056 /* Frameworks */, + E0A53552219A8329005A6056 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ocr_demo; + productName = seg_demo; + productReference = E0A53554219A8329005A6056 /* paddle-lite-ocr.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E0A5354C219A8329005A6056 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = "Li,Xiaoyang(SYS)"; + TargetAttributes = { + E0A53553219A8329005A6056 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = E0A5354F219A8329005A6056 /* Build configuration list for PBXProject "ocr_demo" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E0A5354B219A8329005A6056; + productRefGroup = E0A53555219A8329005A6056 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E0A53553219A8329005A6056 /* ocr_demo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E0A53552219A8329005A6056 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E0A53564219A832E005A6056 /* LaunchScreen.storyboard in Resources */, + ED3752B324B9D4C6008DEBDA /* ch_det_mv3_db_opt.nb in Resources */, + E0A53576219A89FF005A6056 /* lib in Resources */, + ED3752B424B9D4C6008DEBDA /* ch_rec_mv3_crnn_opt.nb in Resources */, + E0A53561219A832E005A6056 /* Assets.xcassets in Resources */, + ED3752BC24B9DAD7008DEBDA /* ocr.png in Resources */, + E0A5355F219A832A005A6056 /* Main.storyboard in Resources */, + ED3752BD24B9DAD7008DEBDA /* label_list.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E0A53550219A8329005A6056 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E0A5355C219A832A005A6056 /* ViewController.mm in Sources */, + ED3752A924B9ADB4008DEBDA /* BoxLayer.m in Sources */, + E0A53567219A832E005A6056 /* main.m in Sources */, + ED3752A424B9AD5C008DEBDA /* ocr_clipper.cpp in Sources */, + E0A53559219A832A005A6056 /* AppDelegate.m in Sources */, + ED3752A524B9AD5C008DEBDA /* ocr_db_post_process.cpp in Sources */, + ED3752AC24B9B118008DEBDA /* Helpers.m in Sources */, + ED3752A624B9AD5C008DEBDA /* ocr_crnn_process.cpp in Sources */, + A98EA8B624CD8E85B9EFADA1 /* OcrData.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + E0A5355D219A832A005A6056 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E0A5355E219A832A005A6056 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + E0A53562219A832E005A6056 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E0A53563219A832E005A6056 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E0A53568219A832E005A6056 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + E0A53569219A832E005A6056 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E0A5356B219A832E005A6056 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/ocr_demo", + ); + INFOPLIST_FILE = "$(SRCROOT)/ocr_demo/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/ocr_demo/lib/"; + OTHER_LDFLAGS = "$(PROJECT_DIR)/ocr_demo/lib/libpaddle_api_light_bundled.a"; + PRODUCT_BUNDLE_IDENTIFIER = "com.baidu.paddlelite-ocr-demo"; + PRODUCT_NAME = "paddle-lite-ocr"; + PROVISIONING_PROFILE_SPECIFIER = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "\"$(SRCROOT)/ocr_demo/\""; + }; + name = Debug; + }; + E0A5356C219A832E005A6056 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/ocr_demo", + ); + INFOPLIST_FILE = "$(SRCROOT)/ocr_demo/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/ocr_demo/lib/"; + OTHER_LDFLAGS = "$(PROJECT_DIR)/ocr_demo/lib/libpaddle_api_light_bundled.a"; + PRODUCT_BUNDLE_IDENTIFIER = "com.baidu.paddlelite-ocr-demo"; + PRODUCT_NAME = "paddle-lite-ocr"; + PROVISIONING_PROFILE_SPECIFIER = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "\"$(SRCROOT)/ocr_demo/\""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E0A5354F219A8329005A6056 /* Build configuration list for PBXProject "ocr_demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E0A53568219A832E005A6056 /* Debug */, + E0A53569219A832E005A6056 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E0A5356A219A832E005A6056 /* Build configuration list for PBXNativeTarget "ocr_demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E0A5356B219A832E005A6056 /* Debug */, + E0A5356C219A832E005A6056 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E0A5354C219A8329005A6056 /* Project object */; +} diff --git a/deploy/ios_demo/ocr_demo/AppDelegate.h b/deploy/ios_demo/ocr_demo/AppDelegate.h new file mode 100644 index 00000000..191a9a3e --- /dev/null +++ b/deploy/ios_demo/ocr_demo/AppDelegate.h @@ -0,0 +1,17 @@ +// +// AppDelegate.h +// seg_demo +// +// Created by Li,Xiaoyang(SYS) on 2018/11/13. +// Copyright © 2018年 Li,Xiaoyang(SYS). All rights reserved. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/deploy/ios_demo/ocr_demo/AppDelegate.m b/deploy/ios_demo/ocr_demo/AppDelegate.m new file mode 100644 index 00000000..454f23c6 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/AppDelegate.m @@ -0,0 +1,51 @@ +// +// AppDelegate.m +// seg_demo +// +// Created by Li,Xiaoyang(SYS) on 2018/11/13. +// Copyright © 2018年 Li,Xiaoyang(SYS). All rights reserved. +// + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. +} + + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. +} + + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + + +@end diff --git a/deploy/ios_demo/ocr_demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/deploy/ios_demo/ocr_demo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d8db8d65 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/deploy/ios_demo/ocr_demo/Assets.xcassets/Contents.json b/deploy/ios_demo/ocr_demo/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/deploy/ios_demo/ocr_demo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/deploy/ios_demo/ocr_demo/Base.lproj/LaunchScreen.storyboard b/deploy/ios_demo/ocr_demo/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..bfa36129 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deploy/ios_demo/ocr_demo/Base.lproj/Main.storyboard b/deploy/ios_demo/ocr_demo/Base.lproj/Main.storyboard new file mode 100644 index 00000000..6d4e7b51 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/Base.lproj/Main.storyboard @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deploy/ios_demo/ocr_demo/BoxLayer.h b/deploy/ios_demo/ocr_demo/BoxLayer.h new file mode 100644 index 00000000..9a3cc10e --- /dev/null +++ b/deploy/ios_demo/ocr_demo/BoxLayer.h @@ -0,0 +1,20 @@ +// +// Created by chenxiaoyu on 2018/5/5. +// Copyright (c) 2018 baidu. All rights reserved. +// + +#import +#import +#import "OcrData.h" + + +@interface BoxLayer : CAShapeLayer + +/** + * 绘制OCR的结果 + */ +-(void) renderOcrPolygon: (OcrData *)data withHeight:(CGFloat)originHeight withWidth:(CGFloat)originWidth withLabel:(bool) withLabel; + + + +@end diff --git a/deploy/ios_demo/ocr_demo/BoxLayer.m b/deploy/ios_demo/ocr_demo/BoxLayer.m new file mode 100644 index 00000000..9ad0c4ee --- /dev/null +++ b/deploy/ios_demo/ocr_demo/BoxLayer.m @@ -0,0 +1,80 @@ +// +// Created by chenxiaoyu on 2018/5/5. +// Copyright (c) 2018 baidu. All rights reserved. +// + +#include "BoxLayer.h" +#import "Helpers.h" + +@implementation BoxLayer { + +} + +#define MAIN_COLOR UIColorFromRGB(0x3B85F5) +- (void)renderOcrPolygon:(OcrData *)d withHeight:(CGFloat)originHeight withWidth:(CGFloat)originWidth withLabel:(bool)withLabel { + + if ([d.polygonPoints count] != 4) { + NSLog(@"poloygonPoints size is not 4"); + return; + } + + CGPoint startPoint = [d.polygonPoints[0] CGPointValue]; + NSString *text = d.label; + + CGFloat x = startPoint.x * originWidth; + CGFloat y = startPoint.y * originHeight; + CGFloat width = originWidth - x; + CGFloat height = originHeight - y; + + + UIFont *font = [UIFont systemFontOfSize:16]; + NSDictionary *attrs = @{ +// NSStrokeColorAttributeName: [UIColor blackColor], + NSForegroundColorAttributeName: [UIColor whiteColor], +// NSStrokeWidthAttributeName : @((float) -6.0), + NSFontAttributeName: font + }; + + + if (withLabel) { + NSAttributedString *displayStr = [[NSAttributedString alloc] initWithString:text attributes:attrs]; + CATextLayer *textLayer = [[CATextLayer alloc] init]; + textLayer.wrapped = YES; + textLayer.string = displayStr; + textLayer.frame = CGRectMake(x + 2, y + 2, width, height); + textLayer.contentsScale = [[UIScreen mainScreen] scale]; + + // 加阴影显得有点乱 +// textLayer.shadowColor = [MAIN_COLOR CGColor]; +// textLayer.shadowOffset = CGSizeMake(2.0, 2.0); +// textLayer.shadowOpacity = 0.8; +// textLayer.shadowRadius = 0.0; + + [self addSublayer:textLayer]; + } + + + UIBezierPath *path = [UIBezierPath new]; + + + [path moveToPoint:CGPointMake(startPoint.x * originWidth, startPoint.y * originHeight)]; + for (NSValue *val in d.polygonPoints) { + CGPoint p = [val CGPointValue]; + [path addLineToPoint:CGPointMake(p.x * originWidth, p.y * originHeight)]; + } + [path closePath]; + + self.path = path.CGPath; + self.strokeColor = MAIN_COLOR.CGColor; + self.lineWidth = 2.0; + self.fillColor = [MAIN_COLOR colorWithAlphaComponent:0.2].CGColor; + self.lineJoin = kCALineJoinBevel; + +} + +- (void)renderSingleBox:(OcrData *)data withHeight:(CGFloat)originHeight withWidth:(CGFloat)originWidth { + [self renderOcrPolygon:data withHeight:originHeight withWidth:originWidth withLabel:YES]; +} + + +@end diff --git a/deploy/ios_demo/ocr_demo/Helpers.h b/deploy/ios_demo/ocr_demo/Helpers.h new file mode 100644 index 00000000..2437472f --- /dev/null +++ b/deploy/ios_demo/ocr_demo/Helpers.h @@ -0,0 +1,31 @@ +// +// Helpers.h +// EasyDLDemo +// +// Created by chenxiaoyu on 2018/5/14. +// Copyright © 2018年 baidu. All rights reserved. +// + +#import +#import + +#define UIColorFromRGB(rgbValue) \ +[UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ +green:((float)((rgbValue & 0x00FF00) >> 8))/255.0 \ +blue:((float)((rgbValue & 0x0000FF) >> 0))/255.0 \ +alpha:1.0] + +#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height +#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width + +#define HIGHLIGHT_COLOR UIColorFromRGB(0xF5A623) + +//#define BTN_HIGHTLIGH_TEXT_COLOR UIColorFromRGB(0xF5A623) + + +@interface Helpers : NSObject { + + +} + +@end diff --git a/deploy/ios_demo/ocr_demo/Helpers.m b/deploy/ios_demo/ocr_demo/Helpers.m new file mode 100644 index 00000000..13191d4f --- /dev/null +++ b/deploy/ios_demo/ocr_demo/Helpers.m @@ -0,0 +1,17 @@ +// +// Helpers.m +// EasyDLDemo +// +// Created by chenxiaoyu on 2018/5/14. +// Copyright © 2018年 baidu. All rights reserved. +// + +#import "Helpers.h" + + + +@implementation Helpers + + + +@end diff --git a/deploy/ios_demo/ocr_demo/Info.plist b/deploy/ios_demo/ocr_demo/Info.plist new file mode 100644 index 00000000..bcdffa3c --- /dev/null +++ b/deploy/ios_demo/ocr_demo/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + for test + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/deploy/ios_demo/ocr_demo/OcrData.h b/deploy/ios_demo/ocr_demo/OcrData.h new file mode 100644 index 00000000..b6f397b9 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/OcrData.h @@ -0,0 +1,15 @@ +// +// Created by Lv,Xiangxiang on 2020/7/11. +// Copyright (c) 2020 Li,Xiaoyang(SYS). All rights reserved. +// + +#import + + +@interface OcrData : NSObject +@property(nonatomic, copy) NSString *label; +@property(nonatomic) int category; +@property(nonatomic) float accuracy; +@property(nonatomic) NSArray *polygonPoints; + +@end \ No newline at end of file diff --git a/deploy/ios_demo/ocr_demo/OcrData.m b/deploy/ios_demo/ocr_demo/OcrData.m new file mode 100644 index 00000000..d5850dd4 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/OcrData.m @@ -0,0 +1,12 @@ +// +// Created by Lv,Xiangxiang on 2020/7/11. +// Copyright (c) 2020 Li,Xiaoyang(SYS). All rights reserved. +// + +#import "OcrData.h" + + +@implementation OcrData { + +} +@end \ No newline at end of file diff --git a/deploy/ios_demo/ocr_demo/ViewController.h b/deploy/ios_demo/ocr_demo/ViewController.h new file mode 100644 index 00000000..f51b6a68 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/ViewController.h @@ -0,0 +1,16 @@ +// +// ViewController.h +// seg_demo +// +// Created by Li,Xiaoyang(SYS) on 2018/11/13. +// Copyright © 2018年 Li,Xiaoyang(SYS). All rights reserved. +// + +#import + + +@interface ViewController : UIViewController + + +@end + diff --git a/deploy/ios_demo/ocr_demo/ViewController.mm b/deploy/ios_demo/ocr_demo/ViewController.mm new file mode 100644 index 00000000..175d0192 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/ViewController.mm @@ -0,0 +1,536 @@ +// +// Created by lvxiangxiang on 2020/7/10. +// Copyright (c) 2020 baidu. All rights reserved. +// + +#import +#import +#import +//#import +#import "ViewController.h" +#import "BoxLayer.h" + +#include "include/paddle_api.h" +#include "timer.h" +#import "pdocr/ocr_db_post_process.h" +#import "pdocr/ocr_crnn_process.h" + +using namespace paddle::lite_api; +using namespace cv; + +struct Object { + int batch_id; + cv::Rect rec; + int class_id; + float prob; +}; + +std::mutex mtx; +std::shared_ptr net_ocr1; +std::shared_ptr net_ocr2; +Timer tic; +long long count = 0; + +double tensor_mean(const Tensor &tin) { + auto shape = tin.shape(); + int64_t size = 1; + for (int i = 0; i < shape.size(); i++) { + size *= shape[i]; + } + double mean = 0.; + auto ptr = tin.data(); + for (int i = 0; i < size; i++) { + mean += ptr[i]; + } + return mean / size; +} + +cv::Mat resize_img_type0(const cv::Mat &img, int max_size_len, float *ratio_h, float *ratio_w) { + int w = img.cols; + int h = img.rows; + + float ratio = 1.f; + int max_wh = w >= h ? w : h; + if (max_wh > max_size_len) { + if (h > w) { + ratio = float(max_size_len) / float(h); + } else { + ratio = float(max_size_len) / float(w); + } + } + + int resize_h = int(float(h) * ratio); + int resize_w = int(float(w) * ratio); + if (resize_h % 32 == 0) + resize_h = resize_h; + else if (resize_h / 32 < 1) + resize_h = 32; + else + resize_h = (resize_h / 32 - 1) * 32; + + if (resize_w % 32 == 0) + resize_w = resize_w; + else if (resize_w / 32 < 1) + resize_w = 32; + else + resize_w = (resize_w / 32 - 1) * 32; + + cv::Mat resize_img; + cv::resize(img, resize_img, cv::Size(resize_w, resize_h)); + + *ratio_h = float(resize_h) / float(h); + *ratio_w = float(resize_w) / float(w); + return resize_img; +} + +void neon_mean_scale(const float *din, float *dout, int size, std::vector mean, std::vector scale) { + float32x4_t vmean0 = vdupq_n_f32(mean[0]); + float32x4_t vmean1 = vdupq_n_f32(mean[1]); + float32x4_t vmean2 = vdupq_n_f32(mean[2]); + float32x4_t vscale0 = vdupq_n_f32(1.f / scale[0]); + float32x4_t vscale1 = vdupq_n_f32(1.f / scale[1]); + float32x4_t vscale2 = vdupq_n_f32(1.f / scale[2]); + + float *dout_c0 = dout; + float *dout_c1 = dout + size; + float *dout_c2 = dout + size * 2; + + int i = 0; + for (; i < size - 3; i += 4) { + float32x4x3_t vin3 = vld3q_f32(din); + float32x4_t vsub0 = vsubq_f32(vin3.val[0], vmean0); + float32x4_t vsub1 = vsubq_f32(vin3.val[1], vmean1); + float32x4_t vsub2 = vsubq_f32(vin3.val[2], vmean2); + float32x4_t vs0 = vmulq_f32(vsub0, vscale0); + float32x4_t vs1 = vmulq_f32(vsub1, vscale1); + float32x4_t vs2 = vmulq_f32(vsub2, vscale2); + vst1q_f32(dout_c0, vs0); + vst1q_f32(dout_c1, vs1); + vst1q_f32(dout_c2, vs2); + + din += 12; + dout_c0 += 4; + dout_c1 += 4; + dout_c2 += 4; + } + for (; i < size; i++) { + *(dout_c0++) = (*(din++) - mean[0]) / scale[0]; + *(dout_c1++) = (*(din++) - mean[1]) / scale[1]; + *(dout_c2++) = (*(din++) - mean[2]) / scale[2]; + } +} + + +// fill tensor with mean and scale, neon speed up +void fill_tensor_with_cvmat(const Mat &img_in, Tensor &tout, int width, int height, + std::vector mean, std::vector scale, bool is_scale) { + if (img_in.channels() == 4) { + cv::cvtColor(img_in, img_in, CV_RGBA2RGB); + } + cv::Mat im; + cv::resize(img_in, im, cv::Size(width, height), 0.f, 0.f); + cv::Mat imgf; + float scale_factor = is_scale ? 1 / 255.f : 1.f; + im.convertTo(imgf, CV_32FC3, scale_factor); + const float *dimg = reinterpret_cast(imgf.data); + float *dout = tout.mutable_data(); + neon_mean_scale(dimg, dout, width * height, mean, scale); +} + +std::vector detect_object(const float *data, + int count, + const std::vector> &lod, + const float thresh, + Mat &image) { + std::vector rect_out; + const float *dout = data; + for (int iw = 0; iw < count; iw++) { + int oriw = image.cols; + int orih = image.rows; + if (dout[1] > thresh && static_cast(dout[0]) > 0) { + Object obj; + int x = static_cast(dout[2] * oriw); + int y = static_cast(dout[3] * orih); + int w = static_cast(dout[4] * oriw) - x; + int h = static_cast(dout[5] * orih) - y; + cv::Rect rec_clip = cv::Rect(x, y, w, h) & cv::Rect(0, 0, image.cols, image.rows); + obj.batch_id = 0; + obj.class_id = static_cast(dout[0]); + obj.prob = dout[1]; + obj.rec = rec_clip; + if (w > 0 && h > 0 && obj.prob <= 1) { + rect_out.push_back(obj); + cv::rectangle(image, rec_clip, cv::Scalar(255, 0, 0)); + } + } + dout += 6; + } + return rect_out; +} + +@interface ViewController () +@property(weak, nonatomic) IBOutlet UIImageView *imageView; +@property(weak, nonatomic) IBOutlet UISwitch *flag_process; +@property(weak, nonatomic) IBOutlet UISwitch *flag_video; +@property(weak, nonatomic) IBOutlet UIImageView *preView; +@property(weak, nonatomic) IBOutlet UISwitch *flag_back_cam; +@property(weak, nonatomic) IBOutlet UILabel *result; +@property(nonatomic, strong) CvVideoCamera *videoCamera; +@property(nonatomic, strong) UIImage *image; +@property(nonatomic) bool flag_init; +@property(nonatomic) bool flag_cap_photo; +@property(nonatomic) std::vector scale; +@property(nonatomic) std::vector mean; +@property(nonatomic) NSArray *labels; +@property(nonatomic) cv::Mat cvimg; +@property(nonatomic, strong) UIImage *ui_img_test; +@property(strong, nonatomic) CALayer *boxLayer; + +@end + +@implementation ViewController +@synthesize imageView; + +- (OcrData *)paddleOcrRec:(cv::Mat)image { + + OcrData *result = [OcrData new]; + + std::vector mean = {0.5f, 0.5f, 0.5f}; + std::vector scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + + cv::Mat crop_img; + image.copyTo(crop_img); + cv::Mat resize_img; + + + float wh_ratio = float(crop_img.cols) / float(crop_img.rows); + + resize_img = crnn_resize_img(crop_img, wh_ratio); + resize_img.convertTo(resize_img, CV_32FC3, 1 / 255.f); + + const float *dimg = reinterpret_cast(resize_img.data); + + std::unique_ptr input_tensor0(std::move(net_ocr2->GetInput(0))); + input_tensor0->Resize({1, 3, resize_img.rows, resize_img.cols}); + auto *data0 = input_tensor0->mutable_data(); + + neon_mean_scale(dimg, data0, resize_img.rows * resize_img.cols, mean, scale); + + //// Run CRNN predictor + net_ocr2->Run(); + + // Get output and run postprocess + std::unique_ptr output_tensor0(std::move(net_ocr2->GetOutput(0))); + auto *rec_idx = output_tensor0->data(); + + auto rec_idx_lod = output_tensor0->lod(); + auto shape_out = output_tensor0->shape(); + NSMutableString *text = [[NSMutableString alloc] init]; + for (int n = int(rec_idx_lod[0][0]); n < int(rec_idx_lod[0][1] * 2); n += 2) { + if (rec_idx[n] >= self.labels.count) { + std::cout << "Index " << rec_idx[n] << " out of text dict range!" << std::endl; + continue; + } + [text appendString:self.labels[rec_idx[n]]]; + } + + result.label = text; + // get score + std::unique_ptr output_tensor1(std::move(net_ocr2->GetOutput(1))); + auto *predict_batch = output_tensor1->data(); + auto predict_shape = output_tensor1->shape(); + + auto predict_lod = output_tensor1->lod(); + + int argmax_idx; + int blank = predict_shape[1]; + float score = 0.f; + int count = 0; + float max_value = 0.0f; + + for (int n = predict_lod[0][0]; n < predict_lod[0][1] - 1; n++) { + argmax_idx = int(argmax(&predict_batch[n * predict_shape[1]], &predict_batch[(n + 1) * predict_shape[1]])); + max_value = float(*std::max_element(&predict_batch[n * predict_shape[1]], &predict_batch[(n + 1) * predict_shape[1]])); + + if (blank - 1 - argmax_idx > 1e-5) { + score += max_value; + count += 1; + } + + } + score /= count; + result.accuracy = score; + return result; +} +- (NSArray *) ocr_infer:(cv::Mat) originImage{ + int max_side_len = 960; + float ratio_h{}; + float ratio_w{}; + cv::Mat image; + cv::cvtColor(originImage, image, cv::COLOR_RGB2BGR); + + cv::Mat img; + image.copyTo(img); + + img = resize_img_type0(img, max_side_len, &ratio_h, &ratio_w); + cv::Mat img_fp; + img.convertTo(img_fp, CV_32FC3, 1.0 / 255.f); + + std::unique_ptr input_tensor(net_ocr1->GetInput(0)); + input_tensor->Resize({1, 3, img_fp.rows, img_fp.cols}); + auto *data0 = input_tensor->mutable_data(); + const float *dimg = reinterpret_cast(img_fp.data); + neon_mean_scale(dimg, data0, img_fp.rows * img_fp.cols, self.mean, self.scale); + tic.clear(); + tic.start(); + net_ocr1->Run(); + std::unique_ptr output_tensor(std::move(net_ocr1->GetOutput(0))); + auto *outptr = output_tensor->data(); + auto shape_out = output_tensor->shape(); + + int64_t out_numl = 1; + double sum = 0; + for (auto i : shape_out) { + out_numl *= i; + } + + int s2 = int(shape_out[2]); + int s3 = int(shape_out[3]); + + cv::Mat pred_map = cv::Mat::zeros(s2, s3, CV_32F); + memcpy(pred_map.data, outptr, s2 * s3 * sizeof(float)); + cv::Mat cbuf_map; + pred_map.convertTo(cbuf_map, CV_8UC1, 255.0f); + + const double threshold = 0.1 * 255; + const double maxvalue = 255; + cv::Mat bit_map; + cv::threshold(cbuf_map, bit_map, threshold, maxvalue, cv::THRESH_BINARY); + + auto boxes = boxes_from_bitmap(pred_map, bit_map); + + std::vector>> filter_boxes = filter_tag_det_res(boxes, ratio_h, ratio_w, image); + + + cv::Point rook_points[filter_boxes.size()][4]; + + for (int n = 0; n < filter_boxes.size(); n++) { + for (int m = 0; m < filter_boxes[0].size(); m++) { + rook_points[n][m] = cv::Point(int(filter_boxes[n][m][0]), int(filter_boxes[n][m][1])); + } + } + + NSMutableArray *result = [[NSMutableArray alloc] init]; + + for (int i = 0; i < filter_boxes.size(); i++) { + cv::Mat crop_img; + crop_img = get_rotate_crop_image(image, filter_boxes[i]); + OcrData *r = [self paddleOcrRec:crop_img ]; + NSMutableArray *points = [NSMutableArray new]; + for (int jj = 0; jj < 4; ++jj) { + NSValue *v = [NSValue valueWithCGPoint:CGPointMake( + rook_points[i][jj].x / CGFloat(originImage.cols), + rook_points[i][jj].y / CGFloat(originImage.rows))]; + [points addObject:v]; + } + r.polygonPoints = points; + [result addObject:r]; + } + NSArray* rec_out =[[result reverseObjectEnumerator] allObjects]; + tic.end(); + std::cout<<"infer time: "<(config); + MobileConfig config2; + config2.set_model_from_file(model2_path_str); + net_ocr2 = CreatePaddlePredictor(config2); + + + cv::Mat originImage; + UIImageToMat(self.image, originImage); + NSArray *rec_out = [self ocr_infer:originImage]; + [_boxLayer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)]; + std::cout<_cvimg, CV_RGBA2RGB); + } + auto rec_out =[self ocr_infer:self->_cvimg]; + std::ostringstream result; + NSInteger cnt = 0; + [_boxLayer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)]; + + CGFloat h = _boxLayer.frame.size.height; + CGFloat w = _boxLayer.frame.size.width; + for (id obj in rec_out) { + OcrData *data = obj; + BoxLayer *singleBox = [[BoxLayer alloc] init]; + [singleBox renderOcrPolygon:data withHeight:h withWidth:w withLabel:YES]; + [_boxLayer addSublayer:singleBox]; + result<<[data.label UTF8String] <<","<_cvimg, self->_cvimg, CV_RGB2BGR); + self.imageView.image = MatToUIImage(self->_cvimg); + } + } + } + }); +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/deploy/ios_demo/ocr_demo/label_list.txt b/deploy/ios_demo/ocr_demo/label_list.txt new file mode 100644 index 00000000..84b885d8 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/label_list.txt @@ -0,0 +1,6623 @@ +' +疗 +绚 +诚 +娇 +溜 +题 +贿 +者 +廖 +更 +纳 +加 +奉 +公 +一 +就 +汴 +计 +与 +路 +房 +原 +妇 +2 +0 +8 +- +7 +其 +> +: +] +, +, +骑 +刈 +全 +消 +昏 +傈 +安 +久 +钟 +嗅 +不 +影 +处 +驽 +蜿 +资 +关 +椤 +地 +瘸 +专 +问 +忖 +票 +嫉 +炎 +韵 +要 +月 +田 +节 +陂 +鄙 +捌 +备 +拳 +伺 +眼 +网 +盎 +大 +傍 +心 +东 +愉 +汇 +蹿 +科 +每 +业 +里 +航 +晏 +字 +平 +录 +先 +1 +3 +彤 +鲶 +产 +稍 +督 +腴 +有 +象 +岳 +注 +绍 +在 +泺 +文 +定 +核 +名 +水 +过 +理 +让 +偷 +率 +等 +这 +发 +” +为 +含 +肥 +酉 +相 +鄱 +七 +编 +猥 +锛 +日 +镀 +蒂 +掰 +倒 +辆 +栾 +栗 +综 +涩 +州 +雌 +滑 +馀 +了 +机 +块 +司 +宰 +甙 +兴 +矽 +抚 +保 +用 +沧 +秩 +如 +收 +息 +滥 +页 +疑 +埠 +! +! +姥 +异 +橹 +钇 +向 +下 +跄 +的 +椴 +沫 +国 +绥 +獠 +报 +开 +民 +蜇 +何 +分 +凇 +长 +讥 +藏 +掏 +施 +羽 +中 +讲 +派 +嘟 +人 +提 +浼 +间 +世 +而 +古 +多 +倪 +唇 +饯 +控 +庚 +首 +赛 +蜓 +味 +断 +制 +觉 +技 +替 +艰 +溢 +潮 +夕 +钺 +外 +摘 +枋 +动 +双 +单 +啮 +户 +枇 +确 +锦 +曜 +杜 +或 +能 +效 +霜 +盒 +然 +侗 +电 +晁 +放 +步 +鹃 +新 +杖 +蜂 +吒 +濂 +瞬 +评 +总 +隍 +对 +独 +合 +也 +是 +府 +青 +天 +诲 +墙 +组 +滴 +级 +邀 +帘 +示 +已 +时 +骸 +仄 +泅 +和 +遨 +店 +雇 +疫 +持 +巍 +踮 +境 +只 +亨 +目 +鉴 +崤 +闲 +体 +泄 +杂 +作 +般 +轰 +化 +解 +迂 +诿 +蛭 +璀 +腾 +告 +版 +服 +省 +师 +小 +规 +程 +线 +海 +办 +引 +二 +桧 +牌 +砺 +洄 +裴 +修 +图 +痫 +胡 +许 +犊 +事 +郛 +基 +柴 +呼 +食 +研 +奶 +律 +蛋 +因 +葆 +察 +戏 +褒 +戒 +再 +李 +骁 +工 +貂 +油 +鹅 +章 +啄 +休 +场 +给 +睡 +纷 +豆 +器 +捎 +说 +敏 +学 +会 +浒 +设 +诊 +格 +廓 +查 +来 +霓 +室 +溆 +¢ +诡 +寥 +焕 +舜 +柒 +狐 +回 +戟 +砾 +厄 +实 +翩 +尿 +五 +入 +径 +惭 +喹 +股 +宇 +篝 +| +; +美 +期 +云 +九 +祺 +扮 +靠 +锝 +槌 +系 +企 +酰 +阊 +暂 +蚕 +忻 +豁 +本 +羹 +执 +条 +钦 +H +獒 +限 +进 +季 +楦 +于 +芘 +玖 +铋 +茯 +未 +答 +粘 +括 +样 +精 +欠 +矢 +甥 +帷 +嵩 +扣 +令 +仔 +风 +皈 +行 +支 +部 +蓉 +刮 +站 +蜡 +救 +钊 +汗 +松 +嫌 +成 +可 +. +鹤 +院 +从 +交 +政 +怕 +活 +调 +球 +局 +验 +髌 +第 +韫 +谗 +串 +到 +圆 +年 +米 +/ +* +友 +忿 +检 +区 +看 +自 +敢 +刃 +个 +兹 +弄 +流 +留 +同 +没 +齿 +星 +聆 +轼 +湖 +什 +三 +建 +蛔 +儿 +椋 +汕 +震 +颧 +鲤 +跟 +力 +情 +璺 +铨 +陪 +务 +指 +族 +训 +滦 +鄣 +濮 +扒 +商 +箱 +十 +召 +慷 +辗 +所 +莞 +管 +护 +臭 +横 +硒 +嗓 +接 +侦 +六 +露 +党 +馋 +驾 +剖 +高 +侬 +妪 +幂 +猗 +绺 +骐 +央 +酐 +孝 +筝 +课 +徇 +缰 +门 +男 +西 +项 +句 +谙 +瞒 +秃 +篇 +教 +碲 +罚 +声 +呐 +景 +前 +富 +嘴 +鳌 +稀 +免 +朋 +啬 +睐 +去 +赈 +鱼 +住 +肩 +愕 +速 +旁 +波 +厅 +健 +茼 +厥 +鲟 +谅 +投 +攸 +炔 +数 +方 +击 +呋 +谈 +绩 +别 +愫 +僚 +躬 +鹧 +胪 +炳 +招 +喇 +膨 +泵 +蹦 +毛 +结 +5 +4 +谱 +识 +陕 +粽 +婚 +拟 +构 +且 +搜 +任 +潘 +比 +郢 +妨 +醪 +陀 +桔 +碘 +扎 +选 +哈 +骷 +楷 +亿 +明 +缆 +脯 +监 +睫 +逻 +婵 +共 +赴 +淝 +凡 +惦 +及 +达 +揖 +谩 +澹 +减 +焰 +蛹 +番 +祁 +柏 +员 +禄 +怡 +峤 +龙 +白 +叽 +生 +闯 +起 +细 +装 +谕 +竟 +聚 +钙 +上 +导 +渊 +按 +艾 +辘 +挡 +耒 +盹 +饪 +臀 +记 +邮 +蕙 +受 +各 +医 +搂 +普 +滇 +朗 +茸 +带 +翻 +酚 +( +光 +堤 +墟 +蔷 +万 +幻 +〓 +瑙 +辈 +昧 +盏 +亘 +蛀 +吉 +铰 +请 +子 +假 +闻 +税 +井 +诩 +哨 +嫂 +好 +面 +琐 +校 +馊 +鬣 +缂 +营 +访 +炖 +占 +农 +缀 +否 +经 +钚 +棵 +趟 +张 +亟 +吏 +茶 +谨 +捻 +论 +迸 +堂 +玉 +信 +吧 +瞠 +乡 +姬 +寺 +咬 +溏 +苄 +皿 +意 +赉 +宝 +尔 +钰 +艺 +特 +唳 +踉 +都 +荣 +倚 +登 +荐 +丧 +奇 +涵 +批 +炭 +近 +符 +傩 +感 +道 +着 +菊 +虹 +仲 +众 +懈 +濯 +颞 +眺 +南 +释 +北 +缝 +标 +既 +茗 +整 +撼 +迤 +贲 +挎 +耱 +拒 +某 +妍 +卫 +哇 +英 +矶 +藩 +治 +他 +元 +领 +膜 +遮 +穗 +蛾 +飞 +荒 +棺 +劫 +么 +市 +火 +温 +拈 +棚 +洼 +转 +果 +奕 +卸 +迪 +伸 +泳 +斗 +邡 +侄 +涨 +屯 +萋 +胭 +氡 +崮 +枞 +惧 +冒 +彩 +斜 +手 +豚 +随 +旭 +淑 +妞 +形 +菌 +吲 +沱 +争 +驯 +歹 +挟 +兆 +柱 +传 +至 +包 +内 +响 +临 +红 +功 +弩 +衡 +寂 +禁 +老 +棍 +耆 +渍 +织 +害 +氵 +渑 +布 +载 +靥 +嗬 +虽 +苹 +咨 +娄 +库 +雉 +榜 +帜 +嘲 +套 +瑚 +亲 +簸 +欧 +边 +6 +腿 +旮 +抛 +吹 +瞳 +得 +镓 +梗 +厨 +继 +漾 +愣 +憨 +士 +策 +窑 +抑 +躯 +襟 +脏 +参 +贸 +言 +干 +绸 +鳄 +穷 +藜 +音 +折 +详 +) +举 +悍 +甸 +癌 +黎 +谴 +死 +罩 +迁 +寒 +驷 +袖 +媒 +蒋 +掘 +模 +纠 +恣 +观 +祖 +蛆 +碍 +位 +稿 +主 +澧 +跌 +筏 +京 +锏 +帝 +贴 +证 +糠 +才 +黄 +鲸 +略 +炯 +饱 +四 +出 +园 +犀 +牧 +容 +汉 +杆 +浈 +汰 +瑷 +造 +虫 +瘩 +怪 +驴 +济 +应 +花 +沣 +谔 +夙 +旅 +价 +矿 +以 +考 +s +u +呦 +晒 +巡 +茅 +准 +肟 +瓴 +詹 +仟 +褂 +译 +桌 +混 +宁 +怦 +郑 +抿 +些 +余 +鄂 +饴 +攒 +珑 +群 +阖 +岔 +琨 +藓 +预 +环 +洮 +岌 +宀 +杲 +瀵 +最 +常 +囡 +周 +踊 +女 +鼓 +袭 +喉 +简 +范 +薯 +遐 +疏 +粱 +黜 +禧 +法 +箔 +斤 +遥 +汝 +奥 +直 +贞 +撑 +置 +绱 +集 +她 +馅 +逗 +钧 +橱 +魉 +[ +恙 +躁 +唤 +9 +旺 +膘 +待 +脾 +惫 +购 +吗 +依 +盲 +度 +瘿 +蠖 +俾 +之 +镗 +拇 +鲵 +厝 +簧 +续 +款 +展 +啃 +表 +剔 +品 +钻 +腭 +损 +清 +锶 +统 +涌 +寸 +滨 +贪 +链 +吠 +冈 +伎 +迥 +咏 +吁 +览 +防 +迅 +失 +汾 +阔 +逵 +绀 +蔑 +列 +川 +凭 +努 +熨 +揪 +利 +俱 +绉 +抢 +鸨 +我 +即 +责 +膦 +易 +毓 +鹊 +刹 +玷 +岿 +空 +嘞 +绊 +排 +术 +估 +锷 +违 +们 +苟 +铜 +播 +肘 +件 +烫 +审 +鲂 +广 +像 +铌 +惰 +铟 +巳 +胍 +鲍 +康 +憧 +色 +恢 +想 +拷 +尤 +疳 +知 +S +Y +F +D +A +峄 +裕 +帮 +握 +搔 +氐 +氘 +难 +墒 +沮 +雨 +叁 +缥 +悴 +藐 +湫 +娟 +苑 +稠 +颛 +簇 +后 +阕 +闭 +蕤 +缚 +怎 +佞 +码 +嘤 +蔡 +痊 +舱 +螯 +帕 +赫 +昵 +升 +烬 +岫 +、 +疵 +蜻 +髁 +蕨 +隶 +烛 +械 +丑 +盂 +梁 +强 +鲛 +由 +拘 +揉 +劭 +龟 +撤 +钩 +呕 +孛 +费 +妻 +漂 +求 +阑 +崖 +秤 +甘 +通 +深 +补 +赃 +坎 +床 +啪 +承 +吼 +量 +暇 +钼 +烨 +阂 +擎 +脱 +逮 +称 +P +神 +属 +矗 +华 +届 +狍 +葑 +汹 +育 +患 +窒 +蛰 +佼 +静 +槎 +运 +鳗 +庆 +逝 +曼 +疱 +克 +代 +官 +此 +麸 +耧 +蚌 +晟 +例 +础 +榛 +副 +测 +唰 +缢 +迹 +灬 +霁 +身 +岁 +赭 +扛 +又 +菡 +乜 +雾 +板 +读 +陷 +徉 +贯 +郁 +虑 +变 +钓 +菜 +圾 +现 +琢 +式 +乐 +维 +渔 +浜 +左 +吾 +脑 +钡 +警 +T +啵 +拴 +偌 +漱 +湿 +硕 +止 +骼 +魄 +积 +燥 +联 +踢 +玛 +则 +窿 +见 +振 +畿 +送 +班 +钽 +您 +赵 +刨 +印 +讨 +踝 +籍 +谡 +舌 +崧 +汽 +蔽 +沪 +酥 +绒 +怖 +财 +帖 +肱 +私 +莎 +勋 +羔 +霸 +励 +哼 +帐 +将 +帅 +渠 +纪 +婴 +娩 +岭 +厘 +滕 +吻 +伤 +坝 +冠 +戊 +隆 +瘁 +介 +涧 +物 +黍 +并 +姗 +奢 +蹑 +掣 +垸 +锴 +命 +箍 +捉 +病 +辖 +琰 +眭 +迩 +艘 +绌 +繁 +寅 +若 +毋 +思 +诉 +类 +诈 +燮 +轲 +酮 +狂 +重 +反 +职 +筱 +县 +委 +磕 +绣 +奖 +晋 +濉 +志 +徽 +肠 +呈 +獐 +坻 +口 +片 +碰 +几 +村 +柿 +劳 +料 +获 +亩 +惕 +晕 +厌 +号 +罢 +池 +正 +鏖 +煨 +家 +棕 +复 +尝 +懋 +蜥 +锅 +岛 +扰 +队 +坠 +瘾 +钬 +@ +卧 +疣 +镇 +譬 +冰 +彷 +频 +黯 +据 +垄 +采 +八 +缪 +瘫 +型 +熹 +砰 +楠 +襁 +箐 +但 +嘶 +绳 +啤 +拍 +盥 +穆 +傲 +洗 +盯 +塘 +怔 +筛 +丿 +台 +恒 +喂 +葛 +永 +¥ +烟 +酒 +桦 +书 +砂 +蚝 +缉 +态 +瀚 +袄 +圳 +轻 +蛛 +超 +榧 +遛 +姒 +奘 +铮 +右 +荽 +望 +偻 +卡 +丶 +氰 +附 +做 +革 +索 +戚 +坨 +桷 +唁 +垅 +榻 +岐 +偎 +坛 +莨 +山 +殊 +微 +骇 +陈 +爨 +推 +嗝 +驹 +澡 +藁 +呤 +卤 +嘻 +糅 +逛 +侵 +郓 +酌 +德 +摇 +※ +鬃 +被 +慨 +殡 +羸 +昌 +泡 +戛 +鞋 +河 +宪 +沿 +玲 +鲨 +翅 +哽 +源 +铅 +语 +照 +邯 +址 +荃 +佬 +顺 +鸳 +町 +霭 +睾 +瓢 +夸 +椁 +晓 +酿 +痈 +咔 +侏 +券 +噎 +湍 +签 +嚷 +离 +午 +尚 +社 +锤 +背 +孟 +使 +浪 +缦 +潍 +鞅 +军 +姹 +驶 +笑 +鳟 +鲁 +》 +孽 +钜 +绿 +洱 +礴 +焯 +椰 +颖 +囔 +乌 +孔 +巴 +互 +性 +椽 +哞 +聘 +昨 +早 +暮 +胶 +炀 +隧 +低 +彗 +昝 +铁 +呓 +氽 +藉 +喔 +癖 +瑗 +姨 +权 +胱 +韦 +堑 +蜜 +酋 +楝 +砝 +毁 +靓 +歙 +锲 +究 +屋 +喳 +骨 +辨 +碑 +武 +鸠 +宫 +辜 +烊 +适 +坡 +殃 +培 +佩 +供 +走 +蜈 +迟 +翼 +况 +姣 +凛 +浔 +吃 +飘 +债 +犟 +金 +促 +苛 +崇 +坂 +莳 +畔 +绂 +兵 +蠕 +斋 +根 +砍 +亢 +欢 +恬 +崔 +剁 +餐 +榫 +快 +扶 +‖ +濒 +缠 +鳜 +当 +彭 +驭 +浦 +篮 +昀 +锆 +秸 +钳 +弋 +娣 +瞑 +夷 +龛 +苫 +拱 +致 +% +嵊 +障 +隐 +弑 +初 +娓 +抉 +汩 +累 +蓖 +" +唬 +助 +苓 +昙 +押 +毙 +破 +城 +郧 +逢 +嚏 +獭 +瞻 +溱 +婿 +赊 +跨 +恼 +璧 +萃 +姻 +貉 +灵 +炉 +密 +氛 +陶 +砸 +谬 +衔 +点 +琛 +沛 +枳 +层 +岱 +诺 +脍 +榈 +埂 +征 +冷 +裁 +打 +蹴 +素 +瘘 +逞 +蛐 +聊 +激 +腱 +萘 +踵 +飒 +蓟 +吆 +取 +咙 +簋 +涓 +矩 +曝 +挺 +揣 +座 +你 +史 +舵 +焱 +尘 +苏 +笈 +脚 +溉 +榨 +诵 +樊 +邓 +焊 +义 +庶 +儋 +蟋 +蒲 +赦 +呷 +杞 +诠 +豪 +还 +试 +颓 +茉 +太 +除 +紫 +逃 +痴 +草 +充 +鳕 +珉 +祗 +墨 +渭 +烩 +蘸 +慕 +璇 +镶 +穴 +嵘 +恶 +骂 +险 +绋 +幕 +碉 +肺 +戳 +刘 +潞 +秣 +纾 +潜 +銮 +洛 +须 +罘 +销 +瘪 +汞 +兮 +屉 +r +林 +厕 +质 +探 +划 +狸 +殚 +善 +煊 +烹 +〒 +锈 +逯 +宸 +辍 +泱 +柚 +袍 +远 +蹋 +嶙 +绝 +峥 +娥 +缍 +雀 +徵 +认 +镱 +谷 += +贩 +勉 +撩 +鄯 +斐 +洋 +非 +祚 +泾 +诒 +饿 +撬 +威 +晷 +搭 +芍 +锥 +笺 +蓦 +候 +琊 +档 +礁 +沼 +卵 +荠 +忑 +朝 +凹 +瑞 +头 +仪 +弧 +孵 +畏 +铆 +突 +衲 +车 +浩 +气 +茂 +悖 +厢 +枕 +酝 +戴 +湾 +邹 +飚 +攘 +锂 +写 +宵 +翁 +岷 +无 +喜 +丈 +挑 +嗟 +绛 +殉 +议 +槽 +具 +醇 +淞 +笃 +郴 +阅 +饼 +底 +壕 +砚 +弈 +询 +缕 +庹 +翟 +零 +筷 +暨 +舟 +闺 +甯 +撞 +麂 +茌 +蔼 +很 +珲 +捕 +棠 +角 +阉 +媛 +娲 +诽 +剿 +尉 +爵 +睬 +韩 +诰 +匣 +危 +糍 +镯 +立 +浏 +阳 +少 +盆 +舔 +擘 +匪 +申 +尬 +铣 +旯 +抖 +赘 +瓯 +居 +ˇ +哮 +游 +锭 +茏 +歌 +坏 +甚 +秒 +舞 +沙 +仗 +劲 +潺 +阿 +燧 +郭 +嗖 +霏 +忠 +材 +奂 +耐 +跺 +砀 +输 +岖 +媳 +氟 +极 +摆 +灿 +今 +扔 +腻 +枝 +奎 +药 +熄 +吨 +话 +q +额 +慑 +嘌 +协 +喀 +壳 +埭 +视 +著 +於 +愧 +陲 +翌 +峁 +颅 +佛 +腹 +聋 +侯 +咎 +叟 +秀 +颇 +存 +较 +罪 +哄 +岗 +扫 +栏 +钾 +羌 +己 +璨 +枭 +霉 +煌 +涸 +衿 +键 +镝 +益 +岢 +奏 +连 +夯 +睿 +冥 +均 +糖 +狞 +蹊 +稻 +爸 +刿 +胥 +煜 +丽 +肿 +璃 +掸 +跚 +灾 +垂 +樾 +濑 +乎 +莲 +窄 +犹 +撮 +战 +馄 +软 +络 +显 +鸢 +胸 +宾 +妲 +恕 +埔 +蝌 +份 +遇 +巧 +瞟 +粒 +恰 +剥 +桡 +博 +讯 +凯 +堇 +阶 +滤 +卖 +斌 +骚 +彬 +兑 +磺 +樱 +舷 +两 +娱 +福 +仃 +差 +找 +桁 +÷ +净 +把 +阴 +污 +戬 +雷 +碓 +蕲 +楚 +罡 +焖 +抽 +妫 +咒 +仑 +闱 +尽 +邑 +菁 +爱 +贷 +沥 +鞑 +牡 +嗉 +崴 +骤 +塌 +嗦 +订 +拮 +滓 +捡 +锻 +次 +坪 +杩 +臃 +箬 +融 +珂 +鹗 +宗 +枚 +降 +鸬 +妯 +阄 +堰 +盐 +毅 +必 +杨 +崃 +俺 +甬 +状 +莘 +货 +耸 +菱 +腼 +铸 +唏 +痤 +孚 +澳 +懒 +溅 +翘 +疙 +杷 +淼 +缙 +骰 +喊 +悉 +砻 +坷 +艇 +赁 +界 +谤 +纣 +宴 +晃 +茹 +归 +饭 +梢 +铡 +街 +抄 +肼 +鬟 +苯 +颂 +撷 +戈 +炒 +咆 +茭 +瘙 +负 +仰 +客 +琉 +铢 +封 +卑 +珥 +椿 +镧 +窨 +鬲 +寿 +御 +袤 +铃 +萎 +砖 +餮 +脒 +裳 +肪 +孕 +嫣 +馗 +嵇 +恳 +氯 +江 +石 +褶 +冢 +祸 +阻 +狈 +羞 +银 +靳 +透 +咳 +叼 +敷 +芷 +啥 +它 +瓤 +兰 +痘 +懊 +逑 +肌 +往 +捺 +坊 +甩 +呻 +〃 +沦 +忘 +膻 +祟 +菅 +剧 +崆 +智 +坯 +臧 +霍 +墅 +攻 +眯 +倘 +拢 +骠 +铐 +庭 +岙 +瓠 +′ +缺 +泥 +迢 +捶 +? +? +郏 +喙 +掷 +沌 +纯 +秘 +种 +听 +绘 +固 +螨 +团 +香 +盗 +妒 +埚 +蓝 +拖 +旱 +荞 +铀 +血 +遏 +汲 +辰 +叩 +拽 +幅 +硬 +惶 +桀 +漠 +措 +泼 +唑 +齐 +肾 +念 +酱 +虚 +屁 +耶 +旗 +砦 +闵 +婉 +馆 +拭 +绅 +韧 +忏 +窝 +醋 +葺 +顾 +辞 +倜 +堆 +辋 +逆 +玟 +贱 +疾 +董 +惘 +倌 +锕 +淘 +嘀 +莽 +俭 +笏 +绑 +鲷 +杈 +择 +蟀 +粥 +嗯 +驰 +逾 +案 +谪 +褓 +胫 +哩 +昕 +颚 +鲢 +绠 +躺 +鹄 +崂 +儒 +俨 +丝 +尕 +泌 +啊 +萸 +彰 +幺 +吟 +骄 +苣 +弦 +脊 +瑰 +〈 +诛 +镁 +析 +闪 +剪 +侧 +哟 +框 +螃 +守 +嬗 +燕 +狭 +铈 +缮 +概 +迳 +痧 +鲲 +俯 +售 +笼 +痣 +扉 +挖 +满 +咋 +援 +邱 +扇 +歪 +便 +玑 +绦 +峡 +蛇 +叨 +〖 +泽 +胃 +斓 +喋 +怂 +坟 +猪 +该 +蚬 +炕 +弥 +赞 +棣 +晔 +娠 +挲 +狡 +创 +疖 +铕 +镭 +稷 +挫 +弭 +啾 +翔 +粉 +履 +苘 +哦 +楼 +秕 +铂 +土 +锣 +瘟 +挣 +栉 +习 +享 +桢 +袅 +磨 +桂 +谦 +延 +坚 +蔚 +噗 +署 +谟 +猬 +钎 +恐 +嬉 +雒 +倦 +衅 +亏 +璩 +睹 +刻 +殿 +王 +算 +雕 +麻 +丘 +柯 +骆 +丸 +塍 +谚 +添 +鲈 +垓 +桎 +蚯 +芥 +予 +飕 +镦 +谌 +窗 +醚 +菀 +亮 +搪 +莺 +蒿 +羁 +足 +J +真 +轶 +悬 +衷 +靛 +翊 +掩 +哒 +炅 +掐 +冼 +妮 +l +谐 +稚 +荆 +擒 +犯 +陵 +虏 +浓 +崽 +刍 +陌 +傻 +孜 +千 +靖 +演 +矜 +钕 +煽 +杰 +酗 +渗 +伞 +栋 +俗 +泫 +戍 +罕 +沾 +疽 +灏 +煦 +芬 +磴 +叱 +阱 +榉 +湃 +蜀 +叉 +醒 +彪 +租 +郡 +篷 +屎 +良 +垢 +隗 +弱 +陨 +峪 +砷 +掴 +颁 +胎 +雯 +绵 +贬 +沐 +撵 +隘 +篙 +暖 +曹 +陡 +栓 +填 +臼 +彦 +瓶 +琪 +潼 +哪 +鸡 +摩 +啦 +俟 +锋 +域 +耻 +蔫 +疯 +纹 +撇 +毒 +绶 +痛 +酯 +忍 +爪 +赳 +歆 +嘹 +辕 +烈 +册 +朴 +钱 +吮 +毯 +癜 +娃 +谀 +邵 +厮 +炽 +璞 +邃 +丐 +追 +词 +瓒 +忆 +轧 +芫 +谯 +喷 +弟 +半 +冕 +裙 +掖 +墉 +绮 +寝 +苔 +势 +顷 +褥 +切 +衮 +君 +佳 +嫒 +蚩 +霞 +佚 +洙 +逊 +镖 +暹 +唛 +& +殒 +顶 +碗 +獗 +轭 +铺 +蛊 +废 +恹 +汨 +崩 +珍 +那 +杵 +曲 +纺 +夏 +薰 +傀 +闳 +淬 +姘 +舀 +拧 +卷 +楂 +恍 +讪 +厩 +寮 +篪 +赓 +乘 +灭 +盅 +鞣 +沟 +慎 +挂 +饺 +鼾 +杳 +树 +缨 +丛 +絮 +娌 +臻 +嗳 +篡 +侩 +述 +衰 +矛 +圈 +蚜 +匕 +筹 +匿 +濞 +晨 +叶 +骋 +郝 +挚 +蚴 +滞 +增 +侍 +描 +瓣 +吖 +嫦 +蟒 +匾 +圣 +赌 +毡 +癞 +恺 +百 +曳 +需 +篓 +肮 +庖 +帏 +卿 +驿 +遗 +蹬 +鬓 +骡 +歉 +芎 +胳 +屐 +禽 +烦 +晌 +寄 +媾 +狄 +翡 +苒 +船 +廉 +终 +痞 +殇 +々 +畦 +饶 +改 +拆 +悻 +萄 +£ +瓿 +乃 +訾 +桅 +匮 +溧 +拥 +纱 +铍 +骗 +蕃 +龋 +缬 +父 +佐 +疚 +栎 +醍 +掳 +蓄 +x +惆 +颜 +鲆 +榆 +〔 +猎 +敌 +暴 +谥 +鲫 +贾 +罗 +玻 +缄 +扦 +芪 +癣 +落 +徒 +臾 +恿 +猩 +托 +邴 +肄 +牵 +春 +陛 +耀 +刊 +拓 +蓓 +邳 +堕 +寇 +枉 +淌 +啡 +湄 +兽 +酷 +萼 +碚 +濠 +萤 +夹 +旬 +戮 +梭 +琥 +椭 +昔 +勺 +蜊 +绐 +晚 +孺 +僵 +宣 +摄 +冽 +旨 +萌 +忙 +蚤 +眉 +噼 +蟑 +付 +契 +瓜 +悼 +颡 +壁 +曾 +窕 +颢 +澎 +仿 +俑 +浑 +嵌 +浣 +乍 +碌 +褪 +乱 +蔟 +隙 +玩 +剐 +葫 +箫 +纲 +围 +伐 +决 +伙 +漩 +瑟 +刑 +肓 +镳 +缓 +蹭 +氨 +皓 +典 +畲 +坍 +铑 +檐 +塑 +洞 +倬 +储 +胴 +淳 +戾 +吐 +灼 +惺 +妙 +毕 +珐 +缈 +虱 +盖 +羰 +鸿 +磅 +谓 +髅 +娴 +苴 +唷 +蚣 +霹 +抨 +贤 +唠 +犬 +誓 +逍 +庠 +逼 +麓 +籼 +釉 +呜 +碧 +秧 +氩 +摔 +霄 +穸 +纨 +辟 +妈 +映 +完 +牛 +缴 +嗷 +炊 +恩 +荔 +茆 +掉 +紊 +慌 +莓 +羟 +阙 +萁 +磐 +另 +蕹 +辱 +鳐 +湮 +吡 +吩 +唐 +睦 +垠 +舒 +圜 +冗 +瞿 +溺 +芾 +囱 +匠 +僳 +汐 +菩 +饬 +漓 +黑 +霰 +浸 +濡 +窥 +毂 +蒡 +兢 +驻 +鹉 +芮 +诙 +迫 +雳 +厂 +忐 +臆 +猴 +鸣 +蚪 +栈 +箕 +羡 +渐 +莆 +捍 +眈 +哓 +趴 +蹼 +埕 +嚣 +骛 +宏 +淄 +斑 +噜 +严 +瑛 +垃 +椎 +诱 +压 +庾 +绞 +焘 +廿 +抡 +迄 +棘 +夫 +纬 +锹 +眨 +瞌 +侠 +脐 +竞 +瀑 +孳 +骧 +遁 +姜 +颦 +荪 +滚 +萦 +伪 +逸 +粳 +爬 +锁 +矣 +役 +趣 +洒 +颔 +诏 +逐 +奸 +甭 +惠 +攀 +蹄 +泛 +尼 +拼 +阮 +鹰 +亚 +颈 +惑 +勒 +〉 +际 +肛 +爷 +刚 +钨 +丰 +养 +冶 +鲽 +辉 +蔻 +画 +覆 +皴 +妊 +麦 +返 +醉 +皂 +擀 +〗 +酶 +凑 +粹 +悟 +诀 +硖 +港 +卜 +z +杀 +涕 +± +舍 +铠 +抵 +弛 +段 +敝 +镐 +奠 +拂 +轴 +跛 +袱 +e +t +沉 +菇 +俎 +薪 +峦 +秭 +蟹 +历 +盟 +菠 +寡 +液 +肢 +喻 +染 +裱 +悱 +抱 +氙 +赤 +捅 +猛 +跑 +氮 +谣 +仁 +尺 +辊 +窍 +烙 +衍 +架 +擦 +倏 +璐 +瑁 +币 +楞 +胖 +夔 +趸 +邛 +惴 +饕 +虔 +蝎 +§ +哉 +贝 +宽 +辫 +炮 +扩 +饲 +籽 +魏 +菟 +锰 +伍 +猝 +末 +琳 +哚 +蛎 +邂 +呀 +姿 +鄞 +却 +歧 +仙 +恸 +椐 +森 +牒 +寤 +袒 +婆 +虢 +雅 +钉 +朵 +贼 +欲 +苞 +寰 +故 +龚 +坭 +嘘 +咫 +礼 +硷 +兀 +睢 +汶 +’ +铲 +烧 +绕 +诃 +浃 +钿 +哺 +柜 +讼 +颊 +璁 +腔 +洽 +咐 +脲 +簌 +筠 +镣 +玮 +鞠 +谁 +兼 +姆 +挥 +梯 +蝴 +谘 +漕 +刷 +躏 +宦 +弼 +b +垌 +劈 +麟 +莉 +揭 +笙 +渎 +仕 +嗤 +仓 +配 +怏 +抬 +错 +泯 +镊 +孰 +猿 +邪 +仍 +秋 +鼬 +壹 +歇 +吵 +炼 +< +尧 +射 +柬 +廷 +胧 +霾 +凳 +隋 +肚 +浮 +梦 +祥 +株 +堵 +退 +L +鹫 +跎 +凶 +毽 +荟 +炫 +栩 +玳 +甜 +沂 +鹿 +顽 +伯 +爹 +赔 +蛴 +徐 +匡 +欣 +狰 +缸 +雹 +蟆 +疤 +默 +沤 +啜 +痂 +衣 +禅 +w +i +h +辽 +葳 +黝 +钗 +停 +沽 +棒 +馨 +颌 +肉 +吴 +硫 +悯 +劾 +娈 +马 +啧 +吊 +悌 +镑 +峭 +帆 +瀣 +涉 +咸 +疸 +滋 +泣 +翦 +拙 +癸 +钥 +蜒 ++ +尾 +庄 +凝 +泉 +婢 +渴 +谊 +乞 +陆 +锉 +糊 +鸦 +淮 +I +B +N +晦 +弗 +乔 +庥 +葡 +尻 +席 +橡 +傣 +渣 +拿 +惩 +麋 +斛 +缃 +矮 +蛏 +岘 +鸽 +姐 +膏 +催 +奔 +镒 +喱 +蠡 +摧 +钯 +胤 +柠 +拐 +璋 +鸥 +卢 +荡 +倾 +^ +_ +珀 +逄 +萧 +塾 +掇 +贮 +笆 +聂 +圃 +冲 +嵬 +M +滔 +笕 +值 +炙 +偶 +蜱 +搐 +梆 +汪 +蔬 +腑 +鸯 +蹇 +敞 +绯 +仨 +祯 +谆 +梧 +糗 +鑫 +啸 +豺 +囹 +猾 +巢 +柄 +瀛 +筑 +踌 +沭 +暗 +苁 +鱿 +蹉 +脂 +蘖 +牢 +热 +木 +吸 +溃 +宠 +序 +泞 +偿 +拜 +檩 +厚 +朐 +毗 +螳 +吞 +媚 +朽 +担 +蝗 +橘 +畴 +祈 +糟 +盱 +隼 +郜 +惜 +珠 +裨 +铵 +焙 +琚 +唯 +咚 +噪 +骊 +丫 +滢 +勤 +棉 +呸 +咣 +淀 +隔 +蕾 +窈 +饨 +挨 +煅 +短 +匙 +粕 +镜 +赣 +撕 +墩 +酬 +馁 +豌 +颐 +抗 +酣 +氓 +佑 +搁 +哭 +递 +耷 +涡 +桃 +贻 +碣 +截 +瘦 +昭 +镌 +蔓 +氚 +甲 +猕 +蕴 +蓬 +散 +拾 +纛 +狼 +猷 +铎 +埋 +旖 +矾 +讳 +囊 +糜 +迈 +粟 +蚂 +紧 +鲳 +瘢 +栽 +稼 +羊 +锄 +斟 +睁 +桥 +瓮 +蹙 +祉 +醺 +鼻 +昱 +剃 +跳 +篱 +跷 +蒜 +翎 +宅 +晖 +嗑 +壑 +峻 +癫 +屏 +狠 +陋 +袜 +途 +憎 +祀 +莹 +滟 +佶 +溥 +臣 +约 +盛 +峰 +磁 +慵 +婪 +拦 +莅 +朕 +鹦 +粲 +裤 +哎 +疡 +嫖 +琵 +窟 +堪 +谛 +嘉 +儡 +鳝 +斩 +郾 +驸 +酊 +妄 +胜 +贺 +徙 +傅 +噌 +钢 +栅 +庇 +恋 +匝 +巯 +邈 +尸 +锚 +粗 +佟 +蛟 +薹 +纵 +蚊 +郅 +绢 +锐 +苗 +俞 +篆 +淆 +膀 +鲜 +煎 +诶 +秽 +寻 +涮 +刺 +怀 +噶 +巨 +褰 +魅 +灶 +灌 +桉 +藕 +谜 +舸 +薄 +搀 +恽 +借 +牯 +痉 +渥 +愿 +亓 +耘 +杠 +柩 +锔 +蚶 +钣 +珈 +喘 +蹒 +幽 +赐 +稗 +晤 +莱 +泔 +扯 +肯 +菪 +裆 +腩 +豉 +疆 +骜 +腐 +倭 +珏 +唔 +粮 +亡 +润 +慰 +伽 +橄 +玄 +誉 +醐 +胆 +龊 +粼 +塬 +陇 +彼 +削 +嗣 +绾 +芽 +妗 +垭 +瘴 +爽 +薏 +寨 +龈 +泠 +弹 +赢 +漪 +猫 +嘧 +涂 +恤 +圭 +茧 +烽 +屑 +痕 +巾 +赖 +荸 +凰 +腮 +畈 +亵 +蹲 +偃 +苇 +澜 +艮 +换 +骺 +烘 +苕 +梓 +颉 +肇 +哗 +悄 +氤 +涠 +葬 +屠 +鹭 +植 +竺 +佯 +诣 +鲇 +瘀 +鲅 +邦 +移 +滁 +冯 +耕 +癔 +戌 +茬 +沁 +巩 +悠 +湘 +洪 +痹 +锟 +循 +谋 +腕 +鳃 +钠 +捞 +焉 +迎 +碱 +伫 +急 +榷 +奈 +邝 +卯 +辄 +皲 +卟 +醛 +畹 +忧 +稳 +雄 +昼 +缩 +阈 +睑 +扌 +耗 +曦 +涅 +捏 +瞧 +邕 +淖 +漉 +铝 +耦 +禹 +湛 +喽 +莼 +琅 +诸 +苎 +纂 +硅 +始 +嗨 +傥 +燃 +臂 +赅 +嘈 +呆 +贵 +屹 +壮 +肋 +亍 +蚀 +卅 +豹 +腆 +邬 +迭 +浊 +} +童 +螂 +捐 +圩 +勐 +触 +寞 +汊 +壤 +荫 +膺 +渌 +芳 +懿 +遴 +螈 +泰 +蓼 +蛤 +茜 +舅 +枫 +朔 +膝 +眙 +避 +梅 +判 +鹜 +璜 +牍 +缅 +垫 +藻 +黔 +侥 +惚 +懂 +踩 +腰 +腈 +札 +丞 +唾 +慈 +顿 +摹 +荻 +琬 +~ +斧 +沈 +滂 +胁 +胀 +幄 +莜 +Z +匀 +鄄 +掌 +绰 +茎 +焚 +赋 +萱 +谑 +汁 +铒 +瞎 +夺 +蜗 +野 +娆 +冀 +弯 +篁 +懵 +灞 +隽 +芡 +脘 +俐 +辩 +芯 +掺 +喏 +膈 +蝈 +觐 +悚 +踹 +蔗 +熠 +鼠 +呵 +抓 +橼 +峨 +畜 +缔 +禾 +崭 +弃 +熊 +摒 +凸 +拗 +穹 +蒙 +抒 +祛 +劝 +闫 +扳 +阵 +醌 +踪 +喵 +侣 +搬 +仅 +荧 +赎 +蝾 +琦 +买 +婧 +瞄 +寓 +皎 +冻 +赝 +箩 +莫 +瞰 +郊 +笫 +姝 +筒 +枪 +遣 +煸 +袋 +舆 +痱 +涛 +母 +〇 +启 +践 +耙 +绲 +盘 +遂 +昊 +搞 +槿 +诬 +纰 +泓 +惨 +檬 +亻 +越 +C +o +憩 +熵 +祷 +钒 +暧 +塔 +阗 +胰 +咄 +娶 +魔 +琶 +钞 +邻 +扬 +杉 +殴 +咽 +弓 +〆 +髻 +】 +吭 +揽 +霆 +拄 +殖 +脆 +彻 +岩 +芝 +勃 +辣 +剌 +钝 +嘎 +甄 +佘 +皖 +伦 +授 +徕 +憔 +挪 +皇 +庞 +稔 +芜 +踏 +溴 +兖 +卒 +擢 +饥 +鳞 +煲 +‰ +账 +颗 +叻 +斯 +捧 +鳍 +琮 +讹 +蛙 +纽 +谭 +酸 +兔 +莒 +睇 +伟 +觑 +羲 +嗜 +宜 +褐 +旎 +辛 +卦 +诘 +筋 +鎏 +溪 +挛 +熔 +阜 +晰 +鳅 +丢 +奚 +灸 +呱 +献 +陉 +黛 +鸪 +甾 +萨 +疮 +拯 +洲 +疹 +辑 +叙 +恻 +谒 +允 +柔 +烂 +氏 +逅 +漆 +拎 +惋 +扈 +湟 +纭 +啕 +掬 +擞 +哥 +忽 +涤 +鸵 +靡 +郗 +瓷 +扁 +廊 +怨 +雏 +钮 +敦 +E +懦 +憋 +汀 +拚 +啉 +腌 +岸 +f +痼 +瞅 +尊 +咀 +眩 +飙 +忌 +仝 +迦 +熬 +毫 +胯 +篑 +茄 +腺 +凄 +舛 +碴 +锵 +诧 +羯 +後 +漏 +汤 +宓 +仞 +蚁 +壶 +谰 +皑 +铄 +棰 +罔 +辅 +晶 +苦 +牟 +闽 +\ +烃 +饮 +聿 +丙 +蛳 +朱 +煤 +涔 +鳖 +犁 +罐 +荼 +砒 +淦 +妤 +黏 +戎 +孑 +婕 +瑾 +戢 +钵 +枣 +捋 +砥 +衩 +狙 +桠 +稣 +阎 +肃 +梏 +诫 +孪 +昶 +婊 +衫 +嗔 +侃 +塞 +蜃 +樵 +峒 +貌 +屿 +欺 +缫 +阐 +栖 +诟 +珞 +荭 +吝 +萍 +嗽 +恂 +啻 +蜴 +磬 +峋 +俸 +豫 +谎 +徊 +镍 +韬 +魇 +晴 +U +囟 +猜 +蛮 +坐 +囿 +伴 +亭 +肝 +佗 +蝠 +妃 +胞 +滩 +榴 +氖 +垩 +苋 +砣 +扪 +馏 +姓 +轩 +厉 +夥 +侈 +禀 +垒 +岑 +赏 +钛 +辐 +痔 +披 +纸 +碳 +“ +坞 +蠓 +挤 +荥 +沅 +悔 +铧 +帼 +蒌 +蝇 +a +p +y +n +g +哀 +浆 +瑶 +凿 +桶 +馈 +皮 +奴 +苜 +佤 +伶 +晗 +铱 +炬 +优 +弊 +氢 +恃 +甫 +攥 +端 +锌 +灰 +稹 +炝 +曙 +邋 +亥 +眶 +碾 +拉 +萝 +绔 +捷 +浍 +腋 +姑 +菖 +凌 +涞 +麽 +锢 +桨 +潢 +绎 +镰 +殆 +锑 +渝 +铬 +困 +绽 +觎 +匈 +糙 +暑 +裹 +鸟 +盔 +肽 +迷 +綦 +『 +亳 +佝 +俘 +钴 +觇 +骥 +仆 +疝 +跪 +婶 +郯 +瀹 +唉 +脖 +踞 +针 +晾 +忒 +扼 +瞩 +叛 +椒 +疟 +嗡 +邗 +肆 +跆 +玫 +忡 +捣 +咧 +唆 +艄 +蘑 +潦 +笛 +阚 +沸 +泻 +掊 +菽 +贫 +斥 +髂 +孢 +镂 +赂 +麝 +鸾 +屡 +衬 +苷 +恪 +叠 +希 +粤 +爻 +喝 +茫 +惬 +郸 +绻 +庸 +撅 +碟 +宄 +妹 +膛 +叮 +饵 +崛 +嗲 +椅 +冤 +搅 +咕 +敛 +尹 +垦 +闷 +蝉 +霎 +勰 +败 +蓑 +泸 +肤 +鹌 +幌 +焦 +浠 +鞍 +刁 +舰 +乙 +竿 +裔 +。 +茵 +函 +伊 +兄 +丨 +娜 +匍 +謇 +莪 +宥 +似 +蝽 +翳 +酪 +翠 +粑 +薇 +祢 +骏 +赠 +叫 +Q +噤 +噻 +竖 +芗 +莠 +潭 +俊 +羿 +耜 +O +郫 +趁 +嗪 +囚 +蹶 +芒 +洁 +笋 +鹑 +敲 +硝 +啶 +堡 +渲 +揩 +』 +携 +宿 +遒 +颍 +扭 +棱 +割 +萜 +蔸 +葵 +琴 +捂 +饰 +衙 +耿 +掠 +募 +岂 +窖 +涟 +蔺 +瘤 +柞 +瞪 +怜 +匹 +距 +楔 +炜 +哆 +秦 +缎 +幼 +茁 +绪 +痨 +恨 +楸 +娅 +瓦 +桩 +雪 +嬴 +伏 +榔 +妥 +铿 +拌 +眠 +雍 +缇 +‘ +卓 +搓 +哌 +觞 +噩 +屈 +哧 +髓 +咦 +巅 +娑 +侑 +淫 +膳 +祝 +勾 +姊 +莴 +胄 +疃 +薛 +蜷 +胛 +巷 +芙 +芋 +熙 +闰 +勿 +窃 +狱 +剩 +钏 +幢 +陟 +铛 +慧 +靴 +耍 +k +浙 +浇 +飨 +惟 +绗 +祜 +澈 +啼 +咪 +磷 +摞 +诅 +郦 +抹 +跃 +壬 +吕 +肖 +琏 +颤 +尴 +剡 +抠 +凋 +赚 +泊 +津 +宕 +殷 +倔 +氲 +漫 +邺 +涎 +怠 +$ +垮 +荬 +遵 +俏 +叹 +噢 +饽 +蜘 +孙 +筵 +疼 +鞭 +羧 +牦 +箭 +潴 +c +眸 +祭 +髯 +啖 +坳 +愁 +芩 +驮 +倡 +巽 +穰 +沃 +胚 +怒 +凤 +槛 +剂 +趵 +嫁 +v +邢 +灯 +鄢 +桐 +睽 +檗 +锯 +槟 +婷 +嵋 +圻 +诗 +蕈 +颠 +遭 +痢 +芸 +怯 +馥 +竭 +锗 +徜 +恭 +遍 +籁 +剑 +嘱 +苡 +龄 +僧 +桑 +潸 +弘 +澶 +楹 +悲 +讫 +愤 +腥 +悸 +谍 +椹 +呢 +桓 +葭 +攫 +阀 +翰 +躲 +敖 +柑 +郎 +笨 +橇 +呃 +魁 +燎 +脓 +葩 +磋 +垛 +玺 +狮 +沓 +砜 +蕊 +锺 +罹 +蕉 +翱 +虐 +闾 +巫 +旦 +茱 +嬷 +枯 +鹏 +贡 +芹 +汛 +矫 +绁 +拣 +禺 +佃 +讣 +舫 +惯 +乳 +趋 +疲 +挽 +岚 +虾 +衾 +蠹 +蹂 +飓 +氦 +铖 +孩 +稞 +瑜 +壅 +掀 +勘 +妓 +畅 +髋 +W +庐 +牲 +蓿 +榕 +练 +垣 +唱 +邸 +菲 +昆 +婺 +穿 +绡 +麒 +蚱 +掂 +愚 +泷 +涪 +漳 +妩 +娉 +榄 +讷 +觅 +旧 +藤 +煮 +呛 +柳 +腓 +叭 +庵 +烷 +阡 +罂 +蜕 +擂 +猖 +咿 +媲 +脉 +【 +沏 +貅 +黠 +熏 +哲 +烁 +坦 +酵 +兜 +× +潇 +撒 +剽 +珩 +圹 +乾 +摸 +樟 +帽 +嗒 +襄 +魂 +轿 +憬 +锡 +〕 +喃 +皆 +咖 +隅 +脸 +残 +泮 +袂 +鹂 +珊 +囤 +捆 +咤 +误 +徨 +闹 +淙 +芊 +淋 +怆 +囗 +拨 +梳 +渤 +R +G +绨 +蚓 +婀 +幡 +狩 +麾 +谢 +唢 +裸 +旌 +伉 +纶 +裂 +驳 +砼 +咛 +澄 +樨 +蹈 +宙 +澍 +倍 +貔 +操 +勇 +蟠 +摈 +砧 +虬 +够 +缁 +悦 +藿 +撸 +艹 +摁 +淹 +豇 +虎 +榭 +ˉ +吱 +d +° +喧 +荀 +踱 +侮 +奋 +偕 +饷 +犍 +惮 +坑 +璎 +徘 +宛 +妆 +袈 +倩 +窦 +昂 +荏 +乖 +K +怅 +撰 +鳙 +牙 +袁 +酞 +X +痿 +琼 +闸 +雁 +趾 +荚 +虻 +涝 +《 +杏 +韭 +偈 +烤 +绫 +鞘 +卉 +症 +遢 +蓥 +诋 +杭 +荨 +匆 +竣 +簪 +辙 +敕 +虞 +丹 +缭 +咩 +黟 +m +淤 +瑕 +咂 +铉 +硼 +茨 +嶂 +痒 +畸 +敬 +涿 +粪 +窘 +熟 +叔 +嫔 +盾 +忱 +裘 +憾 +梵 +赡 +珙 +咯 +娘 +庙 +溯 +胺 +葱 +痪 +摊 +荷 +卞 +乒 +髦 +寐 +铭 +坩 +胗 +枷 +爆 +溟 +嚼 +羚 +砬 +轨 +惊 +挠 +罄 +竽 +菏 +氧 +浅 +楣 +盼 +枢 +炸 +阆 +杯 +谏 +噬 +淇 +渺 +俪 +秆 +墓 +泪 +跻 +砌 +痰 +垡 +渡 +耽 +釜 +讶 +鳎 +煞 +呗 +韶 +舶 +绷 +鹳 +缜 +旷 +铊 +皱 +龌 +檀 +霖 +奄 +槐 +艳 +蝶 +旋 +哝 +赶 +骞 +蚧 +腊 +盈 +丁 +` +蜚 +矸 +蝙 +睨 +嚓 +僻 +鬼 +醴 +夜 +彝 +磊 +笔 +拔 +栀 +糕 +厦 +邰 +纫 +逭 +纤 +眦 +膊 +馍 +躇 +烯 +蘼 +冬 +诤 +暄 +骶 +哑 +瘠 +」 +臊 +丕 +愈 +咱 +螺 +擅 +跋 +搏 +硪 +谄 +笠 +淡 +嘿 +骅 +谧 +鼎 +皋 +姚 +歼 +蠢 +驼 +耳 +胬 +挝 +涯 +狗 +蒽 +孓 +犷 +凉 +芦 +箴 +铤 +孤 +嘛 +坤 +V +茴 +朦 +挞 +尖 +橙 +诞 +搴 +碇 +洵 +浚 +帚 +蜍 +漯 +柘 +嚎 +讽 +芭 +荤 +咻 +祠 +秉 +跖 +埃 +吓 +糯 +眷 +馒 +惹 +娼 +鲑 +嫩 +讴 +轮 +瞥 +靶 +褚 +乏 +缤 +宋 +帧 +删 +驱 +碎 +扑 +俩 +俄 +偏 +涣 +竹 +噱 +皙 +佰 +渚 +唧 +斡 +# +镉 +刀 +崎 +筐 +佣 +夭 +贰 +肴 +峙 +哔 +艿 +匐 +牺 +镛 +缘 +仡 +嫡 +劣 +枸 +堀 +梨 +簿 +鸭 +蒸 +亦 +稽 +浴 +{ +衢 +束 +槲 +j +阁 +揍 +疥 +棋 +潋 +聪 +窜 +乓 +睛 +插 +冉 +阪 +苍 +搽 +「 +蟾 +螟 +幸 +仇 +樽 +撂 +慢 +跤 +幔 +俚 +淅 +覃 +觊 +溶 +妖 +帛 +侨 +曰 +妾 +泗 +· +: +瀘 +風 +Ë +( +) +∶ +紅 +紗 +瑭 +雲 +頭 +鶏 +財 +許 +• +¥ +樂 +焗 +麗 +— +; +滙 +東 +榮 +繪 +興 +… +門 +業 +π +楊 +國 +顧 +é +盤 +寳 +Λ +龍 +鳳 +島 +誌 +緣 +結 +銭 +萬 +勝 +祎 +璟 +優 +歡 +臨 +時 +購 += +★ +藍 +昇 +鐵 +觀 +勅 +農 +聲 +畫 +兿 +術 +發 +劉 +記 +專 +耑 +園 +書 +壴 +種 +Ο +● +褀 +號 +銀 +匯 +敟 +锘 +葉 +橪 +廣 +進 +蒄 +鑽 +阝 +祙 +貢 +鍋 +豊 +夬 +喆 +團 +閣 +開 +燁 +賓 +館 +酡 +沔 +順 ++ +硚 +劵 +饸 +陽 +車 +湓 +復 +萊 +氣 +軒 +華 +堃 +迮 +纟 +戶 +馬 +學 +裡 +電 +嶽 +獨 +マ +シ +サ +ジ +燘 +袪 +環 +❤ +臺 +灣 +専 +賣 +孖 +聖 +攝 +線 +▪ +α +傢 +俬 +夢 +達 +莊 +喬 +貝 +薩 +劍 +羅 +壓 +棛 +饦 +尃 +璈 +囍 +醫 +G +I +A +# +N +鷄 +髙 +嬰 +啓 +約 +隹 +潔 +賴 +藝 +~ +寶 +籣 +麺 +  +嶺 +√ +義 +網 +峩 +長 +∧ +魚 +機 +構 +② +鳯 +偉 +L +B +㙟 +畵 +鴿 +' +詩 +溝 +嚞 +屌 +藔 +佧 +玥 +蘭 +織 +1 +3 +9 +0 +7 +點 +砭 +鴨 +鋪 +銘 +廳 +弍 +‧ +創 +湯 +坶 +℃ +卩 +骝 +& +烜 +荘 +當 +潤 +扞 +係 +懷 +碶 +钅 +蚨 +讠 +☆ +叢 +爲 +埗 +涫 +塗 +→ +楽 +現 +鯨 +愛 +瑪 +鈺 +忄 +悶 +藥 +飾 +樓 +視 +孬 +ㆍ +燚 +苪 +師 +① +丼 +锽 +│ +韓 +標 +è +兒 +閏 +匋 +張 +漢 +Ü +髪 +會 +閑 +檔 +習 +裝 +の +峯 +菘 +輝 +И +雞 +釣 +億 +浐 +K +O +R +8 +H +E +P +T +W +D +S +C +M +F +姌 +饹 +» +晞 +廰 +ä +嵯 +鷹 +負 +飲 +絲 +冚 +楗 +澤 +綫 +區 +❋ +← +質 +靑 +揚 +③ +滬 +統 +産 +協 +﹑ +乸 +畐 +經 +運 +際 +洺 +岽 +為 +粵 +諾 +崋 +豐 +碁 +ɔ +V +2 +6 +齋 +誠 +訂 +´ +勑 +雙 +陳 +無 +í +泩 +媄 +夌 +刂 +i +c +t +o +r +a +嘢 +耄 +燴 +暃 +壽 +媽 +靈 +抻 +體 +唻 +É +冮 +甹 +鎮 +錦 +ʌ +蜛 +蠄 +尓 +駕 +戀 +飬 +逹 +倫 +貴 +極 +Я +Й +寬 +磚 +嶪 +郎 +職 +| +間 +n +d +剎 +伈 +課 +飛 +橋 +瘊 +№ +譜 +骓 +圗 +滘 +縣 +粿 +咅 +養 +濤 +彳 +® +% +Ⅱ +啰 +㴪 +見 +矞 +薬 +糁 +邨 +鲮 +顔 +罱 +З +選 +話 +贏 +氪 +俵 +競 +瑩 +繡 +枱 +β +綉 +á +獅 +爾 +™ +麵 +戋 +淩 +徳 +個 +劇 +場 +務 +簡 +寵 +h +實 +膠 +轱 +圖 +築 +嘣 +樹 +㸃 +營 +耵 +孫 +饃 +鄺 +飯 +麯 +遠 +輸 +坫 +孃 +乚 +閃 +鏢 +㎡ +題 +廠 +關 +↑ +爺 +將 +軍 +連 +篦 +覌 +參 +箸 +- +窠 +棽 +寕 +夀 +爰 +歐 +呙 +閥 +頡 +熱 +雎 +垟 +裟 +凬 +勁 +帑 +馕 +夆 +疌 +枼 +馮 +貨 +蒤 +樸 +彧 +旸 +靜 +龢 +暢 +㐱 +鳥 +珺 +鏡 +灡 +爭 +堷 +廚 +Ó +騰 +診 +┅ +蘇 +褔 +凱 +頂 +豕 +亞 +帥 +嘬 +⊥ +仺 +桖 +複 +饣 +絡 +穂 +顏 +棟 +納 +▏ +濟 +親 +設 +計 +攵 +埌 +烺 +ò +頤 +燦 +蓮 +撻 +節 +講 +濱 +濃 +娽 +洳 +朿 +燈 +鈴 +護 +膚 +铔 +過 +補 +Z +U +5 +4 +坋 +闿 +䖝 +餘 +缐 +铞 +貿 +铪 +桼 +趙 +鍊 +[ +㐂 +垚 +菓 +揸 +捲 +鐘 +滏 +𣇉 +爍 +輪 +燜 +鴻 +鮮 +動 +鹞 +鷗 +丄 +慶 +鉌 +翥 +飮 +腸 +⇋ +漁 +覺 +來 +熘 +昴 +翏 +鲱 +圧 +鄉 +萭 +頔 +爐 +嫚 +г +貭 +類 +聯 +幛 +輕 +訓 +鑒 +夋 +锨 +芃 +珣 +䝉 +扙 +嵐 +銷 +處 +ㄱ +語 +誘 +苝 +歸 +儀 +燒 +楿 +內 +粢 +葒 +奧 +麥 +礻 +滿 +蠔 +穵 +瞭 +態 +鱬 +榞 +硂 +鄭 +黃 +煙 +祐 +奓 +逺 +* +瑄 +獲 +聞 +薦 +讀 +這 +樣 +決 +問 +啟 +們 +執 +説 +轉 +單 +隨 +唘 +帶 +倉 +庫 +還 +贈 +尙 +皺 +■ +餅 +產 +○ +∈ +報 +狀 +楓 +賠 +琯 +嗮 +禮 +` +傳 +> +≤ +嗞 +Φ +≥ +換 +咭 +∣ +↓ +曬 +ε +応 +寫 +″ +終 +様 +純 +費 +療 +聨 +凍 +壐 +郵 +ü +黒 +∫ +製 +塊 +調 +軽 +確 +撃 +級 +馴 +Ⅲ +涇 +繹 +數 +碼 +證 +狒 +処 +劑 +< +晧 +賀 +衆 +] +櫥 +兩 +陰 +絶 +對 +鯉 +憶 +◎ +p +e +Y +蕒 +煖 +頓 +測 +試 +鼽 +僑 +碩 +妝 +帯 +≈ +鐡 +舖 +權 +喫 +倆 +ˋ +該 +悅 +ā +俫 +. +f +s +b +m +k +g +u +j +貼 +淨 +濕 +針 +適 +備 +l +/ +給 +謢 +強 +觸 +衛 +與 +⊙ +$ +緯 +變 +⑴ +⑵ +⑶ +㎏ +殺 +∩ +幚 +─ +價 +▲ +離 +ú +ó +飄 +烏 +関 +閟 +﹝ +﹞ +邏 +輯 +鍵 +驗 +訣 +導 +歷 +屆 +層 +▼ +儱 +錄 +熳 +ē +艦 +吋 +錶 +辧 +飼 +顯 +④ +禦 +販 +気 +対 +枰 +閩 +紀 +幹 +瞓 +貊 +淚 +△ +眞 +墊 +Ω +獻 +褲 +縫 +緑 +亜 +鉅 +餠 +{ +} +◆ +蘆 +薈 +█ +◇ +溫 +彈 +晳 +粧 +犸 +穩 +訊 +崬 +凖 +熥 +П +舊 +條 +紋 +圍 +Ⅳ +筆 +尷 +難 +雜 +錯 +綁 +識 +頰 +鎖 +艶 +□ +殁 +殼 +⑧ +├ +▕ +鵬 +ǐ +ō +ǒ +糝 +綱 +▎ +μ +盜 +饅 +醬 +籤 +蓋 +釀 +鹽 +據 +à +ɡ +辦 +◥ +彐 +┌ +婦 +獸 +鲩 +伱 +ī +蒟 +蒻 +齊 +袆 +腦 +寧 +凈 +妳 +煥 +詢 +偽 +謹 +啫 +鯽 +騷 +鱸 +損 +傷 +鎻 +髮 +買 +冏 +儥 +両 +﹢ +∞ +載 +喰 +z +羙 +悵 +燙 +曉 +員 +組 +徹 +艷 +痠 +鋼 +鼙 +縮 +細 +嚒 +爯 +≠ +維 +" +鱻 +壇 +厍 +帰 +浥 +犇 +薡 +軎 +² +應 +醜 +刪 +緻 +鶴 +賜 +噁 +軌 +尨 +镔 +鷺 +槗 +彌 +葚 +濛 +請 +溇 +緹 +賢 +訪 +獴 +瑅 +資 +縤 +陣 +蕟 +栢 +韻 +祼 +恁 +伢 +謝 +劃 +涑 +總 +衖 +踺 +砋 +凉 +籃 +駿 +苼 +瘋 +昽 +紡 +驊 +腎 +﹗ +響 +杋 +剛 +嚴 +禪 +歓 +槍 +傘 +檸 +檫 +炣 +勢 +鏜 +鎢 +銑 +尐 +減 +奪 +惡 +θ +僮 +婭 +臘 +ū +ì +殻 +鉄 +∑ +蛲 +焼 +緖 +續 +紹 +懮 \ No newline at end of file diff --git a/deploy/ios_demo/ocr_demo/main.m b/deploy/ios_demo/ocr_demo/main.m new file mode 100644 index 00000000..22813be2 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/main.m @@ -0,0 +1,16 @@ +// +// main.m +// seg_demo +// +// Created by Li,Xiaoyang(SYS) on 2018/11/13. +// Copyright © 2018年 Li,Xiaoyang(SYS). All rights reserved. +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/deploy/ios_demo/ocr_demo/ocr.png b/deploy/ios_demo/ocr_demo/ocr.png new file mode 100644 index 0000000000000000000000000000000000000000..f628458708df7fdfd731816767ad9373f307c966 GIT binary patch literal 62926 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
W+*qP;#3f$E6gDP#si85shDvn2)#gp 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ıx{OrLK5mO{<=Vzt<=o6{mpMS;CnEZvT-+pc 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 literal 0 HcmV?d00001 diff --git a/deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.cpp b/deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.cpp new file mode 100644 index 00000000..d5db3472 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.cpp @@ -0,0 +1,4629 @@ +/******************************************************************************* +* * +* Author : Angus Johnson * +* Version : 6.4.2 * +* Date : 27 February 2017 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2017 * +* * +* License: * +* Use, modification & distribution is subject to Boost Software License Ver 1. * +* http://www.boost.org/LICENSE_1_0.txt * +* * +* Attributions: * +* The code in this library is an extension of Bala Vatti's clipping algorithm: * +* "A generic solution to polygon clipping" * +* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * +* http://portal.acm.org/citation.cfm?id=129906 * +* * +* Computer graphics and geometric modeling: implementation and algorithms * +* By Max K. Agoston * +* Springer; 1 edition (January 4, 2005) * +* http://books.google.com/books?q=vatti+clipping+agoston * +* * +* See also: * +* "Polygon Offsetting by Computing Winding Numbers" * +* Paper no. DETC2005-85513 pp. 565-575 * +* ASME 2005 International Design Engineering Technical Conferences * +* and Computers and Information in Engineering Conference (IDETC/CIE2005) * +* September 24-28, 2005 , Long Beach, California, USA * +* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * +* * +*******************************************************************************/ + +/******************************************************************************* +* * +* This is a translation of the Delphi Clipper library and the naming style * +* used has retained a Delphi flavour. * +* * +*******************************************************************************/ + +#include "ocr_clipper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +static double const pi = 3.141592653589793238; +static double const two_pi = pi *2; +static double const def_arc_tolerance = 0.25; + +enum Direction { dRightToLeft, dLeftToRight }; + +static int const Unassigned = -1; //edge not currently 'owning' a solution +static int const Skip = -2; //edge that would otherwise close a path + +#define HORIZONTAL (-1.0E+40) +#define TOLERANCE (1.0e-20) +#define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) + +struct TEdge { + IntPoint Bot; + IntPoint Curr; //current (updated for every new scanbeam) + IntPoint Top; + double Dx; + PolyType PolyTyp; + EdgeSide Side; //side only refers to current side of solution poly + int WindDelta; //1 or -1 depending on winding direction + int WindCnt; + int WindCnt2; //winding count of the opposite polytype + int OutIdx; + TEdge *Next; + TEdge *Prev; + TEdge *NextInLML; + TEdge *NextInAEL; + TEdge *PrevInAEL; + TEdge *NextInSEL; + TEdge *PrevInSEL; +}; + +struct IntersectNode { + TEdge *Edge1; + TEdge *Edge2; + IntPoint Pt; +}; + +struct LocalMinimum { + cInt Y; + TEdge *LeftBound; + TEdge *RightBound; +}; + +struct OutPt; + +//OutRec: contains a path in the clipping solution. Edges in the AEL will +//carry a pointer to an OutRec when they are part of the clipping solution. +struct OutRec { + int Idx; + bool IsHole; + bool IsOpen; + OutRec *FirstLeft; //see comments in clipper.pas + PolyNode *PolyNd; + OutPt *Pts; + OutPt *BottomPt; +}; + +struct OutPt { + int Idx; + IntPoint Pt; + OutPt *Next; + OutPt *Prev; +}; + +struct Join { + OutPt *OutPt1; + OutPt *OutPt2; + IntPoint OffPt; +}; + +struct LocMinSorter +{ + inline bool operator()(const LocalMinimum& locMin1, const LocalMinimum& locMin2) + { + return locMin2.Y < locMin1.Y; + } +}; + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +inline cInt Round(double val) +{ + if ((val < 0)) return static_cast(val - 0.5); + else return static_cast(val + 0.5); +} +//------------------------------------------------------------------------------ + +inline cInt Abs(cInt val) +{ + return val < 0 ? -val : val; +} + +//------------------------------------------------------------------------------ +// PolyTree methods ... +//------------------------------------------------------------------------------ + +void PolyTree::Clear() +{ + for (PolyNodes::size_type i = 0; i < AllNodes.size(); ++i) + delete AllNodes[i]; + AllNodes.resize(0); + Childs.resize(0); +} +//------------------------------------------------------------------------------ + +PolyNode* PolyTree::GetFirst() const +{ + if (!Childs.empty()) + return Childs[0]; + else + return 0; +} +//------------------------------------------------------------------------------ + +int PolyTree::Total() const +{ + int result = (int)AllNodes.size(); + //with negative offsets, ignore the hidden outer polygon ... + if (result > 0 && Childs[0] != AllNodes[0]) result--; + return result; +} + +//------------------------------------------------------------------------------ +// PolyNode methods ... +//------------------------------------------------------------------------------ + +PolyNode::PolyNode(): Parent(0), Index(0), m_IsOpen(false) +{ +} +//------------------------------------------------------------------------------ + +int PolyNode::ChildCount() const +{ + return (int)Childs.size(); +} +//------------------------------------------------------------------------------ + +void PolyNode::AddChild(PolyNode& child) +{ + unsigned cnt = (unsigned)Childs.size(); + Childs.push_back(&child); + child.Parent = this; + child.Index = cnt; +} +//------------------------------------------------------------------------------ + +PolyNode* PolyNode::GetNext() const +{ + if (!Childs.empty()) + return Childs[0]; + else + return GetNextSiblingUp(); +} +//------------------------------------------------------------------------------ + +PolyNode* PolyNode::GetNextSiblingUp() const +{ + if (!Parent) //protects against PolyTree.GetNextSiblingUp() + return 0; + else if (Index == Parent->Childs.size() - 1) + return Parent->GetNextSiblingUp(); + else + return Parent->Childs[Index + 1]; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsHole() const +{ + bool result = true; + PolyNode* node = Parent; + while (node) + { + result = !result; + node = node->Parent; + } + return result; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsOpen() const +{ + return m_IsOpen; +} +//------------------------------------------------------------------------------ + +#ifndef use_int32 + +//------------------------------------------------------------------------------ +// Int128 class (enables safe math on signed 64bit integers) +// eg Int128 val1((long64)9223372036854775807); //ie 2^63 -1 +// Int128 val2((long64)9223372036854775807); +// Int128 val3 = val1 * val2; +// val3.AsString => "85070591730234615847396907784232501249" (8.5e+37) +//------------------------------------------------------------------------------ + +class Int128 +{ + public: + ulong64 lo; + long64 hi; + + Int128(long64 _lo = 0) + { + lo = (ulong64)_lo; + if (_lo < 0) hi = -1; else hi = 0; + } + + + Int128(const Int128 &val): lo(val.lo), hi(val.hi){} + + Int128(const long64& _hi, const ulong64& _lo): lo(_lo), hi(_hi){} + + Int128& operator = (const long64 &val) + { + lo = (ulong64)val; + if (val < 0) hi = -1; else hi = 0; + return *this; + } + + bool operator == (const Int128 &val) const + {return (hi == val.hi && lo == val.lo);} + + bool operator != (const Int128 &val) const + { return !(*this == val);} + + bool operator > (const Int128 &val) const + { + if (hi != val.hi) + return hi > val.hi; + else + return lo > val.lo; + } + + bool operator < (const Int128 &val) const + { + if (hi != val.hi) + return hi < val.hi; + else + return lo < val.lo; + } + + bool operator >= (const Int128 &val) const + { return !(*this < val);} + + bool operator <= (const Int128 &val) const + { return !(*this > val);} + + Int128& operator += (const Int128 &rhs) + { + hi += rhs.hi; + lo += rhs.lo; + if (lo < rhs.lo) hi++; + return *this; + } + + Int128 operator + (const Int128 &rhs) const + { + Int128 result(*this); + result+= rhs; + return result; + } + + Int128& operator -= (const Int128 &rhs) + { + *this += -rhs; + return *this; + } + + Int128 operator - (const Int128 &rhs) const + { + Int128 result(*this); + result -= rhs; + return result; + } + + Int128 operator-() const //unary negation + { + if (lo == 0) + return Int128(-hi, 0); + else + return Int128(~hi, ~lo + 1); + } + + operator double() const + { + const double shift64 = 18446744073709551616.0; //2^64 + if (hi < 0) + { + if (lo == 0) return (double)hi * shift64; + else return -(double)(~lo + ~hi * shift64); + } + else + return (double)(lo + hi * shift64); + } + +}; +//------------------------------------------------------------------------------ + +Int128 Int128Mul (long64 lhs, long64 rhs) +{ + bool negate = (lhs < 0) != (rhs < 0); + + if (lhs < 0) lhs = -lhs; + ulong64 int1Hi = ulong64(lhs) >> 32; + ulong64 int1Lo = ulong64(lhs & 0xFFFFFFFF); + + if (rhs < 0) rhs = -rhs; + ulong64 int2Hi = ulong64(rhs) >> 32; + ulong64 int2Lo = ulong64(rhs & 0xFFFFFFFF); + + //nb: see comments in clipper.pas + ulong64 a = int1Hi * int2Hi; + ulong64 b = int1Lo * int2Lo; + ulong64 c = int1Hi * int2Lo + int1Lo * int2Hi; + + Int128 tmp; + tmp.hi = long64(a + (c >> 32)); + tmp.lo = long64(c << 32); + tmp.lo += long64(b); + if (tmp.lo < b) tmp.hi++; + if (negate) tmp = -tmp; + return tmp; +}; +#endif + +//------------------------------------------------------------------------------ +// Miscellaneous global functions +//------------------------------------------------------------------------------ + +bool Orientation(const Path &poly) +{ + return Area(poly) >= 0; +} +//------------------------------------------------------------------------------ + +double Area(const Path &poly) +{ + int size = (int)poly.size(); + if (size < 3) return 0; + + double a = 0; + for (int i = 0, j = size -1; i < size; ++i) + { + a += ((double)poly[j].X + poly[i].X) * ((double)poly[j].Y - poly[i].Y); + j = i; + } + return -a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutPt *op) +{ + const OutPt *startOp = op; + if (!op) return 0; + double a = 0; + do { + a += (double)(op->Prev->Pt.X + op->Pt.X) * (double)(op->Prev->Pt.Y - op->Pt.Y); + op = op->Next; + } while (op != startOp); + return a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutRec &outRec) +{ + return Area(outRec.Pts); +} +//------------------------------------------------------------------------------ + +bool PointIsVertex(const IntPoint &Pt, OutPt *pp) +{ + OutPt *pp2 = pp; + do + { + if (pp2->Pt == Pt) return true; + pp2 = pp2->Next; + } + while (pp2 != pp); + return false; +} +//------------------------------------------------------------------------------ + +//See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos +//http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf +int PointInPolygon(const IntPoint &pt, const Path &path) +{ + //returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + size_t cnt = path.size(); + if (cnt < 3) return 0; + IntPoint ip = path[0]; + for(size_t i = 1; i <= cnt; ++i) + { + IntPoint ipNext = (i == cnt ? path[0] : path[i]); + if (ipNext.Y == pt.Y) + { + if ((ipNext.X == pt.X) || (ip.Y == pt.Y && + ((ipNext.X > pt.X) == (ip.X < pt.X)))) return -1; + } + if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) + { + if (ip.X >= pt.X) + { + if (ipNext.X > pt.X) result = 1 - result; + else + { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) result = 1 - result; + } + } else + { + if (ipNext.X > pt.X) + { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) result = 1 - result; + } + } + } + ip = ipNext; + } + return result; +} +//------------------------------------------------------------------------------ + +int PointInPolygon (const IntPoint &pt, OutPt *op) +{ + //returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + OutPt* startOp = op; + for(;;) + { + if (op->Next->Pt.Y == pt.Y) + { + if ((op->Next->Pt.X == pt.X) || (op->Pt.Y == pt.Y && + ((op->Next->Pt.X > pt.X) == (op->Pt.X < pt.X)))) return -1; + } + if ((op->Pt.Y < pt.Y) != (op->Next->Pt.Y < pt.Y)) + { + if (op->Pt.X >= pt.X) + { + if (op->Next->Pt.X > pt.X) result = 1 - result; + else + { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) result = 1 - result; + } + } else + { + if (op->Next->Pt.X > pt.X) + { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) result = 1 - result; + } + } + } + op = op->Next; + if (startOp == op) break; + } + return result; +} +//------------------------------------------------------------------------------ + +bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) +{ + OutPt* op = OutPt1; + do + { + //nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon + int res = PointInPolygon(op->Pt, OutPt2); + if (res >= 0) return res > 0; + op = op->Next; + } + while (op != OutPt1); + return true; +} +//---------------------------------------------------------------------- + +bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) +{ +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(e1.Top.Y - e1.Bot.Y, e2.Top.X - e2.Bot.X) == + Int128Mul(e1.Top.X - e1.Bot.X, e2.Top.Y - e2.Bot.Y); + else +#endif + return (e1.Top.Y - e1.Bot.Y) * (e2.Top.X - e2.Bot.X) == + (e1.Top.X - e1.Bot.X) * (e2.Top.Y - e2.Bot.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, + const IntPoint pt3, bool UseFullInt64Range) +{ +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y-pt2.Y, pt2.X-pt3.X) == Int128Mul(pt1.X-pt2.X, pt2.Y-pt3.Y); + else +#endif + return (pt1.Y-pt2.Y)*(pt2.X-pt3.X) == (pt1.X-pt2.X)*(pt2.Y-pt3.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, + const IntPoint pt3, const IntPoint pt4, bool UseFullInt64Range) +{ +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y-pt2.Y, pt3.X-pt4.X) == Int128Mul(pt1.X-pt2.X, pt3.Y-pt4.Y); + else +#endif + return (pt1.Y-pt2.Y)*(pt3.X-pt4.X) == (pt1.X-pt2.X)*(pt3.Y-pt4.Y); +} +//------------------------------------------------------------------------------ + +inline bool IsHorizontal(TEdge &e) +{ + return e.Dx == HORIZONTAL; +} +//------------------------------------------------------------------------------ + +inline double GetDx(const IntPoint pt1, const IntPoint pt2) +{ + return (pt1.Y == pt2.Y) ? + HORIZONTAL : (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y); +} +//--------------------------------------------------------------------------- + +inline void SetDx(TEdge &e) +{ + cInt dy = (e.Top.Y - e.Bot.Y); + if (dy == 0) e.Dx = HORIZONTAL; + else e.Dx = (double)(e.Top.X - e.Bot.X) / dy; +} +//--------------------------------------------------------------------------- + +inline void SwapSides(TEdge &Edge1, TEdge &Edge2) +{ + EdgeSide Side = Edge1.Side; + Edge1.Side = Edge2.Side; + Edge2.Side = Side; +} +//------------------------------------------------------------------------------ + +inline void SwapPolyIndexes(TEdge &Edge1, TEdge &Edge2) +{ + int OutIdx = Edge1.OutIdx; + Edge1.OutIdx = Edge2.OutIdx; + Edge2.OutIdx = OutIdx; +} +//------------------------------------------------------------------------------ + +inline cInt TopX(TEdge &edge, const cInt currentY) +{ + return ( currentY == edge.Top.Y ) ? + edge.Top.X : edge.Bot.X + Round(edge.Dx *(currentY - edge.Bot.Y)); +} +//------------------------------------------------------------------------------ + +void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) +{ +#ifdef use_xyz + ip.Z = 0; +#endif + + double b1, b2; + if (Edge1.Dx == Edge2.Dx) + { + ip.Y = Edge1.Curr.Y; + ip.X = TopX(Edge1, ip.Y); + return; + } + else if (Edge1.Dx == 0) + { + ip.X = Edge1.Bot.X; + if (IsHorizontal(Edge2)) + ip.Y = Edge2.Bot.Y; + else + { + b2 = Edge2.Bot.Y - (Edge2.Bot.X / Edge2.Dx); + ip.Y = Round(ip.X / Edge2.Dx + b2); + } + } + else if (Edge2.Dx == 0) + { + ip.X = Edge2.Bot.X; + if (IsHorizontal(Edge1)) + ip.Y = Edge1.Bot.Y; + else + { + b1 = Edge1.Bot.Y - (Edge1.Bot.X / Edge1.Dx); + ip.Y = Round(ip.X / Edge1.Dx + b1); + } + } + else + { + b1 = Edge1.Bot.X - Edge1.Bot.Y * Edge1.Dx; + b2 = Edge2.Bot.X - Edge2.Bot.Y * Edge2.Dx; + double q = (b2-b1) / (Edge1.Dx - Edge2.Dx); + ip.Y = Round(q); + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = Round(Edge1.Dx * q + b1); + else + ip.X = Round(Edge2.Dx * q + b2); + } + + if (ip.Y < Edge1.Top.Y || ip.Y < Edge2.Top.Y) + { + if (Edge1.Top.Y > Edge2.Top.Y) + ip.Y = Edge1.Top.Y; + else + ip.Y = Edge2.Top.Y; + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = TopX(Edge1, ip.Y); + else + ip.X = TopX(Edge2, ip.Y); + } + //finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ... + if (ip.Y > Edge1.Curr.Y) + { + ip.Y = Edge1.Curr.Y; + //use the more vertical edge to derive X ... + if (std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx)) + ip.X = TopX(Edge2, ip.Y); else + ip.X = TopX(Edge1, ip.Y); + } +} +//------------------------------------------------------------------------------ + +void ReversePolyPtLinks(OutPt *pp) +{ + if (!pp) return; + OutPt *pp1, *pp2; + pp1 = pp; + do { + pp2 = pp1->Next; + pp1->Next = pp1->Prev; + pp1->Prev = pp2; + pp1 = pp2; + } while( pp1 != pp ); +} +//------------------------------------------------------------------------------ + +void DisposeOutPts(OutPt*& pp) +{ + if (pp == 0) return; + pp->Prev->Next = 0; + while( pp ) + { + OutPt *tmpPp = pp; + pp = pp->Next; + delete tmpPp; + } +} +//------------------------------------------------------------------------------ + +inline void InitEdge(TEdge* e, TEdge* eNext, TEdge* ePrev, const IntPoint& Pt) +{ + std::memset(e, 0, sizeof(TEdge)); + e->Next = eNext; + e->Prev = ePrev; + e->Curr = Pt; + e->OutIdx = Unassigned; +} +//------------------------------------------------------------------------------ + +void InitEdge2(TEdge& e, PolyType Pt) +{ + if (e.Curr.Y >= e.Next->Curr.Y) + { + e.Bot = e.Curr; + e.Top = e.Next->Curr; + } else + { + e.Top = e.Curr; + e.Bot = e.Next->Curr; + } + SetDx(e); + e.PolyTyp = Pt; +} +//------------------------------------------------------------------------------ + +TEdge* RemoveEdge(TEdge* e) +{ + //removes e from double_linked_list (but without removing from memory) + e->Prev->Next = e->Next; + e->Next->Prev = e->Prev; + TEdge* result = e->Next; + e->Prev = 0; //flag as removed (see ClipperBase.Clear) + return result; +} +//------------------------------------------------------------------------------ + +inline void ReverseHorizontal(TEdge &e) +{ + //swap horizontal edges' Top and Bottom x's so they follow the natural + //progression of the bounds - ie so their xbots will align with the + //adjoining lower edge. [Helpful in the ProcessHorizontal() method.] + std::swap(e.Top.X, e.Bot.X); +#ifdef use_xyz + std::swap(e.Top.Z, e.Bot.Z); +#endif +} +//------------------------------------------------------------------------------ + +void SwapPoints(IntPoint &pt1, IntPoint &pt2) +{ + IntPoint tmp = pt1; + pt1 = pt2; + pt2 = tmp; +} +//------------------------------------------------------------------------------ + +bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a, + IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) +{ + //precondition: segments are Collinear. + if (Abs(pt1a.X - pt1b.X) > Abs(pt1a.Y - pt1b.Y)) + { + if (pt1a.X > pt1b.X) SwapPoints(pt1a, pt1b); + if (pt2a.X > pt2b.X) SwapPoints(pt2a, pt2b); + if (pt1a.X > pt2a.X) pt1 = pt1a; else pt1 = pt2a; + if (pt1b.X < pt2b.X) pt2 = pt1b; else pt2 = pt2b; + return pt1.X < pt2.X; + } else + { + if (pt1a.Y < pt1b.Y) SwapPoints(pt1a, pt1b); + if (pt2a.Y < pt2b.Y) SwapPoints(pt2a, pt2b); + if (pt1a.Y < pt2a.Y) pt1 = pt1a; else pt1 = pt2a; + if (pt1b.Y > pt2b.Y) pt2 = pt1b; else pt2 = pt2b; + return pt1.Y > pt2.Y; + } +} +//------------------------------------------------------------------------------ + +bool FirstIsBottomPt(const OutPt* btmPt1, const OutPt* btmPt2) +{ + OutPt *p = btmPt1->Prev; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) p = p->Prev; + double dx1p = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + p = btmPt1->Next; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) p = p->Next; + double dx1n = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + + p = btmPt2->Prev; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) p = p->Prev; + double dx2p = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + p = btmPt2->Next; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) p = p->Next; + double dx2n = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + + if (std::max(dx1p, dx1n) == std::max(dx2p, dx2n) && + std::min(dx1p, dx1n) == std::min(dx2p, dx2n)) + return Area(btmPt1) > 0; //if otherwise identical use orientation + else + return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); +} +//------------------------------------------------------------------------------ + +OutPt* GetBottomPt(OutPt *pp) +{ + OutPt* dups = 0; + OutPt* p = pp->Next; + while (p != pp) + { + if (p->Pt.Y > pp->Pt.Y) + { + pp = p; + dups = 0; + } + else if (p->Pt.Y == pp->Pt.Y && p->Pt.X <= pp->Pt.X) + { + if (p->Pt.X < pp->Pt.X) + { + dups = 0; + pp = p; + } else + { + if (p->Next != pp && p->Prev != pp) dups = p; + } + } + p = p->Next; + } + if (dups) + { + //there appears to be at least 2 vertices at BottomPt so ... + while (dups != p) + { + if (!FirstIsBottomPt(p, dups)) pp = dups; + dups = dups->Next; + while (dups->Pt != pp->Pt) dups = dups->Next; + } + } + return pp; +} +//------------------------------------------------------------------------------ + +bool Pt2IsBetweenPt1AndPt3(const IntPoint pt1, + const IntPoint pt2, const IntPoint pt3) +{ + if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2)) + return false; + else if (pt1.X != pt3.X) + return (pt2.X > pt1.X) == (pt2.X < pt3.X); + else + return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); +} +//------------------------------------------------------------------------------ + +bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b) +{ + if (seg1a > seg1b) std::swap(seg1a, seg1b); + if (seg2a > seg2b) std::swap(seg2a, seg2b); + return (seg1a < seg2b) && (seg2a < seg1b); +} + +//------------------------------------------------------------------------------ +// ClipperBase class methods ... +//------------------------------------------------------------------------------ + +ClipperBase::ClipperBase() //constructor +{ + m_CurrentLM = m_MinimaList.begin(); //begin() == end() here + m_UseFullRange = false; +} +//------------------------------------------------------------------------------ + +ClipperBase::~ClipperBase() //destructor +{ + Clear(); +} +//------------------------------------------------------------------------------ + +void RangeTest(const IntPoint& Pt, bool& useFullRange) +{ + if (useFullRange) + { + if (Pt.X > hiRange || Pt.Y > hiRange || -Pt.X > hiRange || -Pt.Y > hiRange) + throw clipperException("Coordinate outside allowed range"); + } + else if (Pt.X > loRange|| Pt.Y > loRange || -Pt.X > loRange || -Pt.Y > loRange) + { + useFullRange = true; + RangeTest(Pt, useFullRange); + } +} +//------------------------------------------------------------------------------ + +TEdge* FindNextLocMin(TEdge* E) +{ + for (;;) + { + while (E->Bot != E->Prev->Bot || E->Curr == E->Top) E = E->Next; + if (!IsHorizontal(*E) && !IsHorizontal(*E->Prev)) break; + while (IsHorizontal(*E->Prev)) E = E->Prev; + TEdge* E2 = E; + while (IsHorizontal(*E)) E = E->Next; + if (E->Top.Y == E->Prev->Bot.Y) continue; //ie just an intermediate horz. + if (E2->Prev->Bot.X < E->Bot.X) E = E2; + break; + } + return E; +} +//------------------------------------------------------------------------------ + +TEdge* ClipperBase::ProcessBound(TEdge* E, bool NextIsForward) +{ + TEdge *Result = E; + TEdge *Horz = 0; + + if (E->OutIdx == Skip) + { + //if edges still remain in the current bound beyond the skip edge then + //create another LocMin and call ProcessBound once more + if (NextIsForward) + { + while (E->Top.Y == E->Next->Bot.Y) E = E->Next; + //don't include top horizontals when parsing a bound a second time, + //they will be contained in the opposite bound ... + while (E != Result && IsHorizontal(*E)) E = E->Prev; + } + else + { + while (E->Top.Y == E->Prev->Bot.Y) E = E->Prev; + while (E != Result && IsHorizontal(*E)) E = E->Next; + } + + if (E == Result) + { + if (NextIsForward) Result = E->Next; + else Result = E->Prev; + } + else + { + //there are more edges in the bound beyond result starting with E + if (NextIsForward) + E = Result->Next; + else + E = Result->Prev; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + E->WindDelta = 0; + Result = ProcessBound(E, NextIsForward); + m_MinimaList.push_back(locMin); + } + return Result; + } + + TEdge *EStart; + + if (IsHorizontal(*E)) + { + //We need to be careful with open paths because this may not be a + //true local minima (ie E may be following a skip edge). + //Also, consecutive horz. edges may start heading left before going right. + if (NextIsForward) + EStart = E->Prev; + else + EStart = E->Next; + if (IsHorizontal(*EStart)) //ie an adjoining horizontal skip edge + { + if (EStart->Bot.X != E->Bot.X && EStart->Top.X != E->Bot.X) + ReverseHorizontal(*E); + } + else if (EStart->Bot.X != E->Bot.X) + ReverseHorizontal(*E); + } + + EStart = E; + if (NextIsForward) + { + while (Result->Top.Y == Result->Next->Bot.Y && Result->Next->OutIdx != Skip) + Result = Result->Next; + if (IsHorizontal(*Result) && Result->Next->OutIdx != Skip) + { + //nb: at the top of a bound, horizontals are added to the bound + //only when the preceding edge attaches to the horizontal's left vertex + //unless a Skip edge is encountered when that becomes the top divide + Horz = Result; + while (IsHorizontal(*Horz->Prev)) Horz = Horz->Prev; + if (Horz->Prev->Top.X > Result->Next->Top.X) Result = Horz->Prev; + } + while (E != Result) + { + E->NextInLML = E->Next; + if (IsHorizontal(*E) && E != EStart && + E->Bot.X != E->Prev->Top.X) ReverseHorizontal(*E); + E = E->Next; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + Result = Result->Next; //move to the edge just beyond current bound + } else + { + while (Result->Top.Y == Result->Prev->Bot.Y && Result->Prev->OutIdx != Skip) + Result = Result->Prev; + if (IsHorizontal(*Result) && Result->Prev->OutIdx != Skip) + { + Horz = Result; + while (IsHorizontal(*Horz->Next)) Horz = Horz->Next; + if (Horz->Next->Top.X == Result->Prev->Top.X || + Horz->Next->Top.X > Result->Prev->Top.X) Result = Horz->Next; + } + + while (E != Result) + { + E->NextInLML = E->Prev; + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + E = E->Prev; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + Result = Result->Prev; //move to the edge just beyond current bound + } + + return Result; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) +{ +#ifdef use_lines + if (!Closed && PolyTyp == ptClip) + throw clipperException("AddPath: Open paths must be subject."); +#else + if (!Closed) + throw clipperException("AddPath: Open paths have been disabled."); +#endif + + int highI = (int)pg.size() -1; + if (Closed) while (highI > 0 && (pg[highI] == pg[0])) --highI; + while (highI > 0 && (pg[highI] == pg[highI -1])) --highI; + if ((Closed && highI < 2) || (!Closed && highI < 1)) return false; + + //create a new edge array ... + TEdge *edges = new TEdge [highI +1]; + + bool IsFlat = true; + //1. Basic (first) edge initialization ... + try + { + edges[1].Curr = pg[1]; + RangeTest(pg[0], m_UseFullRange); + RangeTest(pg[highI], m_UseFullRange); + InitEdge(&edges[0], &edges[1], &edges[highI], pg[0]); + InitEdge(&edges[highI], &edges[0], &edges[highI-1], pg[highI]); + for (int i = highI - 1; i >= 1; --i) + { + RangeTest(pg[i], m_UseFullRange); + InitEdge(&edges[i], &edges[i+1], &edges[i-1], pg[i]); + } + } + catch(...) + { + delete [] edges; + throw; //range test fails + } + TEdge *eStart = &edges[0]; + + //2. Remove duplicate vertices, and (when closed) collinear edges ... + TEdge *E = eStart, *eLoopStop = eStart; + for (;;) + { + //nb: allows matching start and end points when not Closed ... + if (E->Curr == E->Next->Curr && (Closed || E->Next != eStart)) + { + if (E == E->Next) break; + if (E == eStart) eStart = E->Next; + E = RemoveEdge(E); + eLoopStop = E; + continue; + } + if (E->Prev == E->Next) + break; //only two vertices + else if (Closed && + SlopesEqual(E->Prev->Curr, E->Curr, E->Next->Curr, m_UseFullRange) && + (!m_PreserveCollinear || + !Pt2IsBetweenPt1AndPt3(E->Prev->Curr, E->Curr, E->Next->Curr))) + { + //Collinear edges are allowed for open paths but in closed paths + //the default is to merge adjacent collinear edges into a single edge. + //However, if the PreserveCollinear property is enabled, only overlapping + //collinear edges (ie spikes) will be removed from closed paths. + if (E == eStart) eStart = E->Next; + E = RemoveEdge(E); + E = E->Prev; + eLoopStop = E; + continue; + } + E = E->Next; + if ((E == eLoopStop) || (!Closed && E->Next == eStart)) break; + } + + if ((!Closed && (E == E->Next)) || (Closed && (E->Prev == E->Next))) + { + delete [] edges; + return false; + } + + if (!Closed) + { + m_HasOpenPaths = true; + eStart->Prev->OutIdx = Skip; + } + + //3. Do second stage of edge initialization ... + E = eStart; + do + { + InitEdge2(*E, PolyTyp); + E = E->Next; + if (IsFlat && E->Curr.Y != eStart->Curr.Y) IsFlat = false; + } + while (E != eStart); + + //4. Finally, add edge bounds to LocalMinima list ... + + //Totally flat paths must be handled differently when adding them + //to LocalMinima list to avoid endless loops etc ... + if (IsFlat) + { + if (Closed) + { + delete [] edges; + return false; + } + E->Prev->OutIdx = Skip; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + locMin.RightBound->Side = esRight; + locMin.RightBound->WindDelta = 0; + for (;;) + { + if (E->Bot.X != E->Prev->Top.X) ReverseHorizontal(*E); + if (E->Next->OutIdx == Skip) break; + E->NextInLML = E->Next; + E = E->Next; + } + m_MinimaList.push_back(locMin); + m_edges.push_back(edges); + return true; + } + + m_edges.push_back(edges); + bool leftBoundIsForward; + TEdge* EMin = 0; + + //workaround to avoid an endless loop in the while loop below when + //open paths have matching start and end points ... + if (E->Prev->Bot == E->Prev->Top) E = E->Next; + + for (;;) + { + E = FindNextLocMin(E); + if (E == EMin) break; + else if (!EMin) EMin = E; + + //E and E.Prev now share a local minima (left aligned if horizontal). + //Compare their slopes to find which starts which bound ... + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + if (E->Dx < E->Prev->Dx) + { + locMin.LeftBound = E->Prev; + locMin.RightBound = E; + leftBoundIsForward = false; //Q.nextInLML = Q.prev + } else + { + locMin.LeftBound = E; + locMin.RightBound = E->Prev; + leftBoundIsForward = true; //Q.nextInLML = Q.next + } + + if (!Closed) locMin.LeftBound->WindDelta = 0; + else if (locMin.LeftBound->Next == locMin.RightBound) + locMin.LeftBound->WindDelta = -1; + else locMin.LeftBound->WindDelta = 1; + locMin.RightBound->WindDelta = -locMin.LeftBound->WindDelta; + + E = ProcessBound(locMin.LeftBound, leftBoundIsForward); + if (E->OutIdx == Skip) E = ProcessBound(E, leftBoundIsForward); + + TEdge* E2 = ProcessBound(locMin.RightBound, !leftBoundIsForward); + if (E2->OutIdx == Skip) E2 = ProcessBound(E2, !leftBoundIsForward); + + if (locMin.LeftBound->OutIdx == Skip) + locMin.LeftBound = 0; + else if (locMin.RightBound->OutIdx == Skip) + locMin.RightBound = 0; + m_MinimaList.push_back(locMin); + if (!leftBoundIsForward) E = E2; + } + return true; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) +{ + bool result = false; + for (Paths::size_type i = 0; i < ppg.size(); ++i) + if (AddPath(ppg[i], PolyTyp, Closed)) result = true; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Clear() +{ + DisposeLocalMinimaList(); + for (EdgeList::size_type i = 0; i < m_edges.size(); ++i) + { + TEdge* edges = m_edges[i]; + delete [] edges; + } + m_edges.clear(); + m_UseFullRange = false; + m_HasOpenPaths = false; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Reset() +{ + m_CurrentLM = m_MinimaList.begin(); + if (m_CurrentLM == m_MinimaList.end()) return; //ie nothing to process + std::sort(m_MinimaList.begin(), m_MinimaList.end(), LocMinSorter()); + + m_Scanbeam = ScanbeamList(); //clears/resets priority_queue + //reset all edges ... + for (MinimaList::iterator lm = m_MinimaList.begin(); lm != m_MinimaList.end(); ++lm) + { + InsertScanbeam(lm->Y); + TEdge* e = lm->LeftBound; + if (e) + { + e->Curr = e->Bot; + e->Side = esLeft; + e->OutIdx = Unassigned; + } + + e = lm->RightBound; + if (e) + { + e->Curr = e->Bot; + e->Side = esRight; + e->OutIdx = Unassigned; + } + } + m_ActiveEdges = 0; + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeLocalMinimaList() +{ + m_MinimaList.clear(); + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::PopLocalMinima(cInt Y, const LocalMinimum *&locMin) +{ + if (m_CurrentLM == m_MinimaList.end() || (*m_CurrentLM).Y != Y) return false; + locMin = &(*m_CurrentLM); + ++m_CurrentLM; + return true; +} +//------------------------------------------------------------------------------ + +IntRect ClipperBase::GetBounds() +{ + IntRect result; + MinimaList::iterator lm = m_MinimaList.begin(); + if (lm == m_MinimaList.end()) + { + result.left = result.top = result.right = result.bottom = 0; + return result; + } + result.left = lm->LeftBound->Bot.X; + result.top = lm->LeftBound->Bot.Y; + result.right = lm->LeftBound->Bot.X; + result.bottom = lm->LeftBound->Bot.Y; + while (lm != m_MinimaList.end()) + { + //todo - needs fixing for open paths + result.bottom = std::max(result.bottom, lm->LeftBound->Bot.Y); + TEdge* e = lm->LeftBound; + for (;;) { + TEdge* bottomE = e; + while (e->NextInLML) + { + if (e->Bot.X < result.left) result.left = e->Bot.X; + if (e->Bot.X > result.right) result.right = e->Bot.X; + e = e->NextInLML; + } + result.left = std::min(result.left, e->Bot.X); + result.right = std::max(result.right, e->Bot.X); + result.left = std::min(result.left, e->Top.X); + result.right = std::max(result.right, e->Top.X); + result.top = std::min(result.top, e->Top.Y); + if (bottomE == lm->LeftBound) e = lm->RightBound; + else break; + } + ++lm; + } + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::InsertScanbeam(const cInt Y) +{ + m_Scanbeam.push(Y); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::PopScanbeam(cInt &Y) +{ + if (m_Scanbeam.empty()) return false; + Y = m_Scanbeam.top(); + m_Scanbeam.pop(); + while (!m_Scanbeam.empty() && Y == m_Scanbeam.top()) { m_Scanbeam.pop(); } // Pop duplicates. + return true; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeAllOutRecs(){ + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + DisposeOutRec(i); + m_PolyOuts.clear(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeOutRec(PolyOutList::size_type index) +{ + OutRec *outRec = m_PolyOuts[index]; + if (outRec->Pts) DisposeOutPts(outRec->Pts); + delete outRec; + m_PolyOuts[index] = 0; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DeleteFromAEL(TEdge *e) +{ + TEdge* AelPrev = e->PrevInAEL; + TEdge* AelNext = e->NextInAEL; + if (!AelPrev && !AelNext && (e != m_ActiveEdges)) return; //already deleted + if (AelPrev) AelPrev->NextInAEL = AelNext; + else m_ActiveEdges = AelNext; + if (AelNext) AelNext->PrevInAEL = AelPrev; + e->NextInAEL = 0; + e->PrevInAEL = 0; +} +//------------------------------------------------------------------------------ + +OutRec* ClipperBase::CreateOutRec() +{ + OutRec* result = new OutRec; + result->IsHole = false; + result->IsOpen = false; + result->FirstLeft = 0; + result->Pts = 0; + result->BottomPt = 0; + result->PolyNd = 0; + m_PolyOuts.push_back(result); + result->Idx = (int)m_PolyOuts.size() - 1; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::SwapPositionsInAEL(TEdge *Edge1, TEdge *Edge2) +{ + //check that one or other edge hasn't already been removed from AEL ... + if (Edge1->NextInAEL == Edge1->PrevInAEL || + Edge2->NextInAEL == Edge2->PrevInAEL) return; + + if (Edge1->NextInAEL == Edge2) + { + TEdge* Next = Edge2->NextInAEL; + if (Next) Next->PrevInAEL = Edge1; + TEdge* Prev = Edge1->PrevInAEL; + if (Prev) Prev->NextInAEL = Edge2; + Edge2->PrevInAEL = Prev; + Edge2->NextInAEL = Edge1; + Edge1->PrevInAEL = Edge2; + Edge1->NextInAEL = Next; + } + else if (Edge2->NextInAEL == Edge1) + { + TEdge* Next = Edge1->NextInAEL; + if (Next) Next->PrevInAEL = Edge2; + TEdge* Prev = Edge2->PrevInAEL; + if (Prev) Prev->NextInAEL = Edge1; + Edge1->PrevInAEL = Prev; + Edge1->NextInAEL = Edge2; + Edge2->PrevInAEL = Edge1; + Edge2->NextInAEL = Next; + } + else + { + TEdge* Next = Edge1->NextInAEL; + TEdge* Prev = Edge1->PrevInAEL; + Edge1->NextInAEL = Edge2->NextInAEL; + if (Edge1->NextInAEL) Edge1->NextInAEL->PrevInAEL = Edge1; + Edge1->PrevInAEL = Edge2->PrevInAEL; + if (Edge1->PrevInAEL) Edge1->PrevInAEL->NextInAEL = Edge1; + Edge2->NextInAEL = Next; + if (Edge2->NextInAEL) Edge2->NextInAEL->PrevInAEL = Edge2; + Edge2->PrevInAEL = Prev; + if (Edge2->PrevInAEL) Edge2->PrevInAEL->NextInAEL = Edge2; + } + + if (!Edge1->PrevInAEL) m_ActiveEdges = Edge1; + else if (!Edge2->PrevInAEL) m_ActiveEdges = Edge2; +} +//------------------------------------------------------------------------------ + +void ClipperBase::UpdateEdgeIntoAEL(TEdge *&e) +{ + if (!e->NextInLML) + throw clipperException("UpdateEdgeIntoAEL: invalid call"); + + e->NextInLML->OutIdx = e->OutIdx; + TEdge* AelPrev = e->PrevInAEL; + TEdge* AelNext = e->NextInAEL; + if (AelPrev) AelPrev->NextInAEL = e->NextInLML; + else m_ActiveEdges = e->NextInLML; + if (AelNext) AelNext->PrevInAEL = e->NextInLML; + e->NextInLML->Side = e->Side; + e->NextInLML->WindDelta = e->WindDelta; + e->NextInLML->WindCnt = e->WindCnt; + e->NextInLML->WindCnt2 = e->WindCnt2; + e = e->NextInLML; + e->Curr = e->Bot; + e->PrevInAEL = AelPrev; + e->NextInAEL = AelNext; + if (!IsHorizontal(*e)) InsertScanbeam(e->Top.Y); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::LocalMinimaPending() +{ + return (m_CurrentLM != m_MinimaList.end()); +} + +//------------------------------------------------------------------------------ +// TClipper methods ... +//------------------------------------------------------------------------------ + +Clipper::Clipper(int initOptions) : ClipperBase() //constructor +{ + m_ExecuteLocked = false; + m_UseFullRange = false; + m_ReverseOutput = ((initOptions & ioReverseSolution) != 0); + m_StrictSimple = ((initOptions & ioStrictlySimple) != 0); + m_PreserveCollinear = ((initOptions & ioPreserveCollinear) != 0); + m_HasOpenPaths = false; +#ifdef use_xyz + m_ZFill = 0; +#endif +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::ZFillFunction(ZFillCallback zFillFunc) +{ + m_ZFill = zFillFunc; +} +//------------------------------------------------------------------------------ +#endif + +bool Clipper::Execute(ClipType clipType, Paths &solution, PolyFillType fillType) +{ + return Execute(clipType, solution, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree &polytree, PolyFillType fillType) +{ + return Execute(clipType, polytree, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, Paths &solution, + PolyFillType subjFillType, PolyFillType clipFillType) +{ + if( m_ExecuteLocked ) return false; + if (m_HasOpenPaths) + throw clipperException("Error: PolyTree struct is needed for open path clipping."); + m_ExecuteLocked = true; + solution.resize(0); + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = false; + bool succeeded = ExecuteInternal(); + if (succeeded) BuildResult(solution); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree& polytree, + PolyFillType subjFillType, PolyFillType clipFillType) +{ + if( m_ExecuteLocked ) return false; + m_ExecuteLocked = true; + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = true; + bool succeeded = ExecuteInternal(); + if (succeeded) BuildResult2(polytree); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::FixHoleLinkage(OutRec &outrec) +{ + //skip OutRecs that (a) contain outermost polygons or + //(b) already have the correct owner/child linkage ... + if (!outrec.FirstLeft || + (outrec.IsHole != outrec.FirstLeft->IsHole && + outrec.FirstLeft->Pts)) return; + + OutRec* orfl = outrec.FirstLeft; + while (orfl && ((orfl->IsHole == outrec.IsHole) || !orfl->Pts)) + orfl = orfl->FirstLeft; + outrec.FirstLeft = orfl; +} +//------------------------------------------------------------------------------ + +bool Clipper::ExecuteInternal() +{ + bool succeeded = true; + try { + Reset(); + m_Maxima = MaximaList(); + m_SortedEdges = 0; + + succeeded = true; + cInt botY, topY; + if (!PopScanbeam(botY)) return false; + InsertLocalMinimaIntoAEL(botY); + while (PopScanbeam(topY) || LocalMinimaPending()) + { + ProcessHorizontals(); + ClearGhostJoins(); + if (!ProcessIntersections(topY)) + { + succeeded = false; + break; + } + ProcessEdgesAtTopOfScanbeam(topY); + botY = topY; + InsertLocalMinimaIntoAEL(botY); + } + } + catch(...) + { + succeeded = false; + } + + if (succeeded) + { + //fix orientations ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts || outRec->IsOpen) continue; + if ((outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) + ReversePolyPtLinks(outRec->Pts); + } + + if (!m_Joins.empty()) JoinCommonEdges(); + + //unfortunately FixupOutPolygon() must be done after JoinCommonEdges() + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts) continue; + if (outRec->IsOpen) + FixupOutPolyline(*outRec); + else + FixupOutPolygon(*outRec); + } + + if (m_StrictSimple) DoSimplePolygons(); + } + + ClearJoins(); + ClearGhostJoins(); + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::SetWindingCount(TEdge &edge) +{ + TEdge *e = edge.PrevInAEL; + //find the edge of the same polytype that immediately preceeds 'edge' in AEL + while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0))) e = e->PrevInAEL; + if (!e) + { + if (edge.WindDelta == 0) + { + PolyFillType pft = (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType); + edge.WindCnt = (pft == pftNegative ? -1 : 1); + } + else + edge.WindCnt = edge.WindDelta; + edge.WindCnt2 = 0; + e = m_ActiveEdges; //ie get ready to calc WindCnt2 + } + else if (edge.WindDelta == 0 && m_ClipType != ctUnion) + { + edge.WindCnt = 1; + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; //ie get ready to calc WindCnt2 + } + else if (IsEvenOddFillType(edge)) + { + //EvenOdd filling ... + if (edge.WindDelta == 0) + { + //are we inside a subj polygon ... + bool Inside = true; + TEdge *e2 = e->PrevInAEL; + while (e2) + { + if (e2->PolyTyp == e->PolyTyp && e2->WindDelta != 0) + Inside = !Inside; + e2 = e2->PrevInAEL; + } + edge.WindCnt = (Inside ? 0 : 1); + } + else + { + edge.WindCnt = edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; //ie get ready to calc WindCnt2 + } + else + { + //nonZero, Positive or Negative filling ... + if (e->WindCnt * e->WindDelta < 0) + { + //prev edge is 'decreasing' WindCount (WC) toward zero + //so we're outside the previous polygon ... + if (Abs(e->WindCnt) > 1) + { + //outside prev poly but still inside another. + //when reversing direction of prev poly use the same WC + if (e->WindDelta * edge.WindDelta < 0) edge.WindCnt = e->WindCnt; + //otherwise continue to 'decrease' WC ... + else edge.WindCnt = e->WindCnt + edge.WindDelta; + } + else + //now outside all polys of same polytype so set own WC ... + edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta); + } else + { + //prev edge is 'increasing' WindCount (WC) away from zero + //so we're inside the previous polygon ... + if (edge.WindDelta == 0) + edge.WindCnt = (e->WindCnt < 0 ? e->WindCnt - 1 : e->WindCnt + 1); + //if wind direction is reversing prev then use same WC + else if (e->WindDelta * edge.WindDelta < 0) edge.WindCnt = e->WindCnt; + //otherwise add to WC ... + else edge.WindCnt = e->WindCnt + edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; //ie get ready to calc WindCnt2 + } + + //update WindCnt2 ... + if (IsEvenOddAltFillType(edge)) + { + //EvenOdd filling ... + while (e != &edge) + { + if (e->WindDelta != 0) + edge.WindCnt2 = (edge.WindCnt2 == 0 ? 1 : 0); + e = e->NextInAEL; + } + } else + { + //nonZero, Positive or Negative filling ... + while ( e != &edge ) + { + edge.WindCnt2 += e->WindDelta; + e = e->NextInAEL; + } + } +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddFillType(const TEdge& edge) const +{ + if (edge.PolyTyp == ptSubject) + return m_SubjFillType == pftEvenOdd; else + return m_ClipFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddAltFillType(const TEdge& edge) const +{ + if (edge.PolyTyp == ptSubject) + return m_ClipFillType == pftEvenOdd; else + return m_SubjFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsContributing(const TEdge& edge) const +{ + PolyFillType pft, pft2; + if (edge.PolyTyp == ptSubject) + { + pft = m_SubjFillType; + pft2 = m_ClipFillType; + } else + { + pft = m_ClipFillType; + pft2 = m_SubjFillType; + } + + switch(pft) + { + case pftEvenOdd: + //return false if a subj line has been flagged as inside a subj polygon + if (edge.WindDelta == 0 && edge.WindCnt != 1) return false; + break; + case pftNonZero: + if (Abs(edge.WindCnt) != 1) return false; + break; + case pftPositive: + if (edge.WindCnt != 1) return false; + break; + default: //pftNegative + if (edge.WindCnt != -1) return false; + } + + switch(m_ClipType) + { + case ctIntersection: + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctUnion: + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + break; + case ctDifference: + if (edge.PolyTyp == ptSubject) + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctXor: + if (edge.WindDelta == 0) //XOr always contributing unless open + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + return true; + break; + default: + return true; + } +} +//------------------------------------------------------------------------------ + +OutPt* Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) +{ + OutPt* result; + TEdge *e, *prevE; + if (IsHorizontal(*e2) || ( e1->Dx > e2->Dx )) + { + result = AddOutPt(e1, Pt); + e2->OutIdx = e1->OutIdx; + e1->Side = esLeft; + e2->Side = esRight; + e = e1; + if (e->PrevInAEL == e2) + prevE = e2->PrevInAEL; + else + prevE = e->PrevInAEL; + } else + { + result = AddOutPt(e2, Pt); + e1->OutIdx = e2->OutIdx; + e1->Side = esRight; + e2->Side = esLeft; + e = e2; + if (e->PrevInAEL == e1) + prevE = e1->PrevInAEL; + else + prevE = e->PrevInAEL; + } + + if (prevE && prevE->OutIdx >= 0 && prevE->Top.Y < Pt.Y && e->Top.Y < Pt.Y) + { + cInt xPrev = TopX(*prevE, Pt.Y); + cInt xE = TopX(*e, Pt.Y); + if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) && + SlopesEqual(IntPoint(xPrev, Pt.Y), prevE->Top, IntPoint(xE, Pt.Y), e->Top, m_UseFullRange)) + { + OutPt* outPt = AddOutPt(prevE, Pt); + AddJoin(result, outPt, e->Top); + } + } + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) +{ + AddOutPt( e1, Pt ); + if (e2->WindDelta == 0) AddOutPt(e2, Pt); + if( e1->OutIdx == e2->OutIdx ) + { + e1->OutIdx = Unassigned; + e2->OutIdx = Unassigned; + } + else if (e1->OutIdx < e2->OutIdx) + AppendPolygon(e1, e2); + else + AppendPolygon(e2, e1); +} +//------------------------------------------------------------------------------ + +void Clipper::AddEdgeToSEL(TEdge *edge) +{ + //SEL pointers in PEdge are reused to build a list of horizontal edges. + //However, we don't need to worry about order with horizontal edge processing. + if( !m_SortedEdges ) + { + m_SortedEdges = edge; + edge->PrevInSEL = 0; + edge->NextInSEL = 0; + } + else + { + edge->NextInSEL = m_SortedEdges; + edge->PrevInSEL = 0; + m_SortedEdges->PrevInSEL = edge; + m_SortedEdges = edge; + } +} +//------------------------------------------------------------------------------ + +bool Clipper::PopEdgeFromSEL(TEdge *&edge) +{ + if (!m_SortedEdges) return false; + edge = m_SortedEdges; + DeleteFromSEL(m_SortedEdges); + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::CopyAELToSEL() +{ + TEdge* e = m_ActiveEdges; + m_SortedEdges = e; + while ( e ) + { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::AddJoin(OutPt *op1, OutPt *op2, const IntPoint OffPt) +{ + Join* j = new Join; + j->OutPt1 = op1; + j->OutPt2 = op2; + j->OffPt = OffPt; + m_Joins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearJoins() +{ + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) + delete m_Joins[i]; + m_Joins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearGhostJoins() +{ + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); i++) + delete m_GhostJoins[i]; + m_GhostJoins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::AddGhostJoin(OutPt *op, const IntPoint OffPt) +{ + Join* j = new Join; + j->OutPt1 = op; + j->OutPt2 = 0; + j->OffPt = OffPt; + m_GhostJoins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) +{ + const LocalMinimum *lm; + while (PopLocalMinima(botY, lm)) + { + TEdge* lb = lm->LeftBound; + TEdge* rb = lm->RightBound; + + OutPt *Op1 = 0; + if (!lb) + { + //nb: don't insert LB into either AEL or SEL + InsertEdgeIntoAEL(rb, 0); + SetWindingCount(*rb); + if (IsContributing(*rb)) + Op1 = AddOutPt(rb, rb->Bot); + } + else if (!rb) + { + InsertEdgeIntoAEL(lb, 0); + SetWindingCount(*lb); + if (IsContributing(*lb)) + Op1 = AddOutPt(lb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } + else + { + InsertEdgeIntoAEL(lb, 0); + InsertEdgeIntoAEL(rb, lb); + SetWindingCount( *lb ); + rb->WindCnt = lb->WindCnt; + rb->WindCnt2 = lb->WindCnt2; + if (IsContributing(*lb)) + Op1 = AddLocalMinPoly(lb, rb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } + + if (rb) + { + if (IsHorizontal(*rb)) + { + AddEdgeToSEL(rb); + if (rb->NextInLML) + InsertScanbeam(rb->NextInLML->Top.Y); + } + else InsertScanbeam( rb->Top.Y ); + } + + if (!lb || !rb) continue; + + //if any output polygons share an edge, they'll need joining later ... + if (Op1 && IsHorizontal(*rb) && + m_GhostJoins.size() > 0 && (rb->WindDelta != 0)) + { + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); ++i) + { + Join* jr = m_GhostJoins[i]; + //if the horizontal Rb and a 'ghost' horizontal overlap, then convert + //the 'ghost' join to a real join ready for later ... + if (HorzSegmentsOverlap(jr->OutPt1->Pt.X, jr->OffPt.X, rb->Bot.X, rb->Top.X)) + AddJoin(jr->OutPt1, Op1, jr->OffPt); + } + } + + if (lb->OutIdx >= 0 && lb->PrevInAEL && + lb->PrevInAEL->Curr.X == lb->Bot.X && + lb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top, m_UseFullRange) && + (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) + { + OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot); + AddJoin(Op1, Op2, lb->Top); + } + + if(lb->NextInAEL != rb) + { + + if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr, rb->Top, m_UseFullRange) && + (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) + { + OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot); + AddJoin(Op1, Op2, rb->Top); + } + + TEdge* e = lb->NextInAEL; + if (e) + { + while( e != rb ) + { + //nb: For calculating winding counts etc, IntersectEdges() assumes + //that param1 will be to the Right of param2 ABOVE the intersection ... + IntersectEdges(rb , e , lb->Curr); //order important here + e = e->NextInAEL; + } + } + } + + } +} +//------------------------------------------------------------------------------ + +void Clipper::DeleteFromSEL(TEdge *e) +{ + TEdge* SelPrev = e->PrevInSEL; + TEdge* SelNext = e->NextInSEL; + if( !SelPrev && !SelNext && (e != m_SortedEdges) ) return; //already deleted + if( SelPrev ) SelPrev->NextInSEL = SelNext; + else m_SortedEdges = SelNext; + if( SelNext ) SelNext->PrevInSEL = SelPrev; + e->NextInSEL = 0; + e->PrevInSEL = 0; +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::SetZ(IntPoint& pt, TEdge& e1, TEdge& e2) +{ + if (pt.Z != 0 || !m_ZFill) return; + else if (pt == e1.Bot) pt.Z = e1.Bot.Z; + else if (pt == e1.Top) pt.Z = e1.Top.Z; + else if (pt == e2.Bot) pt.Z = e2.Bot.Z; + else if (pt == e2.Top) pt.Z = e2.Top.Z; + else (*m_ZFill)(e1.Bot, e1.Top, e2.Bot, e2.Top, pt); +} +//------------------------------------------------------------------------------ +#endif + +void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) +{ + bool e1Contributing = ( e1->OutIdx >= 0 ); + bool e2Contributing = ( e2->OutIdx >= 0 ); + +#ifdef use_xyz + SetZ(Pt, *e1, *e2); +#endif + +#ifdef use_lines + //if either edge is on an OPEN path ... + if (e1->WindDelta == 0 || e2->WindDelta == 0) + { + //ignore subject-subject open path intersections UNLESS they + //are both open paths, AND they are both 'contributing maximas' ... + if (e1->WindDelta == 0 && e2->WindDelta == 0) return; + + //if intersecting a subj line with a subj poly ... + else if (e1->PolyTyp == e2->PolyTyp && + e1->WindDelta != e2->WindDelta && m_ClipType == ctUnion) + { + if (e1->WindDelta == 0) + { + if (e2Contributing) + { + AddOutPt(e1, Pt); + if (e1Contributing) e1->OutIdx = Unassigned; + } + } + else + { + if (e1Contributing) + { + AddOutPt(e2, Pt); + if (e2Contributing) e2->OutIdx = Unassigned; + } + } + } + else if (e1->PolyTyp != e2->PolyTyp) + { + //toggle subj open path OutIdx on/off when Abs(clip.WndCnt) == 1 ... + if ((e1->WindDelta == 0) && abs(e2->WindCnt) == 1 && + (m_ClipType != ctUnion || e2->WindCnt2 == 0)) + { + AddOutPt(e1, Pt); + if (e1Contributing) e1->OutIdx = Unassigned; + } + else if ((e2->WindDelta == 0) && (abs(e1->WindCnt) == 1) && + (m_ClipType != ctUnion || e1->WindCnt2 == 0)) + { + AddOutPt(e2, Pt); + if (e2Contributing) e2->OutIdx = Unassigned; + } + } + return; + } +#endif + + //update winding counts... + //assumes that e1 will be to the Right of e2 ABOVE the intersection + if ( e1->PolyTyp == e2->PolyTyp ) + { + if ( IsEvenOddFillType( *e1) ) + { + int oldE1WindCnt = e1->WindCnt; + e1->WindCnt = e2->WindCnt; + e2->WindCnt = oldE1WindCnt; + } else + { + if (e1->WindCnt + e2->WindDelta == 0 ) e1->WindCnt = -e1->WindCnt; + else e1->WindCnt += e2->WindDelta; + if ( e2->WindCnt - e1->WindDelta == 0 ) e2->WindCnt = -e2->WindCnt; + else e2->WindCnt -= e1->WindDelta; + } + } else + { + if (!IsEvenOddFillType(*e2)) e1->WindCnt2 += e2->WindDelta; + else e1->WindCnt2 = ( e1->WindCnt2 == 0 ) ? 1 : 0; + if (!IsEvenOddFillType(*e1)) e2->WindCnt2 -= e1->WindDelta; + else e2->WindCnt2 = ( e2->WindCnt2 == 0 ) ? 1 : 0; + } + + PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2; + if (e1->PolyTyp == ptSubject) + { + e1FillType = m_SubjFillType; + e1FillType2 = m_ClipFillType; + } else + { + e1FillType = m_ClipFillType; + e1FillType2 = m_SubjFillType; + } + if (e2->PolyTyp == ptSubject) + { + e2FillType = m_SubjFillType; + e2FillType2 = m_ClipFillType; + } else + { + e2FillType = m_ClipFillType; + e2FillType2 = m_SubjFillType; + } + + cInt e1Wc, e2Wc; + switch (e1FillType) + { + case pftPositive: e1Wc = e1->WindCnt; break; + case pftNegative: e1Wc = -e1->WindCnt; break; + default: e1Wc = Abs(e1->WindCnt); + } + switch(e2FillType) + { + case pftPositive: e2Wc = e2->WindCnt; break; + case pftNegative: e2Wc = -e2->WindCnt; break; + default: e2Wc = Abs(e2->WindCnt); + } + + if ( e1Contributing && e2Contributing ) + { + if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || + (e1->PolyTyp != e2->PolyTyp && m_ClipType != ctXor) ) + { + AddLocalMaxPoly(e1, e2, Pt); + } + else + { + AddOutPt(e1, Pt); + AddOutPt(e2, Pt); + SwapSides( *e1 , *e2 ); + SwapPolyIndexes( *e1 , *e2 ); + } + } + else if ( e1Contributing ) + { + if (e2Wc == 0 || e2Wc == 1) + { + AddOutPt(e1, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } + else if ( e2Contributing ) + { + if (e1Wc == 0 || e1Wc == 1) + { + AddOutPt(e2, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } + else if ( (e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) + { + //neither edge is currently contributing ... + + cInt e1Wc2, e2Wc2; + switch (e1FillType2) + { + case pftPositive: e1Wc2 = e1->WindCnt2; break; + case pftNegative : e1Wc2 = -e1->WindCnt2; break; + default: e1Wc2 = Abs(e1->WindCnt2); + } + switch (e2FillType2) + { + case pftPositive: e2Wc2 = e2->WindCnt2; break; + case pftNegative: e2Wc2 = -e2->WindCnt2; break; + default: e2Wc2 = Abs(e2->WindCnt2); + } + + if (e1->PolyTyp != e2->PolyTyp) + { + AddLocalMinPoly(e1, e2, Pt); + } + else if (e1Wc == 1 && e2Wc == 1) + switch( m_ClipType ) { + case ctIntersection: + if (e1Wc2 > 0 && e2Wc2 > 0) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctUnion: + if ( e1Wc2 <= 0 && e2Wc2 <= 0 ) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctDifference: + if (((e1->PolyTyp == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1->PolyTyp == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctXor: + AddLocalMinPoly(e1, e2, Pt); + } + else + SwapSides( *e1, *e2 ); + } +} +//------------------------------------------------------------------------------ + +void Clipper::SetHoleState(TEdge *e, OutRec *outrec) +{ + TEdge *e2 = e->PrevInAEL; + TEdge *eTmp = 0; + while (e2) + { + if (e2->OutIdx >= 0 && e2->WindDelta != 0) + { + if (!eTmp) eTmp = e2; + else if (eTmp->OutIdx == e2->OutIdx) eTmp = 0; + } + e2 = e2->PrevInAEL; + } + if (!eTmp) + { + outrec->FirstLeft = 0; + outrec->IsHole = false; + } + else + { + outrec->FirstLeft = m_PolyOuts[eTmp->OutIdx]; + outrec->IsHole = !outrec->FirstLeft->IsHole; + } +} +//------------------------------------------------------------------------------ + +OutRec* GetLowermostRec(OutRec *outRec1, OutRec *outRec2) +{ + //work out which polygon fragment has the correct hole state ... + if (!outRec1->BottomPt) + outRec1->BottomPt = GetBottomPt(outRec1->Pts); + if (!outRec2->BottomPt) + outRec2->BottomPt = GetBottomPt(outRec2->Pts); + OutPt *OutPt1 = outRec1->BottomPt; + OutPt *OutPt2 = outRec2->BottomPt; + if (OutPt1->Pt.Y > OutPt2->Pt.Y) return outRec1; + else if (OutPt1->Pt.Y < OutPt2->Pt.Y) return outRec2; + else if (OutPt1->Pt.X < OutPt2->Pt.X) return outRec1; + else if (OutPt1->Pt.X > OutPt2->Pt.X) return outRec2; + else if (OutPt1->Next == OutPt1) return outRec2; + else if (OutPt2->Next == OutPt2) return outRec1; + else if (FirstIsBottomPt(OutPt1, OutPt2)) return outRec1; + else return outRec2; +} +//------------------------------------------------------------------------------ + +bool OutRec1RightOfOutRec2(OutRec* outRec1, OutRec* outRec2) +{ + do + { + outRec1 = outRec1->FirstLeft; + if (outRec1 == outRec2) return true; + } while (outRec1); + return false; +} +//------------------------------------------------------------------------------ + +OutRec* Clipper::GetOutRec(int Idx) +{ + OutRec* outrec = m_PolyOuts[Idx]; + while (outrec != m_PolyOuts[outrec->Idx]) + outrec = m_PolyOuts[outrec->Idx]; + return outrec; +} +//------------------------------------------------------------------------------ + +void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) +{ + //get the start and ends of both output polygons ... + OutRec *outRec1 = m_PolyOuts[e1->OutIdx]; + OutRec *outRec2 = m_PolyOuts[e2->OutIdx]; + + OutRec *holeStateRec; + if (OutRec1RightOfOutRec2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + //get the start and ends of both output polygons and + //join e2 poly onto e1 poly and delete pointers to e2 ... + + OutPt* p1_lft = outRec1->Pts; + OutPt* p1_rt = p1_lft->Prev; + OutPt* p2_lft = outRec2->Pts; + OutPt* p2_rt = p2_lft->Prev; + + //join e2 poly onto e1 poly and delete pointers to e2 ... + if( e1->Side == esLeft ) + { + if( e2->Side == esLeft ) + { + //z y x a b c + ReversePolyPtLinks(p2_lft); + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + outRec1->Pts = p2_rt; + } else + { + //x y z a b c + p2_rt->Next = p1_lft; + p1_lft->Prev = p2_rt; + p2_lft->Prev = p1_rt; + p1_rt->Next = p2_lft; + outRec1->Pts = p2_lft; + } + } else + { + if( e2->Side == esRight ) + { + //a b c z y x + ReversePolyPtLinks(p2_lft); + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + } else + { + //a b c x y z + p1_rt->Next = p2_lft; + p2_lft->Prev = p1_rt; + p1_lft->Prev = p2_rt; + p2_rt->Next = p1_lft; + } + } + + outRec1->BottomPt = 0; + if (holeStateRec == outRec2) + { + if (outRec2->FirstLeft != outRec1) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec1->IsHole = outRec2->IsHole; + } + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->FirstLeft = outRec1; + + int OKIdx = e1->OutIdx; + int ObsoleteIdx = e2->OutIdx; + + e1->OutIdx = Unassigned; //nb: safe because we only get here via AddLocalMaxPoly + e2->OutIdx = Unassigned; + + TEdge* e = m_ActiveEdges; + while( e ) + { + if( e->OutIdx == ObsoleteIdx ) + { + e->OutIdx = OKIdx; + e->Side = e1->Side; + break; + } + e = e->NextInAEL; + } + + outRec2->Idx = outRec1->Idx; +} +//------------------------------------------------------------------------------ + +OutPt* Clipper::AddOutPt(TEdge *e, const IntPoint &pt) +{ + if( e->OutIdx < 0 ) + { + OutRec *outRec = CreateOutRec(); + outRec->IsOpen = (e->WindDelta == 0); + OutPt* newOp = new OutPt; + outRec->Pts = newOp; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = newOp; + newOp->Prev = newOp; + if (!outRec->IsOpen) + SetHoleState(e, outRec); + e->OutIdx = outRec->Idx; + return newOp; + } else + { + OutRec *outRec = m_PolyOuts[e->OutIdx]; + //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' + OutPt* op = outRec->Pts; + + bool ToFront = (e->Side == esLeft); + if (ToFront && (pt == op->Pt)) return op; + else if (!ToFront && (pt == op->Prev->Pt)) return op->Prev; + + OutPt* newOp = new OutPt; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = op; + newOp->Prev = op->Prev; + newOp->Prev->Next = newOp; + op->Prev = newOp; + if (ToFront) outRec->Pts = newOp; + return newOp; + } +} +//------------------------------------------------------------------------------ + +OutPt* Clipper::GetLastOutPt(TEdge *e) +{ + OutRec *outRec = m_PolyOuts[e->OutIdx]; + if (e->Side == esLeft) + return outRec->Pts; + else + return outRec->Pts->Prev; +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessHorizontals() +{ + TEdge* horzEdge; + while (PopEdgeFromSEL(horzEdge)) + ProcessHorizontal(horzEdge); +} +//------------------------------------------------------------------------------ + +inline bool IsMinima(TEdge *e) +{ + return e && (e->Prev->NextInLML != e) && (e->Next->NextInLML != e); +} +//------------------------------------------------------------------------------ + +inline bool IsMaxima(TEdge *e, const cInt Y) +{ + return e && e->Top.Y == Y && !e->NextInLML; +} +//------------------------------------------------------------------------------ + +inline bool IsIntermediate(TEdge *e, const cInt Y) +{ + return e->Top.Y == Y && e->NextInLML; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPair(TEdge *e) +{ + if ((e->Next->Top == e->Top) && !e->Next->NextInLML) + return e->Next; + else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML) + return e->Prev; + else return 0; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPairEx(TEdge *e) +{ + //as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's horizontal) + TEdge* result = GetMaximaPair(e); + if (result && (result->OutIdx == Skip || + (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) return 0; + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::SwapPositionsInSEL(TEdge *Edge1, TEdge *Edge2) +{ + if( !( Edge1->NextInSEL ) && !( Edge1->PrevInSEL ) ) return; + if( !( Edge2->NextInSEL ) && !( Edge2->PrevInSEL ) ) return; + + if( Edge1->NextInSEL == Edge2 ) + { + TEdge* Next = Edge2->NextInSEL; + if( Next ) Next->PrevInSEL = Edge1; + TEdge* Prev = Edge1->PrevInSEL; + if( Prev ) Prev->NextInSEL = Edge2; + Edge2->PrevInSEL = Prev; + Edge2->NextInSEL = Edge1; + Edge1->PrevInSEL = Edge2; + Edge1->NextInSEL = Next; + } + else if( Edge2->NextInSEL == Edge1 ) + { + TEdge* Next = Edge1->NextInSEL; + if( Next ) Next->PrevInSEL = Edge2; + TEdge* Prev = Edge2->PrevInSEL; + if( Prev ) Prev->NextInSEL = Edge1; + Edge1->PrevInSEL = Prev; + Edge1->NextInSEL = Edge2; + Edge2->PrevInSEL = Edge1; + Edge2->NextInSEL = Next; + } + else + { + TEdge* Next = Edge1->NextInSEL; + TEdge* Prev = Edge1->PrevInSEL; + Edge1->NextInSEL = Edge2->NextInSEL; + if( Edge1->NextInSEL ) Edge1->NextInSEL->PrevInSEL = Edge1; + Edge1->PrevInSEL = Edge2->PrevInSEL; + if( Edge1->PrevInSEL ) Edge1->PrevInSEL->NextInSEL = Edge1; + Edge2->NextInSEL = Next; + if( Edge2->NextInSEL ) Edge2->NextInSEL->PrevInSEL = Edge2; + Edge2->PrevInSEL = Prev; + if( Edge2->PrevInSEL ) Edge2->PrevInSEL->NextInSEL = Edge2; + } + + if( !Edge1->PrevInSEL ) m_SortedEdges = Edge1; + else if( !Edge2->PrevInSEL ) m_SortedEdges = Edge2; +} +//------------------------------------------------------------------------------ + +TEdge* GetNextInAEL(TEdge *e, Direction dir) +{ + return dir == dLeftToRight ? e->NextInAEL : e->PrevInAEL; +} +//------------------------------------------------------------------------------ + +void GetHorzDirection(TEdge& HorzEdge, Direction& Dir, cInt& Left, cInt& Right) +{ + if (HorzEdge.Bot.X < HorzEdge.Top.X) + { + Left = HorzEdge.Bot.X; + Right = HorzEdge.Top.X; + Dir = dLeftToRight; + } else + { + Left = HorzEdge.Top.X; + Right = HorzEdge.Bot.X; + Dir = dRightToLeft; + } +} +//------------------------------------------------------------------------ + +/******************************************************************************* +* Notes: Horizontal edges (HEs) at scanline intersections (ie at the Top or * +* Bottom of a scanbeam) are processed as if layered. The order in which HEs * +* are processed doesn't matter. HEs intersect with other HE Bot.Xs only [#] * +* (or they could intersect with Top.Xs only, ie EITHER Bot.Xs OR Top.Xs), * +* and with other non-horizontal edges [*]. Once these intersections are * +* processed, intermediate HEs then 'promote' the Edge above (NextInLML) into * +* the AEL. These 'promoted' edges may in turn intersect [%] with other HEs. * +*******************************************************************************/ + +void Clipper::ProcessHorizontal(TEdge *horzEdge) +{ + Direction dir; + cInt horzLeft, horzRight; + bool IsOpen = (horzEdge->WindDelta == 0); + + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + TEdge* eLastHorz = horzEdge, *eMaxPair = 0; + while (eLastHorz->NextInLML && IsHorizontal(*eLastHorz->NextInLML)) + eLastHorz = eLastHorz->NextInLML; + if (!eLastHorz->NextInLML) + eMaxPair = GetMaximaPair(eLastHorz); + + MaximaList::const_iterator maxIt; + MaximaList::const_reverse_iterator maxRit; + if (m_Maxima.size() > 0) + { + //get the first maxima in range (X) ... + if (dir == dLeftToRight) + { + maxIt = m_Maxima.begin(); + while (maxIt != m_Maxima.end() && *maxIt <= horzEdge->Bot.X) maxIt++; + if (maxIt != m_Maxima.end() && *maxIt >= eLastHorz->Top.X) + maxIt = m_Maxima.end(); + } + else + { + maxRit = m_Maxima.rbegin(); + while (maxRit != m_Maxima.rend() && *maxRit > horzEdge->Bot.X) maxRit++; + if (maxRit != m_Maxima.rend() && *maxRit <= eLastHorz->Top.X) + maxRit = m_Maxima.rend(); + } + } + + OutPt* op1 = 0; + + for (;;) //loop through consec. horizontal edges + { + + bool IsLastHorz = (horzEdge == eLastHorz); + TEdge* e = GetNextInAEL(horzEdge, dir); + while(e) + { + + //this code block inserts extra coords into horizontal edges (in output + //polygons) whereever maxima touch these horizontal edges. This helps + //'simplifying' polygons (ie if the Simplify property is set). + if (m_Maxima.size() > 0) + { + if (dir == dLeftToRight) + { + while (maxIt != m_Maxima.end() && *maxIt < e->Curr.X) + { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxIt, horzEdge->Bot.Y)); + maxIt++; + } + } + else + { + while (maxRit != m_Maxima.rend() && *maxRit > e->Curr.X) + { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxRit, horzEdge->Bot.Y)); + maxRit++; + } + } + }; + + if ((dir == dLeftToRight && e->Curr.X > horzRight) || + (dir == dRightToLeft && e->Curr.X < horzLeft)) break; + + //Also break if we've got to the end of an intermediate horizontal edge ... + //nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. + if (e->Curr.X == horzEdge->Top.X && horzEdge->NextInLML && + e->Dx < horzEdge->NextInLML->Dx) break; + + if (horzEdge->OutIdx >= 0 && !IsOpen) //note: may be done multiple times + { +#ifdef use_xyz + if (dir == dLeftToRight) SetZ(e->Curr, *horzEdge, *e); + else SetZ(e->Curr, *e, *horzEdge); +#endif + op1 = AddOutPt(horzEdge, e->Curr); + TEdge* eNextHorz = m_SortedEdges; + while (eNextHorz) + { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, + horzEdge->Top.X, eNextHorz->Bot.X, eNextHorz->Top.X)) + { + OutPt* op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Bot); + } + + //OK, so far we're still in range of the horizontal Edge but make sure + //we're at the last of consec. horizontals when matching with eMaxPair + if(e == eMaxPair && IsLastHorz) + { + if (horzEdge->OutIdx >= 0) + AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge->Top); + DeleteFromAEL(horzEdge); + DeleteFromAEL(eMaxPair); + return; + } + + if(dir == dLeftToRight) + { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges(horzEdge, e, Pt); + } + else + { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges( e, horzEdge, Pt); + } + TEdge* eNext = GetNextInAEL(e, dir); + SwapPositionsInAEL( horzEdge, e ); + e = eNext; + } //end while(e) + + //Break out of loop if HorzEdge.NextInLML is not also horizontal ... + if (!horzEdge->NextInLML || !IsHorizontal(*horzEdge->NextInLML)) break; + + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->OutIdx >= 0) AddOutPt(horzEdge, horzEdge->Bot); + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + } //end for (;;) + + if (horzEdge->OutIdx >= 0 && !op1) + { + op1 = GetLastOutPt(horzEdge); + TEdge* eNextHorz = m_SortedEdges; + while (eNextHorz) + { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, + horzEdge->Top.X, eNextHorz->Bot.X, eNextHorz->Top.X)) + { + OutPt* op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Top); + } + + if (horzEdge->NextInLML) + { + if(horzEdge->OutIdx >= 0) + { + op1 = AddOutPt( horzEdge, horzEdge->Top); + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->WindDelta == 0) return; + //nb: HorzEdge is no longer horizontal here + TEdge* ePrev = horzEdge->PrevInAEL; + TEdge* eNext = horzEdge->NextInAEL; + if (ePrev && ePrev->Curr.X == horzEdge->Bot.X && + ePrev->Curr.Y == horzEdge->Bot.Y && ePrev->WindDelta != 0 && + (ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(*horzEdge, *ePrev, m_UseFullRange))) + { + OutPt* op2 = AddOutPt(ePrev, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } + else if (eNext && eNext->Curr.X == horzEdge->Bot.X && + eNext->Curr.Y == horzEdge->Bot.Y && eNext->WindDelta != 0 && + eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(*horzEdge, *eNext, m_UseFullRange)) + { + OutPt* op2 = AddOutPt(eNext, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } + } + else + UpdateEdgeIntoAEL(horzEdge); + } + else + { + if (horzEdge->OutIdx >= 0) AddOutPt(horzEdge, horzEdge->Top); + DeleteFromAEL(horzEdge); + } +} +//------------------------------------------------------------------------------ + +bool Clipper::ProcessIntersections(const cInt topY) +{ + if( !m_ActiveEdges ) return true; + try { + BuildIntersectList(topY); + size_t IlSize = m_IntersectList.size(); + if (IlSize == 0) return true; + if (IlSize == 1 || FixupIntersectionOrder()) ProcessIntersectList(); + else return false; + } + catch(...) + { + m_SortedEdges = 0; + DisposeIntersectNodes(); + throw clipperException("ProcessIntersections error"); + } + m_SortedEdges = 0; + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeIntersectNodes() +{ + for (size_t i = 0; i < m_IntersectList.size(); ++i ) + delete m_IntersectList[i]; + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +void Clipper::BuildIntersectList(const cInt topY) +{ + if ( !m_ActiveEdges ) return; + + //prepare for sorting ... + TEdge* e = m_ActiveEdges; + m_SortedEdges = e; + while( e ) + { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e->Curr.X = TopX( *e, topY ); + e = e->NextInAEL; + } + + //bubblesort ... + bool isModified; + do + { + isModified = false; + e = m_SortedEdges; + while( e->NextInSEL ) + { + TEdge *eNext = e->NextInSEL; + IntPoint Pt; + if(e->Curr.X > eNext->Curr.X) + { + IntersectPoint(*e, *eNext, Pt); + if (Pt.Y < topY) Pt = IntPoint(TopX(*e, topY), topY); + IntersectNode * newNode = new IntersectNode; + newNode->Edge1 = e; + newNode->Edge2 = eNext; + newNode->Pt = Pt; + m_IntersectList.push_back(newNode); + + SwapPositionsInSEL(e, eNext); + isModified = true; + } + else + e = eNext; + } + if( e->PrevInSEL ) e->PrevInSEL->NextInSEL = 0; + else break; + } + while ( isModified ); + m_SortedEdges = 0; //important +} +//------------------------------------------------------------------------------ + + +void Clipper::ProcessIntersectList() +{ + for (size_t i = 0; i < m_IntersectList.size(); ++i) + { + IntersectNode* iNode = m_IntersectList[i]; + { + IntersectEdges( iNode->Edge1, iNode->Edge2, iNode->Pt); + SwapPositionsInAEL( iNode->Edge1 , iNode->Edge2 ); + } + delete iNode; + } + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +bool IntersectListSort(IntersectNode* node1, IntersectNode* node2) +{ + return node2->Pt.Y < node1->Pt.Y; +} +//------------------------------------------------------------------------------ + +inline bool EdgesAdjacent(const IntersectNode &inode) +{ + return (inode.Edge1->NextInSEL == inode.Edge2) || + (inode.Edge1->PrevInSEL == inode.Edge2); +} +//------------------------------------------------------------------------------ + +bool Clipper::FixupIntersectionOrder() +{ + //pre-condition: intersections are sorted Bottom-most first. + //Now it's crucial that intersections are made only between adjacent edges, + //so to ensure this the order of intersections may need adjusting ... + CopyAELToSEL(); + std::sort(m_IntersectList.begin(), m_IntersectList.end(), IntersectListSort); + size_t cnt = m_IntersectList.size(); + for (size_t i = 0; i < cnt; ++i) + { + if (!EdgesAdjacent(*m_IntersectList[i])) + { + size_t j = i + 1; + while (j < cnt && !EdgesAdjacent(*m_IntersectList[j])) j++; + if (j == cnt) return false; + std::swap(m_IntersectList[i], m_IntersectList[j]); + } + SwapPositionsInSEL(m_IntersectList[i]->Edge1, m_IntersectList[i]->Edge2); + } + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DoMaxima(TEdge *e) +{ + TEdge* eMaxPair = GetMaximaPairEx(e); + if (!eMaxPair) + { + if (e->OutIdx >= 0) + AddOutPt(e, e->Top); + DeleteFromAEL(e); + return; + } + + TEdge* eNext = e->NextInAEL; + while(eNext && eNext != eMaxPair) + { + IntersectEdges(e, eNext, e->Top); + SwapPositionsInAEL(e, eNext); + eNext = e->NextInAEL; + } + + if(e->OutIdx == Unassigned && eMaxPair->OutIdx == Unassigned) + { + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } + else if( e->OutIdx >= 0 && eMaxPair->OutIdx >= 0 ) + { + if (e->OutIdx >= 0) AddLocalMaxPoly(e, eMaxPair, e->Top); + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } +#ifdef use_lines + else if (e->WindDelta == 0) + { + if (e->OutIdx >= 0) + { + AddOutPt(e, e->Top); + e->OutIdx = Unassigned; + } + DeleteFromAEL(e); + + if (eMaxPair->OutIdx >= 0) + { + AddOutPt(eMaxPair, e->Top); + eMaxPair->OutIdx = Unassigned; + } + DeleteFromAEL(eMaxPair); + } +#endif + else throw clipperException("DoMaxima error"); +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) +{ + TEdge* e = m_ActiveEdges; + while( e ) + { + //1. process maxima, treating them as if they're 'bent' horizontal edges, + // but exclude maxima with horizontal edges. nb: e can't be a horizontal. + bool IsMaximaEdge = IsMaxima(e, topY); + + if(IsMaximaEdge) + { + TEdge* eMaxPair = GetMaximaPairEx(e); + IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair)); + } + + if(IsMaximaEdge) + { + if (m_StrictSimple) m_Maxima.push_back(e->Top.X); + TEdge* ePrev = e->PrevInAEL; + DoMaxima(e); + if( !ePrev ) e = m_ActiveEdges; + else e = ePrev->NextInAEL; + } + else + { + //2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... + if (IsIntermediate(e, topY) && IsHorizontal(*e->NextInLML)) + { + UpdateEdgeIntoAEL(e); + if (e->OutIdx >= 0) + AddOutPt(e, e->Bot); + AddEdgeToSEL(e); + } + else + { + e->Curr.X = TopX( *e, topY ); + e->Curr.Y = topY; +#ifdef use_xyz + e->Curr.Z = topY == e->Top.Y ? e->Top.Z : (topY == e->Bot.Y ? e->Bot.Z : 0); +#endif + } + + //When StrictlySimple and 'e' is being touched by another edge, then + //make sure both edges have a vertex here ... + if (m_StrictSimple) + { + TEdge* ePrev = e->PrevInAEL; + if ((e->OutIdx >= 0) && (e->WindDelta != 0) && ePrev && (ePrev->OutIdx >= 0) && + (ePrev->Curr.X == e->Curr.X) && (ePrev->WindDelta != 0)) + { + IntPoint pt = e->Curr; +#ifdef use_xyz + SetZ(pt, *ePrev, *e); +#endif + OutPt* op = AddOutPt(ePrev, pt); + OutPt* op2 = AddOutPt(e, pt); + AddJoin(op, op2, pt); //StrictlySimple (type-3) join + } + } + + e = e->NextInAEL; + } + } + + //3. Process horizontals at the Top of the scanbeam ... + m_Maxima.sort(); + ProcessHorizontals(); + m_Maxima.clear(); + + //4. Promote intermediate vertices ... + e = m_ActiveEdges; + while(e) + { + if(IsIntermediate(e, topY)) + { + OutPt* op = 0; + if( e->OutIdx >= 0 ) + op = AddOutPt(e, e->Top); + UpdateEdgeIntoAEL(e); + + //if output polygons share an edge, they'll need joining later ... + TEdge* ePrev = e->PrevInAEL; + TEdge* eNext = e->NextInAEL; + if (ePrev && ePrev->Curr.X == e->Bot.X && + ePrev->Curr.Y == e->Bot.Y && op && + ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top, m_UseFullRange) && + (e->WindDelta != 0) && (ePrev->WindDelta != 0)) + { + OutPt* op2 = AddOutPt(ePrev, e->Bot); + AddJoin(op, op2, e->Top); + } + else if (eNext && eNext->Curr.X == e->Bot.X && + eNext->Curr.Y == e->Bot.Y && op && + eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top, m_UseFullRange) && + (e->WindDelta != 0) && (eNext->WindDelta != 0)) + { + OutPt* op2 = AddOutPt(eNext, e->Bot); + AddJoin(op, op2, e->Top); + } + } + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolyline(OutRec &outrec) +{ + OutPt *pp = outrec.Pts; + OutPt *lastPP = pp->Prev; + while (pp != lastPP) + { + pp = pp->Next; + if (pp->Pt == pp->Prev->Pt) + { + if (pp == lastPP) lastPP = pp->Prev; + OutPt *tmpPP = pp->Prev; + tmpPP->Next = pp->Next; + pp->Next->Prev = tmpPP; + delete pp; + pp = tmpPP; + } + } + + if (pp == pp->Prev) + { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolygon(OutRec &outrec) +{ + //FixupOutPolygon() - removes duplicate points and simplifies consecutive + //parallel edges by removing the middle vertex. + OutPt *lastOK = 0; + outrec.BottomPt = 0; + OutPt *pp = outrec.Pts; + bool preserveCol = m_PreserveCollinear || m_StrictSimple; + + for (;;) + { + if (pp->Prev == pp || pp->Prev == pp->Next) + { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } + + //test for duplicate points and collinear edges ... + if ((pp->Pt == pp->Next->Pt) || (pp->Pt == pp->Prev->Pt) || + (SlopesEqual(pp->Prev->Pt, pp->Pt, pp->Next->Pt, m_UseFullRange) && + (!preserveCol || !Pt2IsBetweenPt1AndPt3(pp->Prev->Pt, pp->Pt, pp->Next->Pt)))) + { + lastOK = 0; + OutPt *tmp = pp; + pp->Prev->Next = pp->Next; + pp->Next->Prev = pp->Prev; + pp = pp->Prev; + delete tmp; + } + else if (pp == lastOK) break; + else + { + if (!lastOK) lastOK = pp; + pp = pp->Next; + } + } + outrec.Pts = pp; +} +//------------------------------------------------------------------------------ + +int PointCount(OutPt *Pts) +{ + if (!Pts) return 0; + int result = 0; + OutPt* p = Pts; + do + { + result++; + p = p->Next; + } + while (p != Pts); + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult(Paths &polys) +{ + polys.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + if (!m_PolyOuts[i]->Pts) continue; + Path pg; + OutPt* p = m_PolyOuts[i]->Pts->Prev; + int cnt = PointCount(p); + if (cnt < 2) continue; + pg.reserve(cnt); + for (int i = 0; i < cnt; ++i) + { + pg.push_back(p->Pt); + p = p->Prev; + } + polys.push_back(pg); + } +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult2(PolyTree& polytree) +{ + polytree.Clear(); + polytree.AllNodes.reserve(m_PolyOuts.size()); + //add each output polygon/contour to polytree ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) + { + OutRec* outRec = m_PolyOuts[i]; + int cnt = PointCount(outRec->Pts); + if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3)) continue; + FixHoleLinkage(*outRec); + PolyNode* pn = new PolyNode(); + //nb: polytree takes ownership of all the PolyNodes + polytree.AllNodes.push_back(pn); + outRec->PolyNd = pn; + pn->Parent = 0; + pn->Index = 0; + pn->Contour.reserve(cnt); + OutPt *op = outRec->Pts->Prev; + for (int j = 0; j < cnt; j++) + { + pn->Contour.push_back(op->Pt); + op = op->Prev; + } + } + + //fixup PolyNode links etc ... + polytree.Childs.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) + { + OutRec* outRec = m_PolyOuts[i]; + if (!outRec->PolyNd) continue; + if (outRec->IsOpen) + { + outRec->PolyNd->m_IsOpen = true; + polytree.AddChild(*outRec->PolyNd); + } + else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd) + outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd); + else + polytree.AddChild(*outRec->PolyNd); + } +} +//------------------------------------------------------------------------------ + +void SwapIntersectNodes(IntersectNode &int1, IntersectNode &int2) +{ + //just swap the contents (because fIntersectNodes is a single-linked-list) + IntersectNode inode = int1; //gets a copy of Int1 + int1.Edge1 = int2.Edge1; + int1.Edge2 = int2.Edge2; + int1.Pt = int2.Pt; + int2.Edge1 = inode.Edge1; + int2.Edge2 = inode.Edge2; + int2.Pt = inode.Pt; +} +//------------------------------------------------------------------------------ + +inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) +{ + if (e2.Curr.X == e1.Curr.X) + { + if (e2.Top.Y > e1.Top.Y) + return e2.Top.X < TopX(e1, e2.Top.Y); + else return e1.Top.X > TopX(e2, e1.Top.Y); + } + else return e2.Curr.X < e1.Curr.X; +} +//------------------------------------------------------------------------------ + +bool GetOverlap(const cInt a1, const cInt a2, const cInt b1, const cInt b2, + cInt& Left, cInt& Right) +{ + if (a1 < a2) + { + if (b1 < b2) {Left = std::max(a1,b1); Right = std::min(a2,b2);} + else {Left = std::max(a1,b2); Right = std::min(a2,b1);} + } + else + { + if (b1 < b2) {Left = std::max(a2,b1); Right = std::min(a1,b2);} + else {Left = std::max(a2,b2); Right = std::min(a1,b1);} + } + return Left < Right; +} +//------------------------------------------------------------------------------ + +inline void UpdateOutPtIdxs(OutRec& outrec) +{ + OutPt* op = outrec.Pts; + do + { + op->Idx = outrec.Idx; + op = op->Prev; + } + while(op != outrec.Pts); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge) +{ + if(!m_ActiveEdges) + { + edge->PrevInAEL = 0; + edge->NextInAEL = 0; + m_ActiveEdges = edge; + } + else if(!startEdge && E2InsertsBeforeE1(*m_ActiveEdges, *edge)) + { + edge->PrevInAEL = 0; + edge->NextInAEL = m_ActiveEdges; + m_ActiveEdges->PrevInAEL = edge; + m_ActiveEdges = edge; + } + else + { + if(!startEdge) startEdge = m_ActiveEdges; + while(startEdge->NextInAEL && + !E2InsertsBeforeE1(*startEdge->NextInAEL , *edge)) + startEdge = startEdge->NextInAEL; + edge->NextInAEL = startEdge->NextInAEL; + if(startEdge->NextInAEL) startEdge->NextInAEL->PrevInAEL = edge; + edge->PrevInAEL = startEdge; + startEdge->NextInAEL = edge; + } +} +//---------------------------------------------------------------------- + +OutPt* DupOutPt(OutPt* outPt, bool InsertAfter) +{ + OutPt* result = new OutPt; + result->Pt = outPt->Pt; + result->Idx = outPt->Idx; + if (InsertAfter) + { + result->Next = outPt->Next; + result->Prev = outPt; + outPt->Next->Prev = result; + outPt->Next = result; + } + else + { + result->Prev = outPt->Prev; + result->Next = outPt; + outPt->Prev->Next = result; + outPt->Prev = result; + } + return result; +} +//------------------------------------------------------------------------------ + +bool JoinHorz(OutPt* op1, OutPt* op1b, OutPt* op2, OutPt* op2b, + const IntPoint Pt, bool DiscardLeft) +{ + Direction Dir1 = (op1->Pt.X > op1b->Pt.X ? dRightToLeft : dLeftToRight); + Direction Dir2 = (op2->Pt.X > op2b->Pt.X ? dRightToLeft : dLeftToRight); + if (Dir1 == Dir2) return false; + + //When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we + //want Op1b to be on the Right. (And likewise with Op2 and Op2b.) + //So, to facilitate this while inserting Op1b and Op2b ... + //when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, + //otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) + if (Dir1 == dLeftToRight) + { + while (op1->Next->Pt.X <= Pt.X && + op1->Next->Pt.X >= op1->Pt.X && op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (DiscardLeft && (op1->Pt.X != Pt.X)) op1 = op1->Next; + op1b = DupOutPt(op1, !DiscardLeft); + if (op1b->Pt != Pt) + { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, !DiscardLeft); + } + } + else + { + while (op1->Next->Pt.X >= Pt.X && + op1->Next->Pt.X <= op1->Pt.X && op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (!DiscardLeft && (op1->Pt.X != Pt.X)) op1 = op1->Next; + op1b = DupOutPt(op1, DiscardLeft); + if (op1b->Pt != Pt) + { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, DiscardLeft); + } + } + + if (Dir2 == dLeftToRight) + { + while (op2->Next->Pt.X <= Pt.X && + op2->Next->Pt.X >= op2->Pt.X && op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (DiscardLeft && (op2->Pt.X != Pt.X)) op2 = op2->Next; + op2b = DupOutPt(op2, !DiscardLeft); + if (op2b->Pt != Pt) + { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, !DiscardLeft); + }; + } else + { + while (op2->Next->Pt.X >= Pt.X && + op2->Next->Pt.X <= op2->Pt.X && op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (!DiscardLeft && (op2->Pt.X != Pt.X)) op2 = op2->Next; + op2b = DupOutPt(op2, DiscardLeft); + if (op2b->Pt != Pt) + { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, DiscardLeft); + }; + }; + + if ((Dir1 == dLeftToRight) == DiscardLeft) + { + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + } + else + { + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + } + return true; +} +//------------------------------------------------------------------------------ + +bool Clipper::JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2) +{ + OutPt *op1 = j->OutPt1, *op1b; + OutPt *op2 = j->OutPt2, *op2b; + + //There are 3 kinds of joins for output polygons ... + //1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere + //along (horizontal) collinear edges (& Join.OffPt is on the same horizontal). + //2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same + //location at the Bottom of the overlapping segment (& Join.OffPt is above). + //3. StrictSimple joins where edges touch but are not collinear and where + //Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. + bool isHorizontal = (j->OutPt1->Pt.Y == j->OffPt.Y); + + if (isHorizontal && (j->OffPt == j->OutPt1->Pt) && + (j->OffPt == j->OutPt2->Pt)) + { + //Strictly Simple join ... + if (outRec1 != outRec2) return false; + op1b = j->OutPt1->Next; + while (op1b != op1 && (op1b->Pt == j->OffPt)) + op1b = op1b->Next; + bool reverse1 = (op1b->Pt.Y > j->OffPt.Y); + op2b = j->OutPt2->Next; + while (op2b != op2 && (op2b->Pt == j->OffPt)) + op2b = op2b->Next; + bool reverse2 = (op2b->Pt.Y > j->OffPt.Y); + if (reverse1 == reverse2) return false; + if (reverse1) + { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else + { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } + else if (isHorizontal) + { + //treat horizontal joins differently to non-horizontal joins since with + //them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt + //may be anywhere along the horizontal edge. + op1b = op1; + while (op1->Prev->Pt.Y == op1->Pt.Y && op1->Prev != op1b && op1->Prev != op2) + op1 = op1->Prev; + while (op1b->Next->Pt.Y == op1b->Pt.Y && op1b->Next != op1 && op1b->Next != op2) + op1b = op1b->Next; + if (op1b->Next == op1 || op1b->Next == op2) return false; //a flat 'polygon' + + op2b = op2; + while (op2->Prev->Pt.Y == op2->Pt.Y && op2->Prev != op2b && op2->Prev != op1b) + op2 = op2->Prev; + while (op2b->Next->Pt.Y == op2b->Pt.Y && op2b->Next != op2 && op2b->Next != op1) + op2b = op2b->Next; + if (op2b->Next == op2 || op2b->Next == op1) return false; //a flat 'polygon' + + cInt Left, Right; + //Op1 --> Op1b & Op2 --> Op2b are the extremites of the horizontal edges + if (!GetOverlap(op1->Pt.X, op1b->Pt.X, op2->Pt.X, op2b->Pt.X, Left, Right)) + return false; + + //DiscardLeftSide: when overlapping edges are joined, a spike will created + //which needs to be cleaned up. However, we don't want Op1 or Op2 caught up + //on the discard Side as either may still be needed for other joins ... + IntPoint Pt; + bool DiscardLeftSide; + if (op1->Pt.X >= Left && op1->Pt.X <= Right) + { + Pt = op1->Pt; DiscardLeftSide = (op1->Pt.X > op1b->Pt.X); + } + else if (op2->Pt.X >= Left&& op2->Pt.X <= Right) + { + Pt = op2->Pt; DiscardLeftSide = (op2->Pt.X > op2b->Pt.X); + } + else if (op1b->Pt.X >= Left && op1b->Pt.X <= Right) + { + Pt = op1b->Pt; DiscardLeftSide = op1b->Pt.X > op1->Pt.X; + } + else + { + Pt = op2b->Pt; DiscardLeftSide = (op2b->Pt.X > op2->Pt.X); + } + j->OutPt1 = op1; j->OutPt2 = op2; + return JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); + } else + { + //nb: For non-horizontal joins ... + // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y + // 2. Jr.OutPt1.Pt > Jr.OffPt.Y + + //make sure the polygons are correctly oriented ... + op1b = op1->Next; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) op1b = op1b->Next; + bool Reverse1 = ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse1) + { + op1b = op1->Prev; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) op1b = op1b->Prev; + if ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)) return false; + }; + op2b = op2->Next; + while ((op2b->Pt == op2->Pt) && (op2b != op2))op2b = op2b->Next; + bool Reverse2 = ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse2) + { + op2b = op2->Prev; + while ((op2b->Pt == op2->Pt) && (op2b != op2)) op2b = op2b->Prev; + if ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)) return false; + } + + if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || + ((outRec1 == outRec2) && (Reverse1 == Reverse2))) return false; + + if (Reverse1) + { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else + { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } +} +//---------------------------------------------------------------------- + +static OutRec* ParseFirstLeft(OutRec* FirstLeft) +{ + while (FirstLeft && !FirstLeft->Pts) + FirstLeft = FirstLeft->FirstLeft; + return FirstLeft; +} +//------------------------------------------------------------------------------ + +void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) +{ + //tests if NewOutRec contains the polygon before reassigning FirstLeft + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec* outRec = m_PolyOuts[i]; + OutRec* firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) + { + if (Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts)) + outRec->FirstLeft = NewOutRec; + } + } +} +//---------------------------------------------------------------------- + +void Clipper::FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec) +{ + //A polygon has split into two such that one is now the inner of the other. + //It's possible that these polygons now wrap around other polygons, so check + //every polygon that's also contained by OuterOutRec's FirstLeft container + //(including 0) to see if they've become inner to the new inner polygon ... + OutRec* orfl = OuterOutRec->FirstLeft; + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec* outRec = m_PolyOuts[i]; + + if (!outRec->Pts || outRec == OuterOutRec || outRec == InnerOutRec) + continue; + OutRec* firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (firstLeft != orfl && firstLeft != InnerOutRec && firstLeft != OuterOutRec) + continue; + if (Poly2ContainsPoly1(outRec->Pts, InnerOutRec->Pts)) + outRec->FirstLeft = InnerOutRec; + else if (Poly2ContainsPoly1(outRec->Pts, OuterOutRec->Pts)) + outRec->FirstLeft = OuterOutRec; + else if (outRec->FirstLeft == InnerOutRec || outRec->FirstLeft == OuterOutRec) + outRec->FirstLeft = orfl; + } +} +//---------------------------------------------------------------------- +void Clipper::FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec) +{ + //reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec* outRec = m_PolyOuts[i]; + OutRec* firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) + outRec->FirstLeft = NewOutRec; + } +} +//---------------------------------------------------------------------- + +void Clipper::JoinCommonEdges() +{ + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) + { + Join* join = m_Joins[i]; + + OutRec *outRec1 = GetOutRec(join->OutPt1->Idx); + OutRec *outRec2 = GetOutRec(join->OutPt2->Idx); + + if (!outRec1->Pts || !outRec2->Pts) continue; + if (outRec1->IsOpen || outRec2->IsOpen) continue; + + //get the polygon fragment with the correct hole state (FirstLeft) + //before calling JoinPoints() ... + OutRec *holeStateRec; + if (outRec1 == outRec2) holeStateRec = outRec1; + else if (OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1; + else holeStateRec = GetLowermostRec(outRec1, outRec2); + + if (!JoinPoints(join, outRec1, outRec2)) continue; + + if (outRec1 == outRec2) + { + //instead of joining two polygons, we've just created a new one by + //splitting one polygon into two. + outRec1->Pts = join->OutPt1; + outRec1->BottomPt = 0; + outRec2 = CreateOutRec(); + outRec2->Pts = join->OutPt2; + + //update all OutRec2.Pts Idx's ... + UpdateOutPtIdxs(*outRec2); + + if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) + { + //outRec1 contains outRec2 ... + outRec2->IsHole = !outRec1->IsHole; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) FixupFirstLefts2(outRec2, outRec1); + + if ((outRec2->IsHole ^ m_ReverseOutput) == (Area(*outRec2) > 0)) + ReversePolyPtLinks(outRec2->Pts); + + } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) + { + //outRec2 contains outRec1 ... + outRec2->IsHole = outRec1->IsHole; + outRec1->IsHole = !outRec2->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + outRec1->FirstLeft = outRec2; + + if (m_UsingPolyTree) FixupFirstLefts2(outRec1, outRec2); + + if ((outRec1->IsHole ^ m_ReverseOutput) == (Area(*outRec1) > 0)) + ReversePolyPtLinks(outRec1->Pts); + } + else + { + //the 2 polygons are completely separate ... + outRec2->IsHole = outRec1->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + + //fixup FirstLeft pointers that may need reassigning to OutRec2 + if (m_UsingPolyTree) FixupFirstLefts1(outRec1, outRec2); + } + + } else + { + //joined 2 polygons together ... + + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->Idx = outRec1->Idx; + + outRec1->IsHole = holeStateRec->IsHole; + if (holeStateRec == outRec2) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) FixupFirstLefts3(outRec2, outRec1); + } + } +} + +//------------------------------------------------------------------------------ +// ClipperOffset support functions ... +//------------------------------------------------------------------------------ + +DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) +{ + if(pt2.X == pt1.X && pt2.Y == pt1.Y) + return DoublePoint(0, 0); + + double Dx = (double)(pt2.X - pt1.X); + double dy = (double)(pt2.Y - pt1.Y); + double f = 1 *1.0/ std::sqrt( Dx*Dx + dy*dy ); + Dx *= f; + dy *= f; + return DoublePoint(dy, -Dx); +} + +//------------------------------------------------------------------------------ +// ClipperOffset class +//------------------------------------------------------------------------------ + +ClipperOffset::ClipperOffset(double miterLimit, double arcTolerance) +{ + this->MiterLimit = miterLimit; + this->ArcTolerance = arcTolerance; + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +ClipperOffset::~ClipperOffset() +{ + Clear(); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Clear() +{ + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) + delete m_polyNodes.Childs[i]; + m_polyNodes.Childs.clear(); + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPath(const Path& path, JoinType joinType, EndType endType) +{ + int highI = (int)path.size() - 1; + if (highI < 0) return; + PolyNode* newNode = new PolyNode(); + newNode->m_jointype = joinType; + newNode->m_endtype = endType; + + //strip duplicate points from path and also get index to the lowest point ... + if (endType == etClosedLine || endType == etClosedPolygon) + while (highI > 0 && path[0] == path[highI]) highI--; + newNode->Contour.reserve(highI + 1); + newNode->Contour.push_back(path[0]); + int j = 0, k = 0; + for (int i = 1; i <= highI; i++) + if (newNode->Contour[j] != path[i]) + { + j++; + newNode->Contour.push_back(path[i]); + if (path[i].Y > newNode->Contour[k].Y || + (path[i].Y == newNode->Contour[k].Y && + path[i].X < newNode->Contour[k].X)) k = j; + } + if (endType == etClosedPolygon && j < 2) + { + delete newNode; + return; + } + m_polyNodes.AddChild(*newNode); + + //if this path's lowest pt is lower than all the others then update m_lowest + if (endType != etClosedPolygon) return; + if (m_lowest.X < 0) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + else + { + IntPoint ip = m_polyNodes.Childs[(int)m_lowest.X]->Contour[(int)m_lowest.Y]; + if (newNode->Contour[k].Y > ip.Y || + (newNode->Contour[k].Y == ip.Y && + newNode->Contour[k].X < ip.X)) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPaths(const Paths& paths, JoinType joinType, EndType endType) +{ + for (Paths::size_type i = 0; i < paths.size(); ++i) + AddPath(paths[i], joinType, endType); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::FixOrientations() +{ + //fixup orientations of all closed paths if the orientation of the + //closed path with the lowermost vertex is wrong ... + if (m_lowest.X >= 0 && + !Orientation(m_polyNodes.Childs[(int)m_lowest.X]->Contour)) + { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) + { + PolyNode& node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon || + (node.m_endtype == etClosedLine && Orientation(node.Contour))) + ReversePath(node.Contour); + } + } else + { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) + { + PolyNode& node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedLine && !Orientation(node.Contour)) + ReversePath(node.Contour); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(Paths& solution, double delta) +{ + solution.clear(); + FixOrientations(); + DoOffset(delta); + + //now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) + { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } + else + { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + if (solution.size() > 0) solution.erase(solution.begin()); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(PolyTree& solution, double delta) +{ + solution.Clear(); + FixOrientations(); + DoOffset(delta); + + //now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) + { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } + else + { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + //remove the outer PolyNode rectangle ... + if (solution.ChildCount() == 1 && solution.Childs[0]->ChildCount() > 0) + { + PolyNode* outerNode = solution.Childs[0]; + solution.Childs.reserve(outerNode->ChildCount()); + solution.Childs[0] = outerNode->Childs[0]; + solution.Childs[0]->Parent = outerNode->Parent; + for (int i = 1; i < outerNode->ChildCount(); ++i) + solution.AddChild(*outerNode->Childs[i]); + } + else + solution.Clear(); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoOffset(double delta) +{ + m_destPolys.clear(); + m_delta = delta; + + //if Zero offset, just copy any CLOSED polygons to m_p and return ... + if (NEAR_ZERO(delta)) + { + m_destPolys.reserve(m_polyNodes.ChildCount()); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) + { + PolyNode& node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon) + m_destPolys.push_back(node.Contour); + } + return; + } + + //see offset_triginometry3.svg in the documentation folder ... + if (MiterLimit > 2) m_miterLim = 2/(MiterLimit * MiterLimit); + else m_miterLim = 0.5; + + double y; + if (ArcTolerance <= 0.0) y = def_arc_tolerance; + else if (ArcTolerance > std::fabs(delta) * def_arc_tolerance) + y = std::fabs(delta) * def_arc_tolerance; + else y = ArcTolerance; + //see offset_triginometry2.svg in the documentation folder ... + double steps = pi / std::acos(1 - y / std::fabs(delta)); + if (steps > std::fabs(delta) * pi) + steps = std::fabs(delta) * pi; //ie excessive precision check + m_sin = std::sin(two_pi / steps); + m_cos = std::cos(two_pi / steps); + m_StepsPerRad = steps / two_pi; + if (delta < 0.0) m_sin = -m_sin; + + m_destPolys.reserve(m_polyNodes.ChildCount() * 2); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) + { + PolyNode& node = *m_polyNodes.Childs[i]; + m_srcPoly = node.Contour; + + int len = (int)m_srcPoly.size(); + if (len == 0 || (delta <= 0 && (len < 3 || node.m_endtype != etClosedPolygon))) + continue; + + m_destPoly.clear(); + if (len == 1) + { + if (node.m_jointype == jtRound) + { + double X = 1.0, Y = 0.0; + for (cInt j = 1; j <= steps; j++) + { + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + double X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + } + else + { + double X = -1.0, Y = -1.0; + for (int j = 0; j < 4; ++j) + { + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + if (X < 0) X = 1; + else if (Y < 0) Y = 1; + else X = -1; + } + } + m_destPolys.push_back(m_destPoly); + continue; + } + //build m_normals ... + m_normals.clear(); + m_normals.reserve(len); + for (int j = 0; j < len - 1; ++j) + m_normals.push_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); + if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon) + m_normals.push_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); + else + m_normals.push_back(DoublePoint(m_normals[len - 2])); + + if (node.m_endtype == etClosedPolygon) + { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } + else if (node.m_endtype == etClosedLine) + { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + m_destPoly.clear(); + //re-build m_normals ... + DoublePoint n = m_normals[len -1]; + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-n.X, -n.Y); + k = 0; + for (int j = len - 1; j >= 0; j--) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } + else + { + int k = 0; + for (int j = 1; j < len - 1; ++j) + OffsetPoint(j, k, node.m_jointype); + + IntPoint pt1; + if (node.m_endtype == etOpenButt) + { + int j = len - 1; + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X + m_normals[j].X * + delta), (cInt)Round(m_srcPoly[j].Y + m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X - m_normals[j].X * + delta), (cInt)Round(m_srcPoly[j].Y - m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + } + else + { + int j = len - 1; + k = len - 2; + m_sinA = 0; + m_normals[j] = DoublePoint(-m_normals[j].X, -m_normals[j].Y); + if (node.m_endtype == etOpenSquare) + DoSquare(j, k); + else + DoRound(j, k); + } + + //re-build m_normals ... + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-m_normals[1].X, -m_normals[1].Y); + + k = len - 1; + for (int j = k - 1; j > 0; --j) OffsetPoint(j, k, node.m_jointype); + + if (node.m_endtype == etOpenButt) + { + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X - m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y - m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X + m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y + m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + } + else + { + k = 1; + m_sinA = 0; + if (node.m_endtype == etOpenSquare) + DoSquare(0, 1); + else + DoRound(0, 1); + } + m_destPolys.push_back(m_destPoly); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::OffsetPoint(int j, int& k, JoinType jointype) +{ + //cross product ... + m_sinA = (m_normals[k].X * m_normals[j].Y - m_normals[j].X * m_normals[k].Y); + if (std::fabs(m_sinA * m_delta) < 1.0) + { + //dot product ... + double cosA = (m_normals[k].X * m_normals[j].X + m_normals[j].Y * m_normals[k].Y ); + if (cosA > 0) // angle => 0 degrees + { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + return; + } + //else angle => 180 degrees + } + else if (m_sinA > 1.0) m_sinA = 1.0; + else if (m_sinA < -1.0) m_sinA = -1.0; + + if (m_sinA * m_delta < 0) + { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + m_destPoly.push_back(m_srcPoly[j]); + m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); + } + else + switch (jointype) + { + case jtMiter: + { + double r = 1 + (m_normals[j].X * m_normals[k].X + + m_normals[j].Y * m_normals[k].Y); + if (r >= m_miterLim) DoMiter(j, k, r); else DoSquare(j, k); + break; + } + case jtSquare: DoSquare(j, k); break; + case jtRound: DoRound(j, k); break; + } + k = j; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoSquare(int j, int k) +{ + double dx = std::tan(std::atan2(m_sinA, + m_normals[k].X * m_normals[j].X + m_normals[k].Y * m_normals[j].Y) / 4); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[k].X - m_normals[k].Y * dx)), + Round(m_srcPoly[j].Y + m_delta * (m_normals[k].Y + m_normals[k].X * dx)))); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[j].X + m_normals[j].Y * dx)), + Round(m_srcPoly[j].Y + m_delta * (m_normals[j].Y - m_normals[j].X * dx)))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoMiter(int j, int k, double r) +{ + double q = m_delta / r; + m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + (m_normals[k].X + m_normals[j].X) * q), + Round(m_srcPoly[j].Y + (m_normals[k].Y + m_normals[j].Y) * q))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoRound(int j, int k) +{ + double a = std::atan2(m_sinA, + m_normals[k].X * m_normals[j].X + m_normals[k].Y * m_normals[j].Y); + int steps = std::max((int)Round(m_StepsPerRad * std::fabs(a)), 1); + + double X = m_normals[k].X, Y = m_normals[k].Y, X2; + for (int i = 0; i < steps; ++i) + { + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + X * m_delta), + Round(m_srcPoly[j].Y + Y * m_delta))); + X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); +} + +//------------------------------------------------------------------------------ +// Miscellaneous public functions +//------------------------------------------------------------------------------ + +void Clipper::DoSimplePolygons() +{ + PolyOutList::size_type i = 0; + while (i < m_PolyOuts.size()) + { + OutRec* outrec = m_PolyOuts[i++]; + OutPt* op = outrec->Pts; + if (!op || outrec->IsOpen) continue; + do //for each Pt in Polygon until duplicate found do ... + { + OutPt* op2 = op->Next; + while (op2 != outrec->Pts) + { + if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) + { + //split the polygon into two ... + OutPt* op3 = op->Prev; + OutPt* op4 = op2->Prev; + op->Prev = op4; + op4->Next = op; + op2->Prev = op3; + op3->Next = op2; + + outrec->Pts = op; + OutRec* outrec2 = CreateOutRec(); + outrec2->Pts = op2; + UpdateOutPtIdxs(*outrec2); + if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) + { + //OutRec2 is contained by OutRec1 ... + outrec2->IsHole = !outrec->IsHole; + outrec2->FirstLeft = outrec; + if (m_UsingPolyTree) FixupFirstLefts2(outrec2, outrec); + } + else + if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) + { + //OutRec1 is contained by OutRec2 ... + outrec2->IsHole = outrec->IsHole; + outrec->IsHole = !outrec2->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + outrec->FirstLeft = outrec2; + if (m_UsingPolyTree) FixupFirstLefts2(outrec, outrec2); + } + else + { + //the 2 polygons are separate ... + outrec2->IsHole = outrec->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + if (m_UsingPolyTree) FixupFirstLefts1(outrec, outrec2); + } + op2 = op; //ie get ready for the Next iteration + } + op2 = op2->Next; + } + op = op->Next; + } + while (op != outrec->Pts); + } +} +//------------------------------------------------------------------------------ + +void ReversePath(Path& p) +{ + std::reverse(p.begin(), p.end()); +} +//------------------------------------------------------------------------------ + +void ReversePaths(Paths& p) +{ + for (Paths::size_type i = 0; i < p.size(); ++i) + ReversePath(p[i]); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, PolyFillType fillType) +{ + Clipper c; + c.StrictlySimple(true); + c.AddPath(in_poly, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, PolyFillType fillType) +{ + Clipper c; + c.StrictlySimple(true); + c.AddPaths(in_polys, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(Paths &polys, PolyFillType fillType) +{ + SimplifyPolygons(polys, polys, fillType); +} +//------------------------------------------------------------------------------ + +inline double DistanceSqrd(const IntPoint& pt1, const IntPoint& pt2) +{ + double Dx = ((double)pt1.X - pt2.X); + double dy = ((double)pt1.Y - pt2.Y); + return (Dx*Dx + dy*dy); +} +//------------------------------------------------------------------------------ + +double DistanceFromLineSqrd( + const IntPoint& pt, const IntPoint& ln1, const IntPoint& ln2) +{ + //The equation of a line in general form (Ax + By + C = 0) + //given 2 points (x�,y�) & (x�,y�) is ... + //(y� - y�)x + (x� - x�)y + (y� - y�)x� - (x� - x�)y� = 0 + //A = (y� - y�); B = (x� - x�); C = (y� - y�)x� - (x� - x�)y� + //perpendicular distance of point (x�,y�) = (Ax� + By� + C)/Sqrt(A� + B�) + //see http://en.wikipedia.org/wiki/Perpendicular_distance + double A = double(ln1.Y - ln2.Y); + double B = double(ln2.X - ln1.X); + double C = A * ln1.X + B * ln1.Y; + C = A * pt.X + B * pt.Y - C; + return (C * C) / (A * A + B * B); +} +//--------------------------------------------------------------------------- + +bool SlopesNearCollinear(const IntPoint& pt1, + const IntPoint& pt2, const IntPoint& pt3, double distSqrd) +{ + //this function is more accurate when the point that's geometrically + //between the other 2 points is the one that's tested for distance. + //ie makes it more likely to pick up 'spikes' ... + if (Abs(pt1.X - pt2.X) > Abs(pt1.Y - pt2.Y)) + { + if ((pt1.X > pt2.X) == (pt1.X < pt3.X)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.X > pt1.X) == (pt2.X < pt3.X)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } + else + { + if ((pt1.Y > pt2.Y) == (pt1.Y < pt3.Y)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.Y > pt1.Y) == (pt2.Y < pt3.Y)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } +} +//------------------------------------------------------------------------------ + +bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) +{ + double Dx = (double)pt1.X - pt2.X; + double dy = (double)pt1.Y - pt2.Y; + return ((Dx * Dx) + (dy * dy) <= distSqrd); +} +//------------------------------------------------------------------------------ + +OutPt* ExcludeOp(OutPt* op) +{ + OutPt* result = op->Prev; + result->Next = op->Next; + op->Next->Prev = result; + result->Idx = 0; + return result; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(const Path& in_poly, Path& out_poly, double distance) +{ + //distance = proximity in units/pixels below which vertices + //will be stripped. Default ~= sqrt(2). + + size_t size = in_poly.size(); + + if (size == 0) + { + out_poly.clear(); + return; + } + + OutPt* outPts = new OutPt[size]; + for (size_t i = 0; i < size; ++i) + { + outPts[i].Pt = in_poly[i]; + outPts[i].Next = &outPts[(i + 1) % size]; + outPts[i].Next->Prev = &outPts[i]; + outPts[i].Idx = 0; + } + + double distSqrd = distance * distance; + OutPt* op = &outPts[0]; + while (op->Idx == 0 && op->Next != op->Prev) + { + if (PointsAreClose(op->Pt, op->Prev->Pt, distSqrd)) + { + op = ExcludeOp(op); + size--; + } + else if (PointsAreClose(op->Prev->Pt, op->Next->Pt, distSqrd)) + { + ExcludeOp(op->Next); + op = ExcludeOp(op); + size -= 2; + } + else if (SlopesNearCollinear(op->Prev->Pt, op->Pt, op->Next->Pt, distSqrd)) + { + op = ExcludeOp(op); + size--; + } + else + { + op->Idx = 1; + op = op->Next; + } + } + + if (size < 3) size = 0; + out_poly.resize(size); + for (size_t i = 0; i < size; ++i) + { + out_poly[i] = op->Pt; + op = op->Next; + } + delete [] outPts; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(Path& poly, double distance) +{ + CleanPolygon(poly, poly, distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance) +{ + out_polys.resize(in_polys.size()); + for (Paths::size_type i = 0; i < in_polys.size(); ++i) + CleanPolygon(in_polys[i], out_polys[i], distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(Paths& polys, double distance) +{ + CleanPolygons(polys, polys, distance); +} +//------------------------------------------------------------------------------ + +void Minkowski(const Path& poly, const Path& path, + Paths& solution, bool isSum, bool isClosed) +{ + int delta = (isClosed ? 1 : 0); + size_t polyCnt = poly.size(); + size_t pathCnt = path.size(); + Paths pp; + pp.reserve(pathCnt); + if (isSum) + for (size_t i = 0; i < pathCnt; ++i) + { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X + poly[j].X, path[i].Y + poly[j].Y)); + pp.push_back(p); + } + else + for (size_t i = 0; i < pathCnt; ++i) + { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X - poly[j].X, path[i].Y - poly[j].Y)); + pp.push_back(p); + } + + solution.clear(); + solution.reserve((pathCnt + delta) * (polyCnt + 1)); + for (size_t i = 0; i < pathCnt - 1 + delta; ++i) + for (size_t j = 0; j < polyCnt; ++j) + { + Path quad; + quad.reserve(4); + quad.push_back(pp[i % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); + quad.push_back(pp[i % pathCnt][(j + 1) % polyCnt]); + if (!Orientation(quad)) ReversePath(quad); + solution.push_back(quad); + } +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed) +{ + Minkowski(pattern, path, solution, true, pathIsClosed); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void TranslatePath(const Path& input, Path& output, const IntPoint delta) +{ + //precondition: input != output + output.resize(input.size()); + for (size_t i = 0; i < input.size(); ++i) + output[i] = IntPoint(input[i].X + delta.X, input[i].Y + delta.Y); +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed) +{ + Clipper c; + for (size_t i = 0; i < paths.size(); ++i) + { + Paths tmp; + Minkowski(pattern, paths[i], tmp, true, pathIsClosed); + c.AddPaths(tmp, ptSubject, true); + if (pathIsClosed) + { + Path tmp2; + TranslatePath(paths[i], tmp2, pattern[0]); + c.AddPath(tmp2, ptClip, true); + } + } + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution) +{ + Minkowski(poly1, poly2, solution, false, true); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +enum NodeType {ntAny, ntOpen, ntClosed}; + +void AddPolyNodeToPaths(const PolyNode& polynode, NodeType nodetype, Paths& paths) +{ + bool match = true; + if (nodetype == ntClosed) match = !polynode.IsOpen(); + else if (nodetype == ntOpen) return; + + if (!polynode.Contour.empty() && match) + paths.push_back(polynode.Contour); + for (int i = 0; i < polynode.ChildCount(); ++i) + AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); +} +//------------------------------------------------------------------------------ + +void PolyTreeToPaths(const PolyTree& polytree, Paths& paths) +{ + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntAny, paths); +} +//------------------------------------------------------------------------------ + +void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths) +{ + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntClosed, paths); +} +//------------------------------------------------------------------------------ + +void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths) +{ + paths.resize(0); + paths.reserve(polytree.Total()); + //Open paths are top level only, so ... + for (int i = 0; i < polytree.ChildCount(); ++i) + if (polytree.Childs[i]->IsOpen()) + paths.push_back(polytree.Childs[i]->Contour); +} +//------------------------------------------------------------------------------ + +std::ostream& operator <<(std::ostream &s, const IntPoint &p) +{ + s << "(" << p.X << "," << p.Y << ")"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream& operator <<(std::ostream &s, const Path &p) +{ + if (p.empty()) return s; + Path::size_type last = p.size() -1; + for (Path::size_type i = 0; i < last; i++) + s << "(" << p[i].X << "," << p[i].Y << "), "; + s << "(" << p[last].X << "," << p[last].Y << ")\n"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream& operator <<(std::ostream &s, const Paths &p) +{ + for (Paths::size_type i = 0; i < p.size(); i++) + s << p[i]; + s << "\n"; + return s; +} +//------------------------------------------------------------------------------ + +} //ClipperLib namespace diff --git a/deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.hpp b/deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.hpp new file mode 100644 index 00000000..50d8f6e9 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/pdocr/ocr_clipper.hpp @@ -0,0 +1,547 @@ +/******************************************************************************* +* * +* Author : Angus Johnson * +* Version : 6.4.2 * +* Date : 27 February 2017 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2017 * +* * +* License: * +* Use, modification & distribution is subject to Boost Software License Ver 1. * +* http://www.boost.org/LICENSE_1_0.txt * +* * +* Attributions: * +* The code in this library is an extension of Bala Vatti's clipping algorithm: * +* "A generic solution to polygon clipping" * +* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * +* http://portal.acm.org/citation.cfm?id=129906 * +* * +* Computer graphics and geometric modeling: implementation and algorithms * +* By Max K. Agoston * +* Springer; 1 edition (January 4, 2005) * +* http://books.google.com/books?q=vatti+clipping+agoston * +* * +* See also: * +* "Polygon Offsetting by Computing Winding Numbers" * +* Paper no. DETC2005-85513 pp. 565-575 * +* ASME 2005 International Design Engineering Technical Conferences * +* and Computers and Information in Engineering Conference (IDETC/CIE2005) * +* September 24-28, 2005 , Long Beach, California, USA * +* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * +* * +*******************************************************************************/ + +#ifndef clipper_hpp +#define clipper_hpp + +#define CLIPPER_VERSION "6.4.2" + +//use_int32: When enabled 32bit ints are used instead of 64bit ints. This +//improve performance but coordinate values are limited to the range +/- 46340 +//#define use_int32 + +//use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance. +//#define use_xyz + +//use_lines: Enables line clipping. Adds a very minor cost to performance. +#define use_lines + +//use_deprecated: Enables temporary support for the obsolete functions +//#define use_deprecated + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +enum ClipType { + ctIntersection, ctUnion, ctDifference, ctXor +}; +enum PolyType { + ptSubject, ptClip +}; +//By far the most widely used winding rules for polygon filling are +//EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) +//Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) +//see http://glprogramming.com/red/chapter11.html +enum PolyFillType { + pftEvenOdd, pftNonZero, pftPositive, pftNegative +}; + +#ifdef use_int32 +typedef int cInt; +static cInt const loRange = 0x7FFF; +static cInt const hiRange = 0x7FFF; +#else +typedef signed long long cInt; +static cInt const loRange = 0x3FFFFFFF; +static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL; +typedef signed long long long64; //used by Int128 class +typedef unsigned long long ulong64; + +#endif + +struct IntPoint { + cInt X; + cInt Y; +#ifdef use_xyz + cInt Z; + IntPoint(cInt x = 0, cInt y = 0, cInt z = 0): X(x), Y(y), Z(z) {}; +#else + + IntPoint(cInt x = 0, cInt y = 0) : X(x), Y(y) {}; +#endif + + friend inline bool operator==(const IntPoint &a, const IntPoint &b) { + return a.X == b.X && a.Y == b.Y; + } + + friend inline bool operator!=(const IntPoint &a, const IntPoint &b) { + return a.X != b.X || a.Y != b.Y; + } +}; +//------------------------------------------------------------------------------ + +typedef std::vector Path; +typedef std::vector Paths; + +inline Path &operator<<(Path &poly, const IntPoint &p) { + poly.push_back(p); + return poly; +} + +inline Paths &operator<<(Paths &polys, const Path &p) { + polys.push_back(p); + return polys; +} + +std::ostream &operator<<(std::ostream &s, const IntPoint &p); + +std::ostream &operator<<(std::ostream &s, const Path &p); + +std::ostream &operator<<(std::ostream &s, const Paths &p); + +struct DoublePoint { + double X; + double Y; + + DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {} + + DoublePoint(IntPoint ip) : X((double) ip.X), Y((double) ip.Y) {} +}; +//------------------------------------------------------------------------------ + +#ifdef use_xyz +typedef void (*ZFillCallback)(IntPoint& e1bot, IntPoint& e1top, IntPoint& e2bot, IntPoint& e2top, IntPoint& pt); +#endif + +enum InitOptions { + ioReverseSolution = 1, ioStrictlySimple = 2, ioPreserveCollinear = 4 +}; +enum JoinType { + jtSquare, jtRound, jtMiter +}; +enum EndType { + etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound +}; + +class PolyNode; + +typedef std::vector PolyNodes; + +class PolyNode { +public: + PolyNode(); + + virtual ~PolyNode() {}; + Path Contour; + PolyNodes Childs; + PolyNode *Parent; + + PolyNode *GetNext() const; + + bool IsHole() const; + + bool IsOpen() const; + + int ChildCount() const; + +private: + //PolyNode& operator =(PolyNode& other); + unsigned Index; //node index in Parent.Childs + bool m_IsOpen; + JoinType m_jointype; + EndType m_endtype; + + PolyNode *GetNextSiblingUp() const; + + void AddChild(PolyNode &child); + + friend class Clipper; //to access Index + friend class ClipperOffset; +}; + +class PolyTree : public PolyNode { +public: + ~PolyTree() { Clear(); }; + + PolyNode *GetFirst() const; + + void Clear(); + + int Total() const; + +private: + //PolyTree& operator =(PolyTree& other); + PolyNodes AllNodes; + + friend class Clipper; //to access AllNodes +}; + +bool Orientation(const Path &poly); + +double Area(const Path &poly); + +int PointInPolygon(const IntPoint &pt, const Path &path); + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, PolyFillType fillType = pftEvenOdd); + +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, PolyFillType fillType = pftEvenOdd); + +void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd); + +void CleanPolygon(const Path &in_poly, Path &out_poly, double distance = 1.415); + +void CleanPolygon(Path &poly, double distance = 1.415); + +void CleanPolygons(const Paths &in_polys, Paths &out_polys, double distance = 1.415); + +void CleanPolygons(Paths &polys, double distance = 1.415); + +void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution, bool pathIsClosed); + +void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution, bool pathIsClosed); + +void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution); + +void PolyTreeToPaths(const PolyTree &polytree, Paths &paths); + +void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths); + +void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths); + +void ReversePath(Path &p); + +void ReversePaths(Paths &p); + +struct IntRect { + cInt left; + cInt top; + cInt right; + cInt bottom; +}; + +//enums that are used internally ... +enum EdgeSide { + esLeft = 1, esRight = 2 +}; + +//forward declarations (for stuff used internally) ... +struct TEdge; +struct IntersectNode; +struct LocalMinimum; +struct OutPt; +struct OutRec; +struct Join; + +typedef std::vector PolyOutList; +typedef std::vector EdgeList; +typedef std::vector JoinList; +typedef std::vector IntersectList; + +//------------------------------------------------------------------------------ + +//ClipperBase is the ancestor to the Clipper class. It should not be +//instantiated directly. This class simply abstracts the conversion of sets of +//polygon coordinates into edge objects that are stored in a LocalMinima list. +class ClipperBase { +public: + ClipperBase(); + + virtual ~ClipperBase(); + + virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed); + + bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed); + + virtual void Clear(); + + IntRect GetBounds(); + + bool PreserveCollinear() { return m_PreserveCollinear; }; + + void PreserveCollinear(bool value) { m_PreserveCollinear = value; }; +protected: + void DisposeLocalMinimaList(); + + TEdge *AddBoundsToLML(TEdge *e, bool IsClosed); + + virtual void Reset(); + + TEdge *ProcessBound(TEdge *E, bool IsClockwise); + + void InsertScanbeam(const cInt Y); + + bool PopScanbeam(cInt &Y); + + bool LocalMinimaPending(); + + bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin); + + OutRec *CreateOutRec(); + + void DisposeAllOutRecs(); + + void DisposeOutRec(PolyOutList::size_type index); + + void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2); + + void DeleteFromAEL(TEdge *e); + + void UpdateEdgeIntoAEL(TEdge *&e); + + typedef std::vector MinimaList; + MinimaList::iterator m_CurrentLM; + MinimaList m_MinimaList; + + bool m_UseFullRange; + EdgeList m_edges; + bool m_PreserveCollinear; + bool m_HasOpenPaths; + PolyOutList m_PolyOuts; + TEdge *m_ActiveEdges; + + typedef std::priority_queue ScanbeamList; + ScanbeamList m_Scanbeam; +}; +//------------------------------------------------------------------------------ + +class Clipper : public virtual ClipperBase { +public: + Clipper(int initOptions = 0); + + bool Execute(ClipType clipType, + Paths &solution, + PolyFillType fillType = pftEvenOdd); + + bool Execute(ClipType clipType, + Paths &solution, + PolyFillType subjFillType, + PolyFillType clipFillType); + + bool Execute(ClipType clipType, + PolyTree &polytree, + PolyFillType fillType = pftEvenOdd); + + bool Execute(ClipType clipType, + PolyTree &polytree, + PolyFillType subjFillType, + PolyFillType clipFillType); + + bool ReverseSolution() { return m_ReverseOutput; }; + + void ReverseSolution(bool value) { m_ReverseOutput = value; }; + + bool StrictlySimple() { return m_StrictSimple; }; + + void StrictlySimple(bool value) { m_StrictSimple = value; }; + //set the callback function for z value filling on intersections (otherwise Z is 0) +#ifdef use_xyz + void ZFillFunction(ZFillCallback zFillFunc); +#endif +protected: + virtual bool ExecuteInternal(); + +private: + JoinList m_Joins; + JoinList m_GhostJoins; + IntersectList m_IntersectList; + ClipType m_ClipType; + typedef std::list MaximaList; + MaximaList m_Maxima; + TEdge *m_SortedEdges; + bool m_ExecuteLocked; + PolyFillType m_ClipFillType; + PolyFillType m_SubjFillType; + bool m_ReverseOutput; + bool m_UsingPolyTree; + bool m_StrictSimple; +#ifdef use_xyz + ZFillCallback m_ZFill; //custom callback +#endif + + void SetWindingCount(TEdge &edge); + + bool IsEvenOddFillType(const TEdge &edge) const; + + bool IsEvenOddAltFillType(const TEdge &edge) const; + + void InsertLocalMinimaIntoAEL(const cInt botY); + + void InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge); + + void AddEdgeToSEL(TEdge *edge); + + bool PopEdgeFromSEL(TEdge *&edge); + + void CopyAELToSEL(); + + void DeleteFromSEL(TEdge *e); + + void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2); + + bool IsContributing(const TEdge &edge) const; + + bool IsTopHorz(const cInt XPos); + + void DoMaxima(TEdge *e); + + void ProcessHorizontals(); + + void ProcessHorizontal(TEdge *horzEdge); + + void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + + OutPt *AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + + OutRec *GetOutRec(int idx); + + void AppendPolygon(TEdge *e1, TEdge *e2); + + void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); + + OutPt *AddOutPt(TEdge *e, const IntPoint &pt); + + OutPt *GetLastOutPt(TEdge *e); + + bool ProcessIntersections(const cInt topY); + + void BuildIntersectList(const cInt topY); + + void ProcessIntersectList(); + + void ProcessEdgesAtTopOfScanbeam(const cInt topY); + + void BuildResult(Paths &polys); + + void BuildResult2(PolyTree &polytree); + + void SetHoleState(TEdge *e, OutRec *outrec); + + void DisposeIntersectNodes(); + + bool FixupIntersectionOrder(); + + void FixupOutPolygon(OutRec &outrec); + + void FixupOutPolyline(OutRec &outrec); + + bool IsHole(TEdge *e); + + bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl); + + void FixHoleLinkage(OutRec &outrec); + + void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt); + + void ClearJoins(); + + void ClearGhostJoins(); + + void AddGhostJoin(OutPt *op, const IntPoint offPt); + + bool JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2); + + void JoinCommonEdges(); + + void DoSimplePolygons(); + + void FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec); + + void FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec); + + void FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec); + +#ifdef use_xyz + void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2); +#endif +}; +//------------------------------------------------------------------------------ + +class ClipperOffset { +public: + ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25); + + ~ClipperOffset(); + + void AddPath(const Path &path, JoinType joinType, EndType endType); + + void AddPaths(const Paths &paths, JoinType joinType, EndType endType); + + void Execute(Paths &solution, double delta); + + void Execute(PolyTree &solution, double delta); + + void Clear(); + + double MiterLimit; + double ArcTolerance; +private: + Paths m_destPolys; + Path m_srcPoly; + Path m_destPoly; + std::vector m_normals; + double m_delta, m_sinA, m_sin, m_cos; + double m_miterLim, m_StepsPerRad; + IntPoint m_lowest; + PolyNode m_polyNodes; + + void FixOrientations(); + + void DoOffset(double delta); + + void OffsetPoint(int j, int &k, JoinType jointype); + + void DoSquare(int j, int k); + + void DoMiter(int j, int k, double r); + + void DoRound(int j, int k); +}; +//------------------------------------------------------------------------------ + +class clipperException : public std::exception { +public: + clipperException(const char *description) : m_descr(description) {} + + virtual ~clipperException() throw() {} + + virtual const char *what() const throw() { return m_descr.c_str(); } + +private: + std::string m_descr; +}; +//------------------------------------------------------------------------------ + +} //ClipperLib namespace + +#endif //clipper_hpp + + diff --git a/deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.cpp b/deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.cpp new file mode 100644 index 00000000..df932d5e --- /dev/null +++ b/deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.cpp @@ -0,0 +1,141 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ocr_crnn_process.h" +#include +#include +#include +#include +#include +#include + +const std::string CHARACTER_TYPE = "ch"; +const int MAX_DICT_LENGTH = 6624; +const std::vector REC_IMAGE_SHAPE = {3, 32, 320}; + +static cv::Mat crnn_resize_norm_img(cv::Mat img, float wh_ratio) { + int imgC, imgH, imgW; + imgC = REC_IMAGE_SHAPE[0]; + imgW = REC_IMAGE_SHAPE[2]; + imgH = REC_IMAGE_SHAPE[1]; + + if (CHARACTER_TYPE == "ch") + imgW = int(32 * wh_ratio); + + float ratio = float(img.cols) / float(img.rows); + int resize_w, resize_h; + if (ceilf(imgH * ratio) > imgW) + resize_w = imgW; + else + resize_w = int(ceilf(imgH * ratio)); + cv::Mat resize_img; + cv::resize(img, resize_img, cv::Size(resize_w, imgH), 0.f, 0.f, cv::INTER_CUBIC); + + resize_img.convertTo(resize_img, CV_32FC3, 1 / 255.f); + + for (int h = 0; h < resize_img.rows; h++) { + for (int w = 0; w < resize_img.cols; w++) { + resize_img.at(h, w)[0] = (resize_img.at(h, w)[0] - 0.5) * 2; + resize_img.at(h, w)[1] = (resize_img.at(h, w)[1] - 0.5) * 2; + resize_img.at(h, w)[2] = (resize_img.at(h, w)[2] - 0.5) * 2; + } + } + + cv::Mat dist; + cv::copyMakeBorder(resize_img, dist, 0, 0, 0, int(imgW - resize_w), cv::BORDER_CONSTANT, + {0, 0, 0}); + + return dist; + +} + +cv::Mat crnn_resize_img(const cv::Mat &img, float wh_ratio) { + int imgC = REC_IMAGE_SHAPE[0]; + int imgW = REC_IMAGE_SHAPE[2]; + int imgH = REC_IMAGE_SHAPE[1]; + + if (CHARACTER_TYPE == "ch") { + imgW = int(32 * wh_ratio); + } + + float ratio = float(img.cols) / float(img.rows); + int resize_w; + if (ceilf(imgH * ratio) > imgW) + resize_w = imgW; + else + resize_w = int(ceilf(imgH * ratio)); + cv::Mat resize_img; + cv::resize(img, resize_img, cv::Size(resize_w, imgH)); + return resize_img; +} + + +cv::Mat get_rotate_crop_image(const cv::Mat &srcimage, const std::vector> &box) { + + std::vector> points = box; + + int x_collect[4] = {box[0][0], box[1][0], box[2][0], box[3][0]}; + int y_collect[4] = {box[0][1], box[1][1], box[2][1], box[3][1]}; + int left = int(*std::min_element(x_collect, x_collect + 4)); + int right = int(*std::max_element(x_collect, x_collect + 4)); + int top = int(*std::min_element(y_collect, y_collect + 4)); + int bottom = int(*std::max_element(y_collect, y_collect + 4)); + + cv::Mat img_crop; + srcimage(cv::Rect(left, top, right - left, bottom - top)).copyTo(img_crop); + + for (int i = 0; i < points.size(); i++) { + points[i][0] -= left; + points[i][1] -= top; + } + + int img_crop_width = int(sqrt(pow(points[0][0] - points[1][0], 2) + + pow(points[0][1] - points[1][1], 2))); + int img_crop_height = int(sqrt(pow(points[0][0] - points[3][0], 2) + + pow(points[0][1] - points[3][1], 2))); + + cv::Point2f pts_std[4]; + pts_std[0] = cv::Point2f(0., 0.); + pts_std[1] = cv::Point2f(img_crop_width, 0.); + pts_std[2] = cv::Point2f(img_crop_width, img_crop_height); + pts_std[3] = cv::Point2f(0.f, img_crop_height); + + cv::Point2f pointsf[4]; + pointsf[0] = cv::Point2f(points[0][0], points[0][1]); + pointsf[1] = cv::Point2f(points[1][0], points[1][1]); + pointsf[2] = cv::Point2f(points[2][0], points[2][1]); + pointsf[3] = cv::Point2f(points[3][0], points[3][1]); + + cv::Mat M = cv::getPerspectiveTransform(pointsf, pts_std); + + cv::Mat dst_img; + cv::warpPerspective(img_crop, dst_img, M, cv::Size(img_crop_width, img_crop_height), + cv::BORDER_REPLICATE); + + if (float(dst_img.rows) >= float(dst_img.cols) * 1.5) { + /* + cv::Mat srcCopy = cv::Mat(dst_img.rows, dst_img.cols, dst_img.depth()); + cv::transpose(dst_img, srcCopy); + cv::flip(srcCopy, srcCopy, 0); + return srcCopy; + */ + cv::transpose(dst_img, dst_img); + cv::flip(dst_img, dst_img, 0); + return dst_img; + } else { + return dst_img; + } + +} + diff --git a/deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.h b/deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.h new file mode 100644 index 00000000..1ab55e34 --- /dev/null +++ b/deploy/ios_demo/ocr_demo/pdocr/ocr_crnn_process.h @@ -0,0 +1,19 @@ +// +// Created by fujiayi on 2020/7/3. +// +#pragma once + +#include +#include + + +extern const std::vector REC_IMAGE_SHAPE; + +cv::Mat get_rotate_crop_image(const cv::Mat &srcimage, const std::vector>& box); + +cv::Mat crnn_resize_img(const cv::Mat& img, float wh_ratio); + +template +inline size_t argmax(ForwardIterator first, ForwardIterator last) { + return std::distance(first, std::max_element(first, last)); +} \ No newline at end of file diff --git a/deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.cpp b/deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.cpp new file mode 100644 index 00000000..76d7cd4d --- /dev/null +++ b/deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.cpp @@ -0,0 +1,372 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "ocr_clipper.hpp" + + +static void getcontourarea(float **box, float unclip_ratio, float &distance) { + int pts_num = 4; + float area = 0.0f; + float dist = 0.0f; + for (int i = 0; i < pts_num; i++) { + area += box[i][0] * box[(i + 1) % pts_num][1] - box[i][1] * box[(i + 1) % pts_num][0]; + dist += sqrtf( + (box[i][0] - box[(i + 1) % pts_num][0]) * (box[i][0] - box[(i + 1) % pts_num][0]) + + (box[i][1] - box[(i + 1) % pts_num][1]) * (box[i][1] - box[(i + 1) % pts_num][1])); + } + area = fabs(float(area / 2.0)); + + distance = area * unclip_ratio / dist; + +} + +static cv::RotatedRect unclip(float **box) { + float unclip_ratio = 2.0; + float distance = 1.0; + + getcontourarea(box, unclip_ratio, distance); + + ClipperLib::ClipperOffset offset; + ClipperLib::Path p; + p << ClipperLib::IntPoint(int(box[0][0]), int(box[0][1])) + << ClipperLib::IntPoint(int(box[1][0]), int(box[1][1])) << + ClipperLib::IntPoint(int(box[2][0]), int(box[2][1])) + << ClipperLib::IntPoint(int(box[3][0]), int(box[3][1])); + offset.AddPath(p, ClipperLib::jtRound, ClipperLib::etClosedPolygon); + + ClipperLib::Paths soln; + offset.Execute(soln, distance); + std::vector points; + + for (int j = 0; j < soln.size(); j++) { + for (int i = 0; i < soln[soln.size() - 1].size(); i++) { + points.emplace_back(soln[j][i].X, soln[j][i].Y); + } + } + cv::RotatedRect res = cv::minAreaRect(points); + + return res; +} + +static float **Mat2Vec(cv::Mat mat) { + auto **array = new float *[mat.rows]; + for (int i = 0; i < mat.rows; ++i) + array[i] = new float[mat.cols]; + for (int i = 0; i < mat.rows; ++i) { + for (int j = 0; j < mat.cols; ++j) { + array[i][j] = mat.at(i, j); + } + } + + return array; +} + +static void quickSort(float **s, int l, int r) { + if (l < r) { + int i = l, j = r; + float x = s[l][0]; + float *xp = s[l]; + while (i < j) { + while (i < j && s[j][0] >= x) + j--; + if (i < j) + std::swap(s[i++], s[j]); + while (i < j && s[i][0] < x) + i++; + if (i < j) + std::swap(s[j--], s[i]); + } + s[i] = xp; + quickSort(s, l, i - 1); + quickSort(s, i + 1, r); + } +} + +static void quickSort_vector(std::vector> &box, int l, int r, int axis) { + if (l < r) { + int i = l, j = r; + int x = box[l][axis]; + std::vector xp(box[l]); + while (i < j) { + while (i < j && box[j][axis] >= x) + j--; + if (i < j) + std::swap(box[i++], box[j]); + while (i < j && box[i][axis] < x) + i++; + if (i < j) + std::swap(box[j--], box[i]); + } + box[i] = xp; + quickSort_vector(box, l, i - 1, axis); + quickSort_vector(box, i + 1, r, axis); + } +} + +static std::vector> order_points_clockwise(std::vector> pts) { + std::vector> box = pts; + quickSort_vector(box, 0, int(box.size() - 1), 0); + std::vector> leftmost = {box[0], box[1]}; + std::vector> rightmost = {box[2], box[3]}; + + if (leftmost[0][1] > leftmost[1][1]) + std::swap(leftmost[0], leftmost[1]); + + if (rightmost[0][1] > rightmost[1][1]) + std::swap(rightmost[0], rightmost[1]); + + std::vector> rect = {leftmost[0], rightmost[0], rightmost[1], leftmost[1]}; + return rect; +} + +static float **get_mini_boxes(cv::RotatedRect box, float &ssid) { + ssid = box.size.width >= box.size.height ? box.size.height : box.size.width; + + cv::Mat points; + cv::boxPoints(box, points); + // sorted box points + auto array = Mat2Vec(points); + quickSort(array, 0, 3); + + float *idx1 = array[0], *idx2 = array[1], *idx3 = array[2], *idx4 = array[3]; + if (array[3][1] <= array[2][1]) { + idx2 = array[3]; + idx3 = array[2]; + } else { + idx2 = array[2]; + idx3 = array[3]; + } + if (array[1][1] <= array[0][1]) { + idx1 = array[1]; + idx4 = array[0]; + } else { + idx1 = array[0]; + idx4 = array[1]; + } + + array[0] = idx1; + array[1] = idx2; + array[2] = idx3; + array[3] = idx4; + + return array; +} + +template T clamp(T x, T min, T max) { + if (x > max){ + return max; + } + if (x < min){ + return min; + } + return x; +} + +static float clampf(float x, float min, float max) { + if (x > max) + return max; + if (x < min) + return min; + return x; +} + + +float box_score_fast(float **box_array, cv::Mat pred) { + auto array = box_array; + int width = pred.cols; + int height = pred.rows; + + float box_x[4] = {array[0][0], array[1][0], array[2][0], array[3][0]}; + float box_y[4] = {array[0][1], array[1][1], array[2][1], array[3][1]}; + + int xmin = clamp(int(std::floorf(*(std::min_element(box_x, box_x + 4)))), 0, width - 1); + int xmax = clamp(int(std::ceilf(*(std::max_element(box_x, box_x + 4)))), 0, width - 1); + int ymin = clamp(int(std::floorf(*(std::min_element(box_y, box_y + 4)))), 0, height - 1); + int ymax = clamp(int(std::ceilf(*(std::max_element(box_y, box_y + 4)))), 0, height - 1); + + cv::Mat mask; + mask = cv::Mat::zeros(ymax - ymin + 1, xmax - xmin + 1, CV_8UC1); + + cv::Point root_point[4]; + root_point[0] = cv::Point(int(array[0][0]) - xmin, int(array[0][1]) - ymin); + root_point[1] = cv::Point(int(array[1][0]) - xmin, int(array[1][1]) - ymin); + root_point[2] = cv::Point(int(array[2][0]) - xmin, int(array[2][1]) - ymin); + root_point[3] = cv::Point(int(array[3][0]) - xmin, int(array[3][1]) - ymin); + const cv::Point *ppt[1] = {root_point}; + int npt[] = {4}; + cv::fillPoly(mask, ppt, npt, 1, cv::Scalar(1)); + + cv::Mat croppedImg; + pred(cv::Rect(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1)).copyTo(croppedImg); + + auto score = cv::mean(croppedImg, mask)[0]; + return score; +} + + +std::vector>> +boxes_from_bitmap(const cv::Mat& pred, const cv::Mat& bitmap) { + const int min_size = 3; + const int max_candidates = 1000; + const float box_thresh = 0.5; + + int width = bitmap.cols; + int height = bitmap.rows; + + std::vector> contours; + std::vector hierarchy; + + cv::findContours(bitmap, contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE); + + int num_contours = contours.size() >= max_candidates ? max_candidates : contours.size(); + + std::vector>> boxes; + + for (int _i = 0; _i < num_contours; _i++) { + float ssid; + cv::RotatedRect box = cv::minAreaRect(contours[_i]); + auto array = get_mini_boxes(box, ssid); + + auto box_for_unclip = array; + //end get_mini_box + + if (ssid < min_size) { + continue; + } + + float score; + score = box_score_fast(array, pred); + //end box_score_fast + if (score < box_thresh) + continue; + + + // start for unclip + cv::RotatedRect points = unclip(box_for_unclip); + // end for unclip + + cv::RotatedRect clipbox = points; + auto cliparray = get_mini_boxes(clipbox, ssid); + + if (ssid < min_size + 2) continue; + + int dest_width = pred.cols; + int dest_height = pred.rows; + std::vector> intcliparray; + + for (int num_pt = 0; num_pt < 4; num_pt++) { + std::vector a{ + int(clampf(roundf(cliparray[num_pt][0] / float(width) * float(dest_width)), 0, + float(dest_width))), + int(clampf(roundf(cliparray[num_pt][1] / float(height) * float(dest_height)), 0, + float(dest_height)))}; + intcliparray.push_back(a); + } + boxes.push_back(intcliparray); + + }//end for + return boxes; +} + +int _max(int a, int b) { + return a >= b ? a : b; +} + +int _min(int a, int b) { + return a >= b ? b : a; +} + +std::vector>> +filter_tag_det_res(const std::vector>>& o_boxes, + float ratio_h, float ratio_w,const cv::Mat& srcimg) { + int oriimg_h = srcimg.rows; + int oriimg_w = srcimg.cols; + std::vector>> boxes{o_boxes}; + std::vector>> root_points; + for (int n = 0; n < boxes.size(); n++) { + boxes[n] = order_points_clockwise(boxes[n]); + for (int m = 0; m < boxes[0].size(); m++) { + boxes[n][m][0] /= ratio_w; + boxes[n][m][1] /= ratio_h; + + boxes[n][m][0] = int(_min(_max(boxes[n][m][0], 0), oriimg_w - 1)); + boxes[n][m][1] = int(_min(_max(boxes[n][m][1], 0), oriimg_h - 1)); + } + } + + for (int n = 0; n < boxes.size(); n++) { + int rect_width, rect_height; + rect_width = int(sqrt( + pow(boxes[n][0][0] - boxes[n][1][0], 2) + pow(boxes[n][0][1] - boxes[n][1][1], 2))); + rect_height = int(sqrt( + pow(boxes[n][0][0] - boxes[n][3][0], 2) + pow(boxes[n][0][1] - boxes[n][3][1], 2))); + if (rect_width <= 10 || rect_height <= 10) + continue; + root_points.push_back(boxes[n]); + } + return root_points; +} + +/* +using namespace std; +// read data from txt file +cv::Mat readtxt2(std::string path, int imgw, int imgh, int imgc) { + std::cout << "read data file from txt file! " << std::endl; + ifstream in(path); + string line; + int count = 0; + int i = 0, j = 0; + std::vector img_mean = {0.485, 0.456, 0.406}; + std::vector img_std = {0.229, 0.224, 0.225}; + + float trainData[imgh][imgw*imgc]; + + while (getline(in, line)) { + stringstream ss(line); + double x; + while (ss >> x) { +// trainData[i][j] = float(x) * img_std[j % 3] + img_mean[j % 3]; + trainData[i][j] = float(x); + j++; + } + i++; + j = 0; + } + + cv::Mat pred_map(imgh, imgw*imgc, CV_32FC1, (float *) trainData); + cv::Mat reshape_img = pred_map.reshape(imgc, imgh); + return reshape_img; +} + */ +//using namespace std; +// +//void writetxt(vector> data, std::string save_path){ +// +// ofstream fout(save_path); +// +// for (int i = 0; i < data.size(); i++) { +// for (int j=0; j< data[0].size(); j++){ +// fout << data[i][j] << " "; +// } +// fout << endl; +// } +// fout << endl; +// fout.close(); +//} diff --git a/deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.h b/deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.h new file mode 100644 index 00000000..a0546d2d --- /dev/null +++ b/deploy/ios_demo/ocr_demo/pdocr/ocr_db_post_process.h @@ -0,0 +1,10 @@ +// +// Created by fujiayi on 2020/7/2. +// +#pragma once + +std::vector>> boxes_from_bitmap(const cv::Mat& pred, const cv::Mat& bitmap); + +std::vector>> +filter_tag_det_res(const std::vector>>& o_boxes, + float ratio_h, float ratio_w, const cv::Mat& srcimg); \ No newline at end of file diff --git a/deploy/ios_demo/ocr_demo/timer.h b/deploy/ios_demo/ocr_demo/timer.h new file mode 100644 index 00000000..e98be1bf --- /dev/null +++ b/deploy/ios_demo/ocr_demo/timer.h @@ -0,0 +1,87 @@ +// +// timer.h +// face_demo +// +// Created by Li,Xiaoyang(SYS) on 2019/8/20. +// Copyright © 2019年 Li,Xiaoyang(SYS). All rights reserved. +// + +#ifndef timer_h +#define timer_h +#include +#include +class Timer final { + +public: + Timer() {} + + ~Timer() {} + + void clear() { + ms_time.clear(); + } + + void start() { + tstart = std::chrono::system_clock::now(); + } + + void end() { + tend = std::chrono::system_clock::now(); + auto ts = std::chrono::duration_cast(tend - tstart); + float elapse_ms = 1000.f * float(ts.count()) * std::chrono::microseconds::period::num / \ + std::chrono::microseconds::period::den; + ms_time.push_back(elapse_ms); + } + + float get_average_ms() { + if (ms_time.size() == 0) { + return 0.f; + } + float sum = 0.f; + for (auto i : ms_time){ + sum += i; + } + return sum / ms_time.size(); + } + + float get_sum_ms(){ + if (ms_time.size() == 0) { + return 0.f; + } + float sum = 0.f; + for (auto i : ms_time){ + sum += i; + } + return sum; + } + + // return tile (0-99) time. + float get_tile_time(float tile) { + + if (tile <0 || tile > 100) { + return -1.f; + } + int total_items = (int)ms_time.size(); + if (total_items <= 0) { + return -2.f; + } + ms_time.sort(); + int pos = (int)(tile * total_items / 100); + auto it = ms_time.begin(); + for (int i = 0; i < pos; ++i) { + ++it; + } + return *it; + } + + const std::list get_time_stat() { + return ms_time; + } + +private: + std::chrono::time_point tstart; + std::chrono::time_point tend; + std::list ms_time; +}; + +#endif /* timer_h */