From 8d856ff08be2497eae2855ad368412329eb1748e Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Fri, 29 Jan 2021 16:32:30 +0800 Subject: [PATCH 01/66] add lite --- deploy/lite/Makefile | 80 + deploy/lite/clipper.cpp | 4394 ++++++++++++++++++++++++++++++++ deploy/lite/clipper.hpp | 423 +++ deploy/lite/cls_process.cc | 43 + deploy/lite/cls_process.h | 29 + deploy/lite/config.txt | 5 + deploy/lite/crnn_process.cc | 115 + deploy/lite/crnn_process.h | 38 + deploy/lite/db_post_process.cc | 301 +++ deploy/lite/db_post_process.h | 62 + deploy/lite/ocr_db_crnn.cc | 409 +++ deploy/lite/prepare.sh | 9 + deploy/lite/readme.md | 269 ++ deploy/lite/readme_en.md | 246 ++ doc/imgs_results/lite_demo.png | Bin 0 -> 96358 bytes 15 files changed, 6423 insertions(+) create mode 100644 deploy/lite/Makefile create mode 100644 deploy/lite/clipper.cpp create mode 100644 deploy/lite/clipper.hpp create mode 100644 deploy/lite/cls_process.cc create mode 100644 deploy/lite/cls_process.h create mode 100644 deploy/lite/config.txt create mode 100644 deploy/lite/crnn_process.cc create mode 100644 deploy/lite/crnn_process.h create mode 100644 deploy/lite/db_post_process.cc create mode 100644 deploy/lite/db_post_process.h create mode 100644 deploy/lite/ocr_db_crnn.cc create mode 100644 deploy/lite/prepare.sh create mode 100644 deploy/lite/readme.md create mode 100644 deploy/lite/readme_en.md create mode 100644 doc/imgs_results/lite_demo.png diff --git a/deploy/lite/Makefile b/deploy/lite/Makefile new file mode 100644 index 00000000..4c30d644 --- /dev/null +++ b/deploy/lite/Makefile @@ -0,0 +1,80 @@ +ARM_ABI = arm8 +export ARM_ABI + +include ../Makefile.def + +LITE_ROOT=../../../ + +THIRD_PARTY_DIR=${LITE_ROOT}/third_party + +OPENCV_VERSION=opencv4.1.0 + +OPENCV_LIBS = ../../../third_party/${OPENCV_VERSION}/arm64-v8a/libs/libopencv_imgcodecs.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/libs/libopencv_imgproc.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/libs/libopencv_core.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libtegra_hal.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibjpeg-turbo.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibwebp.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibpng.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibjasper.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibtiff.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libIlmImf.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libtbb.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libcpufeatures.a + +OPENCV_INCLUDE = -I../../../third_party/${OPENCV_VERSION}/arm64-v8a/include + +CXX_INCLUDES = $(INCLUDES) ${OPENCV_INCLUDE} -I$(LITE_ROOT)/cxx/include + +CXX_LIBS = ${OPENCV_LIBS} -L$(LITE_ROOT)/cxx/lib/ -lpaddle_light_api_shared $(SYSTEM_LIBS) + +############################################################### +# How to use one of static libaray: # +# `libpaddle_api_full_bundled.a` # +# `libpaddle_api_light_bundled.a` # +############################################################### +# Note: default use lite's shared library. # +############################################################### +# 1. Comment above line using `libpaddle_light_api_shared.so` +# 2. Undo comment below line using `libpaddle_api_light_bundled.a` + +#CXX_LIBS = $(LITE_ROOT)/cxx/lib/libpaddle_api_light_bundled.a $(SYSTEM_LIBS) + +ocr_db_crnn: fetch_opencv ocr_db_crnn.o crnn_process.o db_post_process.o clipper.o cls_process.o + $(CC) $(SYSROOT_LINK) $(CXXFLAGS_LINK) ocr_db_crnn.o crnn_process.o db_post_process.o clipper.o cls_process.o -o ocr_db_crnn $(CXX_LIBS) $(LDFLAGS) + +ocr_db_crnn.o: ocr_db_crnn.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o ocr_db_crnn.o -c ocr_db_crnn.cc + +crnn_process.o: fetch_opencv crnn_process.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o crnn_process.o -c crnn_process.cc + +cls_process.o: fetch_opencv cls_process.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o cls_process.o -c cls_process.cc + +db_post_process.o: fetch_clipper fetch_opencv db_post_process.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o db_post_process.o -c db_post_process.cc + +clipper.o: fetch_clipper + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o clipper.o -c clipper.cpp + +fetch_clipper: + @test -e clipper.hpp || \ + ( echo "Fetch clipper " && \ + wget -c https://paddle-inference-dist.cdn.bcebos.com/PaddleLite/Clipper/clipper.hpp) + @ test -e clipper.cpp || \ + wget -c https://paddle-inference-dist.cdn.bcebos.com/PaddleLite/Clipper/clipper.cpp + +fetch_opencv: + @ test -d ${THIRD_PARTY_DIR} || mkdir ${THIRD_PARTY_DIR} + @ test -e ${THIRD_PARTY_DIR}/${OPENCV_VERSION}.tar.gz || \ + (echo "fetch opencv libs" && \ + wget -P ${THIRD_PARTY_DIR} https://paddle-inference-dist.bj.bcebos.com/${OPENCV_VERSION}.tar.gz) + @ test -d ${THIRD_PARTY_DIR}/${OPENCV_VERSION} || \ + tar -zxvf ${THIRD_PARTY_DIR}/${OPENCV_VERSION}.tar.gz -C ${THIRD_PARTY_DIR} + + +.PHONY: clean +clean: + rm -f ocr_db_crnn.o clipper.o db_post_process.o crnn_process.o cls_process.o + rm -f ocr_db_crnn diff --git a/deploy/lite/clipper.cpp b/deploy/lite/clipper.cpp new file mode 100644 index 00000000..176d8654 --- /dev/null +++ b/deploy/lite/clipper.cpp @@ -0,0 +1,4394 @@ +// 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. + +/******************************************************************************* +* * +* 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 "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/lite/clipper.hpp b/deploy/lite/clipper.hpp new file mode 100644 index 00000000..384a6cf4 --- /dev/null +++ b/deploy/lite/clipper.hpp @@ -0,0 +1,423 @@ +/******************************************************************************* +* * +* 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/lite/cls_process.cc b/deploy/lite/cls_process.cc new file mode 100644 index 00000000..9f5c3e94 --- /dev/null +++ b/deploy/lite/cls_process.cc @@ -0,0 +1,43 @@ +// 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 "cls_process.h" //NOLINT +#include +#include +#include + +const std::vector rec_image_shape{3, 48, 192}; + +cv::Mat ClsResizeImg(cv::Mat img) { + int imgC, imgH, imgW; + imgC = rec_image_shape[0]; + imgH = rec_image_shape[1]; + imgW = rec_image_shape[2]; + + float ratio = static_cast(img.cols) / static_cast(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_LINEAR); + if (resize_w < imgW) { + cv::copyMakeBorder(resize_img, resize_img, 0, 0, 0, imgW - resize_w, + cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0)); + } + return resize_img; +} \ No newline at end of file diff --git a/deploy/lite/cls_process.h b/deploy/lite/cls_process.h new file mode 100644 index 00000000..eedeeb9b --- /dev/null +++ b/deploy/lite/cls_process.h @@ -0,0 +1,29 @@ +// 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. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "math.h" //NOLINT +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" + +cv::Mat ClsResizeImg(cv::Mat img); \ No newline at end of file diff --git a/deploy/lite/config.txt b/deploy/lite/config.txt new file mode 100644 index 00000000..670b2ff0 --- /dev/null +++ b/deploy/lite/config.txt @@ -0,0 +1,5 @@ +max_side_len 960 +det_db_thresh 0.3 +det_db_box_thresh 0.5 +det_db_unclip_ratio 1.6 +use_direction_classify 0 \ No newline at end of file diff --git a/deploy/lite/crnn_process.cc b/deploy/lite/crnn_process.cc new file mode 100644 index 00000000..7528f36f --- /dev/null +++ b/deploy/lite/crnn_process.cc @@ -0,0 +1,115 @@ +// 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 "crnn_process.h" //NOLINT +#include +#include +#include + +const std::vector rec_image_shape{3, 32, 320}; + +cv::Mat CrnnResizeImg(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]; + + imgW = int(32 * wh_ratio); + + float ratio = static_cast(img.cols) / static_cast(img.rows); + int resize_w, resize_h; + if (ceilf(imgH * ratio) > imgW) + resize_w = imgW; + else + resize_w = static_cast(ceilf(imgH * ratio)); + cv::Mat resize_img; + cv::resize(img, resize_img, cv::Size(resize_w, imgH), 0.f, 0.f, + cv::INTER_LINEAR); + + return resize_img; +} + +std::vector ReadDict(std::string path) { + std::ifstream in(path); + std::string filename; + std::string line; + std::vector m_vec; + if (in) { + while (getline(in, line)) { + m_vec.push_back(line); + } + } else { + std::cout << "no such file" << std::endl; + } + return m_vec; +} + +cv::Mat GetRotateCropImage(cv::Mat srcimage, + std::vector> box) { + cv::Mat image; + srcimage.copyTo(image); + 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; + image(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 = + static_cast(sqrt(pow(points[0][0] - points[1][0], 2) + + pow(points[0][1] - points[1][1], 2))); + int img_crop_height = + static_cast(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); + + const float ratio = 1.5; + if (static_cast(dst_img.rows) >= + static_cast(dst_img.cols) * ratio) { + 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; + } else { + return dst_img; + } +} diff --git a/deploy/lite/crnn_process.h b/deploy/lite/crnn_process.h new file mode 100644 index 00000000..29e67906 --- /dev/null +++ b/deploy/lite/crnn_process.h @@ -0,0 +1,38 @@ +// 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. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "math.h" //NOLINT +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" + +cv::Mat CrnnResizeImg(cv::Mat img, float wh_ratio); + +std::vector ReadDict(std::string path); + +cv::Mat GetRotateCropImage(cv::Mat srcimage, std::vector> box); + +template +inline size_t Argmax(ForwardIterator first, ForwardIterator last) { + return std::distance(first, std::max_element(first, last)); +} diff --git a/deploy/lite/db_post_process.cc b/deploy/lite/db_post_process.cc new file mode 100644 index 00000000..0c1c8b92 --- /dev/null +++ b/deploy/lite/db_post_process.cc @@ -0,0 +1,301 @@ +// 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 "db_post_process.h" // NOLINT +#include +#include + +void GetContourArea(std::vector> 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; +} + +cv::RotatedRect Unclip(std::vector> box, + float unclip_ratio) { + float distance = 1.0; + + GetContourArea(box, unclip_ratio, distance); + + ClipperLib::ClipperOffset offset; + ClipperLib::Path p; + p << ClipperLib::IntPoint(static_cast(box[0][0]), + static_cast(box[0][1])) + << ClipperLib::IntPoint(static_cast(box[1][0]), + static_cast(box[1][1])) + << ClipperLib::IntPoint(static_cast(box[2][0]), + static_cast(box[2][1])) + << ClipperLib::IntPoint(static_cast(box[3][0]), + static_cast(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; +} + +std::vector> Mat2Vector(cv::Mat mat) { + std::vector> img_vec; + std::vector tmp; + + for (int i = 0; i < mat.rows; ++i) { + tmp.clear(); + for (int j = 0; j < mat.cols; ++j) { + tmp.push_back(mat.at(i, j)); + } + img_vec.push_back(tmp); + } + return img_vec; +} + +bool XsortFp32(std::vector a, std::vector b) { + if (a[0] != b[0]) + return a[0] < b[0]; + return false; +} + +bool XsortInt(std::vector a, std::vector b) { + if (a[0] != b[0]) + return a[0] < b[0]; + return false; +} + +std::vector> +OrderPointsClockwise(std::vector> pts) { + std::vector> box = pts; + std::sort(box.begin(), box.end(), XsortInt); + + 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; +} + +std::vector> GetMiniBoxes(cv::RotatedRect box, float &ssid) { + ssid = std::min(box.size.width, box.size.height); + + cv::Mat points; + cv::boxPoints(box, points); + + auto array = Mat2Vector(points); + std::sort(array.begin(), array.end(), XsortFp32); + + std::vector 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; +} + +float BoxScoreFast(std::vector> 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( + static_cast(std::floorf(*(std::min_element(box_x, box_x + 4)))), 0, + width - 1); + int xmax = + clamp(static_cast(std::ceilf(*(std::max_element(box_x, box_x + 4)))), + 0, width - 1); + int ymin = clamp( + static_cast(std::floorf(*(std::min_element(box_y, box_y + 4)))), 0, + height - 1); + int ymax = + clamp(static_cast(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(static_cast(array[0][0]) - xmin, + static_cast(array[0][1]) - ymin); + root_point[1] = cv::Point(static_cast(array[1][0]) - xmin, + static_cast(array[1][1]) - ymin); + root_point[2] = cv::Point(static_cast(array[2][0]) - xmin, + static_cast(array[2][1]) - ymin); + root_point[3] = cv::Point(static_cast(array[3][0]) - xmin, + static_cast(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>> +BoxesFromBitmap(const cv::Mat pred, const cv::Mat bitmap, + std::map Config) { + const int min_size = 3; + const int max_candidates = 1000; + const float box_thresh = static_cast(Config["det_db_box_thresh"]); + const float unclip_ratio = static_cast(Config["det_db_unclip_ratio"]); + + 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; + if (contours[i].size() <= 2) + continue; + + cv::RotatedRect box = cv::minAreaRect(contours[i]); + auto array = GetMiniBoxes(box, ssid); + + auto box_for_unclip = array; + // end get_mini_box + + if (ssid < min_size) { + continue; + } + + float score; + score = BoxScoreFast(array, pred); + // end box_score_fast + if (score < box_thresh) + continue; + + // start for unclip + cv::RotatedRect points = Unclip(box_for_unclip, unclip_ratio); + if (points.size.height < 1.001 && points.size.width < 1.001) + continue; + // end for unclip + + cv::RotatedRect clipbox = points; + auto cliparray = GetMiniBoxes(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{ + static_cast(clamp( + roundf(cliparray[num_pt][0] / float(width) * float(dest_width)), + float(0), float(dest_width))), + static_cast(clamp( + roundf(cliparray[num_pt][1] / float(height) * float(dest_height)), + float(0), float(dest_height)))}; + intcliparray.push_back(a); + } + boxes.push_back(intcliparray); + + } // end for + return boxes; +} + +std::vector>> +FilterTagDetRes(std::vector>> boxes, float ratio_h, + float ratio_w, cv::Mat srcimg) { + int oriimg_h = srcimg.rows; + int oriimg_w = srcimg.cols; + + std::vector>> root_points; + for (int n = 0; n < static_cast(boxes.size()); n++) { + boxes[n] = OrderPointsClockwise(boxes[n]); + for (int m = 0; m < static_cast(boxes[0].size()); m++) { + boxes[n][m][0] /= ratio_w; + boxes[n][m][1] /= ratio_h; + + boxes[n][m][0] = + static_cast(std::min(std::max(boxes[n][m][0], 0), oriimg_w - 1)); + boxes[n][m][1] = + static_cast(std::min(std::max(boxes[n][m][1], 0), oriimg_h - 1)); + } + } + + for (int n = 0; n < boxes.size(); n++) { + int rect_width, rect_height; + rect_width = + static_cast(sqrt(pow(boxes[n][0][0] - boxes[n][1][0], 2) + + pow(boxes[n][0][1] - boxes[n][1][1], 2))); + rect_height = + static_cast(sqrt(pow(boxes[n][0][0] - boxes[n][3][0], 2) + + pow(boxes[n][0][1] - boxes[n][3][1], 2))); + if (rect_width <= 4 || rect_height <= 4) + continue; + root_points.push_back(boxes[n]); + } + return root_points; +} diff --git a/deploy/lite/db_post_process.h b/deploy/lite/db_post_process.h new file mode 100644 index 00000000..06dbcb2c --- /dev/null +++ b/deploy/lite/db_post_process.h @@ -0,0 +1,62 @@ +// 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. + +#pragma once + +#include + +#include +#include +#include + +#include "clipper.hpp" +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" + +template T clamp(T x, T min, T max) { + if (x > max) + return max; + if (x < min) + return min; + return x; +} + +std::vector> Mat2Vector(cv::Mat mat); + +void GetContourArea(std::vector> box, float unclip_ratio, + float &distance); + +cv::RotatedRect Unclip(std::vector> box, float unclip_ratio); + +std::vector> Mat2Vector(cv::Mat mat); + +bool XsortFp32(std::vector a, std::vector b); + +bool XsortInt(std::vector a, std::vector b); + +std::vector> +OrderPointsClockwise(std::vector> pts); + +std::vector> GetMiniBoxes(cv::RotatedRect box, float &ssid); + +float BoxScoreFast(std::vector> box_array, cv::Mat pred); + +std::vector>> +BoxesFromBitmap(const cv::Mat pred, const cv::Mat bitmap, + std::map Config); + +std::vector>> +FilterTagDetRes(std::vector>> boxes, float ratio_h, + float ratio_w, cv::Mat srcimg); diff --git a/deploy/lite/ocr_db_crnn.cc b/deploy/lite/ocr_db_crnn.cc new file mode 100644 index 00000000..200d3464 --- /dev/null +++ b/deploy/lite/ocr_db_crnn.cc @@ -0,0 +1,409 @@ +// 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 "paddle_api.h" // NOLINT +#include + +#include "cls_process.h" +#include "crnn_process.h" +#include "db_post_process.h" + +using namespace paddle::lite_api; // NOLINT +using namespace std; + +// fill tensor with mean and scale and trans layout: nhwc -> nchw, neon speed up +void NeonMeanScale(const float *din, float *dout, int size, + const std::vector mean, + const std::vector scale) { + if (mean.size() != 3 || scale.size() != 3) { + std::cerr << "[ERROR] mean or scale size must equal to 3\n"; + exit(1); + } + 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(scale[0]); + float32x4_t vscale1 = vdupq_n_f32(scale[1]); + float32x4_t vscale2 = vdupq_n_f32(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]; + } +} + +// resize image to a size multiple of 32 which is required by the network +cv::Mat DetResizeImg(const cv::Mat img, int max_size_len, + std::vector &ratio_hw) { + 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 = static_cast(max_size_len) / static_cast(h); + } else { + ratio = static_cast(max_size_len) / static_cast(w); + } + } + + int resize_h = static_cast(float(h) * ratio); + int resize_w = static_cast(float(w) * ratio); + if (resize_h % 32 == 0) + resize_h = resize_h; + else if (resize_h / 32 < 1 + 1e-5) + 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 + 1e-5) + 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_hw.push_back(static_cast(resize_h) / static_cast(h)); + ratio_hw.push_back(static_cast(resize_w) / static_cast(w)); + return resize_img; +} + +cv::Mat RunClsModel(cv::Mat img, std::shared_ptr predictor_cls, + const float thresh = 0.9) { + std::vector mean = {0.5f, 0.5f, 0.5f}; + std::vector scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + + cv::Mat srcimg; + img.copyTo(srcimg); + cv::Mat crop_img; + img.copyTo(crop_img); + cv::Mat resize_img; + + int index = 0; + float wh_ratio = + static_cast(crop_img.cols) / static_cast(crop_img.rows); + + resize_img = ClsResizeImg(crop_img); + 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(predictor_cls->GetInput(0))); + input_tensor0->Resize({1, 3, resize_img.rows, resize_img.cols}); + auto *data0 = input_tensor0->mutable_data(); + + NeonMeanScale(dimg, data0, resize_img.rows * resize_img.cols, mean, scale); + // Run CLS predictor + predictor_cls->Run(); + + // Get output and run postprocess + std::unique_ptr softmax_out( + std::move(predictor_cls->GetOutput(0))); + auto *softmax_scores = softmax_out->mutable_data(); + auto softmax_out_shape = softmax_out->shape(); + float score = 0; + int label = 0; + for (int i = 0; i < softmax_out_shape[1]; i++) { + if (softmax_scores[i] > score) { + score = softmax_scores[i]; + label = i; + } + } + if (label % 2 == 1 && score > thresh) { + cv::rotate(srcimg, srcimg, 1); + } + return srcimg; +} + +void RunRecModel(std::vector>> boxes, cv::Mat img, + std::shared_ptr predictor_crnn, + std::vector &rec_text, + std::vector &rec_text_score, + std::vector charactor_dict, + std::shared_ptr predictor_cls, + int use_direction_classify) { + std::vector mean = {0.5f, 0.5f, 0.5f}; + std::vector scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + + cv::Mat srcimg; + img.copyTo(srcimg); + cv::Mat crop_img; + cv::Mat resize_img; + + int index = 0; + for (int i = boxes.size() - 1; i >= 0; i--) { + crop_img = GetRotateCropImage(srcimg, boxes[i]); + if (use_direction_classify >= 1) { + crop_img = RunClsModel(crop_img, predictor_cls); + } + float wh_ratio = + static_cast(crop_img.cols) / static_cast(crop_img.rows); + + resize_img = CrnnResizeImg(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(predictor_crnn->GetInput(0))); + input_tensor0->Resize({1, 3, resize_img.rows, resize_img.cols}); + auto *data0 = input_tensor0->mutable_data(); + + NeonMeanScale(dimg, data0, resize_img.rows * resize_img.cols, mean, scale); + //// Run CRNN predictor + predictor_crnn->Run(); + + // Get output and run postprocess + std::unique_ptr output_tensor0( + std::move(predictor_crnn->GetOutput(0))); + auto *predict_batch = output_tensor0->data(); + auto predict_shape = output_tensor0->shape(); + + // ctc decode + std::string str_res; + int argmax_idx; + int last_index = 0; + float score = 0.f; + int count = 0; + float max_value = 0.0f; + + for (int n = 0; n < predict_shape[1]; n++) { + argmax_idx = int(Argmax(&predict_batch[n * predict_shape[2]], + &predict_batch[(n + 1) * predict_shape[2]])); + max_value = + float(*std::max_element(&predict_batch[n * predict_shape[2]], + &predict_batch[(n + 1) * predict_shape[2]])); + if (argmax_idx > 0 && (!(i > 0 && argmax_idx == last_index))) { + score += max_value; + count += 1; + str_res += charactor_dict[argmax_idx]; + } + last_index = argmax_idx; + } + score /= count; + rec_text.push_back(str_res); + rec_text_score.push_back(score); + } +} + +std::vector>> +RunDetModel(std::shared_ptr predictor, cv::Mat img, + std::map Config) { + // Read img + int max_side_len = int(Config["max_side_len"]); + + cv::Mat srcimg; + img.copyTo(srcimg); + + std::vector ratio_hw; + img = DetResizeImg(img, max_side_len, ratio_hw); + cv::Mat img_fp; + img.convertTo(img_fp, CV_32FC3, 1.0 / 255.f); + + // Prepare input data from image + std::unique_ptr input_tensor0(std::move(predictor->GetInput(0))); + input_tensor0->Resize({1, 3, img_fp.rows, img_fp.cols}); + auto *data0 = input_tensor0->mutable_data(); + + std::vector mean = {0.485f, 0.456f, 0.406f}; + std::vector scale = {1 / 0.229f, 1 / 0.224f, 1 / 0.225f}; + const float *dimg = reinterpret_cast(img_fp.data); + NeonMeanScale(dimg, data0, img_fp.rows * img_fp.cols, mean, scale); + + // Run predictor + predictor->Run(); + + // Get output and post process + std::unique_ptr output_tensor( + std::move(predictor->GetOutput(0))); + auto *outptr = output_tensor->data(); + auto shape_out = output_tensor->shape(); + + // Save output + float pred[shape_out[2] * shape_out[3]]; + unsigned char cbuf[shape_out[2] * shape_out[3]]; + + for (int i = 0; i < int(shape_out[2] * shape_out[3]); i++) { + pred[i] = static_cast(outptr[i]); + cbuf[i] = static_cast((outptr[i]) * 255); + } + + cv::Mat cbuf_map(shape_out[2], shape_out[3], CV_8UC1, + reinterpret_cast(cbuf)); + cv::Mat pred_map(shape_out[2], shape_out[3], CV_32F, + reinterpret_cast(pred)); + + const double threshold = double(Config["det_db_thresh"]) * 255; + const double maxvalue = 255; + cv::Mat bit_map; + cv::threshold(cbuf_map, bit_map, threshold, maxvalue, cv::THRESH_BINARY); + cv::Mat dilation_map; + cv::Mat dila_ele = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2)); + cv::dilate(bit_map, dilation_map, dila_ele); + auto boxes = BoxesFromBitmap(pred_map, dilation_map, Config); + + std::vector>> filter_boxes = + FilterTagDetRes(boxes, ratio_hw[0], ratio_hw[1], srcimg); + + return filter_boxes; +} + +std::shared_ptr loadModel(std::string model_file) { + MobileConfig config; + config.set_model_from_file(model_file); + + std::shared_ptr predictor = + CreatePaddlePredictor(config); + return predictor; +} + +cv::Mat Visualization(cv::Mat srcimg, + std::vector>> boxes) { + cv::Point rook_points[boxes.size()][4]; + for (int n = 0; n < boxes.size(); n++) { + for (int m = 0; m < boxes[0].size(); m++) { + rook_points[n][m] = cv::Point(static_cast(boxes[n][m][0]), + static_cast(boxes[n][m][1])); + } + } + cv::Mat img_vis; + srcimg.copyTo(img_vis); + for (int n = 0; n < boxes.size(); n++) { + const cv::Point *ppt[1] = {rook_points[n]}; + int npt[] = {4}; + cv::polylines(img_vis, ppt, npt, 1, 1, CV_RGB(0, 255, 0), 2, 8, 0); + } + + cv::imwrite("./vis.jpg", img_vis); + std::cout << "The detection visualized image saved in ./vis.jpg" << std::endl; + return img_vis; +} + +std::vector split(const std::string &str, + const std::string &delim) { + std::vector res; + if ("" == str) + return res; + char *strs = new char[str.length() + 1]; + std::strcpy(strs, str.c_str()); + + char *d = new char[delim.length() + 1]; + std::strcpy(d, delim.c_str()); + + char *p = std::strtok(strs, d); + while (p) { + string s = p; + res.push_back(s); + p = std::strtok(NULL, d); + } + + return res; +} + +std::map LoadConfigTxt(std::string config_path) { + auto config = ReadDict(config_path); + + std::map dict; + for (int i = 0; i < config.size(); i++) { + std::vector res = split(config[i], " "); + dict[res[0]] = stod(res[1]); + } + return dict; +} + +int main(int argc, char **argv) { + if (argc < 5) { + std::cerr << "[ERROR] usage: " << argv[0] + << " det_model_file cls_model_file rec_model_file image_path " + "charactor_dict\n"; + exit(1); + } + std::string det_model_file = argv[1]; + std::string rec_model_file = argv[2]; + std::string cls_model_file = argv[3]; + std::string img_path = argv[4]; + std::string dict_path = argv[5]; + + //// load config from txt file + auto Config = LoadConfigTxt("./config.txt"); + int use_direction_classify = int(Config["use_direction_classify"]); + + auto start = std::chrono::system_clock::now(); + + auto det_predictor = loadModel(det_model_file); + auto rec_predictor = loadModel(rec_model_file); + auto cls_predictor = loadModel(cls_model_file); + + auto charactor_dict = ReadDict(dict_path); + charactor_dict.insert(charactor_dict.begin(), "#"); // blank char for ctc + charactor_dict.push_back(" "); +std: + cout << charactor_dict[0] << " " << charactor_dict[1] << std::endl; + cv::Mat srcimg = cv::imread(img_path, cv::IMREAD_COLOR); + auto boxes = RunDetModel(det_predictor, srcimg, Config); + + std::vector rec_text; + std::vector rec_text_score; + + RunRecModel(boxes, srcimg, rec_predictor, rec_text, rec_text_score, + charactor_dict, cls_predictor, use_direction_classify); + + auto end = std::chrono::system_clock::now(); + auto duration = + std::chrono::duration_cast(end - start); + + //// visualization + auto img_vis = Visualization(srcimg, boxes); + + //// print recognized text + for (int i = 0; i < rec_text.size(); i++) { + std::cout << i << "\t" << rec_text[i] << "\t" << rec_text_score[i] + << std::endl; + } + + std::cout << "花费了" + << double(duration.count()) * + std::chrono::microseconds::period::num / + std::chrono::microseconds::period::den + << "秒" << std::endl; + + return 0; +} \ No newline at end of file diff --git a/deploy/lite/prepare.sh b/deploy/lite/prepare.sh new file mode 100644 index 00000000..daaa30c4 --- /dev/null +++ b/deploy/lite/prepare.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +mkdir -p $1/demo/cxx/ocr/debug/ +cp ../../ppocr/utils/ppocr_keys_v1.txt $1/demo/cxx/ocr/debug/ +cp -r ./* $1/demo/cxx/ocr/ +cp ./config.txt $1/demo/cxx/ocr/debug/ +cp ../../doc/imgs/11.jpg $1/demo/cxx/ocr/debug/ + +echo "Prepare Done" diff --git a/deploy/lite/readme.md b/deploy/lite/readme.md new file mode 100644 index 00000000..4775c19c --- /dev/null +++ b/deploy/lite/readme.md @@ -0,0 +1,269 @@ +# 端侧部署 + +本教程将介绍基于[Paddle Lite](https://github.com/PaddlePaddle/Paddle-Lite) 在移动端部署PaddleOCR超轻量中文检测、识别模型的详细步骤。 + +Paddle Lite是飞桨轻量化推理引擎,为手机、IOT端提供高效推理能力,并广泛整合跨平台硬件,为端侧部署及应用落地问题提供轻量化的部署方案。 + + +## 1. 准备环境 + +### 运行准备 +- 电脑(编译Paddle Lite) +- 安卓手机(armv7或armv8) + +***注意: PaddleOCR 移动端部署当前不支持动态图模型,只支持静态图保存的模型。当前PaddleOCR静态图的分支是`develop`。*** + +### 1.1 准备交叉编译环境 +交叉编译环境用于编译 Paddle Lite 和 PaddleOCR 的C++ demo。 +支持多种开发环境,不同开发环境的编译流程请参考对应文档。 + +1. [Docker](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#docker) +2. [Linux](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#linux) +3. [MAC OS](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#mac-os) + +### 1.2 准备预测库 + +预测库有两种获取方式: +- 1. 直接下载,预测库下载链接如下: + + | 平台 | 预测库下载链接 | + |---|---| + |Android|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv7.gcc.c++_shared.with_extra.with_cv.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv8.gcc.c++_shared.with_extra.with_cv.tar.gz)| + |IOS|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv7.with_cv.with_extra.with_log.tiny_publish.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv8.with_cv.with_extra.with_log.tiny_publish.tar.gz)| + + 注:1. 上述预测库为PaddleLite 2.8分支编译得到,有关PaddleLite 2.8 详细信息可参考[链接](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8)。 + +- 2. [推荐]编译Paddle-Lite得到预测库,Paddle-Lite的编译方式如下: +``` +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +# 切换到Paddle-Lite release/v2.8 稳定分支 +git checkout release/v2.8 +./lite/tools/build_android.sh --arch=armv8 --with_cv=ON --with_extra=ON +``` + +注意:编译Paddle-Lite获得预测库时,需要打开`--with_cv=ON --with_extra=ON`两个选项,`--arch`表示`arm`版本,这里指定为armv8, +更多编译命令 +介绍请参考[链接](https://paddle-lite.readthedocs.io/zh/latest/user_guides/Compile/Android.html#id2)。 + +直接下载预测库并解压后,可以得到`inference_lite_lib.android.armv8/`文件夹,通过编译Paddle-Lite得到的预测库位于 +`Paddle-Lite/build.lite.android.armv8.gcc/inference_lite_lib.android.armv8/`文件夹下。 +预测库的文件目录如下: +``` +inference_lite_lib.android.armv8/ +|-- cxx C++ 预测库和头文件 +| |-- include C++ 头文件 +| | |-- paddle_api.h +| | |-- paddle_image_preprocess.h +| | |-- paddle_lite_factory_helper.h +| | |-- paddle_place.h +| | |-- paddle_use_kernels.h +| | |-- paddle_use_ops.h +| | `-- paddle_use_passes.h +| `-- lib C++预测库 +| |-- libpaddle_api_light_bundled.a C++静态库 +| `-- libpaddle_light_api_shared.so C++动态库 +|-- java Java预测库 +| |-- jar +| | `-- PaddlePredictor.jar +| |-- so +| | `-- libpaddle_lite_jni.so +| `-- src +|-- demo C++和Java示例代码 +| |-- cxx C++ 预测库demo +| `-- java Java 预测库demo +``` + +## 2 开始运行 + +### 2.1 模型优化 + +Paddle-Lite 提供了多种策略来自动优化原始的模型,其中包括量化、子图融合、混合调度、Kernel优选等方法,使用Paddle-lite的opt工具可以自动 +对inference模型进行优化,优化后的模型更轻量,模型运行速度更快。 + +如果已经准备好了 `.nb` 结尾的模型文件,可以跳过此步骤。 + +下述表格中也提供了一系列中文移动端模型: + +|模型版本|模型简介|模型大小|检测模型|文本方向分类模型|识别模型|Paddle-Lite版本| +|---|---|---|---|---|---|---| +|V2.0|超轻量中文OCR 移动端模型|8.1M|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_opt.nb)|v2.8| +|V2.0(slim)|超轻量中文OCR 移动端模型|3.5M|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_prune_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_quant_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_quant_opt.nb)|v2.8| + +注意:V2.0 3.0M 轻量模型是使用PaddleSlim优化后的,需要配合Paddle-Lite最新预测库使用。 + +如果直接使用上述表格中的模型进行部署,可略过下述步骤,直接阅读 [2.2节](#2.2与手机联调)。 + +如果要部署的模型不在上述表格中,则需要按照如下步骤获得优化后的模型。 + +模型优化需要Paddle-Lite的opt可执行文件,可以通过编译Paddle-Lite源码获得,编译步骤如下: +``` +# 如果准备环境时已经clone了Paddle-Lite,则不用重新clone Paddle-Lite +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +git checkout release/v2.8 +# 启动编译 +./lite/tools/build.sh build_optimize_tool +``` + +编译完成后,opt文件位于`build.opt/lite/api/`下,可通过如下方式查看opt的运行选项和使用方式; +``` +cd build.opt/lite/api/ +./opt +``` + +|选项|说明| +|---|---| +|--model_dir|待优化的PaddlePaddle模型(非combined形式)的路径| +|--model_file|待优化的PaddlePaddle模型(combined形式)的网络结构文件路径| +|--param_file|待优化的PaddlePaddle模型(combined形式)的权重文件路径| +|--optimize_out_type|输出模型类型,目前支持两种类型:protobuf和naive_buffer,其中naive_buffer是一种更轻量级的序列化/反序列化实现。若您需要在mobile端执行模型预测,请将此选项设置为naive_buffer。默认为protobuf| +|--optimize_out|优化模型的输出路径| +|--valid_targets|指定模型可执行的backend,默认为arm。目前可支持x86、arm、opencl、npu、xpu,可以同时指定多个backend(以空格分隔),Model Optimize Tool将会自动选择最佳方式。如果需要支持华为NPU(Kirin 810/990 Soc搭载的达芬奇架构NPU),应当设置为npu, arm| +|--record_tailoring_info|当使用 根据模型裁剪库文件 功能时,则设置该选项为true,以记录优化后模型含有的kernel和OP信息,默认为false| + +`--model_dir`适用于待优化的模型是非combined方式,PaddleOCR的inference模型是combined方式,即模型结构和模型参数使用单独一个文件存储。 + +下面以PaddleOCR的超轻量中文模型为例,介绍使用编译好的opt文件完成inference模型到Paddle-Lite优化模型的转换。 + +``` +# 【推荐】 下载PaddleOCR V2.0版本的中英文 inference模型 +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_slim_infer.tar && tar xf ch_ppocr_mobile_v1.1_det_prune_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_slim_infer.tar && tar xf ch_ppocr_mobile_v1.1_rec_quant_infer.tar +# 转换V2.0检测模型 +./opt --model_file=./ch_ppocr_mobile_v1.1_det_prune_infer/model --param_file=./ch_ppocr_mobile_v1.1_det_prune_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_det_prune_opt --valid_targets=arm --optimize_out_type=naive_buffer +# 转换V2.0识别模型 +./opt --model_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/model --param_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_rec_quant_opt --valid_targets=arm --optimize_out_type=naive_buffer +``` + +转换成功后,当前目录下会多出`.nb`结尾的文件,即是转换成功的模型文件。 + +注意:使用paddle-lite部署时,需要使用opt工具优化后的模型。 opt 工具的输入模型是paddle保存的inference模型 + + +### 2.2 与手机联调 + +首先需要进行一些准备工作。 + 1. 准备一台arm8的安卓手机,如果编译的预测库和opt文件是armv7,则需要arm7的手机,并修改Makefile中`ARM_ABI = arm7`。 + 2. 打开手机的USB调试选项,选择文件传输模式,连接电脑。 + 3. 电脑上安装adb工具,用于调试。 adb安装方式如下: + + 3.1. MAC电脑安装ADB: + ``` + brew cask install android-platform-tools + ``` + 3.2. Linux安装ADB + ``` + sudo apt update + sudo apt install -y wget adb + ``` + 3.3. Window安装ADB + + win上安装需要去谷歌的安卓平台下载adb软件包进行安装:[链接](https://developer.android.com/studio) + + 打开终端,手机连接电脑,在终端中输入 + ``` + adb devices + ``` + 如果有device输出,则表示安装成功。 + ``` + List of devices attached + 744be294 device + ``` + + 4. 准备优化后的模型、预测库文件、测试图像和使用的字典文件。 + ``` + git clone https://github.com/PaddlePaddle/PaddleOCR.git + cd PaddleOCR/deploy/lite/ + # 运行prepare.sh,准备预测库文件、测试图像和使用的字典文件,并放置在预测库中的demo/cxx/ocr文件夹下 + sh prepare.sh /{lite prediction library path}/inference_lite_lib.android.armv8 + + # 进入OCR demo的工作目录 + cd /{lite prediction library path}/inference_lite_lib.android.armv8/ + cd demo/cxx/ocr/ + # 将C++预测动态库so文件复制到debug文件夹中 + cp ../../../cxx/lib/libpaddle_light_api_shared.so ./debug/ + ``` + + 准备测试图像,以`PaddleOCR/doc/imgs/11.jpg`为例,将测试的图像复制到`demo/cxx/ocr/debug/`文件夹下。 + 准备lite opt工具优化后的模型文件,比如使用`ch_ppocr_mobile_v1.1_det_prune_opt.nb,ch_ppocr_mobile_v1.1_rec_quant_opt.nb, ch_ppocr_mobile_cls_quant_opt.nb`,模型文件放置在`demo/cxx/ocr/debug/`文件夹下。 + + 执行完成后,ocr文件夹下将有如下文件格式: + +``` +demo/cxx/ocr/ +|-- debug/ +| |--ch_ppocr_mobile_v1.1_det_prune_opt.nb 优化后的检测模型文件 +| |--ch_ppocr_mobile_v1.1_rec_quant_opt.nb 优化后的识别模型文件 +| |--ch_ppocr_mobile_cls_quant_opt.nb 优化后的文字方向分类器模型文件 +| |--11.jpg 待测试图像 +| |--ppocr_keys_v1.txt 中文字典文件 +| |--libpaddle_light_api_shared.so C++预测库文件 +| |--config.txt DB-CRNN超参数配置 +|-- config.txt DB-CRNN超参数配置 +|-- crnn_process.cc 识别模型CRNN的预处理和后处理文件 +|-- crnn_process.h +|-- db_post_process.cc 检测模型DB的后处理文件 +|-- db_post_process.h +|-- Makefile 编译文件 +|-- ocr_db_crnn.cc C++预测源文件 +``` + +#### 注意: +1. ppocr_keys_v1.txt是中文字典文件,如果使用的 nb 模型是英文数字或其他语言的模型,需要更换为对应语言的字典。 +PaddleOCR 在ppocr/utils/下存放了多种字典,包括: +``` +dict/french_dict.txt # 法语字典 +dict/german_dict.txt # 德语字典 +ic15_dict.txt # 英文字典 +dict/japan_dict.txt # 日语字典 +dict/korean_dict.txt # 韩语字典 +ppocr_keys_v1.txt # 中文字典 +``` + +2. `config.txt` 包含了检测器、分类器的超参数,如下: +``` +max_side_len 960 # 输入图像长宽大于960时,等比例缩放图像,使得图像最长边为960 +det_db_thresh 0.3 # 用于过滤DB预测的二值化图像,设置为0.-0.3对结果影响不明显 +det_db_box_thresh 0.5 # DB后处理过滤box的阈值,如果检测存在漏框情况,可酌情减小 +det_db_unclip_ratio 1.6 # 表示文本框的紧致程度,越小则文本框更靠近文本 +use_direction_classify 0 # 是否使用方向分类器,0表示不使用,1表示使用 +``` + + 5. 启动调试 + + 上述步骤完成后就可以使用adb将文件push到手机上运行,步骤如下: + + ``` + # 执行编译,得到可执行文件ocr_db_crnn + # ocr_db_crnn可执行文件的使用方式为: + # ./ocr_db_crnn 检测模型文件 方向分类器模型文件 识别模型文件 测试图像路径 字典文件路径 + make -j + # 将编译的可执行文件移动到debug文件夹中 + mv ocr_db_crnn ./debug/ + # 将debug文件夹push到手机上 + adb push debug /data/local/tmp/ + adb shell + cd /data/local/tmp/debug + export LD_LIBRARY_PATH=${PWD}:$LD_LIBRARY_PATH + ./ocr_db_crnn ch_ppocr_mobile_v1.1_det_prune_opt.nb ch_ppocr_mobile_v1.1_rec_quant_opt.nb ch_ppocr_mobile_cls_quant_opt.nb ./11.jpg ppocr_keys_v1.txt + ``` + + 如果对代码做了修改,则需要重新编译并push到手机上。 + + 运行效果如下: + +
+ +
+ + +## FAQ +Q1:如果想更换模型怎么办,需要重新按照流程走一遍吗? +A1:如果已经走通了上述步骤,更换模型只需要替换 .nb 模型文件即可,同时要注意字典更新 + +Q2:换一个图测试怎么做? +A2:替换debug下的.jpg测试图像为你想要测试的图像,adb push 到手机上即可 + +Q3:如何封装到手机APP中? +A3:此demo旨在提供能在手机上运行OCR的核心算法部分,PaddleOCR/deploy/android_demo是将这个demo封装到手机app的示例,供参考 diff --git a/deploy/lite/readme_en.md b/deploy/lite/readme_en.md new file mode 100644 index 00000000..58f6a574 --- /dev/null +++ b/deploy/lite/readme_en.md @@ -0,0 +1,246 @@ + +# Tutorial of PaddleOCR Mobile deployment + +This tutorial will introduce how to use [paddle-lite](https://github.com/PaddlePaddle/Paddle-Lite) to deploy paddleOCR ultra-lightweight Chinese and English detection models on mobile phones. + +paddle-lite is a lightweight inference engine for PaddlePaddle. +It provides efficient inference capabilities for mobile phones and IoTs, +and extensively integrates cross-platform hardware to provide lightweight +deployment solutions for end-side deployment issues. + +## 1. Preparation + +- Computer (for Compiling Paddle Lite) +- Mobile phone (arm7 or arm8) + +***Note: PaddleOCR lite deployment currently does not support dynamic graph models, only models saved with static graph. The static branch of PaddleOCR is `develop`.*** + +## 2. Build PaddleLite library +1. [Docker](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#docker) +2. [Linux](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#linux) +3. [MAC OS](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#mac-os) + +## 3. Prepare prebuild library for android and ios + +### 3.1 Download prebuild library +|Platform|Prebuild library Download Link| +|---|---| +|Android|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv7.gcc.c++_shared.with_extra.with_cv.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv8.gcc.c++_shared.with_extra.with_cv.tar.gz)| +|IOS|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv7.with_cv.with_extra.with_log.tiny_publish.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv8.with_cv.with_extra.with_log.tiny_publish.tar.gz)| + +note: The above pre-build inference library is compiled from the PaddleLite `release/v2.8` branch. For more information about PaddleLite 2.8, please refer to [link](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8). + +### 3.2 Compile prebuild library (Recommended) +``` +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +# checkout to Paddle-Lite release/v2.8 branch +git checkout release/v2.8 +./lite/tools/build_android.sh --arch=armv8 --with_cv=ON --with_extra=ON +``` + +The structure of the prediction library is as follows: + +``` +inference_lite_lib.android.armv8/ +|-- cxx C++ prebuild library +| |-- include C++ +| | |-- paddle_api.h +| | |-- paddle_image_preprocess.h +| | |-- paddle_lite_factory_helper.h +| | |-- paddle_place.h +| | |-- paddle_use_kernels.h +| | |-- paddle_use_ops.h +| | `-- paddle_use_passes.h +| `-- lib +| |-- libpaddle_api_light_bundled.a C++ static library +| `-- libpaddle_light_api_shared.so C++ dynamic library +|-- java Java predict library +| |-- jar +| | `-- PaddlePredictor.jar +| |-- so +| | `-- libpaddle_lite_jni.so +| `-- src +|-- demo C++ and java demo +| |-- cxx +| `-- java +``` + + +## 4. Inference Model Optimization + +Paddle Lite provides a variety of strategies to automatically optimize the original training model, including quantization, sub-graph fusion, hybrid scheduling, Kernel optimization and so on. In order to make the optimization process more convenient and easy to use, Paddle Lite provide opt tools to automatically complete the optimization steps and output a lightweight, optimal executable model. + +If you have prepared the model file ending in `.nb`, you can skip this step. + +The following table also provides a series of models that can be deployed on mobile phones to recognize Chinese. +You can directly download the optimized model. + +| Version | Introduction | Model size | Detection model | Text Direction model | Recognition model | Paddle Lite branch | +| --- | --- | --- | --- | --- | --- | --- | +| V1.1 | extra-lightweight chinese OCR optimized model | 8.1M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_opt.nb) | develop | +| [slim] V1.1 | extra-lightweight chinese OCR optimized model | 3.5M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_prune_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_quant_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_quant_opt.nb) | develop | +| V1.0 | lightweight Chinese OCR optimized model | 8.6M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.0_det_opt.nb) | - | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.0_rec_opt.nb) | develop | + +If the model to be deployed is not in the above table, you need to follow the steps below to obtain the optimized model. + +``` +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +git checkout release/v2.7 +./lite/tools/build.sh build_optimize_tool +``` + +The `opt` tool can be obtained by compiling Paddle Lite. + +After the compilation is complete, the opt file is located under `build.opt/lite/api/`. + +The `opt` can optimize the inference model saved by paddle.io.save_inference_model to get the model that the paddlelite API can use. + +The usage of opt is as follows: +``` +# 【Recommend】V1.1 is better than V1.0. steps for convert V1.1 model to nb file are as follows +wget https://paddleocr.bj.bcebos.com/20-09-22/mobile-slim/det/ch_ppocr_mobile_v1.1_det_prune_infer.tar && tar xf ch_ppocr_mobile_v1.1_det_prune_infer.tar +wget https://paddleocr.bj.bcebos.com/20-09-22/mobile-slim/rec/ch_ppocr_mobile_v1.1_rec_quant_infer.tar && tar xf ch_ppocr_mobile_v1.1_rec_quant_infer.tar + +./opt --model_file=./ch_ppocr_mobile_v1.1_det_prune_infer/model --param_file=./ch_ppocr_mobile_v1.1_det_prune_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_det_prune_opt --valid_targets=arm +./opt --model_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/model --param_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_rec_quant_opt --valid_targets=arm + +# or use V1.0 model +wget https://paddleocr.bj.bcebos.com/ch_models/ch_det_mv3_db_infer.tar && tar xf ch_det_mv3_db_infer.tar +wget https://paddleocr.bj.bcebos.com/ch_models/ch_rec_mv3_crnn_infer.tar && tar xf ch_rec_mv3_crnn_infer.tar + +./opt --model_file=./ch_det_mv3_db/model --param_file=./ch_det_mv3_db/params --optimize_out_type=naive_buffer --optimize_out=./ch_det_mv3_db_opt --valid_targets=arm +./opt --model_file=./ch_rec_mv3_crnn/model --param_file=./ch_rec_mv3_crnn/params --optimize_out_type=naive_buffer --optimize_out=./ch_rec_mv3_crnn_opt --valid_targets=arm + +``` + +When the above code command is completed, there will be two more files `.nb` in the current directory, which is the converted model file. + +## 5. Run optimized model on Phone + +1. Prepare an Android phone with arm8. If the compiled prediction library and opt file are armv7, you need an arm7 phone and modify ARM_ABI = arm7 in the Makefile. + +2. Make sure the phone is connected to the computer, open the USB debugging option of the phone, and select the file transfer mode. + +3. Install the adb tool on the computer. + 3.1 Install ADB for MAC + ``` + brew cask install android-platform-tools + ``` + 3.2 Install ADB for Linux + ``` + sudo apt update + sudo apt install -y wget adb + ``` + 3.3 Install ADB for windows + [Download Link](https://developer.android.com/studio) + + Verify whether adb is installed successfully + ``` + $ adb devices + + List of devices attached + 744be294 device + ``` + + If there is `device` output, it means the installation was successful. + +4. Prepare optimized models, prediction library files, test images and dictionary files used. + +``` + git clone https://github.com/PaddlePaddle/PaddleOCR.git + cd PaddleOCR/deploy/lite/ + # run prepare.sh + sh prepare.sh /{lite prediction library path}/inference_lite_lib.android.armv8 + + # + cd /{lite prediction library path}/inference_lite_lib.android.armv8/ + cd demo/cxx/ocr/ + # copy paddle-lite C++ .so file to debug/ directory + cp ../../../cxx/lib/libpaddle_light_api_shared.so ./debug/ + + cd inference_lite_lib.android.armv8/demo/cxx/ocr/ + cp ../../../cxx/lib/libpaddle_light_api_shared.so ./debug/ + +``` + +Prepare the test image, taking `PaddleOCR/doc/imgs/11.jpg` as an example, copy the image file to the `demo/cxx/ocr/debug/` folder. +Prepare the model files optimized by the lite opt tool, `ch_det_mv3_db_opt.nb, ch_rec_mv3_crnn_opt.nb`, +and place them under the `demo/cxx/ocr/debug/` folder. + + +The structure of the OCR demo is as follows after the above command is executed: +``` +demo/cxx/ocr/ +|-- debug/ +| |--ch_ppocr_mobile_v1.1_det_prune_opt.nb Detection model +| |--ch_ppocr_mobile_v1.1_rec_quant_opt.nb Recognition model +| |--ch_ppocr_mobile_cls_quant_opt.nb Text direction classification model +| |--11.jpg Image for OCR +| |--ppocr_keys_v1.txt Dictionary file +| |--libpaddle_light_api_shared.so C++ .so file +| |--config.txt Config file +|-- config.txt +|-- crnn_process.cc +|-- crnn_process.h +|-- db_post_process.cc +|-- db_post_process.h +|-- Makefile +|-- ocr_db_crnn.cc + +``` + +#### Note: +1. ppocr_keys_v1.txt is a Chinese dictionary file. +If the nb model is used for English recognition or other language recognition, dictionary file should be replaced with a dictionary of the corresponding language. +PaddleOCR provides a variety of dictionaries under ppocr/utils/, including: +``` +dict/french_dict.txt # french +dict/german_dict.txt # german +ic15_dict.txt # english +dict/japan_dict.txt # japan +dict/korean_dict.txt # korean +ppocr_keys_v1.txt # chinese +``` + +2. `config.txt` of the detector and classifier, as shown below: +``` +max_side_len 960 # Limit the maximum image height and width to 960 +det_db_thresh 0.3 # Used to filter the binarized image of DB prediction, setting 0.-0.3 has no obvious effect on the result +det_db_box_thresh 0.5 # DDB post-processing filter box threshold, if there is a missing box detected, it can be reduced as appropriate +det_db_unclip_ratio 1.6 # Indicates the compactness of the text box, the smaller the value, the closer the text box to the text +use_direction_classify 0 # Whether to use the direction classifier, 0 means not to use, 1 means to use +``` + +5. Run Model on phone + +``` +cd inference_lite_lib.android.armv8/demo/cxx/ocr/ +make -j +mv ocr_db_crnn ./debug/ +adb push debug /data/local/tmp/ +adb shell +cd /data/local/tmp/debug +export LD_LIBRARY_PATH=/data/local/tmp/debug:$LD_LIBRARY_PATH +# run model + ./ocr_db_crnn ch_ppocr_mobile_v1.1_det_prune_opt.nb ch_ppocr_mobile_v1.1_rec_quant_opt.nb ch_ppocr_mobile_cls_quant_opt.nb ./11.jpg ppocr_keys_v1.txt +``` + +The outputs are as follows: + +
+ +
+ +## FAQ + +Q1: What if I want to change the model, do I need to run it again according to the process? +A1: If you have performed the above steps, you only need to replace the .nb model file to complete the model replacement. + +Q2: How to test with another picture? +A2: Replace the .jpg test image under `./debug` with the image you want to test, and run `adb push` to push new image to the phone. + +Q3: How to package it into the mobile APP? +A3: This demo aims to provide the core algorithm part that can run OCR on mobile phones. Further, +PaddleOCR/deploy/android_demo is an example of encapsulating this demo into a mobile app for reference. diff --git a/doc/imgs_results/lite_demo.png b/doc/imgs_results/lite_demo.png new file mode 100644 index 0000000000000000000000000000000000000000..c9daf1b2e6d8fa65da84b4ed36013db3970d52e3 GIT binary patch literal 96358 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%0A6t+wnAOfL6~YU;GqxH1*gyXM&ENXf58W4V z`$amT+K-<)-8;j5wdLQt>yCrjvbzK9mzn;YhKok9hhR$t92{G>oR8{f03@m@%=dXvfO8yY|%!}`yOj? z{>Ug(QLLx-a)evR@H27tcBqH3U>#&4@`)g8vl30+g3ZV0BQs121+sHNEYe&W{%qiT zAe%Q${x^bjRm*=zZ?Um`*(SGnggh?`z)OHNdNUvXoa2-$TLKJ|>tnha(-3&sXL8ZI zs*p(;qhT3LYte*f4*7Qj?&fT1aVL{B`$WxytsLGRIQh*-bMrQfO*&~HG4~B_%%Tbf zBUieZdIf;&1|K^<Q&DDzu@R3>V9ktWM)iRPx>&DGG%P;$QWLSo>b+*P`k9fbQ`aBHZs%%)W8pV6n`q>+nH#A z>7;pCPb;ugU$97ha0gx3#+UP+$#$lacNyc@@{f#Jn-~?xeCxG?e)#B#v0eo zhn{o@P2jc7Pds=3=P=vgy?k8n{{%zv;%}s=hW#I^0KqEP3^FMP2>m+C^FqjOufERP zmKLCsQ+NRMd-V^P->pu$z(Tm+*O%BEIw8+T7O)q-hsxP+oE(&q#>_BiHU?K#k#J%I zDP&pI%@#e=`0}d_Ga=0C^UIxZ_(J+B z)tec&H?oY@GZp}C}xjT9-#Ff&2MzOD8@ObuGq^j&y?JpTPr{NvIq zn)mA3P%%}p;9L2e|0~Kwdh?Q+wv-YmX?ZS{F`Z^gPu`yXY7oK8erUI7|9LU??i0<%3HeGeA}9tMiqy zk^A7n^H!R%i3&!HV);Vc1YRaG$kp_tvfgOOOx<3yGOh%0leaa5c!9+mq;dt4v&%fZ z6>FQqKD})&tGp|928Z)i$4)SY`LA1^UYcDySG;dltsD%AjkI4rt!0R*?|&7KGK6Uz zX4`Cdyc+T{XqoQzz|3B0s739j&5C*9nL)|4~PH|{U za7-c3*AK)T4}m&a#RE|#_Kg**pza7_R_hQwlsdLjqEgn7aj+a)}xx4P&IMeB{_%5Dh`|zmfxU`&S~v*ef&^ zT;9`L;GSSZ9tgQiK;$Qmh9VAS-a1IB4|k7BBx%xEvX%+2Dq%Qdg>>XOhR;-cX-~qG z>Wj%48w5$7Z34$vK%Q9&1nr4oXQTPeTt<_=dUc8Ub`SsGf^08LJytj;yuVh~ZIQo_ z6eAX4YIgbl@o(@nJsJ=i{ zqwwx>r!3`^e$&~tiG8s$549}fqR?;U5*T^p6EkrMUW(3;S&XQ{RhE89 z5uv%WMza<@r^)u)KIc&u^uB}d(z~TP{e>MB?Y~9LCo7U=M||szCgteNBzNSX>-#8X z;V!`zIOeX5ZoY#U(x_UeB}#jBz;IkW^Q8DSC373Z+11$`b4KUsyevqR*je{;n<8|= zCd{h>d!}g0{hf%Gme+<7IhejoOup)CV^uD>S`4W<8{p^kF8TdzWFc}Qaec|TgYr)j zHrs3n9N26_y0+rvXegB*0(s+7ZkBn62dQ1xqpzh+FRQ3jA^%hR7eKb(?A3N}wkThH zZ9Roan&OFhIgetyvQwDBcbXr2z9Y;XNjmu$yF=pB-PKPbNwvs0&a}JiP{cSwGp>vp zvqIjCimd5G1t-Fl<3Fq@2gXz#ygfSSd$#b+Hx1W*q(-a1(^3t#;4rpy3`CI9!6isn z$+6_oH#V`+YdZk*aN{CCfp{VoBdx0HQd~*!+k2%6*(wn=J9iQYi%e>=ixLwDDMnB8 zu1n{Rpc>db(=aV|I}Lz^z_QQ$AVNevSw!jb6&G7uylM_JzUmglAnc&ffq8VS-4;b8i(#juG9np92K;M+!R@5~CQx)oS^k~?Txwk`lm z!?${K2Q$x>r7^pWEAr!Rtw=Y_B&aU)X3lCd*9#iRE*yUv{bae$WBq_$n0Rw6AU4Q~ znb2Pj^R?OSv}yKlnQ$*vD%@F=!^UU8Vw|z5BAZ>ET6Rie4t|)2fA%7C@NqZ1WJ~5m zBoS6UsoKO>;Xj1>Rgd?J!0Xm5)eGQTxmq0Z!ok}dKL>Ne#kQ+YFRR;=^xAigqW}5M z2d3q(V+^C0A$$-@B)CmK5H*XgsqcT5?-@k$YD#8vH`#Tz5I~)s$jJEYW=vhOb&+Oq zu(}T(q@gS)l!Ufe65n4dB3Q7hA(&ZyBam{f?0Na%8!{^|`7oGTppr0Tyr>?w@r|$H z{&K0;a2v5Fr}YYn)~MU*#_eTqW`Ba;md6%(qnQSUPOIi?L$=N`EqH^WsgsI%9c_^j&Bu%k=nyL}SUbC>uMRkCLW`-6qV)OIC^(T zSDOnZ0&HBc0+VP999?%MyYKV|5{GZ#FJn0I!Gn3G_w%k&8UEap(=aO9U;|Ln4_;yy2tJ1;`+?!XdlmYapZH}_Z9Sg-;X z*mCw|o&_-K;@6VD^Vq*Ih;bC6tIs#Esvv z+Uv`CU0yt*EigG0NH}e+!uKVlD++#T|DO|p+pX?pDM-o_HHK1Xv1Ot!vzs6Y{t0++4{ zYVSU$q{6a)qoL=f?o12hQ+*3qNlz;)Ul>#ZE@9rYh{7eheX_xhnv2TJb*y5{9I}9v z1)Gf#x{m6O*cvA_d0Ir?hpa@b58MBkeUI5jfWp;6A)?sBbW^_Y^|6T`QTT7q9R@Ok z^;s9r8D7&+f)x6(1hN*(-E=AnpV?4P~2Sy`w<`c^|CGd z!rHvlGb(f*_!ra}r(czi7ioWT9iuR(4#$eHtr**?-_77p%H0P(m6)cBs-kxoi01l$ zP?rzwiMgt+wmCF9=Y_dT_4h$N*#rKr6}2%u?AIwuKO{a#pCxo9feU{HCl?TX&eN2j^Syn{*wgh~czdgZ!xr`#A4I7d^o|yvopsFi4!u8?wg6`m%w+nDY+jb%w=Iitc^O=eH*z;f zr>CWv=p0{6&Q662sMy|lUiJp}NcDS)e8wyd4rULEy| z+WR>6DWs`?WFgu*_-otkv*HVJp)?t)veV~O^P(NgMl+#hf>IDoH?|)Xv4jm%ps~F< zy1N;vFN=uOa6~&>^)~zIaX<=4?5zx+3# zYZlnXbI=-!v|lC&bjrnJZqqPT^#4xPlacBhTj|yHS*#&07)Wg z2MVVAO&8`NI(~q)MfmGE3x6*UU!uy->f3}D$88;-NT3m?yCBqsRE9o^r!0mss+Nw}IJhMQx%v^cjZrr~l}59&Z-WgjF{EuSdwusWR>0TB z<)ovK{Y_eBDF$@LB-rS(v0gE^sJ`Rlp4M7@!N@<$hQ-G6@MO73WA(^*hc6@FR$cfm{(v4%uF5-Z~v$ z`qU`@kooC2tzXgW7^G{q)EcTgb>}Ord*rY92o)D|AMP!>XIE-tch_BCVZ}K#dAU*< zgYG{u9AUSe=7#AG4|bg}5#yR!_O=+j9r61ZC)sF+td_N6sSs{1Fo;&?|L!W^G{c6I z6!t@gPWW*r&5GpuaOC0tw&7aa1OL0pq_TFRySnkvCn0e4X9nrRnhtw2f8Vt~nCxLh zmn5`rq>c{g+};WIyNB9=>}~H+Jt)W$NpeNzgh;(3kNaxzXW!LI~iM;=R#Q^p6j= zuiur6RIhm2PL$s}o}8{XOOP&~*Y_rOp-Vbvp^Bs^aBypXSE(r73l3{nL^)Q!UBB=a zpACk~Y7@%!`rcQ#%_tascs~6XuXt!eG-f1uVeQAt{qIZK6z%u_S~-N8FPWVVBK-7> zxtw^xu83gb1>YqzQ;!|kh+@voPD8?~C#$w0s&_iliEycUJ!yQMvC&&PvDpXvh~MI2 zsVy0r@=G#={rf)AhLKBJ6tyr+w5^`}W-c6IP6@3}j z^S(2iX?9_#Wi`^i1~$&_@8aIe=m5lO)Mm~V6cll6_t1X#=1A!zMr^LICI zT+pnmcG36YqaF`;5wnP0CcBlX8YtR8%?k41%ycUC9e4esOC%P#O-OV}b6 zKKBsbk_xeUxveQIx@#J5J6FzSsf6&Hahv~6z$vc2e~d3@|LN~$t`9$N+(hU6<&Z|d zQ3lOFXBX=1(39?ya`SYO3#=Q<+xd&1NLS)NHn%2sIjl)sFRX+(pPVitoIWA1ZL`N` zuYyZXK+qLysSosr4l2)R_K9JsCPCPxkF~#KOroy`PS8tQE8YD{}*{{zPVSMr1X6l#q%jd!^Wr32J2=xek-^@)iOW<;l^g% zu--QP;#19FXb1L3w2$AB%%rb8`&wM=C}Gtm5$$5-hZG4rMgqa#KSw;2Mwu?%n<#1$ z$4|ZAg8Otn;l5+9^@Df zi(95gbgFuyb?N;Q7mGcBwtL!g0xi*R&K>L zU9PTt?aXneYz{i0;Y6Q_(ig>(u{GHA?Yh2IpZ9Ol*bWtA9J!8unIbI3kI@~B!_gGo zdchYFf0i0+K$N{|bBGn$VmD6S+Zp!Y=pjl%ZOzs;!i)@jJ_CNYY^!QcJEJ3EAiqx0 zbo{21A$=I<-C7lHgg?KaxfKDgu^-GX5(DmTg3S4QE{88^fv8?kEUhc_Si89m=(oXP zZxvk?LS2^@k*${y>(H>`#LI~!rGHFIM(?-R`iPbyJFSc;W+f>QZL%iW|DqNxKNNIl zm|x#9HmNH|mP;<~7`TQ36gZB8?7z-L38EQ0`#Yr49E)8SOyn;-cca(>x~s%GJt3;F znO8GQ~B{!{3h0?Ljl?1g@65+9E)tc);ZRnM zr=u*%1^n4kuc|`AcK`LR{lS6dx%)&llZ`lqU9j^=G#=3ChsuRdSd&8h?#_xLpTHvr zg?sXhU#-iFfRrMQBRajQvMm9zBt)@YTdnUR_o$sXvsrE4IDZOfTkq==Ugc9|E)!1O zz!DR)8&)$gxvxUr_nB8)`@4>DkpDuw6G=U;XUJnDw5g-r7NHjJ!)6$BhGw-IZ#^gW zYYUC~`u^UAFGI)dC*ExBf>HmSF<$sX>0swl*hg|j{=o}GMeJky zj(A_tm|y8G-oIj~$CPwskE!;3Q6W{$Cm@77In&dl+l1FgM65+X0HmE|kbR5t%JJ$u zRXRX9Wsw1;VD@JZLom-X>t9&4en980_L2RYhcK0jz$Jn~V`Y{7$a15KHcvsS;;Ddw&FNun@` zzqU2qel80eZZzte?8W?xoFN2fyID21Gy%zu&+DVv69~qI9^T7+yy5m$Y=~HOzsnvg zi#Bxq%15vn>8Be>n&!&~kz4;jFgLjwNLJ86C#e1w7%D);8JX=kwEkh!;((w8Tcf); zOoC+u6qey&N*_a_kM>y+&m<`=DRZFSN3{80<~}Lb^hW1-SbnpViRj;#EpD$O=WkA#^QnIpv8<<>d7d!b zU%}%;`IsA$VHCVbO`9XOZux#)!bB{PuHWQ8vqV?xq}x%7A`~!yPfZDB!8);V*i(fjHSD`>?Qx%m$BUDI7irwxi#B zF&W0Kbg}QiYC8#x*^as}Nt18mY%*gx4=3B6*L_$@Adp8x*<>w2*iV}*eS{MEJN!ls zqG?=WvW;Ze$&DAsye z+$E+%bL|BN>SnAJo^hgHUZ0Yy`|V_23Uy{nM=XV{zq9^`MoScog$Vlkb(D?baNmT# zVA>eVoVVZ<1ZZUA>S*;r7`?)ht0jfDC}eJXYVTVbk^F}QW-4>5J++}gTc)E0m<<+r zGKHU0sE%rwL#s=nvBs@SB3ob7qRQsAj+sg}mQCv2OL=3tq`JuvT6Ov0%nR4YrG5`M zY3<^>iy->HVf3CoADa<)&hq|@k$&ACNf3@Txdr^=nkQsw)Y zR3Eb{APL{$gsXrfo$GyDD6o&XJJ|o22JaN68l0rZAU%B^-m59MoWM_>E`0B!Ruvg` zTyD>hvMeqEUx9-5Sti>+}18OVCB{B3YR9jt!gq)z>Q`!~vq*QOkh~-i=m_Gb009i=jB>Pe1-w&BN^D`nJEBTcOF+eb zv1C3N?4RXC!2D^fAoeb#Tq&y!f%7Yg8l(3kt9p73O;=m96mlB-w~$nB3c>PuN?kL? z3JYABZm^&}CliZlk*DnNAJ@VlI`Fxw4qs&0T2w?LTNZs!14N&jBN}F!yPRGTi*H*- zX9RTC^v0EEsXT`o2W4&Y#GEU)5UphajI`W;opsXG%Xr=E!h^uV30;WuZMdg36$mO# zn@)se4it*GpL?Il9=_h6G^V=+;$GjmDHj}tcHPOXZOCEo`Kx?j+u36p!^h99Es8wf zvy+(%WO*f^H0Dm`?sb}NKhpJ$VpNIgR59lFZJA4R1P*ebbuL9h62^=#;E2(x$n+$0C zOBWJ&H@_WmZP#+T!>0EAHG9l~QR(Fi;U3a!?jH z)(Z3cwlLTiajr0A{YE*o+4e5Ck5Os(x`tH*y)1ybduTQ~c~tI|Yv;Ge7M%#>YHOF~ z`R)HWZeb#88}T4w{d{Z;qkb7c{CYp~xwhe}5OoG{5ZSkjOib>D3uM>k0{WNEq7mE zFa>_^@4h!%58_f}kJ;HUbV(yC-I;p2>BvH}{q*EChcN5JaRt1z+|}=Ub9qpvMu7|q z<*it@>2XlfWW=XYU^^)#v5vT~`1t~grIcJY0-eKdLPZAy3%l)%gt1!!Wj`OP z*vcUS@5p4ag(Iiuqm@!W*(Y(ff}yWb9Nmu!WeGDfvS*C+*4g1Srm)z>w!p*)6R zP|}_(vFcQs47cYB%O&>{C%e+%R3N|4&4tKr7L)Rr*;9B@zEZyuXU_-2tsjy=t85nh zfhX`~e~fho8`_ez+{SMo^E0m#*l&1a5IR)RGe5%FZQr7+TV8WwswjO#C%Xw(wr!-; zasm9cUtCH?CgQRisk~Rr-mG+piC<_E+ixM;Cl+yi4;WSxSu<#zaY!|3>DKM3Sr-TT z7t@FcnVXF)q%CC?=wfxgF!gp_ENhq11+|iMXmF*Nb=Noj{tIrGbCMmgEWdkUTF|l= z^4>`&NFl1OX36zoVCba^Cn#n0|G`rK6HopBwf{$K#miByzMv3}eB2#yXuqUs_zcm0 z*8e8W4obY^hjaIks0edq#r}3Sx%rvC=vYuH;u4}dC#Hb4Dyy!-ZT+MVS@KjaB6X^z zMQ)ahJ-00y3BFfYoZ-}MtP4K#*4gKM>`3YoH`VE*0v8^SQmj>)^scL+toUX#zbHv@ zqaPtioE0Y&Q-UtCv!t;z;Zf-*Eb%?!q({Is*8V7b&Wl%lF`->6UMT$#@0b74Ea%me zG4~@ZyyF#(^{ms!Ozl67*{b;dME*7mc^&vY=5h-asIZRayq_#O){o2>%!(*g{RB$& z{o#3{Pdv|8l2)m4n**%Qti%gEE8DB=`IaL#&?&8&cWT1e?1NgWy;40ANP$4%w!_`k z<7a_~3R%X?`Myl=Z?ruhdj85kwCa!HuQMLgqw2B+iO0_>GH;N3RLfa}+fbeu*k8-+ zA?^Jt5{%y)d;tk2uo{}G8V;loJr^FkUqD~@k8R6bue_x$wG;Oc0g55W`zcu+Wg84? z)X)!4`dMA{8%&dLj`!W!^l<`(1%cr{@3jy9im7B#Y^ALWH5We4tUo&7UhYi8+<-)+6)xOGOF*LHF|JZ zUV!4VvbIU=^JY~F1yemJlC(YCd2}Go_7!L>Fy?jfZ2Cflc)ofc3q(9wZ1Xx@KD#{z z2UgxaEFwz7FE@~GWuFNucC&o6?3t)T%AD40qKE%x#C@;x`NXC?xE3+O_P$>cWt?86 zpUL~fG^Q~ z>cLUmzIVCi<&0Y(+`1#P<~`PjYYU7bm%E^r@f4?a-I(XEYW!_UX$}?MPGP!4u@ak> zb-L~w_~+nPlwjtmVo5D{m;c3WQdN9ogqL9}+-ZBSHcR9DBDbMw)51RBn)R608Iz@2 zygIb&8%c$qU_k#`erai*)hEh*IXBA|(=t$iX*BdcnrktHH|_8$Hv(z1?E2`mr$@bL zYe2Fx2HssglA)jnI>h7pUgd8YFY!Hb6e_sysA2HfZ%B}{rRHqaKoC_b|GGv93`RLG z;IGxPhq;wUSPfuP`x6^(Fd+WLK&j4sj=uF$mZfFiqu#Jf;0KDTfQNSS1jeUfMsy`@Y$kAw`~t4;s=U( z$zGE_VuboHa~D9$)z4=T&7Ag}=2)R@c-GjZQ)8ub|9E(uvAEmrWX^;4KCOiC0Vj}}bK=tHR3);i6w;eBU zxNz&~g@51ZgKn{h9(>%D4)T;FvsNwN{i+Ue*U@*^Z?LLwe7eL2@RnEBB1kJD zs^Ak^CUDIr`qdD834G=*RXM;*j+;#mc`xFazG&Q#5C72N9+~li$JklE2^ytkd25rH zUCD{M2@1A@SS2lUDNDYG3iP76&9=!*wV>Q3_K~EZ3QmUTWDh&yXivE^|DP2xlDHa8L3eO z#tP?gke;(*OvXQT7=r%pbFy6dsuC~LJ#ubZ>h+OQ^FguXj?sV6|Chs|*@n;jAyTkUMD&g-lw|*h} zEYmR2{}HL^MRelsl&vKvwaGhpg*o8eK(3!z`r;_g!IQ^e$|G%)v+*me%e+O3f8KHp zt2+#ShyQi3j6R?Hu%zNY;<0ra%4~;;Cr!$vnn*&jwE5Z$Yp$`UkdcKP0KI8yN#Bqr zTzzp&edOSW&LL8yYSA6;y?rcAd7grFLFM;oM+#X?j*&~iAKj-qtmF`fqt<#%S_I>q zkel$>f5jUme-aNRK2e9uaCU77{fwUIc~3=F=pqwDb<3)b7(fjKj?E6lXkZF{*%=uf zLe+cdv`gl5h%tytMFT`(s$(@KzJ?+-L8B$OjP z?0RDOB7Xb}M`Ej{mq(5bnL1#TKs~83gqkQ-p>#p-I3tt^vG=t$Y}}dj8NK`8?2(^@JPWgnaM|8sy2ehYgi+*G{JPJgxD@|%a9Xu zWzKL>sYe5$2!{sZyk}_fsZ%`i1oZGJ>T&Zfv37WEn#Pvf@Huy!zH7nrM)`DXixIvJ zOb;KvK8LR{Ce=2S_py79ag%DFSeK9);fbr;;p8dZ#hUjRrQ_(Xv&mE@{IG(MxsnPK zR*=CpLq#h<0on+A3-4c|5SlGGJi$Y7yfrCpy9&-WP=|#H0a90d^^#TfCGurWCDC#A z?OOM7b|a0iYJGRugJ8^Fn}{P3HzwAI@+2jE3Ih?|AJuWoJ!sLL-jQ&G6F=+#ZKs(? z)k3^u6{X2j{&*65OeK0L=f!w693n($HCDS>mtDz0ML~CtnsT&%dvdiC~qKqMK91D6Vi|xcVUy_`gLf>ezW) z-rsh_l^E^~C(|gT~0xE=O*pN#_gL)yfV7OQ4B4YA+tqJvcm!YKJ%ES9!yL= zSLHmb_JDG}CI30kV>;iU=gTCIxndhR$F z`uvfANm_;sNK>%W5+@Tc;nQd7&&BBAQSnQDXUbjNYj1sXA@mkhJBsS%GK=Bv<0~w8 z`|FnsiHk#qTY3|QJpzay1Lu>5wdp=5I{e)8=3uqc|H2 zA8pi4vXRw}T1^gTVq|3mlI9z@anIZ@0nfwmIjW;$N0lT9S}*DOLDY=p#F=3K{DO>s zw-3FhIytu3#<`*N_Xs}8yCndEafVE}oYn9+MPlW%F>$)mmu@{YJ0y)3?E|@UFRC4c zJc2(l!9-3y;J-LV2Fo3HmTELd7zyxB6zu~YYdzdivvMEz8&n57u=5qn!64N) zj_MGW24GJUaI4qPH|EX32&Fb3Z^B7263}E3$_naxYb3=19yGB;-w7--DH?yP&&X2fUThwet5kfIAXjn4D`_v``*Wnjh8K`nj{ScRhyml+QIx z{**SGTog!u6lZ=9&w}3_jz0@dO%70S_~`eezf~9JW`cNlybBu8>${-%63iC~IaMOZ zQGR=_;h=gFkG^hCvVp7>)1H@&`(3PrTozO5_jw8%dYXXAo->V_FgVIeNEk0&w;)F? z<8{@`r(Q^-=mL{NpKwY~AtB5`X!ncIq}3dY{IhhTY`ulXP|hjY5C+ z|H=!F7(4<%YN_?9mL^zpRPr%ED%(ey>!kRQ+*8^QNQ=*akS--<79pim-n5`cF0ADm zGK2EG7yxb;K0?MSU;qt1J;DoY9jfQ{vxvNfw#r!@{WQP#xP14TWqCxKUCFr^P>wQ;2tn2P-i3JGR61bqosnH}E|g zKAk%n-Cnvd6QUUG7@+ug;)f`9CQm?3Imh|#AZt^qIG!HBbK*fEv`JI)Ty+~bWHQ(aWgQ)SLWmQLTZa4V+QyR;y+B#zhikVu}vsS^#_ z4Ned3sjsk^8_w`VcC`epTdXdht@OO9K8jzwhdT@Cxwe|I&%C=K2|8_Fd1lPZe{Tk- zgvz0h^69^h(CDK}$K{bwOA(^iIvRV6s#?#s;YTzU?1>}PJF7uqNCptz##GSBu8U~F z$xCB0X+p)vXEovcF`pZ4*+u4blvE*^g8#9aQ*J**9~*bNMeLG>G+rV@eWSftfvAgR z^JP@$x;b4G`n2PJYoz)2SA|;BM{Vfec{0$NY!Z|4892^ z^U!ymb=3Hl7ANb~+3Am3cysNAS4|-AuiYq0f_eca6q$SdMeUTN+MW{BQ*{61VOPl< zBopx=!mX~IW;O5zMg@|7c-Yi{GMDWtr|mO5d~fGY9Z2eF)-xIMSiE?V?X| znB%?gIKr7}i5Ut6Bl_5JORMkD^1ehdOR^v6Lslyenm_`DPukkU9|HYLJy-@bQ zI$WH-K3aLWkYh!6cmFSo4z+W5lS-tu<I(=9?4Vw6HGHR*XEZK78He&{CD!bzie4`|i(Y-4h2NgQqBz3Xv`mZNJvC!&hD* zFfy@u5WjX`p@$a{2kCoE@Ws)^_05D_?^lAfeJ+(}^2N8~6Ri^zB3xZaS7jUUPp9Wc zi8~dJ>0V(sUq?r_0qP+VE+TUWu{o2T1(f3lNBCgOlDG%gdxaAl-a+I`-cU`8(atI2 zV}l`|6#)G0T17O-jLHO?b@yMi!8tTe{vx$vz%4m-uz&K{1*gY?2jtIT{N?G9qv(_nbM6pGS!d+cXK z6KTWxn8{Bh)f`=K-@#xrJgf@_p#suc6kJ03mhW8Jg6$+`eYFVer#4V@Ly~2;KgY-L zGE!@^V!%5~CsVFy?5v@7rDYsFt{{#tkhMrDP4u66_#TV~=h|up%dDy+h{BYsO2^t* zp3|xv>zzfAXL7CB@mgQ7?SUQHP8Z`pGORI6Vho=%5m5N+cu#(w5+Y(=y;+vFoY0%c zw}&*wg_RU78hS(xs8z1EmIJ5=)sNM?tX|4TR1nIhF)u&J^0M$vwMkHCzD|nOz z^>Z5szCG-HW*3#VZtyz?xn0;*v>JtVU^2Rue#3ak?`jj*V&Xy>#YM~e=D_&-yxvoL zrgkBblT-`+M#4tGCFA2j+;MU(%9|@C>uuA7oe`ym?azSj{LiT?G!#ya!=S~!9B+RJ z=lp^JjpFy$8MW5GaY-*AF}F+}B)bQKp4)Zij~Z0%p3GC7u)!Xsw#2Ta6`Z6SyuaP^ z?yd^C0`Ofnnsi&>=om4zQuX2xZ%!kxlaiL-Q$p5lKC}sl5+LX(5P;n~H$!=4oOID+ zW!I)0an45BV>#YAD^ITv1AjM-uage2QKyg6w$%sjmO`{MqS|!V2r_y@{xcJA<0XiWFCCY? z7>#}(_rmMydgl}lo}^LcKC(|QUt9nSe9_}+oU?S-ZS@V_eRo44+JY0FnL+gmyt%ApCXR(bpGv}8dz z*wpv*9SkO9b`OF$o zrfoxIx7JTJ-$kQxU{M2IeHE$sDmPrd2?vuoEMAsF-ggk0a$+CPzde_ZVEaphGS1^_ zb%=mDRy9I#9y7+5az%^?k)6!VD`7iXAor_@f~NK0loPn3eQi~cA64Y--=EhwQ-N)W zbZKX+y4cx5NEu^2ADgw4-kWm=o`tCRTx9j+ao4#w>O7}SQr7d$S(f{OVkJbd`tX+X zY@L&(MU8I{Zrh>nZMRf}{zyF}U&&X>Tgi4|{QbW5w#zGIuDJ2#{aRNMe;e$YSteg% z%!C-)pMuu_NDFvIMpwpS{xyWSXCOVoz%GL9WLX?Yy(NgGi>#=STFsS2JbmwHe!h{>S*Zp~Aeq@*I}ZQx%w`%|g|oO*;#=k+~T`!Wdr1u+Kls>(XV}~+C3!3!mW=T=A`pm+cXc3A`dUp$5*Ra$*bWp--DYnVX zB8Xb#uzCNyBPCo|UhxOuaeshK@TVGGY+AQg(Mqq}j=xVe&J1XQP)oaKf?!q+J2uaV z>Z;(#E}PP=%0QV?T+%gkz+?wdW8SHKsnZ?KP|n)Ha{u+GoSkVmG~aRk7n1F03vaQiQ8-Qba$d7gaDL&sbB-Hc%N=cyH|oKd z_FH(pQz9I3BDPTf*TC3tE8?ZI(Xw_2m=CHXZ~Izp226*3C-%A%AvDFaYO=oj1)&=+zB@O4Sb?=O^w12 zZ+GKfO3wAAy93V_+^(CS_@AyY@wQeqarc<^%imG;t?w`ZFBtFdbIiu_KlKdR2z7^X z(<~?oVh|^hdU*HWYi^-qLlPllS>8Y_N0O=+ndqNM6BmQmEmqquBP9^25Te{N zq^ZB{8WIB?#7taNAQbkOL))#xh2CbMmXO5kc6^9D(c8L z`e>jKltjV}U0%RQaRuCSECZ@2!}A*7#hsWmi0!8;(ht++{V?*x(6b+)BTl@L`N@%# zs}NwJ6)#0cNe5dS7EKQGz-Ww`K-l&swkdHx1l$mqC3CU;22PfON`5igbMJg5n5NF& z@iK6({kW0>U3bQ9KhXiw(L?b`X)`bQ2*W@izsUA7?#9mV^JlJZJ=O;v1p6Vp{gc;q zZBUR>mVAoxLwO(SEv!tPXK)$V#!fYpMjMdP4gy2o}D@l!wb!TF7)-y4oC-k>xA$CkQQMZ`(NAs;$xsJ(n`&$>MP(>Tstmgze4Se`{Fv6h;E+@2KEybT2e3(gUo zmo!#_F5wQgkUsZ8MF1c9?Dd7+w8CFrzpFx#5+$d#YX2)I%}0p?;<3MR>Bqe3^J+YY zlxAprRr~y|+jr1c&)2PS!=Y%Ke+>fR6^7YS+vhi-FcYSmT#0upH{VwsV&i&J=-giA)Vbk`)p$y&j@T4PCTwU?owo!{hClh;Zdk|MzB0rU@ zzS~wU3H!<6jRs~KXuMqvu?gX5C^01_$#yRO zi%~J`U0L{-z~H(YU3#$>lkv%1(YU!)Sj^rp#aYx28l0d?;&+rRrXx&SH3c|)a=MN* zAMa^44lUY+#WQ?A-~bu%RwAHJMw+c<<{w%99ke(~v#qyH*aAfUv=54u#& z{XkCBk1p{d_O;1=4&&WxhSrxfii{0gZdPhQUq`-W`y{PS;y(M3r)k^wz_*j^9wJ9* z%t@t$dofo-Syz9`#P?%y->}rb`UfAwH@$yUd^AgOh4myL$rm} z!*4el!v=dj*p5Iwae~!KQwuU`FhXSbiio9oghZT+e2n~PMI7owxL|k)*Ad%kOu`v-^YCJ^aZRoV=Fg@o{+((Cn(l|K4@Tjb$1w@z5#2U zVaooLPI-uRzvuCYg-0KI!k!y{V>=_;)h8i=njJDtM$hPfzh{bD90&Q&V;s@=>-;Kl z6QkOWDPsCmAH17Q6ERLMUN?H~B;hf*in6tpKB?#00aGmnpp7J^ZlH>u|6S(i+6ZB; zm>x1>0U}*i5<6Fq#2yb3sZ86$R%f-{*O{*iX&;dg(Z!6tNvsFRA&lSs;UKju%S!d= z{$AF5SHf9Prt9y zj$}S|wzXU|bl_eLQN{Gj_iex!hDE+XM2pFXa^taz2)h{!>ck3wA;Y^I>rl6eB_z?? zqz@DcS3j(1-8M6CMemTi=h0Z%Ub_$L-{QbEG5syQi|XKMQ26K@J<-ydWz?OnQeMGI ztBdGye4)$FaJ>hFbKUmya3YL19FU7*m(uS-J30PdKTYc*H|dR!xtT}%`6K#AtG_G(uyNL(_kGY9AWwlzk1uA#4%o6%*~uWBo58R}dUZgFB-Xv$#slYoj&-JNNv*6=eg-|U2LkEr3oX7|e270+h2llvs}@30m&NeWp@*nd_fctYl6zGpI@FdwZe_AcZBM z)0Cvcr&}Y$8w{gnq$JB>YVX>Pm|%h0v?}kln*7YREy`*kzO+;EYCp<(58j{bNfvB3 z)`BRV-|yc-t52+QHpxlt)dVadOyV6JyF8cS0CRK#7UkKAo3Zt|gK|~r6hVoNO z0;R}l9g**WzYjWpIaG~rBd9iU7ftUI38vcHnRhB$mvWQ{!MW9 zuR8Oh1?1c6FWq*5^Q50x)kTqwS1K?oOk!PVsH~Ac1H?Oy+q-I5nORabM9bYx@1mZNi%w>Wq}|i?dOi znXQk|CfAS7B^;4LVr!&$uM%>}i9}#_?%NyRk1C)LhrAI=)9KOH`C8f$!-%QH+lmb6l8ePk2NC8E_SsQ0G#jerK)hlR<}nf3R&>NpShwgUi@d z>mFK!Hw5ON@M*h+VieNV%Hz3kH*Exj8I|$q5D)Q2T`}LTI5V&J4#q8yoYLRbV}r^Axg|VX8>HK}7O8es(E&^e<{6&NMWXf^zW}~| z$uSL8)^gR~uwSG4MQguS&Qasty@J!Zu8^ie7M-PI2)cW-Maskw4M{4)55^e*KD-@k z7I6G}rl#nx+c;WlRYP%$zfUn|2?l)shX~z`TnC%*+}D*Prl?c7&|MWNMN8jTYfUV*9%(C~?YnJ#YDHuBK6QXSDc!YmH7t-nN6?J##Oh z5#qXZV11)*YZn)QLvu{b6mb^E^czcCEIfzt^ye!4we#^z)+zi#2G!*O>NXx~pH_`cWVv9)P(;nqG*=_mSgwG6wQQr_{>bvZH z)MkEoetC}FaM1lyKhn%cef36bp7RtyNvNB4VRN+g72{&ed;UzN-M3x1jaJg_k&CbI zuCdY7ZlXUw$=aSj#oAPr$`JiK-bS=OFcn4*OglS%o+l3)F07R8Tw~uHZ+lCq zrkgf~-Jqb$K($;rmztwakXAoQy!}P>&dIaC)?izA5>~X7Y2R=SV_>n%%~SWa`jMJ>vo{b>+u0c(lzu2k}bMI#V9Kl&{p=fyoJh z`(X=>>mgOURl{=!<@A(+Igh@*3uFEW8YhW<|4mqTTv#E;!xz!PLzNFP4GC9iSm?&u z>%abKlCIhvBmstQWbwEpSriRf?<~fFx%v!gQeRcRLM~uU1)YF-0hj@}~WkVlIHxsq>zS+36|{5@wnd@QVa5)ZQlYyNMBzt;2?CAR1wf9 zs#i9v+IVp0hW7qOC0FI}|Ig^C5f(14_s7a6n-C1UO~Gc)f@$sojGWe8w1?u|a% zr;E%p70X`bN>ypZBFu+QNnu6g4ZN+^!^1o9-z+iuvW(m) z#Oqz5XC{&h38eyI<35LADc_7cm2+D=`6{KNUS@}Fy?lH8{qcRDT*TqCw|xtro+M!N zV#U%}zRgyyY{ZH-dc%urHIVZ)1|^+V@Ixk zYPv0q+$eXtKgvl-&c%?g^c=}Dn^HwwL{G?M^7-AQ;7V*5ObP4MkMTQ&UuH8Ar8D#a zy>tA(6-WI!q3P=Oq0RuG?`_nlL=tI<$O=Y~jH?Ht^Qa)>Z^kvC5l47akk?ODc*2aK zOpX~8?VF?8dj72t<#TC5Lb4(4_2_%Wc42u>pmnsdr*Fw;d%e4IKYUPCZ=SyRYd|Yy#Z#XZ=)_~7C-w~RYMIx zhd!T|d=`z9CE}yixul5jOrt5uAt~xj17dnKlp(L>u|i{d7ZrsmTHTa&js%#)A|eCe zCL3!U9X|qh*6!7pk_ni2<=-sa$=^N2qi4|~G(h#uJ?e9SxASL4FC-9*76jgr#F^dT zmnYW;bEjC}o8AA@w>)K;huV(ZC?*}P8nfh^if36NBn!21Fs>vrxWEv7!ks>06Tun_ zvTC#J2WRD&&u$+PAYEJZ?WKH6Sexl^buK)NS~|Z!&cS=XMDzD>5TiStk$%YBDT(xGqc~4DkEil7+2`jCiOWS;|9W? zF(?$4Ae(r$`fS)X(^OCbJ&2nt7jWBrL)^t4c(iC2oh`W7YeLW3x;=bjW=(0(Yb7Q} z8R%y;bOKOWKO4!{`Eaq?zs)%+dO8Nb_a@fC(1^KD9;cQIz6h9*lD5$h7`yo@phvRk zb|vpx-bu}{q81X28JGlDB`&};#|o5Rsf>?3V2DU4h3|g8cP75tlKfCds?*A<<-juy z66)TX>%j!sXBnw3RvOU~E#Y@}xf6%^xM*F6wYgb?rgs!=%oEc41cjJI!7;7%^5GNO zbXlRhyF5?39_x2qPx*g7VX8D4H7*wB_O6tr>ahN;8pN~cukdp#+4`sl`qU0TmU?n? zQ!#|UeE6E};pVf^mj=(dAsH>=1O-XO>kWI({3lRndBqtf`&ySkmmI!D9ITB2uSucl zbrN`5WU-}cC4P7M#t(G{3^HB;vgVyI=GfOH;~T0k?fshL+5=zZ=;#W z@HwtPIk>&4YN)pmo!b~~gn$_t2$75njP%Oz*PEIPL@aL-J&gJ%dxzYJ27GVhyLZmf zH7kmQPSVeQR?5_8R>8;d*WG;FA{Y;ej3>UanG3iu6wXnv;6vjXzt_YOw55vNnP4Eb9m1%V4~t5;FpS&?3w^Ym{QlVL zO;y&3T{-I8UquGe<{tXkvF;khi9Npi&CD1O^lM{E0^nxZgwx?Zvc}4Qgy_czYhzoM zun2x8_Cp^?#@;O7GiVBX1$U>D%AbTagdiW)A1WIHsT*Ho4&g|OC!X?~65+E~T@!b% zJ>SN$c|yOagJu;?5m72S2U1t`a{UW{%_(XY>rF}TeugZULOP?*cZm}`JOt8IlFmwR zpUSCwI|~T;UW|R^ur(g?s(|stssU|E!=v4-S%csfB9zX1LiMS(CyAfC`c|i+WqRwr z4v;IgHWEC3`;}VN6(i}dlL5z`%*`^VgX!Ps$>{qW*7)~a^zd6?r!{|K+I!iQ;tKK1SHn98VEp(%cA(iN z;&|b~h->`2Dkj~LiknD-PB9=S`8kB1G(b{@?w1pXUP*tV-yFRPA(VYL1T00Vi+xZ; zX_Wsbkcu!0HA&m0v7Cp7xx%}!Gtq4#W6tI@yRMCvvNNGnTptaqYr=!&KSjD&Kyx}w zD&??n1s0am%HLeL^$cFH1dx=%u$k&fb;Y~ti1O7GyrWH+qSM8}H)GGU<4gyw&4%K$Z zKho;|#AV^_!RqAjEs*rr70iKy)}#7Gp0!cOk$)CZ{~;LP8b@>rN0N|=wO4;q6R=&? zvHcDYKbn1J_h~GoSUG!^b%QpYGX#IW%Zt3lP~l~S4M~z&8{?6zuy$Qb%m$A<%ZJMR zkuy8tDV*TPUzaRwKRineTPROeBNI!uz4Y8vx05j8Za^3MBP2UEXmxc zH9Q)py^NGH--earsE@o$C-YOtCvF5Fxpn~@rMgb67l*iM~ zh(ym(n#a?koFvN_i*Mc39zg+to24-Up7;tx%m+;M@QtFdC$gV8qH`q!BVvqrvB({I zOK9s=3I?xxUFBh#bX1&me;oaJw*rfw2$1YLa6ERl{dU&NCGv)*Ncu+Amh4PshVXTu z@>;uRohO|Nl8ID)2%TG|r0Z!%H>Ur*NdGz!(;K+^bE8kxzwB7}#) zUj8G#6B13~$CE6?509IWv2hIu8oI1*QxbO9Fr;47wOw@9Z+1$wwxD&UI6)0;g!wA^ z=mSB6vz~H)c*c*T<_j)4(_SY`=!+jvj_nPM8j=ZgU%wh7s1mdN(2(tk+GNt1*|A93 z4?1F)3z6IbTkM32Lc;&k;w}+J_nuI=& zwTx462ddo=`9=R3y00(z;??~rf$m_%{7_HPr|Xw2xLtvjL|%%oNF(RCfs_YgHLTL} z_!D$kT})F^Apl|pUcDV|3bQR@Mw+i2z0TJSNnvy5->h-5h=CzvZMEzRF-k#J5psTq zgo`f3nCyuLM@&EkYPnNDGCoa6xx-y|7H6Y2R0C@%$yhwhLT1&3rqKYHV3){lAg+2e7*RFqGAJ}}Z%^>OYIY;R8C z#b%Di_Gw!zE1`teS!p^(5cha!%@p0I{vXYhgEV6-%Cp%hov%|Dc5(9wcw**boMTgP zy1Mb#@xB+%BJsYlB(p;VH}x)xNS0&=H!t{u0xlJ+d~s)9zHv*j>V#f13F*hnte=1} z1KcJB65EW$*F9z~ekot_Vz&cFnCh2u3ywJCf1o5ct$!9~xnyqOFp)L=DmD1dWn#N+ zz_}@$UHB(nNG8I!wYAI#vrC;eqLy`x$e<>WDO7RQdRs7!@vWMo~zdMPjU)ATZ_zPwsp>^R|{sX3pW1mp3hSCz3f$1V2Ze zHq_r4wk(IAfp(%U_B=2v;;sLoY-zDA&%dN6bncq^Gy}nHL_5;h?Yfyj@X6}je^FaB zM-BjjM>G7^@qF;T$Kj~=_mg!!qD{;>MG`*#mkoupYlhH1ME*lN?4-cY280L<_V@?U zML(}49D$BoG75WU-=hg!@MzDCt519OsRNGo`+$TenNcS|@W4&R*=SK*)af;HM~G3B zn@CbWqJe!^C`K}(F+XBDTytcuxa0YbYOmwy&GhGY9b-kmgnnIJg(DW)TNon?z+sfd#;&=%RehggS7BiQmjlND2!I3klS2^J|Ss{G3&Kx7C zVikukQk(UT7MKEWPa3sY%BeNUI;nn8^tq5=L@U=zs9LmuY+;Nt_7$@Jo^&o%RzZUj zWZU*gb{4-Zy5LmXRuJIGVocG|Jp7fWCp%(NWwlJ%AXns)p}%2a3JJGEPx};$p$P|2s4i7=$lb51eO^ z;+geaX*9**eu*lq1_Up@m}zK&Mp?5XE*i5t3k{ArG;k0pANP-Rh;`(_s1pM z`sm1GeGqswZ3sUK4~=l^I4UB?6pakD6sAsNlw>Bjq)9UP*5g_d%xZAVI%MGc`mhni zjgX^g{KIgdlZCmUyaG9Fay3F=;X#UE%~&T?rq9colXS;09pv1zt2VBa@Ptyr&+e`Z z#J*ZE{LoG?FjTSB3)N*Gelz^5r$~#~gJrTin!@F7VG8G__7h6L)>9LQrg6pHr++Et zTMbp*E*7t~#blTrS$ZTZ>0P8XGH{#kn(WoC_WVA72XsAVG16`-Nx+kc+X0n83>E@;UK~?^BaC-9O(` zy3cD<9;x{BJ;gijaVGbGjbd(QMgc_W!!m~}M^&!qQUyM?YMPQTNz92fkn1?{#J2gXcs%V{$xeKls+(3ygy1OSX2T>A;I4bO zBTTDa?oDFMkJUQAbc^OmM9pg^fRr=P)@SF7#=_x;s9?I>gP%-w>lNJ+~o`OpT-B*s+Aq1ro%kB%yRS3KL!1STjjg@Ar zsOuNrPBv_XA4<#T=dqu`Td|$)O_p~NB1*2cD^r;hS+&}J>vYt|f+vE{j1Rf$EuGVl z&y7ORO~w(~G~eHCLoqL%&sr~i>s(w9?PU+QxvL}IY zq)}~1o*v)c^sUSxUuMu!p8js`2M}5RIbj zA8xD)>aAf8FzkWzLf8kg-2+7NMaUZW612(DJjvgwB9k}KV>G#7qltO}xi`+&H5J=N zK}2|vXNunzElq)!r!j_IH6Dy1yf`Qn^6x)vuS?E~7qo~Thh~Ahy2@OL9t3}`TjX&I zG&XFXd>9_bq`WgYEtL^l!0}z0;|h6WJ#3tvTYKQMyaY|Ad%mf#5-;+>AykcUGX$Fw z*NR^uwxsEt(PDKl9jdixWp#MAb=t_goMwxqV&Rxx+D^Y2l~_Nzp9Yee`K^aO!fc7s#p?M(p3%ig)t^)ZT2Nm ze!c7;A6zBG<<)CYUviRu`fyCr6tLj@uGtiUAvS~c!i6{-n^56%*Xmo6EL@7S?W?w> zZveUZEy3#h>Da$90g?uN`4l$p-P&C-u*bOQLR#cudexoWGW;Aiv+(G(Wa>=sIM}^s z%=FhVuFl`1u={rrKpVOfNc)^KyOv!Na}2fN8ZVL1ycCx3+DlP0rP!laH`g_T8kyJ4 z@rI>EIGAKA!7kv;oNA1cLUxBz7~u4}-naz?i?#mV`1f}hjHQT1EizJw-x4<{1)H9B zJo2F7DH_=&^&r#;k_f{LB&ntV(S}WEJXe0t>kw86LGScsuHd`?N)T9}>J9vbwrt+J zzU3!Hmv}Z>53PK$tsp(o>o>xlZfrmNGII6~9w*H;;t)^S}z`N($ z;D_2B+6oM(GZ>5N*FEEc#$VSWSL@0%#0|s#(3{(sG6@kYI;l=~Asy#W(PAi`%?Ro? zq<_Dd5)BEik?#?+m6z@0{<(>??xm8gkUY(HHS^Id3i8Ep4U(yTa(T4r^QFQG?XCNM zS0CTrOZ^Y^8+t-wF%$l7YZY78&Y5$7&0&an{6I{l~lN3w2NA zWj_t_YDDnW5Tx?-4)jthg+B$3(SqjTncE%&se_WKpR-6EE8P^?U2=~&su&~J>t6L? z$i3@ZR43LY#gm`O#_ zC3<9&8Mj0O{UZ*(tZl0G{}}BO5xuO>bsOa3m#pG@m-B~821dp1b6j*DU$kD{+pIsR4$hZo4TuKED+bBl3{&lYWkb=rjM_jVq@| zlf3WR0wjNqrcs6F0C&?PHbZa=R3~aL=E^itNAl?P!%2s4W6>#Io`q=oS0;-aVtKvB z3Nw%JVwbUcsU?l<=F7+pksQ2pzpNOd&xe-X^lo9U6Q3)lqd(mBf#Ms88K%cla~v1D z$p6{DFYtYF`~~4Xl0&7|4#F>#yzcBKy^o zh&fs)+2k`8F;a0ur4xu(19Nj}ok@tJZp=j8Rg5Vv-tD(Z7)Zh+f;trmKW7mr-meyM zK4xc~7Ili02#Ufc4(`jmzdrl5&lo_i|Lidfxz!deE=`8scDP+kC6nqCN-A<;k00sp z_kuCJTonP`>~u}ndGP{tcpB(QMG?Hhpt;RZoYf?Tf03UDmz#uA3Gcc^9AKE)EjV%g)2xD`5^z4F4D^qVnFPD9| z3)V0e6DRT!qF+VphUmRDNp?-y<*$7H=86KIIeA`^!YaZPEoOUSyF1x7N6X*MXrR zfyE6pz_9c9MS=w&#&-QhpZGI5U4v=Oy_N_0WE{!FZ%6EeF7YyWBct9jWGXZSRL@Ei z2RkQ+8z|vr^s{`N@>gUEz`BV*raLYMAc`_!(y)IF)hDDQ24%sy(e$8r6i{!q$ z-(=vGZwC$gBhU4MUPh{rGqAvL=`XLMQugHa_`&?@e!*1kTAbMl$>`k$5eD*qMHj(l zM9LlVA8AXllr)HpigAsoM3tji=q!x+gY5f@`(nehcD#-flx|Z zcXpf2=tyhq6ncf2>vmf*wunoSjNw?_@yHKyGhR+4&0mX`=qABza-zt41Yi3}0j!Q# zg)I_}-}S|!Y-G`J$~wT`dA13;3S6>ldRlW%Fos|9H5>dzj?*+9`)b|L|6a*d?nqzk zjpuYaHpJD>cAb9LzB69Pbu^HY?HfBzFhW5}r`ZnJTHw*!#;>d?<8tiao~hBJmFu@_ zq~&ncZpxW(c)7X->XAl~xH}ZiG-%E1yzy1}znsp98fgWbZCXs5%3vT;8oTSe$bB3# z*74slN)2Ym{lKQIZE>P zH!0y=-dQi+eTby8I*RerPf*a!dU{22AN^9@13}CDkU}cJ&3|#{!GtljGH`a^Tc z?P@)|I*RK2(1YvqchsYed)J5jir9=JEKH-UQ{#7sr@3;9E@vYN{al({fvR zD=!@f^d`x7&42F-U%t646anh)3#FX4qboD7$b8oNdyF(ZO}R>ROHF7-p zU1Qd=rk;`%1e=Se~z? zdvF&@Y2KVHzqxEzw=?ts Kvxr}P&#q6>T$Y8JIk*&7HIV56`#@^0KP*}7p?|nlu zT$XE5vMH=Fk6mufBn^qLR*?H>dpYqFW|q124U5Z3T4~#kV2?yYKC;mL`a(E8WBtnS z=P~u2&}|)FVK4vh)Aa_m9`(oVVlp}#VSAzh9f2nY%m47mzP>q;-M4-Y^FB|Mv78*xL~C|g;R`PD7nb$9n5_1l2&7Ep3M^{tB!LneZ_(_NLw^B z61FaErpvcfh^o79BUIWaqs0nr0))&M4u5MWf1#zbpJX(D%qU&EaW@Uw(0^MM*GF}HeS2B5@5%;OQK*c$ zE)tJByto`Lzg_K^*|`eIs4-bQ3i(~frl{|PTb9~tfsUufDFMN!h! zNV?BMaQ2M2F+>JFa_VwBoY9z*qX*dRA0MFkqDyQ3b8y;H}*FmVpxA#3Yy`D-{= zcS~oF8WOM6pV4!@;;0#K{qD#^ifFwQ(Q#jne(}qPhV@GY_%%Jw8sfr%VJ>d-mM8>4 z1UBkc0PxNN0qgQI&yaWX^Usj3z84K;vBB1=6JiA29C?!fZ3@m_&B5`Q4JXoUls-kk z+WmBM+(9ehW-x4^P2uN=U-{^*>ccgcX9)}1J%GPCoz;I;cyln?)3bd#I_2%R-hi9) zTW#7O@IJ)N{f4Jl$IUZq7V2HXQG@NBC-p={mItBW2MSKWS`F;I6-~^m;!6(a~;V?2}{t&W1T2 z$+b1#TJmNqbJ=m~&&eJZy)3XpH;OLcNwZ2DIP4=SUMGKuQIKNRD? zV4&C(omT_+2utqLcK)Uc!k5)TxxYRIkz%E0jbOX&4=2Vs3M-VyH;G1M)pP-N3|4=_ z5U*q)gyNhbj1WQR%)Z<(6GCyOiFxY0J_9lnaCyn^Rc{KN^5N!rTGD=Q29pBX!|$ll zXBu*U*ZVrG>n%X$$pklpT6Y+7zeiJ^=BRd7nObH4Ez1@_s=~=MvB&4AIb!#3w+wRm zZ5F*goMcDM#=?oB0h5ORi?6o;YU|zpy>SQ{thjqA?i7dOQrrmzTA&nbp;&M)?zC8o z6e(KVf@_iD6bn||EkFo)^E>Cv|J?h&@4d5U_B=D0*|TTz>?PmzS?lq2TVdP=GIpiC zWng<|63+9uLqsBwG9xkRZ6DuLCaV0QuMI0(`sY}|A1V;iFibDNW=!eLm=KN4CCXs-max88Q zf~`w?jE^EG{&X1XHmfMJ;4rmj8L;1i(d55%5L%uh^%~Y-8pyz|o--b%)!k7w+@3Uw zj1N$@POw)*hyTui=~eqJ|`Vq}RI@!oQ;bne>e`zM)^*-+DT-$)0JSUh7?AZTu z;2UD3Xeex^OGNN(_i}rmTAI%b6T<8lRu%?w(pJPD^Ls-d8SZ9F6qdJ(*!HmhC@pH; z@;+UmhQFdX+@WN#PCF(Da<>2~gT2H4itvlPeSY=cL!<7Ln!a0xItf8H{X7rwSdZ-* zBaqb)WD1l+6mWwd4~Gy!>w)st;X^J8H>?ZG(J@;mR?9$k{J&djfhLAsw`8%lK}D(s zm8%OnSP(PeTT#^_y{_qzizx&=K&tW6)+9W>4eX^ZIjSbnF3Yr9nY@NA&q)7@1+slG(k!386XZ8XA4 zx=9w=iLE3Iv4FU>_?#iL?0=}NOpBym=MFzpHvq~Mv(HmvjEm{Qfm?o-A5F$+@Ddtr zPi{vyx)aG|cw#)}qw&(flo~|)pHU9V;xNyhVoUkG$wkt6WkiiqBasqz;<*dFiQG!d zH*w%O%$sT%{Fx2Qd+J7Zu>UBHt>xlaky*eK`DnBr>+o}b(Pg#oX5yq-R!1d!*G86V z{wq;FqT`J8g{b)uto{OUmo4Y0f)f?lnV8lB;j{nLUwoO6n9A-QDP%x3P)WXr1~S2@ z=bb1?AgIT>?6Vq1Q?#-WN^hXq!bvGg_#MRV__}M$9XGu9F{jxtI@?&O{%dXlUJ()Ui|E-GoT@P@_=w>|i+Fd?{0*!fmB zGwEbTpwQkKv1RX?o-`q6u(^b|o4S7LBYRPNG1mCE$nq85lUOvBju72rrVDa#;RTipAg*Zt)E*jx`^ zNmcC>3#-{*kkCI@iL`COI(@xjC8mg;lI)E(k_lJz;#OIABR9;!wp+C{6Nv%HaOqR^( zUUjgSyxw=DwJ#v`jCnHko_S9Y)34CdRzIbe`xE!_m$2-Vcq)bXszh<6l6^UB&i5@o zrUIs79i&wASk8H;QP-SgL{q^OM(xaV`%4f^rQ>t!r!Qv5urG2P=zD*XK8?o{Mm3{a zd1HaQ%X2!FKVlu@p3%hfXlvs_eVe8$fP4U!N_I8M@4jTVdhM-iUpOijruR-YhM_jX#-b5cD~b`}9Jb6+NO z$VuOl8NWygdLwWExw^r{XZv_+KdG|lNKu-0*f3v7vg@Q?&@Z)ZqYf_P^Q);8A$`tm z{PKjDWnw)QmT0p@K~Uq-8dT48hxsCVlm5#R<__)H(NMgu{fnvr>1( z598o~{qKx}3wji5pe)6BpIQ=ckY1gg*9-LZSHpD0j2;;w& zQF+M((Eke%dx2(-*CTr~#k+Xar}g$TL50+KhxXmxo6xZ=jRf^!z8KmSfn5?soqDxt zpbAO9Rl*+t)4lAWI38)Nlk8(qgySkQ2@1U{^lb{u{&_VMKV>a8aO_oL3+@ZY<-uLpJCeLvZ9zz# zvywWcR>Hfz3S#iK&;wZohBzkEo9fJ8?u#D{k_Kk32Y*w#mjbNZc z%mxFeYsz#~UjQMR!W&r4GWVS0>v8kd58r@TxuRtJXFqu6VK35}D6p1lV(~`JU@XdZ zK(Vm>QRbdOjs&7%yg8TGQ@HU6yCY1s;Ef-_d)jVMEM_tJn>>R)e?NyWEhQGJ%B{-6 z??4&x*^*_=makmXl_!NLoptn0*=X42gg|Q&jxWaaot-p2O5jwZUty?3<5PFi)y34S z%7*uouR4SO7KscN`7a21P2C$Ky_=qMVzL!W&y!}zMd=DA0;@PwJz_LIJqd-W(CzkS zd`h`kmoYa;OG@<8C6R43RyA^RWDZjg1{ZbncwC~C4}cx?$M{JCzSu^T3lR@Fl2U1@ zn@Txd5tZUSq;HTPy^eG@+UrYNXo{^B#=P4m8xWLmI$SXMk3C(wr#viD!5zYl{#+bW z0pi*&GGq^g+e$W9puR8~*8ViNQq38ALH0LT`}X|`kD|72!n#ZvIJ^)L4NJuG>_+Qk zrR!nm$@=!ilp>mBJuN>I!)F2|EM%5;YOfhu`8zONE-#M(at4`?sNBmQGOeHskSn$z z8`?|^wIBgpL59Rfsj(Y5BHX^Qco#RM4?JIG(ujI+X`zzo@XMN}j>TIPz~Q1E;-2yI zkJ62C2CxkH6ZZlxJ!R`unQ{eG@G$;D$|4;rV^ukd%%bI3_!;74C4DDeC+~#G+gqNf0#d!bUM|;| zCbn(D1!RLwP7nWAxteq$a=fn5x3>8Pc|>72~_Z}R~#;;diL#hn1C-XABOtE z&-}!}fYwYQ;#%1FapDkry?nb*A{g6o5jT-k6Wcu^_!`1yK@e{`OgUtZhl5@$Nh486 zAe6SDHc&#o?f(Kb`A&ZJ-g7<*eKt+4SK!U60JTs4#4oQJVmH7K<$Pg{uX^f@{FvC5 zlav9(JkZ5S#xGng_&n%zG$irrzBuev3D}rn7O@5_{V_+=U*bBSPki=J-l)`oV&f3! zn~6%}`YIYd8}<4KCoTV#X7ZJO)raj6 z{L8jKT1unbf4eMcCr*#nI&xgzAw8_Z*@uJdAZ$7vvujVdYRll=oiBa!M*^13H zu=w$>ID~JmF~b1*|FfjLuHt9?xl0(^|8?6VERF@2?#YKMXF8e2J2?nytasDBLHns` zg#bcaYfQ~C;w0`AX{y4b;rtd?V<|c^qS5w}IFFzNWhj*hnoiQ=(3zBV*?E1H%a}J; zOQCxc66M%Tycv8fG`w<7VoHvPQ~#^-lK|u`=lA_P(#xsywcQUvEym^1f~%^+seX{a z?b{O1YHa--+7b3~%4YJPfMKff8=^ri>bT#*lb^K2_+4g({}f=VMwM15@QRM0uJ zUV+^Yo5rafwe5ogX^j4R12r!D+h1XlqIWU-?K8xY0{-wMd&xg6bTv9+unAYgh?Tuo zV%Ylui_p>r_4%lsjm3b12v32In(gKzz=`+$<}PbKIHyc*O0xJAQzK7$A~ps1*Mwqu zk*R^f4=zipmdMipl74k+7t8DYF!nB(*!NKp0Y3K#zrWkuh&o0Y#APkSg&gC_ClpX^ zvuQop2yJ&OGQtX%%#2+FFA5uM;lz4zG($Aizht6cHVSkm_bB&eu^ff9L#rm=O!(Uq^{08W|Q55zWZ_$=B zQaK;{GO_l0GVo?keQk5ljt@BjUpcoAnS0)~vd@{ZwVx{A(5uI)F93rvFl~9_?@ae@=i4y_y z)8cTt-KykI%5RQ@m>8Jiat{%=q<8y$oGQi!)=L5ggpS5@7CvF5zpT=3vVW~qwTEpe ziDwPo^1WW%t%;XNp2a`SgR_<6(>S<-_wE-%!{JTh<=n?#F0+=`E6XR7Rob`?W&4ux zs;QXyEA_6Qb6IU&yp%F>{Gcyviz=p^!R84UNLB@MKp_Ov(wTMfrB-1=YU^LGXX*!n zI@n%Vju3HNjrBC2wnYcT>V;~hi28n<_<#aup0){smi7##`l&Sc5gAL%qs9c2msB_A zb2~Tw{z;4i##`UdAF`dRv|JgRW^wFI-W`tDft&*$(f=#={{FvY0iS0M(KR!4jHM- zp$PHa1Nr_8_r9Oy4v6l#2qRCG8j&1Jibbq_jO5L_i_6iVYe$;F2&oA`4$k*vO@IH6 z$Fsx2GKl1a0cKP_DF`d&%P?3L1-H7=K3?=RXP~0RW~1F@0c;zNQ!`pYrefFb@`%Zy zbHSN^jXNfiM_tg$%Y~V<@%xeP-M~v^TIl4fQu-iiro661g8?9FkXEqu!B((k&8m!2 zpn021yIZeBuyLwrhK4@Gw`Eh-dL3Fo-_f2Q3+?JoiZ$xORfYuEn{08=r&QA0f1;I_koN&W!H@5|oNwPF zO1fdTTqICBM?o;atztt9;hl$56Y_3`Od@fQJYx9|cvI~2!@L}^d<<{jJRC`0C2-Ai z?egQ)dI;zif-b}Zrk$Elm@a605|bLw7n)(b<#hn*1ewo>Y!CJ zMunsE<2BRFnKTeyB+v5v=8E?i(M^2 ztZ1?y+@Uq|(EE*mL5rJ{q%`F!^UXHOkhID?Oo|KfRM`NL-&sI8{7v%y8gMoq|`UDP6Cd9F~aDy1@fj_ zR#-BRByH^>RfziO8zq0L7Mpj$*SS{ew>y7+_$TVW@0R5Vet|&+q*1E|o?F{jPXf{*gFNnMu{yOr{i1*-L+%RfT?%?% zGP)~H_A`2&%lGsPk?~umn(CX$FaH7)S(2U3QwwX(ib72wi!hP8My?WM)bwM=RfutSeI83K|r5H!KGPA zi4AlEV6RF=u5IzYyDYhdMSs4{IVS~(yd$94iF@YX=7UwJu}`2~p($pbta*{QP8 z;$GbeH>LQXKccj}HsrJj+SW_cE0x8K(7c<~7LJT%#u!<3S%`N3ygLhLGL_K~7G}=t ziP+e^yMaP7N!aYk;QEkYobdkd3i!X~mmb7pnMcc)mxjTLIS-0(p?-31l=o|kO=rS{ z{0ooa&6WJxfUq}ugimMqK+>3&1)$#L#*ABp!c zv7=7hNLxo|{#EcbyT49^Tgx>8OG@&A%=x>>=DV;EjSOXRv|`rTzcSCY``IR$_T3~{ zWgTZG-i{}mY`-Jvd3o`x&g`d%uyWgWpRzC)W*GeyTX2JWNnst@mrX}T`Lz-&I1^ld-FKF?B(4P^ zL4~Lk25WiV##&8j;Y-p)GVNPUVIt@|ye~jqW*PRjDS8Bpnr>Dj}l2 z982Yh7)WEKd06N2rT(GoH&up+Xb@b1{~5TV_as^S#d`1K|{e(JjT z!^(8_ue)9Br!+lZb`Jyd;F~qlr>xZ+gm{;R$%miyS7pd%6GjzMek9w|Jsq?1Hn)<$ z{!33ia>Oe{lneMN@f#~S>O1MuGi#r)18QSIsrl;W-B(HAhZ=WEnJ+@gOT)c#_p>HK zwZ&gqrsd)H6LWfptt?h6bXDk+I2*xz`FXiv3zIg*QYeI}*$(`QnYlR6F@Fw$w>ciL z9b|_Zj{bgfDLYt89`t=9=ePt?()NQ0B5J~&h0-pV#uBIFH%R-RT~z77+~`UHTn^O|~BA9$a>9u%gcfRA&jp zOlH7gWQ0^LQai{pI|)Zo#)oEb4r8$A7-eqPwdZ~9&UQ*l}GN*`?T<8F(r$GrEv`r~M zv-k41S5G?opxF%E$69f45^z-+UO~3* zRr7<+RgA3tbrfD&<7h2JUd#Ud<&-?OCRP|hfjjjVOjsCq^?4q5Co(rs_Qxom`*B%y zcqY1i%49K5vcsF{g(@r_wM|hR+(U!+#CD5UgS)}?yZBG_1{HI{In|(D34jm%*-pgM z*&45Nmjq4N&dV>vq+kFvZS|@$Lr-H{#)k99pjRqn z5 zpSn|gfBQB!^Zpg8<5=q$0xE}GEk84tlU8O;q86%{uw^C1>WRtWE4#875?jP{hj3^O zd2W4G%%)DZWe=$XOw$4k^y^nEuH?{TKj0|2Ndjdc`ei3C^WN)*^{A__w3_1ZXyRF9 z8@>Q;UTRP_zRm)=m*8j(0Hw>d&M{IQNaZ zU0nfLdm*H8*T6Ext&Vo+>M|^~Yt9CycJ8?Q3-wM|qCC`DV*cb#t1sp5YPBUi?Vd+X zMagC`Mac47xkx)_7NwRf7!<1w{=eDs+q;bU>RtCaz>ciD9d$pVipT59rAVW0fg zy(4#ZNzTLdo~s$%dl6-w~2Z*uYy?B6Tdiu-4a?m09-gY7T; zTLg_XaO?o1evrq$ZK|uL?x?BMlo7bN4mwkmI6G!s>$`>}47Z(;M1fqDzvk|&9QPzx2E8tyf$dw)A^SkZwiAu3nffau)^|bC#ZXq-DP&*Hd z_Za1O@H-4L4E@k_Fa_m!7x@Zivck^0HI@vs_vYEd_Zg!v&vfyf#suc>g4`;bk!7%$qQgE(N2P%-XXwUnRQRIF|4k0-J(a2wbtDHPriV{Q2*D>wyqX_hn>3-E`bCG?XzpAf9mVB&Jasc`!hAJ*{CP{&*DN3 z1rU7S_~kltCeLu&l-iehpLqa=mvsNwX3)ai32z{h%s9J~Wc>f~q2?$$w8Q-|7zkeO z3=+NF2YdbT<`5I(_#!R|-Koh5NV^M#7Iy3q4VvV zPUL^7(BQXc55$-SC_&kcH!aLvTr>qN=OJh7yl#N-IP<=?MxyNFNM9)^o}jvP%<_AE z?d*5qgThHQyRvkV*xHu!2ie}wYSIMz*CfX}w-z9fmzzQKZZ0Vs`UFz&tGX&RVrTaB z@i;K2>+bRvGVVCOym^`otI3x&T#mZgSYJNmr-XC{Ew)(%DsspYf{~5Hu8eXr0V}4A zgBkAwNvc6z)42~1TGxH^jdLWHyN6odkieg3ewL`LVO0;XmXvV-4n{&3;L=cea+I~t`g2yu-9Q{33WIgCyd|a!Rkp$Zz;yQ^#ZTVGm;WN z#;CrJGM*6q5zTG)DZx+r4U{MQ!?G;4OILRFRs)fu&Q>#}6-jM__N%U24o!S#Up$<= zq1J7xa7yi?*layBt`~hVSZ&D51uf4<*E2SPj8xU0U^Y@#%dkOO0P$L5(*b0o>)DNP z@9M|Nt9VIGa(X&bl9G$_L6*SvxJ!X_GVLw@urt5&K&AaowRzU<=q>4>tnl?Bf!B@m z>7qlox))9Oa%&`*rMAak8pnwrGi1TP+Z}6y^!v#@ZD_Q6Ie5ZVzxa4Q($qbZbwue9 z&bPd{P&#`-@I9!a;HfVNghDs}oSEH{>N7=qvU=JQ>#2XoR?&?0eDKGF)9G}qAq$)W z%k|1ZmVu{j`ANi}>UGD4GBt&XF1o@F`38l=7ClBOen-nqkljvAC0fefEo!!M9hr7+oMYCbPvRyoV!6CTwwJ-a6x7jD2pU|3mPm98~;Vq zOT};HY>yRZSPLeo6U17cg(@%my>9q@y?<#e4BdRNK0h?dXxiML>VwIs zc-n6tCDu-=#dzYcIi|5E`XcVad`G-l)G$)I$y_l;63n6{^|uU5L3SVrtZq}t#FKyP zgP=H;CBTT`n+xB0Q1klFhA>ipHEIW}M~%Dz;&?`F!Y{QSA)+C!bja2c95gXwxbNS6=>ZU*ioU-M|8j%@eDE z>3h}UokN5++i{6iBRoQqnB?YG8GpJ+Dj7C3ySn8=P2=+%3Z`)Xr(XMaNrRRAiv@3@uIk#D0ON)*&oapAf^ zXVT)Y61BA(SzdB8tKe;H?1Ay7MMbBH+l7xNJ=nY6?IqhV`}XHxp@*<1`^HwDFZN_9 zcQ32>5qnV&XO4edmltJ57AfpI{w<@P}zq4ZT-rO3ym)G)GF_J2+=@uecSHH%ZmhxB~hmHiU} zlIzu3z2jAt7IO6Dns>ms8NB#Vz+-$3=;0~W0VKGZ8kco$9q**C!+2o+^Mi+qmm*@q zLPU3%1lGd327IFvlrw+xt1bh-IAObl8Nd|$q&F`G1E9(mWv<*vETTAw+eo@CJ01#4 zKI_N4EbfKcovNQ&xf1*kUt>#k=2PzhzV1DgAVL=PWgRTx2L>WhicWD5O2@7?`gG=J z-;!vxfTv+Lxx^_~PQr|+AZB|)qa3YD7LIsk}?suE*o zeL*0NLHD-7#xO?7;5VYg0&kXgPsn&wQndH`jb0pUhB$@+i*y6vHAo|)th}1e@(<(e zLTsoIGF4?kyi4)2>&ud#UKtEUX3zd~F!voOuG+Ym8c-=?V>#j40`8`Zc?IYko$*h{NFUQ>e;pVf2xbP2pGyS{tgE1E(hkI0^$7ylqiPU z`gb?}1p_)tr6Bm8f|5#@sbm>p%1$^+jJcWCk&Uk@;O^++?`di@T}b)exKmV=?WKg} zJ1c=|^m-agtb0wF<_owTGhfw6J9@cg%;df8woxJFq8y0TYle6reX+&W!XFy&f__FS z{zQz^4qwso?KBY%_f_yBKzDo0 z$v`glmew2}FZTv-OWF@ZDWRd%*{2W3F0Y$?hqEbeL{$zK>{;H?R=h9#o1BV!&3URG znv;f}tPyhrBHJDHjc~CXy?X=l9t0=HnBe6Pm zI|;1>tfxC>`*ESUnG!0u*Cp_;zvS(4-ZQ4vRwX&O$xLKwF-L_IMBa5F#Qp|H)g}$KW1SS9je^M=@_oFD|aO~?RhgATD z&N9PbCE}yTFe`r%XL$68;)N<%8W!Lgme^bB;ok~R0_HAtDp~(dLccgy9m(3iio?Gb zUzoptsQ!1MV_u+5uGk;iZ1fng@r#xY^mw=)h$}8bQ@imD6T|IG&VPHt{|nqPTbv>; zLDKDIfh|$mLbEUD9hK24e-*^{VGG%>4!)8z$Y+J_<~9x5%&xkt0LqiRgwzv9Sn>`L z?vN^x)+K~AHK$CW2=A;3bqO=+D@M6D3$u#psUshWv&dY6E{=oSMCRWCr)h2DgE2|HF=z@|3bVVY-FiL4t-+VBnnP1Cl!+jJw|A6`D&+8p_RwPD zw}S@-jle{*XXyFeo(GpLi9H~0jw3Upr3Vd{#gHzuc1Sn2XA_~YSbL`8za1npUrDuL z2l@tyFng7*#4nr8RVfjEZuoNK^5j*^L4aJ-pLVXS0>_4X*6!&X4tf9tmcd&j^Ly8d(YlOWBlD^3BBxaj?osNZibK^;RVZc=1-1;$u zx;lB~D%bPG>_@)%J|-Zdo^vAVHu&VSrlI=Vahz?otxKHo{CZemj(3L$q^3-_?poU_Ni=BOBBqZ$tO z5y~xEYANREWxc$`W%_mT1$IT{jMw6LYm>e8%s&3D3lsned0X!zb@a&F=+|O^N_xt} zVaJRU%~|W0|6<(WnwrCa`LU=YchNG}mjwpFNTk6A!i-TjGHFI`#q_ujg6h=Yd>atU zJ5=(;yz^8&q#5Zksm=?mE%h2S+Oe2fEECg*_MqFp&`^C-iC0huN>(Ga{Re3RQ3)oMJxp)_?DCnT=L|`U@+mKW;Wq683wf=n_r%i{k2Q-%KI_uUanbMV zYRVAViFFV3k!4>2b)T^p1|qKd3H`F!(X1GL^79>ht6UjR#BxDJ^G(jTc8EEw$ws97 z>odOV$0ekD1;24v#P2;B6>A$DRP!vt?5w2i;mZf6;&0LUYx4|$&fbl?|GN1P=^_4Y$1DEygvJ=^?t6zj5>JF1z+P#{1- zbY4`!zb>XHkSY|ou_kb8S@CPAK(qswUbN-(dB$1Ep+Nx?kz7_fCnl(c|GM?(nIeYk z70ubvT?@zUL2#(&d7$R;i`%dRUc|i=#fJ4cOrW>@kQ3~O;AaBgUI(HiqJ)S3_sYyh8xL$8tv%-b( zA)~?{|4JWjhCt@I?n6r2XkRJlm`R$B$lqIo=q#it+|?EN z*MA=zi$U+&(DrDJPv+LF+C0(!Vz~b}e*fOupvnYEclgJ!Gjsi#ZsEBVES~#QpkeUf z#AMO!->wh6{JoR-yY*5+RkDUbY22MAmn10Y`}fk?b!&A=!2HhX+6y{xmt#eOxf3eBMZ684e6@6+ zxdi69{>%+NZu2aok{5pP^iarP%3Mx=91QXZ9IHNg-wnIhp^)1y>Pw;HT9_#SPjv=Z z%YvWC>Ac4jEDlG<$3h>n+LWFE`&+_di-um`fPynU&8t&(Vt#A20D!$NM}K7725E9Z zW4ni!UYATg&7N95*jq`nSJ3rzc2|2;?ZwK)3_S~MX}UIFJPDLklW8uUS#nFVqdV0I_rZHt zYi3e83Ayk;JFVDR>5%WI^T7k()S7e#-bqWnwvW<_pHPE!eFd7am??yzjt$1%-N)S- z{gk1Oz{gjxe#;9Ku?yf|^;Y08>q>M%oCY4z45(j6$aTckibtTqXV~=cPC~hwhMQAv zONvi>#>0$WtkgX6kE%nCoBy0%baP=${YuDMGY|H?0h|Im(g(|yRFy4jAyv&h68Tm8 zjrCbr$ccFKS=tF=lO2|vKC^-T?MtF~Py~4pVP@Sc{!kicP}YTeJqWoA>lzQ5IrFn# z11$j5OUMm8)@wDnSsq-@7N&R3R^qlQH6?g5%A*7tb*LdTmqqZ4Xry7tazWf-((y+f zQHrpmN6&#R&AM=TvJ>p$ewL@hN^EaF0fl(sXvcl11Ld?04%XA*!})!*@P6Eak4!?n z;YicJ-E`v(ahDP*n%as$FG@i1*~6^t6_zL4Bo6~YSFz!S`NzB)aE9y{i~?jFQVA`{9+Dt26 zQBIe)>2P^wAllp_Gp(}4a(uU!+v7IrMUM~5U!hI5^BJvLh6W4~(IHIB!O!*^AFnF#`&OP%uv zH4?qr2fS7))ddXX-*zTBqTbXOrmY9{w%mH94#vP0&d&PSH8t)T$=^MWCG0Z&{`hCz zH829C+wuJ@Opu!gY6J3xCo(p8Zy-oXcctJ_>4pMiG}JHX_hS=AVHp?4ug0~DnF`5{ zgSQy41+w*Ksn*gG{GjAH6p>LB`bYAW)h;T()^NuLQFU&!yWrlI3thk@ZX)|xPt|!) z)#WMdU0wC*C5cncGjJQ~a1b*HibMv%kq}6gURRS*K^83`6m|ZD&9mXFmcTJ)FLuh! zW#gU?yt>WinrW_Ac^D{jMY0BSj!cS3{H@OHFEx14;aBjP1*0q1R?Ys+JGjCt{PeKh z{69h&8sEbjlVX!_BOdAlcD;f>!k%f6bUz%OB-);RV<&gJ&Qm&WZHs@b{F7yd=!BENBRXgCl!+Ek$ zL*APKm~tHZ-F@BcXa^$oljpd{!UY<}Ipa#ftSifjoOU=WjDAdtK z=n?@4u^{*@d^};l-<49}Ymr%K8qUO?){qyT@@{{jjA0?3Rk`OHJY%gkdPwjY#? z5H#D!2TB8(W1gst1cdAN@Z_KkOa2K5r>M>ZkicJm+w{lK5+~S+dV&t1xATFgHfq*{ zyVE_rIiO*C*oGnhhDhLa&+kgv<7;5Rxzfs-yWX1stOMoRXdj%0(_+4(6XEH9RCg_j zd3M9F_H$1^@`Nq9w<^vg))Tz8q@FgA&-utj_!p7nZZsIRoP~`vTjPUbjP)O@mL+HE z1>gI@vRbxltJkptf&&sr@6Qh18%b^+N75^#O6Zz?VFKv%&-v;?7{9<_a9D=6;f#uT0EM2IQd~SQa)_3!*K>x9A z?&f?WLO0o3Zpo_f$7FEIN$X=t+6{V^^3(DV;8D2s-2*8$M%1fN%g8cW+m*e_>t!z{ z;LSTWK?Ay-{%$qTN_Y&04!t=&t!zE7w!a`7|+^^NknE zCN&3Niu3#=#f=8l^MP?g#GUMob)&ZxCpGaiINvrxlDr0KT&}^Yd-IFFf*yjD%%;@9 z+(*Dz!WUq)b73vFqLV+Yo9%8%2A1FYdo^%p{3M1&D~7Zcm&f4p!qve5&=tYGe0#yG zbO3AsWf%L_9}D5FrKvW!v9eRapMl8RNrrE4;RW3BS<@n)9n?^#CjGsq(wMobNR}cs zdayZzch&7+jwa!|f8c21OnFbCvXo7*HULLHs0MwnnzVE2^)GUrlCV2X4b+Y4walvC zpJ)(=m|r)GpC~@cQj^@EL3>vn?y_k<9MtWqDg$GD9&aXr#rF^?R$KSO;M`wh$@JOo z_mLFH4{zc#DbCRXFb6Zx$mJsS|J$J~>s@kha1Q4bKxuk}*q}q1wwJjFid&C&U!`_g zp4j|9e7$8*n+@2l+d_cg!QCB-yKC`crATorUc5jF?(Wj!R-|ZgcP)@2#U;3VAwZCm z_uI4QoISJW%=0Jzl9@bq-)pUFL7xW{ov@MyU~C8@*Z<+L$?GigHS@U1*4G{mYGjgPnhXjGKRfmyNyi zWJIh$3dZk0BPmJ=ihg$9j@k0&eM((CY>V?LXqJtS#K2-yvE$Uu6rM0)r;l8|`dR`E z#D%+xS}~G((tF;2yfKm%{ly(dz%?;umKb5)YoyWn8LOWx^&|DUlwAtnwSu5hx!|7c zv($V8{iu-ivLRX?_8vW8?2M=gQZS4ZJdc!;7^c3QgSq}m0S`S^R}q9=BE$H(L{N7Y zVTC&Fd%G6_gKzdcbB2WL)=!iujV~i}qdzbE)aXn8>{4SSnX<{3_&NbK#RklRI3aAq zm#<3#f&R@>&1Q(iF&ThVrr{O++sTppH~W4(8a;oOK(1%twe*>X%!j7GnoJUq7(&mf zD3Hz@K)yj%E%;vrl0X{iVI4E3SIszFwtC`aBd#{%NAx8yTu~1`M{;=1>BvCFxnCV7 zwGE6oC$EV2shmLhD?Da7R^ZkXk8Bn7WA`KdNE`_1l#Q39JlZS(X664=Hao??_*Nr2 zP>r-l77|!-yua!HpQ7d~vVjP>dLghYuowT_$m^#k#By;HA-46Hwk)c67UxV$7LkC* zMOZ}`gd&9uUZe9T^~in(Lg5Gr^GN~0v9UofS`{|+w){?gHI2t4z4xq&qJXnNGnFt( z)6(1eQdT;yfP%8s4q*%<1aySNSE?cLsS&|44cFkO$g43&?utrYEr^-sE?kl*BfBSF z*lO~t3LWUJ`!v%{X(02nwA)T=5)xg%@>I+;%06aJU>{K`@2k)VnXF&l4px?o*S?Ae zkBd#dyIyewBkVWV{?SxlGwJZJ3fSDL=!o<11ewWE@{(@|wKcD!&Xtd?kJ*ut<1n{e z+KGensN#udB%Q%=?+Asj&JwW1iOCB?@mdKEiwJZb#^dtlOG3rG>aT$GW0z-RN6(pp z6z#C86;?*f%{6Z>{C2F@s$TZzl-ndz!CJ}gfhFF2ICny6ZVi?8Zo< zz5SEKsFg~$Bx~KaXUtlWq2_2(>G+eTxu!Z`QH;nB4J2?&>q~Y2IZA*0f zzb=P)-FM-0+2yIccSU(`D3!OAPdX_YKYmyrJ@M6AC>Hvl=)*WP^Jv9}&{zLg7~_9l z%l{Kxg^!or0*w&dIQGA9=K_m1BCcP;{dW|20;gN)+-H3gIMzCyp#jdBKi3xSZ!0ww z|05Ip7#w6a>w=Xtn(EroR*l4=8+4tc(DhQ4L81vVReZ@;=Hn8BWI8m-Ul&oyVPvX+ zOpop^Za-A2)jr6ikntbxd)kza`oAs787$@F&$+z7QqBd51idX=VnmoU- zxI+pistr4Z)TV8d>aWjHw|x%T+wT>9%rU%r5S=cuf7~NR`Dx>w1>}SWfaK@M_IX$v z#mvz%KD<-qdkzo`e5H4aX1Y1b0oI-}HorybA%2M}Uxr}$=>g6aUH_?vsUZtrKkIyD zXLOnSdqRQL@G12(+a^iC6WLEm4u4t^Wn0ZISL3#uMR9N@4<#j?j6m|d1?u43pnt99 zjDVryRN#%bz2s-Jc0Gj{Lgj@eajQ2`R0@JQW!+`ag>pX4W!Sji{)EQ4uRytlHm}vr zj+@oxJ?80uFf4e*0L!ARKX{F)PoHF1^vT_+^cUVhy+gxNBnnAIL*X4C+FHT+WR2c` zgpesI(xK9mInF+x0MA>GmXowIGeb;T!W^6LB@qHej76fB&5pVs%HL_B38|>jO|nzL7fQ%O3hWTnI^poxPUlaoq*sRht-Vl46w5 zY91gKzVGGgl6#+>QVL>Biia(o z|2z(75kwS%u7tkZz3a9?9Kk`{O z@2|-&dg3}Vps_4bD5Hlf1~-^S9sNEJEZJ$y85l#*2DWSCc|(J>=dm=?)FAo1X#+KD ziD+hdjunskefrqBNo?OUW9|9(MJO+<}vBruuLaai!SY$2*(gR*{z- zbrpQ=o4z>A+d>vz^*aNHS?kumrGBoCOda$~jg;LT;8Tm8M#(?fBZa2^Z&3B^&zTY} zEq+PYtf|0^vqg@-MmKR(k1fQp1T|G$*Zba%`ac^)3qQ$E5`NB8c5Hfmt2w{+E0hBwUo-qvI24fWO#eENNR%eAkRX#|pbwU^h%sNBSq^U|im25a2LgYa%%dHi)F|brBr?^o zkcw`q207pl6t}$G;#MDXJ4w4^x%HJuW>LLit<9!zS6pv357*6Gh zD=^)sC2*BT0$RIpy=J?Wm;xyby{Eq zU1sA9o_?g(GpPenVrjVfMi$cK)fYxNhDG4ts!_sQf}=i}e8nTa{|ZjRmMt5(Q^B$8 z@}7~=xx;K-LIjEP613x2IKH5wAT zU!v@?8Vld-6jLXiLS}G^Aa#v#=u+o z9#^%&j*6}rQp>EcU1PPcFQ>{yUukSEoPym-#sHEo0_7bKZ{r`2Pa<9AkzK4AC4mGa;1O zH1fyzt6(+f(EkJUX(}Z)ajgA*HTliE>Qmv{OCLzUfIWz?_-RI4$pVM zC2*T`4l_X48Ly2WSL!3Zf0fb`6Wb^Cu+LLJPD~G%;(25S5n~wq49)Dt=z0pmhE`)% zSnc(68qi}zj6aV*hIB~A*nFd43`y;}0o4C?;dQ^Q=$&q;`EB{bi>O0!Jlsc5Tll~Q zB1ZE`!I3h5Y}n7|I$`urSd)Bv*l*3Snp+9INWw_%(D|w1Jxc6!{>n{4QPwrwPe{WD z5Fu)fDFbJ@J5Op-7~xV+8;gBme}F|^$J-N`D}y8V8rCtvGZw}Vd=m`OsF8>3{*i`( zh~4<2P3U=?{khc?_ZZ@F%Gy}ZeFbD@6C|!-ZXr$9+%9nwq;`&FKn5+K3~=du=$oUx zLD^XF?CCXLMF76KNO-Tm3Qvj_cWc=G*}1j~AQcbh*HLMMRf@J4n_WFT0ibQSFJ)NW zrT=YCPVWzNp4o-9tX}D7X8sS)kQiLS)w4H1AA_F559Hkcv$&(I_JBJbGZ7@zW@&XB zGiR`L*OVk*&$X6`YjS@=$wK-DNFtmh<2;4OOe#0GQh%#fm%bzoSSbOh1ZfWy)mKs9 zU_mBdOGRFPoj0^W?`{kzMJSWBoO@vTraX)M@Fdlnrn#?s_$HL^&m&%x_3xAlxrTM- zhmi_P1vOniLO!nf+&s^Stvk3%1v>_qsW1C3s7PWK2J`Gqztc%3IvAj1dn0AZW=#nj z-=L{$I$%W*z%JbBT~!3BENCb0oOX|OKJM#CzWAFU9A-~xeP!pYH8#~|t3R21PSXbk zGM0m4!zzXK=Jm+=S3(0ODqV20_x59Hgd-u(u%m~Lh_s_X)eGMZPybTrhpPmpR|vwA zN7KOmld6BEn8@>}iLj6hORP`I-y5N?@CcV4?;x^I&*Y&3yd?tVYddc~b<3p~1BJRQ{tfJ6W64sG-z!yi=oAY2QY0IAwzG=A)jlYNW- z5T5@XkqB=&1t1^x8!--DU9w=CA^i*8^S?rD%5=}+|3YC;{`b?H1Pi7qLX==5qaMDC zXJplb_eogH0JH9w0$Un@*%LR&bffGjTScqu5_O};pwbsN;#~Yiis>Jln#FTqg*x_Q z>D0?n5X^ZXt)jCBLHW)c*F}U8Z#WRHT%8r3|AOzcWoFqJ*uGX?oP*PF)+d?!|E&(x zkpEiE_40ru9J2N!A z8w$O?g>z##j4F`k5`fone@o+Ri?;4RMqc;LJi!+P&o|b+llM&9bglicg(K5yCkM*< z@AbQ%jmsKTbA1&#WwAB(#p_+4Czj{dWx*^Tma|B0g1z3aAoT(E?|h_^bW@iME6r8U zB`ZZa_8;OZM)wUr{*?7nLD85FoM(RC5BQtI{)sl0Hu=cP;>qFqr}^sl`Q>AO@CLcM z#5Rs?+$H5#Pm4Q9=eDTvw&}fi<-EWoT@`2P1|o}o|DcoW;t~ow-b4H!%Tp|lR*HLd zQX1z}cbC`R2m8ygC!6yYfdw5C(}x?*rhAffN*&qQ*2l>6v4a%E!UJYf}-n=~t`(X#D6Knho>I9-Rc?Wh--&ZxIyq zgD!P@O2QnBb3?v~RY?O~u-=ExlDCke(kZ$>52Si zcjHsfx|pAj2@5TeA8pmx#dZGzN>4uFIVX+rLP7)UT8Zut^6ohyi#v_H#Irz~TNA&_ zh7iK!s-2pKOig80a9>R}QBpVtkZuhb$ubJd8(9v+Vk?2;^`}cKUZj_lT)3`g!7kJ> z({#SI1QlAJrdyww+v=Z{@$&hQTvUYr_6Eo8a2X`E_YcHPhXn^+AP-D*x!XbVH=wMmvS}djA?>5W{zx^_%R3N_g4$+HkpPCy&`>CJc>=}^hZy6U_|*Dc zg=%(Cn2J@D^9*LFE94?+54B}qyf5i}U5L-_{G5?Hp498NDOs2Ip)vl>ILvI|trY@A zzq1|QfqWcujCv^_9X;x&0>n0$fLd6(dM*0ha^k7ZbjmCe#XYPV^@VT!UD&?<>v4c> z+|#Mxon}(oLq`%pK(obvTQU5}Ir=#mLhjt^`R{7)5^QA&xZ{svIDIXGT!2+u-&%-=I&nFo(%@4hY{>u8zl2BWc|hjM`H8tr^TL^Y zTW(_7|4S{??J1OSN_?1wD3j1wA`25gR{Gm{ci#$D!hM?Omq@h4&a-h<*R~`(iKEM=J%J>fS>ESnKq9aGo92)DUSw> zrxafL!`C*rkVf*4yZ^06UfB#zqDRpAkDOcy@z4`jV^E@$1Agxpy`v=uBFoSgT4EDfXcsqK(Uu(ccwneBQiIoH0lY>UXO;Pbq%949vaH@WYmCfixXp2zM$|o>7vAId6gkgJ zN@Zjo^D0bWFz;u>MpXqg7E4Bs5WVYQG_!V}jp?Ken*~P~RGhzg`S8;O)`Yf`VD*aCb9N5f)4Ha6y2@rmbjufFF#+ zBZK{sR3T&4{xKy{)8WsZiTalpQ+|%}#@eQRYvU*v3{kWO)gP$%Jy%X%fwgt#$ueX9 z-Xp)7zH>{3dM|#}#&yG>`@8j|g#N3u89ueqt;f~vH(#UwojHUnG6!sNPO|UfOzYg= zDCy{koXm8AWM3S`t}wAE8gY@%z{7yUW2wON`Y>ha^(WklRD(1C&MSS{6NRK}m%hx5 zo8p}(7GzC|?qqQ7IINX%M_u=!vLv0gZ{<4_If^;_F`|W(;I)K^NV)IG+mcaV*!~Ai zHf++3$Zo^bVFQATfi~{vc?6|i%1(Cp-x(p$Z+7a;#aW$HBFkz%MaUIDAkVwT#~A)i z68X)v_tc632aNJ;-Qid@0yhhP=>E;5bcV&e%dh(#L`Xv8KruAtRa0)i{AQ*#F!)0L zH~LT-!*hmCz~bY;ky#BX;(Vf`S+h+S$sb^#Tm2OE%4^qUL|+X_o0a`2GvaHjXz9JO zacMOTyS&s9JpSN>AN|$mIFU$KLs#>#Fv}cD758}6xCd1#i)-0ZiLI| zP`1~}tSO;IK4FRBZ4*2t|F$}4bibuJI+_EbsxVT=l~^L};93~%&!~^AufMiLW+e_^ zrjSSDCUq%gae|0vGA-Ye%a$jFsc)!X_-pWw8<AnKR)IA9PG{b!}v5L9u$!!joI2qrz_C+Xo)Y0JD-b;g}q zc!mbpsrT|u7~-#Yd4xp* z+}2}{LIfM?0%b|cl;zPUL~su*rP_66THOfxh6Zvu-ivHD>2yUlyu-v-e0PyV_gYA* zurdiFfg&?(N499L_vUqWl1h>(bXYdVwLu|8CS65JsDwxg2c*E1E4y=o{Ms-SFxv0m zf%mD^l(A;RDA*{pHDpwVQn13=x-aZ{y3`S5;Dw`QS8HOFf9&*JKJtwP5D9kmuKB9t zsJZ-ogUr%AepF^nzK%|TI7iyn)3vgk=o>!(n2_eRmsvD=dw7pE2I8fmSFX~W_rLUj zm&ySS@o(}PsxsryX;bAi4uqD(6)iWVl<{dp;nXXPe=B|^u5bfHHq?3qLoo4Bm*S*h zok=3W-(^og9((zG`hel`&Oo)}g(^&T_mcN>sa3d_x^%=6{M0;hPD5 zYt@ZX{@)3G^-5zq7Z*SFe>LZJy}`^a$n#rCq64^}J{PA5`3UtlFyE4=%b6?@#gu>vo*T_v}hlIF;Wak}59#!ber)KY)kd z2$iHEKvj?G@#9rJX*{1>@?(t*_dTw$WkxZ%#Bi7D@Yi);vp7lP6h@OS49uc%3h$#J z=+qWH^oN(#x~+)X6o$FH4;f?}gF80WM0pTbz~(>FhM4{D-5(tN#&7A>Ngy(5jJoT} zF}>0JW6_HeB@Y%0dKh~ zwIoozuy3OBm-4)1tTEq6sb{xThX7IB-!n@8jQ<+jn*IQLHiTRPj`Vc}lto>)0~>Mi z`loye9MlexO*)f#qvp)X%dQ=h;n_0{@=N*iiB@;6M47FZ z!#SDG9fa?QPLCb!>=%~cla}M?MvNriFTS@{{E5K}69Cwb0&Oz)x=u}I(NZtdA|d2_ z>Oq%a6|Ub(N{l}C5s2) zV0E@gA8N+dh4^hH3~SD%%*UF#mpJWk%463A#OV^ z@m*o|GlCK@89N8(^RET7vftk3pKnw9p;h7eb+^ zMSUcK%lO7pOo?kMFiOlUzb4y%#q9-=V4Mqbo=yfplg0>cPR3^C%9nl4WM?N=P|p^= zv@QGRy3{^cht*^yY8-tROmUK(I=q0-vE(0q#|_A?CSsFfU!0cN(W2s9@z8P*nNNfE zk72wg&YT|>O)qLHOI(>Jx9*}s9Vk(f3OK{mpNL>G;K#?aMG|CV$InTM_EpCxn0GGs z36SD1SI!IA^wHXw<_;YGmYJ0JSp(?1yCSL1st{r3ptB%K7Lw%rS}raoV)Rj&uH3;u zUu5m*US=Xffeu<50M@QF15y_b@|eE959S%$Y$o{N+P(n{PH(aAl6eH_Oeu~uOS-@x zL(b&$_*0hM&-B%l9eb29L6$&7*Ujq;-0ZoDHp-cqH@hluR)fc(n@G}-3);`+4M`uh z>I*e?qzp`o=odPs{U6D1jCxG{jzaMWxex(wODRDZ}34-WT**B`&0V?OgF2Np@xnoOtJ14jmq|6~+7S%~y=_WR5Duj6TgPg} zR4=>4qc)j*@!61#`kE#J{_Pq65vfLi6IwvAQ#yBE8Fr3f3I_`WUb^z<8y=NeQg~(1 zaUqGTJ;dg8p2sfn>&zf+|GT5rMxXc6W3KUrwFsCXbT{02CqHb94m9qqt|;18=#T zBP19`o4!DjWp?NUC;uZ{8Ao7RM}@AYIy;urfs z3`D3B$)qU4f&K(^7HkgQbFMsZNF53fdymo^G)gj-pZP@|jV!`=Wa#Mx2DA3%CZpRa z`g5=vu9{}Nm<2QE8>`iU>wEB#51OM{m@4TmSd*Cdl5fM5LeFUjBO4kEp2p_?c57m# zD%*sGMOsiUquj2LG2~g6IGLGCzgh=Y-rqf~EQ0)RE#FbtSh1f~nlxr&y0z z|5obF#_kI_dvH$p%hR}SFRVH?ECK9Ujt=#7fc!@L{h}+EPlC)n`=_m%wecnf`v@n+SLDe#+x#iDO$z6A;B7S zDe|J22ue~EbDT^PVAmsiSzL$mP={b`$rgo1$x6`q(0hJUC@w@|6_#^N zckiJo@(IX_|LzZx!j6;U^S+>_8a}2~`T}IZ4ZAS6#W(YDEGZyM{5dIS<=1+-*H2MJ z5k|Ku+-XwikB~;lH^y`1{;1dTDN>bwiKx+MA#s{Q%VJXW(cxh~at!rshY2TpV3a(6 zKwn8y-He3eOtk!_g!?<;H`gH}1vYE4S7yUc3+T-MvJ<*~G%(I}!D*z8*T~XC ziV3{qH6r{R==?wRbcQ7EZ&U6cFXcztg~_JnEcfqd$;QkjuErxtI`aKr&AMD*Plz_PjZcU$Zm77meAu%rQs2%uV?Dwt zA+7X!x)<|o^hkXX=Am15CrrxHAcj2^AQt}$g4bl*}RmLt?TU8O^c+y=PNYDDDqQAGERYSU=J%~ zMEULg^HrP7ffJSXEUgMxGt9M^jwuSKZ78Y6{IiV0U;brDH>W)?p(!9=dhJgx=QUYZWw6y3v%KoEZt{f}310_<>4d4CMg0$Z>KB4-=t8ZwVrV|2 z0;Z#M6sm)1T$XtXDG6iQZH^u956P8x&CtWZCz0yu`i`BPzja&_WczQlk zkEa@l^BzL2D0Rg)X~O^Oe$O!`1DWk}Fz;&$btHvxWAjnM{+LSgXf*l=H{EVC5&5^` zasle|lnjJf*hrYP;k4=3RC|s)=NrudbD7H$#ub{?)QUr-bdAF0=)P0ZQZs5W6zbq_ z11I?b_wbF!D>kwk+UmmlHecCB@@C0_HgcSM}1& zD=XkP)mg#HVc%_lzQqnIYEZDYd#bnGiNlym`Z||Px|r1VE!1ys=Y!4NDCO5{6TjPY z#_8ws`vXN+f<;C^WEjH+8{AdaRYDkNK4&)E%gpF4`)eFF-01k{7P4P9lDuU~axxF0 zn4dU#Eh{ER%(c(maIQS$3-0JIHF$nOw-r-@?zC7K`x_Dg9Ab?2S;%{sslj3YwpM6j z>vnFAhw6j>L}xr%B+3%1B?YZ-u^mUK~KSV_)x+QqQhsO0H~wF2@_}*fVaM` zy|}RbZXVt8c(&#DZ2~#cngPzR&LN=5cx%F3(08H_Y%p_aR}GBdniAvCP1Brr4eJJY z4E_U@hfe6skaZ$*;<$quGRMnj!k&O|$Cp64i(SmmUNyCo}Akyz{8P&@CZd51) zO=!?ni9fP72Eyd@IR)eT?PYN?A;mY;Rk;l=HM=Rg@ohD>=5IH<=c(7tzDDvchBs<$ z22?SKsC*V9!zR6Zyj*_C=ex`J8hH953^`!6*9Py;ysD`I%pxMXFsr`IqbtYOTe1i{ z+5U2OsKhM!AMP6z-yPV8XztoCJ*~;f04<`?IO8ymqrYsI-?0;T)#V6d?4t)f+RL66 zA{ubT!YnZ^`j%Rs^W>_=dkb6xlelQ2rT~allG*hsVB|gUwF9PzpTGJy#^lVIhjfI8 z@b{s|7vg=LsF>@Zf*-QKFf8h~$bB{N8L{L3%9qH@@06((rrfpeZv4HHy`;Og_PfP{yR{cZJJ8`B+N|2X#4zNhV`mbX5Z{ReYA$uQ;C(`$xBQ8Jsdo&hBp6O z<%Hy&nS{`MdWsCzccam-*JM9z;~Qf&gLIev(^&1Oym<#>ji*_0@R)$xtuE%#<>|&8 z>mR%~4%OWlBTao2-EukFws?DREmg`_&?WLBmmhG-?tGjduYDx$y1H0ay-_+9_`j1y z?VUcaa8Kas7*Zsg4G+=1`(Cpb1Yz81_ILjspJ)&+%$FCu|1((xM*oBR9X1AkXx8aE zpG8>eVK$XeHT(e{o6565`wN?~vzL0<-isdL$V(vpyDj`;Hx+fPXv|SbCF_pW-}FUA z#6Z zn8d!AscSp&5re<Y#tkRx0-%U zSN{q7{4E*1FDd%9auxrAzCy&U2_3a@CwKe{N9j+W7XtH#m{%WSHj>s2*M2v#p3N=O z&~bPEl-9GCsJxw}Yzru_`hJ~N`M`y3-;*ZoU}_Y=#+|>Sz4`OfE^X-1KrdAD{zK8~ z$abtFq^IRH_pHnLiF^0{YFQj}R|=B(@(1dX)!g2FV%fKlm!0|by~XyI0VeNBCK3Wi zim2a?paP-Pz`_8u5eCQOo%`PXQ`j+f&^hXjp}^w>u&>3qeus-}s_4VW^*Qr`Zpzxa(Os)l5zuBf=-<~Rtw>((zn)+wM%D)G}El9-=#-Yed%a8dsOB5T^63V zS5jas5RNRe0^^WFRO62KXKm4sB?^3JmaU9k*M|7*hvrRwad&>c|M)j~{yQ`d$Zc8* zOa2oysMSWlBP2wcT+GStaWCXT%5@K1%ehQoxRy!+Tu}u?usSv_s3JFd#@?;?Ip(d*uDA=7!w68( zR)pe0T7HK^=9g90T6i0u)B%x4!VUxFlf}&O{+K;?+Tg_~+UAdJgUScTX#&!hUqHq} zoIeHTzT}8-2&TR&E9GnURYNPTUz_ z&Keg3i+JL`$sjE>X*ouz$lKoC4&adQ>TpK^SO@~JHis@KJb;}a3iKc%SL)R0G^rSA z);>rgy4%3;e#E~^LR+R=jN=*$Fm+z!?W6!uKxwIE8v)T$l?_l~8O!4AKs1-8w! z%?JM>E-Y1C$2^EN2Q$phGQZ%m%(B=Ba|V4;YtB5)t>Ni1RSP zaWU{_ythqJF|t^)f>z!MgcOJ4@rA=Z5q0ceku)dWmQh8&U;M zJC}}Z*(yFST=}Xtq0p!6p)kn%YAj6OQZjrZ(y~Tc zMvcBhz^?epfi5%7KYK*(QyAE-R~AYGfpZMTYO`;Qe;Obq;sP*2io%a8avycbi~Wbg z-eGA=K2PtvJ2u|lBX8x%)%`u)qOCvd&(hx%n)y^nwXNZb5(eisMzz=<`Z2_TRFmuT z+D8fmiQQm7c0KhAfqKkmO zxs;9KsPM1tS?f6M>t?yFyJQ&%4w52WK(kDjo?Nw3Cmac{$5`bG%OJQ7kSAQ63GQ`M zh#Mj3l(;YvtYvjmPd(Q>K@Q|Co9$TJJKh;F8+FEuBr->*Bk1(}*Y*a-HFE{l*Jh;~ z@Yni6F8Gubbz99@!##59`@nrzu&*i_io{}_kPO&oCh}mrS+!3tzFgkv%Ga(+T4&`! z1s>AuV#a<>;@T!=zASz@Qww@?o#9}jyPeNOdCnJEMF0}X!Brdv-C=Xf44VC23_Cd9 zy+u3AH>gb(X7BXJH2oU&CYa&;*!e_wiG0ofq5Aun268t+j`DUSaU_z0w(Xf=z5;vD z_U=V|^)@!g!nolp07i@coh(Y^CR5bFCZ|xsf=r!Q-${sf|D>6-71O|Rq7;GcsJP*K zN-4KraZmPFXOPYP9hUsxRAhZg3rowrJxsTe_sjO+p5cNdlK023mmsKr+WmDf_wN1$ zdb931coQ>NAHAqeW7I8}PS}{^Ls(7d_8$*E<>#SlZ+2o5I+GnUPWrT~k=wUS12aPr zh*scUQg1mQi=YB$|C=xnX%2ceQW0`LFgC+8;1n6RB{-Rna-dpKM&`hWrLR|%&OpH* zR1Ml=ZbYXn^KQH69gN@=1MDOY%UbltYG3E;)i$IawRMlqns1Wl3lH{1lWcpny0>yZ zzTVgK~C-*UCTG+MLYmn$b10`gZ=MOE3|~2|SqYS_MWPyg`9C zGu(F3*2~0cMn;WFQR@pw4ZD?vUvevB^~Y&H_IaE~xQe#5jxIko(DBnZT9t4^_!P~F zSMKEOf?nb|?XYe|>w*sYeYi+0dQ-=432X&~#7)IH&*>-$p=q?%uv2g$&5mG`XkdlH zZ&!sXUq({Ow5T%gGA6JWm(P}cxHLwy-P8GTZ8+Y7CJSJNNIU+eKDaYr7x|Z)HMUz+ zvbGzcGy9O_@7JhBzaM&wQ2^wYJPSC?3ae0=i~ekv-+J}8@rGCMkxX1QK4N3txEYE@iY>UAx|~MAfOSkQa3wjyDP{czp4WLZ15BW4Pr?N zh5cT>d*+=@MDNyZNeql+G2`xgcx>J|CfcM(-wvVcRGgZ2TOKjNvZNqJ?|xh~i1IVA zyH}gNF86hY6z|>dTbc^r(E>!b)=?ohuH}AMywJGYzAm&d3AfNNX)$BPdq0wwspft5 zB@uUTg(%X1Ht9ajv@^9663b!P*F{P2!)->~1rj-b30iH+9GXGYoi{A?!%I%$0hNje zGOs4k6bQA6jM1F8Ml(Bo<2_ zBLZnExlW>#6Lew8c(OqBu-GIX53IhLW#X;N-PMHd_c4sK_#Cq4-$<4tvaPAD8>!C$ zC=2M&*nbFJdkV2HeSgP$TyKLVu{=l7~0YNlmw)A-=HqB_sPk!+?k=&Z|uVBTl_!FW% zu19xy{*PKw3&c=4T{Iw%BL?1{JX}s)3~5s8E7Zo#!&Np)fFjRL)t_xA{b56|CCm02 zV!{l-$m##M-C-I4L&n?1&t$)g1{o7A_+H&v97ia!FOPZ3hJ)O-|BdJP?B*qDx*#F^^`%2cDbCXNsTC`r*5!Et+g@nGTp_}wQ)OBI_ z`v=BkotHHa`<&$~v_AO5a}VU&wY&S}{8}QX7NO)x;O-y8U!))d_Ycp3k}kfp{4tG_ zPM32)lSp`s;8k=)RM^-qW1^;VxOuI;N4gbK)9(GF+h(>eCySlYoliD&aD-k_77!8~ zmEnS7K^L>d6>H|}>w>-|BEr2*>x^JqiNyD>^$ODC1h+C3_4fsLRCWVJ`d27jNsur5 zyND?NZM1sVzrzYv6b-gD)?s3>7I}5XLCXck1u)(XNic88_9wC_$bHWEMZh`;tj8+S z5t0Tz)w$vI(jM((x=s57$}E$16LOf zRttY0Y)X+}f zpFl2JLRUaYzH2Fu8e)(UClci39~ zjF$N!${!L&;OMFwv)E_CNT5Yw*zx}UvyK7!*tz+@F6zzP# z$E6}JVWYvM&F0bUB2+NcS=E&nn*-Xc}VrO8-2@e#`5D@ERtW`Bb#Y7~mlCW)VcNetx!|d85ng6+c_%MOlkacWnJxa zRaHU=VNlm!0};ThXLIesFsuT4_TWEuvV_(l0p;+x6hKZ{+Y}X zN6hYz-t~~n@EFI^!|;-X)LZTH&7euK%wnfCJhZJrI>&E`Q+;sdFMrymacXUy&IkM% z*fzT)oF*1l1#x#>cWa!!S&!h05y_I#di0AOJCakzTab7zNC8NNj$4dFsAO{|F{%GP`QM)!>J*`59tAMo zouQW^137U*JqE+8S90J&2d`c!A-a^0sofO0VKu^*1140KV)TkXCw-vMKSNZ6EZ+Uf zZlhbzyWJ-6x4i+p-WD!r4=fxBT>to^*79}l@>21&>uaJv%81~%`T=>jk=>Z9RNfjm z8#8@gZ z9+Rq8L4fq=ULGvJ7l4jKWI2)r{UfSiw8-iaf7_g0q3iAXlI`{L<=0kj9~%gw0z zk-`W+eVq6TEE}fz5Cv!>5$-wbpFcQ}s^EDYjg^pN^SnA8jIVKi#}+I`6vYW^L!mCx zo$!CDA+T`uRhl2?$r<_}ehCmE@*Y~ey4*xTp2i)ukZ2+ARjN8$x=`ecH0vZlB9`pN z0(2iilKAiW%SJJNejk%t;A^DqG+ax+^{DX9C|ekeOdGg8sm`+&*m_+S!Ogx z5ib;v3w6qBIu#PNQkhK>rrd*pXZ3O!z+F8tOtp4MUMT+_Zwp9g^3NbDbXXFV+!o|1 za3*tLobyvs$WeGP_EZE9NXD{s1II=0EBf(Ng@?ukqx&>d;%OBVf=x;E17BD+=mwiM zxMF?g-HMmHzURNN#&)_pW^YPbkM-Y_u6A;_P{;BG!d;(#eP4Px&HiBEzLko7X{Rb#9J8O2;RZ3QJ+MWck6T*n8g3 zg;+TZaq;}C0LM5V&sc2K5DivhAk`p(K(bFMG~K3huRa&BtTwZFUsu69Bpp6Y%6Np6NcL$k0{cQDXtBaN35Y)OjDHfkcVEb zTZe7y;9xZ?ssg6pvS^pPHkkT$YYd0lcZ_R@w%Xg8rl$5|Ks&J3L9zf8HiHq#0~IOx zC5^h-H)D`~wfw(+Li$n0|K(%F=dXYAalV`39wq?jN21ZvJuW~PLmao)yyAijKhj7qhG!`BRfC?DrcK05%cw4+H4xYW0gNHMV^ohzLUc`w}NHeDvWMfb(Bcq*+`yn z)$v5kOOMX#4>UALHJkUCrOc&O*uKezf=WT2n#DG$S_z3Vug0jU#=DD2?u?eq+^IIV zTXREU%QXqYc^+o<))2YGzc+~(gFlY0XhG05`HP)$(Qkoskk8qxeEyKv+Fh%AiA{d_ zcr|L(lt{Of7G6$OA-RSpWq92mAZI#QJV`rx;;zcpHX9+M{A|&#=Vj&<(XC&mb4+Xo z@tDd&)4wUKh^gGug3~%%S;8G73H+vTXjgslX|G|kedT!_LTtw}C+(niFW$Trp&{SU z@=Kh#4ZOp`e#k@UN<_!aI4i?xeumg~Zv(29liWnyB*p(j*IP!l6@OpexD$dDcXuri zT#Gwxf#O!6xD^c!2`H}Aw7Hg22sL{W;aPlQxxKl}s0CZ$O zUAwzn4VBexh0x6dqCkr~zWqrwD>=Yl3?fN%Y*xn^Ex~AC!)!d|OYrU3R9>^#dVTZ6 zLtV1NV*mlQRuLELK-RTsMEZyhI)GT$bBB`oxJld=h@cG0S=^1lW+D_cAP+LV}HU?JM_xyJkHguwl zG+B?3u|DOwdtOAiLV1_>G_mXt--<=Y9nz3zn7NcVH!yqmUi0J1t05W}$$biEJURyV^bRCMh`ikyPObD5fXZF*hSE{1@Gt& z?(=XTY9(wsuNP++sB8JLZ>yQfIc(QXkRNv~3{dK}Hvmtr`|^2~KYTl-o0xp$FFXe! zI-mXyQHSmuV7aI*f$>2*)l|nT9!y6stH{wd@eUNJ1@5{XUf5SGj@05iCT-{bD?*e{ zReue2)1i>utLG^LSf_TQE6ury+R^EeXHIn?Mqs1T`E%RN)`5;^PT#Bh>`}ERLLY<4 z@~bZVs`t&8PesU-VKT_RqaL?AZJiRn#ndhMHGeIPTlp@wi(mfN4|Teu;Eq9*_^gum zW+CtVmOzm=O-(1FMxCm^e#^OX_}wlAm~@^5U@V6umQGjq1}s^4W-8Gb%(2rk;Yavv z?mgso_4RGtRi4o4y=Sy~s#}N2s68mmV8LMNN)8D52N_(L*|UBqUFr?E4^dDz6CLSy zjzqgKFUEIU@gx~ceJklo8l-V6|1C1_<#L@nyLv+8F$Eg0@!z(anKh?obRpW;)kB_( z{cKgZwmz*a)|dUpK& zGhllBzK&UBbbI})STgV9`tm{gdEmW3cgBC8CH?oIQY!l}Zn+)9>5qkC^Jb$Rs>?~~ zYP0Hukf)2&c2)uVYU9yZ=o(P^#x4TQp_(eFwSJ>h>&8_pFn#_V#VF;pbV)FTw1M`; z*LEt05kC|U6dcZY_)L_K^Xaxbb>G9zAjXc3ZBk(J1~$*LEBrA3@|8qs1IbU$;OI<8 zAU0ktt8s4OV(b^PHY;u+A|Q{$ru4F(JS*RQ_pN6+|CS6K9XmJ>L*}Sg@85lO{yX8DSXAe`OB625WNVvMz5_?MG)j)*vI@7* zVCmE1x{F9i52{Py8;pz2fm-6{am$)Wj=ViLG6Mo}~bK`rAw6 z&MPtXwjx@Yr@w&GSZheSsx>@8Dt=P}-O?BLEm`XzaaGPZJccOZ_+UD-iQ$f#A`0y9LF3vu4r-z*t zSmNN+OWHahvKH3+8TW*OiujEfwvRTsNnztKNYG~iZ28-L?3Ww4Y(TOC zR@Kjte3^v{fx(uoUL@Ma=lz^(fQHa1PTvQy+qs9jH!S|BO(A!ZBKLX4dK7+sL8UKk zn$fM%ZR!bZTffu^*dE-OdFoe|7V7wkuR$|rH%Xys`fR~0R#`N3;;$J4vLs?Rh#$KsTnND;Kk-HE=kpvcyNDENWPUZCHxhJl zu42$|I(7S!24#L4GP8f`R0!+i``0&UYvmdl{t<8X4t!LoJxj)L*q`I2!0gpJFL?ad zle{nv7MXxhIh`|iq8GC_6$R%3u*tC@K(Qk$>-Gg9T1C>Xl^ya#79d6c@1N_NbRG#b zoX`G~{)?g;|4_oMoD5LKuQ_Z+ zud-B?V6{b1U_x*tmrT0OgH+M@HMC$;d%?L&gM7#38#f3jd!Tg3N+4KHtmIwH>A*5<&Y`2LB|dZ}XQ=ieBB* z?u_mEPW@+Xg<{V_IIw}*+fRP`$a?QRp?akOvF&O1z`CldI z!k^`&KXwy*KkOuoj>^sff9=!ub-b2J{5F3r`6)M;cN;%^w~bVq5SkcQcX1Br#HCBG zH-%#GHayquSVm9Fe)f&-$>%-J^&nsfYeoRS&n|!Fq9uRkT_^JG90C!!>wr!w?@^T9 z3sGiv@6R_mdXoql%EQ@S z-4$QL@}x4(A3ZrvewGFq(K{bz(q*TM z7Pu|@!nY$AY*k;!_hMIdK#hi|J(1RFt9PP5qOeV9-@M4?g+G7FKc`+3JNJ1LZ@h7z zwMv5NA(sDdrS<=)wf}WPGA}PT%lpNX5ZTS;XTfrJKDIG!yP-<>OB2|$Lu2}E1ry5) zH2U}Qz_&LaZ%TCmWDouIN;O*ht?$>c&&m{Sc6d|5S9pB2MNv@J!)N4Qr?uA@oHW~! zux|0dT0~o90u?r7&Q2vxVy#i{dS8X8I4b^0VzyEwVF_Uvq)Hxc4OVb!?rkBI+%teK zE%l1B0>IdqitXaZp+M1#SeOE6YE>!I{qjIq%8#Ju!z+p7&o@tzmpK_eVTLqXuevf7 zQ?ynbiJHrD`@_`3FBy71qb5gy3M5f5(X+Et^yzd9MI$M5-k%Bf6V((@L?aid_=YN-vYXZtP%do|t_&7ARe-$-;-efOv z(z$yRdh;sP#XM!8FzeV=78sT$2Lv(1B8dY&T38EIr<$u3`;+0X0cmMxqQ}w?H_H5@ zhOtnYs&5BP2?gI+O1y}$1>VO8uJRF|1{rtJoZ9I%JlX>fy2SvUu^I1Dx1wP(fmYA!&9nKGvuwqnT(`7m! ztcvRkx60mjtgkb|Q&V`TW!us$*9e{{h|jx06r8r$KvixY{EBhm0BE6@|V z?lOvyTI*nF_|lQs8bYjydWlouuV@wZk^9Cst?665Shj>NHZ(`c@aOEZ4;9;OaTX%| zq-*LdE0S*uUt5Oyw&$e}eA}G*UoGMP_fge*Cj5Cp3DuqUWr#K)ZoOHM6XmOwzgHP| ztDYUzYO%}w&FS|aiF3e~U+1__(c5E7aBKBTEqc0tyG=FC!26-jS-+E^ax5O{SlUwy zS^I?l7&$H2DKA&z?pM;{pOU87+aA@5F{VhW!($;=D7kdQO~^PQ|8&Tv`1vtJqzdyD zC)6EKFcx>akST9M@xT>l!62=U{gRY#2pl%k$(9GzhE8j@=MYmgs$jaLs;wIeu&Z z?@|mcl%%)mQeTGC-50TG=Lee z^RJ6gKynZ$;rx}b+)aiR-l@$UiM)smg`>e4vdXb|Yi2Q4wHNsOWl) zqE7!_gOo^5y86uV?qp1DWd0>KIGgJ1SS?q#!OSoN{}!JA183q(kFHs(dwEaf70q0$ z`3ZyM@00k#O_$e|dG`{1G@t)1Pf=@r_D+*3EZ6sUA+}CiVwali&A6~zw*%zbA%X|t z#^+D<2y z&UhZeeuWZ`gPUr~xJ9uX7Elw&pi|AGmg9129G%W;^$sM~qApeP{0fAun0b4kfzU_r z8Td&}Afb{HC-AL36V*AwWAH zRq@LFMJe)$FcJ6;dMEUrjp0r4Wf!(VI3EK{&n(O{J6Zd$!-0A2lYCCo`G4Aqmlpe+ z4)^hU`w3N$2<_@5O=r@_^_PeA65|;NMj&>vVCkZG($yLBJloqRGOo8o--8{@N-z#b zc}doG@4Tkh3rBtQ*xBb)yAmWgJ7!Aqj|fytO@m%GL3JfFnmDdruA+k4N4kt`O<7B{et|j{jU7I(T?*VyR&4E@L!SU3Q6n* z-!;x5X*V}jQ{eYw5@ZoTp77@-6W1>mT@A(1pH!VdYw}xjdm}%M@jd434dhST#{_|w0UY|fX z)ntBlU$K~KM_@%0@Gtl?l0`c?llf#{iG)e6Kq!_^?aa{wMAAZULt-!Mif%$@qEyQK z*H2omTK^J+Qr>IciJ+#dV4c;=tRZLST9MW+-dTKD2F=ehl3hvdptbtiihC>T@EmJn z#KXg6u@i4=Ub<)&Q6R%)#;)JfXY1v@xm1Yu0ppG0yN-JEbeOgGWOoPQ3^<@*nQMynSfDQ=9%%_!o9Z)rj9NR{p;%&z3?TpP4`6+JwL>3tpue-TEm z1|tcA00cP!T|URH&N|aDEP59D#T`|E+b_UfFLNXonBxT~}qd8(X@$87TE4$vpkAlbjw0-;R7A zpd3mwfBP`fM;&&Scw~12y;b~0jAb>y;QQ)NTHT>a-0)oZ9$Df$V z{TA&#RohxHfQ+4;1tkKeM~$_0082!Fz=TZ3Nu5U6Sj7`=BTaHMrrw$IcS;_GG#8~}h9qLSdG3IBwy{C!RpXHO9 zG>dn47oK@LXVD7PN(C~-XWcoAI8NkG_qF%--!?YC*b{O*`AziQYdv)}=LL&hLv z-@5~|NNUQY>NLKMW*PTJ>YhZk(UDL$RgN2d3(gJ;e^j}|c9qT8qp=PQ-V|$cJ*un9 zyzx?@K+p9?cd@12wp4{t%;(1+Y^xu?|9V|RXPgpGys5Ap|Lo%)SR?M=I%+|*LIf9l z^IA;Surj>5;TPLIrZNl>B+)mo{k&WuD)c|$)uM4;Gp6^~q|Sj#Yiw@L?^^Hb`UiW0 zqIIMoYyR52g^z8|cZ{eS4IO6ZTM>~!6J~O{;Ape4OzJO z?6FYFRq`^1fl!^VgK^`BrnGCV2JSNo!#8#MIYlIE8T@nwtih>ZL)wjP%Y~p~_mX~v z0Q-}kwM|IEb#BmAYTR;pYdGjbp7i5Q@YNb(M+VHtSC~NOEura>pDEts%8n(NSPcmc zP;2?nN9qz=+1089?8trPqHAx&<0xRSS>@z9-&NfHZB>IF5dfl)#;U7Sc@l@IKfzI76#0c>G zHX7VA5aj+ZDud2sp98^e%NEyiBlXwhlM1Oa?yWG3JhXHDx>&xyWdV21;XeD@{Q^Gk zCWzmn8M+L@;y*rn{lX@p;S1> z&|B;_jX|rSXrXBbbj)ho4~~j5`FWX?9Mo?o@&{~A0DS@r!ec_(($~c5whsY^ecIlj z!;E}<+U&lrPqv~mz){18!?f_-8h!9&=O)cp)>qMv*P0x#>f$DU=R0R_jI`oM$=Ok( zEI-?v_s&c-HLCvCvonppUa9p^is5o6clgND;Vxot)uv=Z_AD#s3g17y{qLgXDXq&3 z!7zhE5GpF~arva}e*(U6eS=r+UXC$TO5XK_fp(RGM~#$pa%-J6M)R?)R2W?3x{0ml zf7s2{fq6rHNE7s+;JooNQl`>>m#lNluO7R(=Rh5W3L|7lSPAi0Op-)lwn-Jmxc(p^d4 zUdxDk2e@>2Kb|rn7bC zeddG>hi;`yL^hBIY#hn~%WB8rATPKu`q|}qrNp~krA=1)xJgQGOytMJwN%fsIqm)h zYT8=@A9Oq|WT-#}Q|gkav9vY7y{%`Eaw{hJ0YBa=0fWYUy5dOEf#@sg*RCuS`;{k5eLpU4llHn))Gh4mk8 z$iLk)iuirMA%ESpXqyXm#6?t9uuuntw`Jn4oJ}id2h{UoB^_X|PMzG8#)81D+4A%( zElU7hZK`S3Lj!L&&YoD1G_&2d3ft*Nv`0_2-(?8UY4-(DQ*A24*Ph@{zIF@B3#oNV zYxhGk;WKA<(~Ib~`lW|iYhlU?M)qW|mK*lWQ)6?mwROuIM?i1fTTB!8Vw(GIA^4jC z3+^kKAA#*lgC~KTBNJGGG$HXFO+KYLuhq}?8fs-ju5ocEcE+?Pw-YA}HyHi}gr8s} z9N$9nu+LaduEZoO9@CWi`)7?z+wE(T{8+U|u-?|aA+E^De80#+WOYTh`3Gx(vzoI> zchLlxGr}XW<<=TCMd51f1(gus$K)7q4Gss{@1P~G5>feR%{0qfY-ER|&L4>t;kO@_ z?)o=V{VD8t@aY0B!8=6xfF;fSNlNjVnsBp&SDX9K62M<&G75u}a3Gb7m`6c6EH742 z(uyW&;+_m}L%W7ZVYulFMKYr3&jLy^5XoZ9e@(Tob1a#icNQB3h2 z1t7Z#w<6zG$8W;jLdfs5mF)QsA;bJ4=h*(s@~wDZ+8Uu)e}kz|10%xN<;vP2=a8dr z28Spn%30Cnppf#g<%Q2LW${)y@7k1ECa@2ZRQpi#^7a|^)pCgfYR;S z-@k*-yjwQx1&pllc^ZY|in!tPs?j6bftRfSTXhlL>)g4{(L+-ZvRyS>AfBboq{5|< z57v zQN^UIdZ!s-r|#FDEErKD?`*iffK6tA>7@CGatV2}K~&i;Yfz zo#eYu`QxVCGLW9yi-M3-720S+TC7xJa%$2N^JBGiiZH43wV@6Ng+qG&q&FcGq$!O9 z*nyN=>TR{o(JTflC7&Jgz61(C(-SdS(Yu<&_NcD8LNN(xhW*2HFy<*3(f;kzmMiV0 z$5`Lp${_R!fBlC7ktN#btgCDK^^dF{-QXpOgkt63a&Fh_9z7Xd-baKQ zH#~@+j8aGe19Y>)tGlB7EsKcA!qg}a{-f2-nwI%qgjXTvb(M5KMM6lBKr_7yR`)v$ zOgZKDE8~YMZ>7V_35SkFJ~L<8PjOs-NR#YTmc0zHEU&E^B;z}RowDH{UUl(b_3eZp z#&hThdKL}kw`9km<3O>#csVyiT;hYXy_dL<&L4zNCrQfRXV!nGcYK7%?C*akVnOyM z^B*IrAm=Nh4%Ijud>+XEDm$);6D^%T*v2xPfGp!Tha?yRSr*&@A`_r1@!~cI^?^XMDZebyC z@hHUC>$4#8H2uv^>RxhyreE>Ge@VoS>+~4g-r;*hy%Pnc1HQ4_zD%u0J#1dho>>g- z^rL}=K3O@wJ5nV5Zx~SI!1WC2$Rpiv7-AH&2WW_KFuC3O(^m+uMCA_=XjZ>e_6)Nx zrw3_t!~4_T|HkN?U`QfCcs+eaG2oVb^PHh&%J4 z>DF4#?Xw9NrrWIxx#76HW7;D)(t6lTs4R;-+sW(X!m)WeMO0mCwvR1&#&m)0;zS3))@2f_g&LSF~0f7L{L8vJ2% zj}?y7-1z(>BCa$Xr0qDrwDDjh6IagY(l4BOL##PV z&-=2bL>X>|#jHo(BJb{%Jmoj3ES+}#^jLgq3q2RB2q8F)UjiSN%4VydDrLLxP2{}B zgj3-~3Ha~k!8pI+1gsaWv$WjGe4x;aYwmh}e-NrbU`;)1Eu+m_S06Fq(JLYZvl71(GY3uIsa;0K9;eij$Yy_2S z!ZliXexM8Hu?kVq=V0ROME>7}@%OaYtBy_I;hdB=`c34P+k^}~_--l8g0HQ9D__%* zrfjEzCbe|LrtfA}2`J_iuYneEC7I6G* z``&^y4cekr%0Rp)tJTn?w(TM~g6R9H4~I!`^-Gu}DcDT&Zz_%#r2nwgO0AS?nM^ds z%F&*^_10}Rf7RfozCG}|_<^DT#ckI+jnL{fidbX)+QQ1X0uR(dZH_PdeKqVRMeBl| z6l;rzK+?>C>t0h80gQF=zso${fRcB!?*2w)9769RsXkZG6SbdUyq2fNR8~&)IW}yq zw|E9YleCGFE8-VZQ#D)q2Fy81tVmRb!^@L1D+hI%j`2)nOvHz&bcvza9Zie($^q1= z@E{$-vL#|hm^Eg0M@O-?%a_b|{S&}l4<;r3>mwC{bmO3JH$4|ZPR~DPMK%#F?jzJ{ zQ!9}zp(aKF)F@uEW0pG#duG$2xY)Addu}HfT9$Y%g2{Pvf!s`l)(H{WM^nANRm*Yw;23TsGx62mLXcOzli#9SV28aq{ZLT)Xc#e zXE(+RDLb^_k0TxUrTZ}8{-sU{gGaW0C^>?Tfqx^?>+nEm~R z3%B%5e&?sUIZ3HxL+rt9zk}Al|4MW@rt!U?arSTzX=(xK*4-VikTWKZ+0geWNIGpY zG%W>f#2xb++5;Jn>-yb4W2<`V#kOnhnf+;VZO|9l5Eh|P$vlyd7#*RQ7+6}>i74(! ztdsaP2wK%8?}~PFy>1{xqT?#V&ou|q9H`$I4r%`*1H`tqc`46BFP$*~pao@u%M&}$ zQ&rQ9xC(AEu8Ei;&wO^oPmg(3(}pE_!Q1Pi6SpZoW0@B9SE89Wq7#1dS|TUo=p|n6 z>*G=ZA>s`go=NZXaGxeVFSPuDc6%z}O?_ltl7!?m41PMIz=PsMVAq8M`GcX^{0gD= zwYks`-GlNO>`i~ZI z>nU(aF}RzGslz0@M6L319a_GE+6K1i(L=r86}U`~&EBLFaTMGDFydNi>^Y0E_X=A? z2Cx4r_@SX_1wMlX>B_G2r}Y+Y+fw71%Zc^32+j$Z%$rH@6ItQdrW(jRzLdu$Iqu13 zT})dNFV0_|hu=JqgsD-Wy>+XN3oo#W|7413OM6<-khfcJzI4{1y?yqO3wMlkcVF?H zNBzvW;K~-E9lu|lSWst+6WSZH;*^!0Bplpn2Zm7v=I6KfOxbst9fWZdeGMU)!=Jd$gHQ*s$PA{ermNI~F$ojui@ebCd1 zq`OG=4*p{I;s|erJPyMgJyu@)zSytOO}m=Q&@UFn*zaduAth2EZblr724f0m2?c5>BQ|%7*2h5XTv*_wI(~#<-k2PUF~KG+hdV$IN2%!VE%M(y8}dzuHQEN-pDQ= zUs9ID@StkQqqG%qxg6X$@?_O?lCHh%KU7bf0L1) zr(yy(1NK-i6`p8gI)^J5g|)uZQ0jSO%Y3ZsYFyN&un3_Ph5C{KpjSxGj2N~+Pmy_- zrw7~v8LrKa7yGR-+7;z;HI=zI=UK(y4c1PojbX{R*>$7L3L zN3R9kw6SFKOa3pY@(Z7=f=gF)^=J9yz=}8lHSMDaXd7yobk$4aR%UC#QjmG;K2&I( zj3Mz$+duE=20@6T4(ks?;s^syIWgs9xgjq2EeD3i!GVxZ^8dO*xZ{B?f(WzPW;eb0M z+>`^ZtqGt^NXQ;`vJLKUqfTI8S+=E53Jc91IFG01h*wxShkN&&_s`AA3rJ4w*@Yb4 z+ozJ|9(L!v#DloUNWyMVv8RC_LTp|@WvKLYrx==BSX6+e6a9|8$$9qQ>zPOQp}r1I zEU0(KJCS#75l*0Kj}i~W1!ljlM|1Kd9R#)icG5VQw)sx4mBBEHvVf@)nM8po<#p8; z;>XyiONtp754zDIk>Hzm0ax{MD`*Cq2uF-gB`nYp9q1+(iuAP68Si>O(fjU~9j=^p z-#QBe*D=-l-(ZKy;6?=^%FioL1s>XTtpt)!i)7+9vslmx3B`o&YJ7mq)d+&}(ZqNc zct(=4Zy*wU5D&_iy(nK`o0Gs4nzNYwLHOY~6ZD-wgSwabR|D!r)4bO5N;QdH< zwJ9hXUVKC6xtsW_5g|CcXX&Y;`mk0DW0?8%p57b<|;80P%Lq+{+tgF=L5XC+~!# zdc6B+-A7wG@lMnPonNHRPaW-V#x~>IH1t|&%#R!fq5oNg693$HW%+4P{CHAXsQAjb za+AC3@?gdXhmhEALl*=ByZE9uL2M2WaCbj1_rOZ1;k} z4l_vHSY;wm7OzuaOVLYrCaUh&3Z|6|x^AKA@hgFr0W;T?CY=ie$kZHl>xvyjNZCHQ?Qv*Ak zUL1$YdjI8ZOy z+;^!LTJRKL&sp$Ha*fvMef|yQcwv7GlP_qyv0d&#H3`QL$(z&>hwk`>;C+kOk^C zLTvKYEjDI}RNo6wZ#KIr4>Q!rSIyZ$rB+a~ak51pLRtc$QmqnUDxO{Mdc5RyEC6of z#FPzC5#2+%vq&+t=)#|2VFR!fs|=RoCVCN|>LAK&0KmP%2f&}1?G~~QXi@@UfP-Vn z>-`hZBE+ek{>R(~y; zx$iZ?4zH61Ci8$`BL0PTs~@inxeXz{&ckn|zjw%XdvmFzg8fm>S0?riZt7zTvhYWD zxny7-p57!T2Qw^^Sk* zjg>_qSKYznwxP8On6ejaP)Q4;Cgc{;$)x(b!?9Yp!=L&kV_f!E58yR@T1)gM`L@8F zA@M7Z=z;=4;nYDa0~Pt{A#Z^fVH z9oU$7J)-BxxFc@`(?j_^RbS?ZUB2t-N%Kvb4V(Q_;MXm-*VHzQ8f-0Ocxa+qETB1$ zViK|;t!)tzKiQQfYMawn8lUq^Z@LBZ>-$s5%7{@_QWDWHaD-ahH*y&t-7 z2F6V=Ry!bi!kyw(Ze9D+NAv#|>@1a6_82jB%ftO_c{*^Ojt=`N^iy)Kbkr2~>1a2r z_!NIYu_J=|>S#i|y{$4BwJR9Um!)|Tv$t&m#?IGXh@7`AI%=9@<(D3_jk>OEFTq_cK|FGH1h zybCZ_sXzVCH6i|ja3MS+T*s&|^5(~02<7veJkghcp03x``lz36$#70C0`D36GPKI1 zh2cV(8xj}(3R=0vl8A(4xV15@NVO2qEF$B6;*1;(W?!EabL!%x77@cU|CjW0c^{U1`Ak3Qw&i!=0Pfz5k^0#)AgiwE58mmWG((549H zHA&R9+GWfTfBukD5!aUPMfT;qr3Bu|^5l9Fj8*sruMXViC)L45Ca0AAwk2_NnX`N7 zP6=InKx)Q^7j8vUId$8GWZp>i5Xx%s>>S_p$JN_hu=0WE*s|;f|5N$$Jd*WFczMm? z1Nw5t`hjSI@>O;rxKBl5Z7)h=Vt9-wi> zV2A#$ojLJ}kwAcA`qSk*=ZKE1XCb~;w$!rR@c+c}Fa~;e%|1Sdp1yM7`5(j6ceZhb zLq>N2j?P!YDV`=~;bWs}V4m&9?FY&K0dN;7Ns6;bPmxk(iO~-Q>ST;z;7-UmxUv{0 zC^5M(*6mJR=_x@B2vT$naef9Qd>tSm1k8G}SFVyzLQJY!awzJ$Q9BJNl@xXj(8O&e%IH7D0T?F`(n~OAu<#5lfT0eGm`@aM3Z{(~^ZuN|~+QF!1n~NI6 z1TqdPfmv_c8GtR9jP(1xLrufdN3I}ETZMn%p2eyau^cJ3-PTmgy^34UIWi5jnv(ls z(ht(E@CEf(&VAPj#2WlKMYHV@7W za0^I3Tss4^Dywso1jL&JT4H)d@LzdKs*bmw5H7-MN5bzt_jel(_o0MJzO0Asx4BX! zu#_wJv?Nr=<+5Pq+|VIpLIJRAieh&f`!sr*m4^q)+0ac0)hq6#zm`w z*NfL8Xh8hw14`JKzj8qrziEahhiy;^r~`>3FblWGj$}j<-F$$UJ&V3Cp$fC!?E}<^ zJt46h+uw5g<9!hm+b{mrFN7}ezYhoKXd};7ox6)B*3_NuD99C7ZijuFl2 z0=YP|9#yWlK@wH)&m+{Rje+B@Jovc(Q3*6aur}ZC(@H2>2thbyCbukga#7HBkKBj* zOlhBqt832G37K7*gjI{M{ys@jI}4t)#Pz*ZG|3^X)a0YfaIEJiD345~y6uC~i+~SJ z=RlrVW?xnf|F&FhZX*b|%9jo!urNSo!Rr$~g$4!V2{q`s?mdHqD>0_KRr+>ZuAKQ! zC5K1?%yxWHJ{Z~Kz7}I6OM|g`ZnLkTG!ZzP0jGGiH=HOA+Y4MjuK6c);w0b|Qq&cj zG?75HhS>TroVlCTZQ;S-h`O&~+S{F4JhVA~&`K&h>Nr4mBELh=k&>A%(5gwa+;3yiuAfZhKf``cTYZn}lTPd8&g7iE^)Gd%sSV!9mWGEip{Z zM@;Qs)qJ)K4h%ZqWeY&;sV8m5{fdshp&sNpHh5nWq>0NuCZG(Q#h@bWj$Xnzx&@Qouw;scz`J*JBIh_ZG(%|Wk4I%UcY zitiYLs3YPVCl|tB_!tGcO~9G;>Xy>*laFSk0#r4x)GRyo7!H-kgBuFz@E zt_$?5@6u>Z9oaHzn&XLZgjt>rN}6@c)z`z^I}bYI3u zV9Yh>(Nw7cB@1#S@|hZ^`;D)XTuL7@iZTx950+!q6e9gCWtZI9mPd=oBUtYADyGLz zAc%W;b6OLlS^Sy~jn}TpU)WVb(BsXAy+1Dv43LFN zuQM)gsXF}#qLlPRLVk5KJo2UPu4mo~dNaFHbZ<6mTqZ%bWT8teMiYjH83EEb4ifYw z1yU1XeMPejpE}bFy+scW4Q_P>OVu?tqGw=*$*r&oKysc5<$8|Pi{5_giU!KFpp{GI zPqF$E`BCx?@1UWekwqj+ZOnf%p;uHY0_KHX-YNCl5fJJ81#(;a6*CA|PU5x7Ml>x* z)FzYo2!`GSgc)|O1RQWZK7z}TdgW|V;Z~`q^RV~)rh^2oJmt+tb8@>rQ7XVq^A5?a z`Rui&+djYgxzm^Y{;2N^zx#Z%{kq%KZL@O+i>a>Z*c_l^JcD2%z_{3C1fjUSZEK}b z?&JN*`cjMQySakyL8{JxbZFyaVjFN9meXeH| zCBgp~g}VhGkXTd~Bx!C$g+StEn~f;m-BO^ko@`>xU)G^Ty}_JE5kR6SVAtj0zNvcg<>kKk@+ONVrF>A7MKN6_wDO&BI+@(PJjRHz zb8K$inL-TZ`hlR{mCfw0Go6yq{@6?Bp-Hr}qND()j7Ur9JF|KGWR&nw-WOrTfH+Zd z*0}M=&z*w%xF+dG|H!&6IzYZhdZbxokM5a$K*|fp+U07Y8#*Ujgt-=|ojrP%C^JKY z1MVqQ_mCm6=v}3C$&eif<_+OvpbbY;(B%YD4a=eB8v5RaHT!s^L{9zNa@`ztkcvb* zD|uLw^zH>?m~i=sjG#*2pgs+wgL_e}c67SI?49)Uiw?ww02@XB5GMHR@#bNy55p{R z#GR^yC*uNPp@#Tcx&GsrSmNl4X)@i=@BeUDNURlQ+$AUE&~365l~-bA_rAQm>6vMP zGD&Eg;Y=t?jFc+k>znb42P}-;uY*A-%1)P1@Q^~(@IN8Xv!p8u`3QCn_7;-4X7%E8 z&dHq!Z!Bcwe}oLkh{iZ7Gh;WO>)U)_U%V9EoE=c|njps*;9WXv_rVn*u$;BrUiF zFnGTI*W+5JA+-otJp~hVMitkl1<4HrCrAcKYyRDwc9-hS-?82Ny$$>-qIOEWB0T8p z6<2)~?bkgmxQXdvLANi_I@1t-m6f_k9~mh#+=+K&YV{D=IO89)HovAofm#)5k7I6d<0U{m}%HOfS!w>JfzCw-(}1TqN8 z_ht%JtNC))7Wt_hEOmZ;7=^3xQUeUxUg$;@v~Rnq+GT;_TC#ghV@EW+?S>C35^#7l z6ou_3vvL)3P4}m5J$*(}_B$}oy?JPz5A8F4jMU5a8g9pMsUKIR5;g#(p2kcPn+EA- zb-|twMjhbl4L+i?(C&sFu#sOjM!fkIUD&J5>Ds20fM(r|>3o}lpHxk|(c&m)+5)?V ziTyVU?Fc&w#Tve~=Y3l(HD)HmudNP?<*P}qN~?5u;@YTJEXihF`}uu{>ZGA zOxE1@{ktxwd8Z*H_2Zf}OruX1=CZA3;+5mF70)%h<*9(E0LsqVR?a~ViNHa$1yz4LGLf|#s(B*slyu)1@CXC-MM zZve`wB4QuP<_Di}`T~Tbeg4RYH3G$4gE!8;bXUW7xO?r_ITG(Zo2|cZe**OeOQv9A zvb<>yNHTfG%*v}y<%~bH%IUcQ`PAvgF(9+a?GSJr-u8LNRHPc%`z<)@LiSIIzWQ*N zkk-KE<|u8DN-z{IRStn-kMyv|2ESIWQ0`Q5QyKUA<>lI7kiMT%P)kOinmO-WJ-#uT zP#2jcl5rf}MK^67&upPWkwD+N@|-4z{<^4XaS5}(gEx_eun3xR>TPq;=Tw~fZK@jAtH{qG}|-tZ<<#M31v$T;LEJvG}X zy6|>qFw~Pg$n2;Ld+c!DkSK5Dx4}+QAH$z2*;+1&qhKCNl1@Mbq>4Xf;ZK|A-UYkZuH{gYlYu^cv7p8bNt@!IZY>>|LiuPclNUFn+v0oeVcDBZ%@WMs zy;Z=c61g#&N2YFq+V#FrL+~JEDB2+;_MIbuTFX!PqgHn>5__7RMN=iy1;+;WNM;K( zFE1Vj6$Kj2|9HcpoJ)#AT1o@hx3s#}5L-tOwf-!dbsQ^|pZK_avS_~`!8?ka(0oAI zqmW{fllj3&kG($)br05g=U@t+1x>EVOO3cThcZ}l|6`#Ao5n91fE}@zAnIF_^&q&P z6JKACCb04czrkU6`3WSDp&j7zbWv%e?6|wfTzH)TmvO5h;xchRrH=!PGE^m(*~jFo z2ToCBaoCGl|3P6?96qkhRdzh^2x|-K&eBi*E+8^28|HGE`@54j{sW>hP*yr3w7yfh zKuVn*$g|~=_QP1BRYbQrG>xPb%0WyDq{UI2I5grGY5xgk9w>hLQ@bBnLbjskmKCnv z1Aa0s?U0U1w_F^3d1vr|tsuIC(_M=|nQFPV0YWwe>T!)JS_PQ@ws8WPu*o!L4 zWD=IL>{)f}$yqf+3GXYNR1Le7jDHneSY<~;FebdzRJ1Hs9}0rOvGl~}uC ziJV+Bqr{7#74aC<7XUV{)Wyg$RAqEm2A&27WWlZ+xP+Zx#t;kEok#FzVQXj!VC_iF5p^k*3bADpP|f-Xy4AsOpx=gJe;`^!QDG4s)yOuTW%xWVb)ereYe^2urm2$>EoN zH-2|n4pawwsND1*=`@ol^$PvI0npR8HXj!{rD;!ig`8`A_=F05JCf z;HmkEenK=E?H#3 zX{Bc~(O&kE`(RJB1dcz8$FR`1YBC0F1EmNZWU<{Y2)Ge!^d&UXz{0^ezkbT>C{Dw` z0}YPH6MAMA?#Tda2eZ0^`4%LYb*!`{K)wxL*j{WGz-pan`gdl|K?n_`5vuw*iKaij zNtDs^k4!BFWsb8Yne=)Qu}Z8d@SnQ-E3YS-yiQX0VTuuo%K&1Mp1b#A2 zy)^?Y?0dcJV0L`$FqKMwj>Y$Ti`?*1DFWpqoMg_PD5c&wdOw!mKo*my%ZV*J^cQ!A zN&G8MqL|Q>B^Q3UX!m(s`oWyMRM7?Dwz!Z#;kyR4|HsD&A04&r-wB|k|6+qZywv6BO0XP zg6zz+O1q*`9`dG3VK<~;z=D%)f@n|<)syAp->)PUi(hh;J@U_=Yg@x(+!9g7sMCGz zP(dtd`Um4qox~F?t*CyikXEKRzhdxqT|-vw3IJaNw7_IN&_3nqB8eXXLFr7ShR~{> zWcBEhcJ^NKE3tk&e(5GktiZ4Uh353W^*-^gu93$*%EQC3O!oNM?q@7|-8)d&LQjgY zDY?2pH~EI^JikUrw#=rYwx5}L50rt zY73!tOs;`=BQ3`V9jJO``4pUOF_=$uOGZ%hGZ*4PB;2`FlBypPGbA1&5!%)usT2;l z>DDNL5ybFyhfVdB`qQP__~H^$vtiVq(OG8F7KN7=?Cx57gt#l2Kuj9!78$oD<#@3G z?TTm?=*it5T*`M;o`HJW3VKxlGEu8OGbgdf#M2ayr|z*$8FD*kygb2wc-U??-H8{J z7>T}?4%kg@DM$G0TRxUxeMXEPGLQv~vwnqQ3TDMO>2*F5oLF^rZl+Bo$*RZFt6tNd4fB1H zPyaSIX&tHINq@uWFNO9frTGvkul1P|Z&Lw&3CCluDJpWd`qkek`^Ts{umhtnbo|oc zjq%tl10@h8kP0L50<-IWjo6hltOb?y3*Yqy@gaju`<8-h4ukycZj9<W3qMnxhYjr?HEWB;F~}17xXiWT4f_Hy9DF+io|Wc{=wbF-r^ns zwfm9Vw57Fte(k-)ScX z+p~6A@Jj^57jfOTH;5uc_vT#uzCHMDG#X+!bl*bfx_)4#F3`_{^WT$oj#3*n7!oSq z`0H}49L?e-M2%4!t?>SOA@XiN>=id3o(C79(7?Hujz&0hXqvdi8vmuw1fFrRu9V)0 zfys}$fK{noLa{&?`;B?zT3h=hJ04tTu#O=5c@JWTIgq!>>+xa?U@qodk>~eHhNa*y&rU>xrhIsY zrGr_oTtZ7<6^nWYYo01JhHdaXA42fUaxn}G%TpQ%8sp@?RRN~4Z+ZH z*K+}0jG)ZjPRA#s0do0i3U{RQId zTCixj|BE|n0tMD>iGur~#|oVBkJn$xlb^kmZPh>Tet71?nD5VsGDA)-3Cub8O>74I z=A8U~c4AX3dxzSV|DTS`hs^WkJS56B|NnK&pwco?jXj7xr3x$q`!+tYjejUwpmIao zCmR8Iy42W#010sdbd#97Mf_`PUvo5@_2khzh8KC7KAVi&z0^8$XxB*3UQu&~w1OQ1 z;{#P^$3H#c|3Ya65}|QQZ#@v}ul^jRAI4$j8-0EGQw4v3df;5#n>4lTW5*L=Cp0^Y z9#g(;>KVK+#+Ch9SSRm#UuB#OajAEd9-w>nvLa(wnc4% zY#F@c-P%zfW@GISkz)C$kz4ZW6_ACp_CY_^b{@iAIH(HEIg+aSH^^F^bsGk2-QMnI zdAMxZT$pEL(Jka@TqOsoeXlMVw6A2#Q}-}oZ=-s`rl8iBdwxvw?6Ul^56!;jutQ1* zn`<(nmq!?q*z4f?(c)ieeN-H}J8z>>M0qN&+LUY<#`_E0F+zy@$*MK98K!Wa4zz=Q zc(fcAFml^g37-;yrKa?&SV#T_aEfoZeROfxi5DIzaa>L*qrl_%96I9k+Uedqzah-z z;WOji1Xl-2YsI@)*zmvB20{#!7k8yFD&WnwkGr4WcGE zqnMI?YE40kuyZP^?Nd^MBqM}|x}TIL&+1yv!^TQTr0mi(>0h4AMd!l0nA(<_&~fp} z-k|FLj;3aT}Rks}>X8;FQQPvxS2n)zMCB^y<>v9c)4Wgb*>=M#S>rq*yue`2uCE(Fpkv zWvX7WNQ3R!V%13ilFw(Y8U78oJFzrt_(Szu24hqO&O z?#wdAMtG#|N>Hsp9@#B!zbkUtoDly&dfTX+hf=8exp#x=8}esSYh9WmX=*lcPn%aqr> z3s&_vnofPf3Lnf^PVPq}C5eu~7Ko4i+t`KPg6;!@gqTaB69DVD=R%`Hc_b60m@ZIO zTq4H-Xk##3hCy>6goeTCJYGeA2tmUVk;{~~3I59L>=;Y&l|!ba`?aTpEPHR9fz{Gb z0WGUdyYHENd#hPda7{#=M}_1A>x3l_I+LNgoj^;z6#!hJv<%`B)dJ@AWv4UUZ5YI( zN;8HwF_^+3E|~%?rVgL_R%>8y%NrHa> z2H8H;1o_$XL|RT~rT(Ou`o+h}8%LC3$?RogRd}U4TJ54rG+02!ll~C5M_`kNL%WMOollC!<Zw*u!s#FQ~!R= zNY{G_TA_SAWVelhM0nWr9U8-goM;C3mw%pIaURbL8EC^E+@yho#cjSq=!V*jGB-hL zKhUGj{2wli!eCaxT_*f(@6?0t^ff5f?ylBZrYtB-<(3!kJx*9ZT4Tws6D(XF-sKMm zF?C|XNjZ~C)89_Tv)aUUh9Tz+X)JHt{52fdqCu_&v6jm1#+(3WhKn6D>fWenZwpl! zTlu?sFZ!JQYA;RqXqeUwGo$K$WXaZbVsfVoLKcmoxERz}XZNq*|EEIqzh~H_d_SJS z_ZLf`_7I6MT-@etIopYdHu~#de`W3e6=hpMbwYQDE4sOQes8D}hW7=;tL7JN{|5OnRjn5i6801aF2XApbfdxPn|fL3Js$tD;S zvZ3g!kUH|>s%Pjta7JJm_-AC0WN&Elch0xkn_$S+v(nAt=>Swjg601=7Ho~py@$s; zWa>4@jNt(O7{L|gp= zJE&?!ZCB9TpJt6b=@XbYjY#naU3N=sD92xIimW%{bcH?1f|@+cx_FE|?C_`2**pw< zeT6$%SS3Tbz!g>o=8H$uZQF3Z4mDB`KG-sDYBSt8hJ!Y_gp}DNN!p3bnxHpsk;F0< z65Tx_1(`Jqf66vU=bQmijN4vJ*M+5I3;K)v8fwc6us~5ZRc#0rN=`gQ6TJ)$v8dlb zNnFe>YIEN9wXpOK1Q^EP=!Hf-{gVx8^h*W$n1us_2>CxvDMIVPrxL5kB$b$oDwe$C zC=Tc!_*=Yi2b7dbyVE7~o$JZDAN#o+@<5?~K!_vlAn9$6u-8Yx3`GwJ|Iq_Ue$&&W z)BU`Y^=Y)K0}ULVk(db^+B+s0~7)pMV-0{D(31|-MXdHt%W#YG| zc-I~s+f0F9@Nk-{{mgV6^h`DaE587eJ8BxHmH)*Rt6_ioIXf=6`v&k=@34$~GmuPR z9gu?4p1yrX%TK_8me6dfzpR@fawS{C;u!qEOLk8l750Qf#dZk~zmu{?D0GAi^z8&g zM)uf8uHRYy%U-#7sNU(7DJgf{Tui#E}RC;^2;9d+OnnkyFRn2O|Sp3dYCq0-$ zB+rOfq((`%%+Su*^SSi9J(s5D=ke89`gPa*<0+9ET zDI_qGK$aF$Eg>S>r#X5v0{0Ru32EDF#)ECH{6u@+=Q9F3!JnV9;4+{GWktsKO5jNT z@eGNJ_hUf}yJ^JSU7Tl~q0IznCsix`*7Wla-<9{9mmf=!LsPvqT>NhuRJ`B9_s=*Ta9~^V9OGba zcda+AC9U$Qddx`x}n{nT2P{>S} zqUjGvq4)4GHI;B+%PN}Z#KKP=`E-=Ju7<#)_s+$f#>!*hOqlrt9^(p!^?=+rhv`3_)Iq;i8f{wRmuk z;wILpgZdDe5yqobPZ)THHlTOQCmf`RLN+iLheJSB;HQCqnx z+CJVmwQW!&KZtUR@(+P>>Hh+kxug+mD##+<|1J0_$^OD+2X%54 z96hCX*vZT}crg4@9=?zKg!~+O zlFS!9z}yNoHag0FkpZrHF_2y{k>)GBfEu zREltpQ}|!55Gw&A;{MQs%j@i=ZtVKNP<~z%kbI#B?bT zvVNPVtil|xAc>@>GMqa^WU5-{uePUZ=RzIK{)Qthsf$Fd&n&>scTYO0b>3SDm%IvW zuH4lbi=Fd7cSC=%!T-?CY*$|gNkR7&^?)r^?Iz!U2ULtJ6io_7L5aSLtv=&z{YnQL z^wov^yr}nYw*K&3TDd&%D=(+_OB-J!yyxUw(&Fo@w@5y#s^lRwZ!}ARP{HjiY|Wzw zjbnG3oC_i~(=^oXeuzL96($xg&Om>xdUym_n3CeGj6)!F!(5BjNHVOno+x7zy?4|{ z>>}96>tP|6^*0eJCqELWt!sbBSiQkDC~fEU!|Bw2;Oa5jYM32b)VZDC8RZGk@^!iI zBsj2F(iy?wh2mIuFbp=G=v&|8%4X`z`1`a6*moOUP`q(Cq5e~gVd;O`Rg>{6(%Hwh ztLo9wq4=_+eC97+|L+#dZd~SO2s8ry`>n7DK?rut-#JYEsZOxat_xT^|GXGPp z=Kr==hT;-HIfcO|!}RgL%zB#IcV|9&YGII;`jAOi)3e9&>6j7!8!?)SCQ7JaPEi++ zy{_Mswl|}G_K9e%j0yWbSQO1v|7VBS-)~hvPgem4q5r#9`rd`-;!@K?HFT1PD?~-c zY^5fJ+r4@H)31&f`h3^@T4y^4Bz6_|kGKc3do;uNR!KbwHeJZfSf6Db^H(C24ibK@ zwxu14pKmpVwiMd)VLW_cysF8K~uFUK-%i zo9BCQB>M8=t>^t84?h3xN$BQYKTjY(LZPd*^gn6l{(+-z@O|YR0lb-)sa|7O1THj) z5EVYAHW9;P_<4vYQ+Ta?f07Ic9j}kRm3?aqF^{MQe4V>KT@ui}ShUk?It&Lxi1Of1 z5^=!Xi(t7*%OBXTne5xE(BnI&NL2ad_MIRg=5KO@_d8n0P>vmKkO3(>@OTaaduGL3 zPMExxJ<N z-$~zOC^FTx_HQb@SAqSkc|XO;EOQ^DpXVr(FX+83rwBiZky(u~lQ)NFEEeRYpJy}k z)iPSCIO4xK*fxI|=;Y=$KKpqsrYt{ml_{antr#{a(OB};QE@olb@Lx`pYfF&x-GPa z9#+2ac|g?)=l}bs^|Fb3`nme6`fJRHMqyIem|BDjpKXorBg2uPHoMZ$=-%YbCxj%J z3ojOf%!Pas4R^!7@8R(zI9Inv$~NCUqrKRYqzTt97*(^7{7 z$A7x|jsftkX2;v;(kc|QLE0M;JZx@bHqDsme2|Mw$+GBqX0VKF#E=if#apRV9R{y( zg-Z0(T7?N{$`;-S-pbGInS$daMLXvMg@)xX52#;qGTk#;eBYA4iA^huoqPbha00%+ zXnbYUJjtRGyW&Y3(w|GyuX9pe)ZsS^v!t~Rh`vnU-*Rvy3H;#k!L1l*A?aiW-OSXV z9x^>%#tA@iqU)`nyrwq@ZxqZCUo2M$BL&bFgZFC@y;0uo)Hv&Z+!dm59FQORCTnzFFkt*8RMj!-Tz$Dpe66hUYISem%$2t=bJ9yy%kG=Z?s%;*i2 zV1hTMXb|{`l2VA~_bQl{nT1n`Iia$O3^7%>h&D^ecqS_G z5H0m4jLv0i#RUNk^5n+xsTMYhEQ|%%xGH>It#v>Az13O>9PN`&&$HYftJf=}4i(vs z7>D9Ul~EBh-i)7dFT^yJZx()4W&1YnwDxsc`OM^!v}WyfXWo;^m`f3&T%DQ0qz@)n z3+>0t5E}EtKi$&P=O1*J3*@GU3Oef`CdD2b9$TwqQ7N#YL~OEkvbGdXPVLn%R&L^* zeuVD#e?3gmOM}FK1+Fx(i$h_>%ir||JPAm{O+r&J4ZD8v}9HcVI6I@ zL7cA~sOYE2GsGjzX1dSy7hib>BcWti7#INbfRFT?sI;Rr}c`^c^ zWeYK8EUyHdNRNTktzBRhZOsq0+6@!SvRMH6c)r9g<&^53pu0`;!DgJz1|n1^|DIM>p38uZ+di>?fiz zgcX7NYA1<@0YPf-YYgfWU}MJ~nw}!(a;vSpM9knU=bP7uuYIDK2_1g1D}2){b4R}? zf6Fw$ZyjEF-znrS%qj9*odCpAH*}y*Tm7irN5 z++IKVosau`R|81{7LG_YAk|H3PS;NVMFnobTWHXU<5jABooZHgvv3aMo(vNg(h-|9 zlphh*d63-FNMU=~WxKWxN4ov)AulAdRzKFx!^k#5bpB9F5PghFdBCl4MSkn}eDj$m z#&OmMtP6ZpC2s`lqPb(eqXtXK!G%dhDb z-PFrG+Pb7b)we|1^4plyTG|;EMff(DLzj97QID6&JVLbP9V!ql^wu5_7L8%G>Y#+< zL-u}NJbf{fOIp94!=-=!P`V%Po!X2ZfEf8IR2q{(BW8w|O5pm~ZpC zm+R*3E>)}^3Q=K1Rh~qH4&Flfe9FwWZ#E0<#V;ZT1fOwv)PhF-veO4xSY2qc=&;mI zE)?;&j5=3;T&qR|4jhrqDfN@QW0WR(j1piIZ9H!@=3+7q{IEZ$;n<=UPiJ7qcJyw0 z;C>|Wd#cv@jw&0`gm%Y=QqQ;bOjxX3m0jWqX>60Hoe-AgphJIcPKQ5x zw%o1WqFETm!i8Ut_Z#h|@_QB?F;8M@zp zaisXq#qj3RJkwiDm?rhd$A?*3IorV-sT5`52C5VT=dr=}q~%IK!sOOS2TvQH2Tc=` zJ1$uE-PDXb_9y`Bh4yj=wbX4(P85z;mIdASagZx#8m78kHwkn_3Yxj^$Ru0-HstNj zgmh7#XCTi|mb1mY?Xrilk!nPOdsrGOqf6VLM-HkKu00!-;^4(?wwL!y6{TaJyL5<4 zA(E^J0l4IzEVAF#zSzzR<|1KUzE_FXkJFseq?v%MW04F*N7j=kW2dXF z1_ncpNqbr#(Ugj>$yu38P3HJFyHzbr$bmmP%&bx&wj6l;)ms=8m@1SlOMZ` z|81(a90Lj0*oNlkPq;QymK+wKn{JvP+lqvlJ^3d{Ymu6A3FU9KLKRWOpu7IX`{y|R zjmJ@ZUp=O1=RXhUjg#n391n=KE;gAkVSB~sz2{)bBc``lMCj`nI4_tN4GoOVq47I)>x2Fvi8jfG^^Zk_@)F~?I-}R8(L@3wp2?JZ4>njop&W1I zP2sAD4^yx2XNmpaAs@5yB6f*qzSp+D^`**?(4Oo)k34v7ye23|I7a`)_N`Cf-=dI8 zI_b%BB*^$6cmbYWuLt3|$Q~km_h^+FQ`YokyTC69B_(G)*!`1YXC->6^EU%T5okL<$fFpj%>G$mM03nLz)(COW1_{>xUBB{$b^>2)zD?Fh40iG+{mM zzn0xI!o-PCG0Da=YGI1)f<&b^%w;K~TElOuF+1q8UVxgB-ITPd;U_nj)D26gA_Mb& z*#!cJucUk#muZ7>I9$zxZ?$tlWrMnB`Zn?6xMzY%;nXu+yZ5w*Ofmv4v%B3#ZbL4I zUp8Db`UFB*N|H8A0+F9l*lSwyGXmc9pQ6&7HUEH1vr~NVT9M&ZjLy!1=6T9POl%Hg zu%oj4#02Gwk3IsPNq3l@UnkssNXq;ncY!T)-}ez=W7T=jhM3GiJ&Cdt0rjGf#5>RG zna#fIRo=1+M$|vNS(L?W@~v$bt4vsjtL?Vk>>q2r3YtE=&TzP_3Oor^mv4>i>|jbs ztS)qcxp+JzIq0sh`7Pvic3gjx3-E)P``uoxwO#!^>F_dtLwMBER)4W>KHhl?5Biq{GiE-$ULr16!Do|QA3O3DMf9pe&l=v;$ zIOA6f(4PnW$vd4j-%rB>R?~i3J@XfO$!R7fXzQDw`UkDW-;c} z@^H0WB<1bCUu#luv%eK<67ah6`bygO?La_iP@9HFy?$VWB}n1W{XQXR2Hq5&JFwSZ zVBUUfJuqdSdFSx^x68M1VlDfh8$;3q!^W6upM;d>zi-QrezoyEIp6LM2Gv6*ZU3qm zehH``VM~p0R=Ire6c69sFu9|up-hd?xo Date: Fri, 29 Jan 2021 18:05:18 +0800 Subject: [PATCH 02/66] update lite doc --- deploy/lite/readme.md | 38 ++++--- deploy/lite/readme_en.md | 233 ++++++++++++++++++++++----------------- 2 files changed, 151 insertions(+), 120 deletions(-) diff --git a/deploy/lite/readme.md b/deploy/lite/readme.md index 4775c19c..d09a7e88 100644 --- a/deploy/lite/readme.md +++ b/deploy/lite/readme.md @@ -11,8 +11,6 @@ Paddle Lite是飞桨轻量化推理引擎,为手机、IOT端提供高效推理 - 电脑(编译Paddle Lite) - 安卓手机(armv7或armv8) -***注意: PaddleOCR 移动端部署当前不支持动态图模型,只支持静态图保存的模型。当前PaddleOCR静态图的分支是`develop`。*** - ### 1.1 准备交叉编译环境 交叉编译环境用于编译 Paddle Lite 和 PaddleOCR 的C++ demo。 支持多种开发环境,不同开发环境的编译流程请参考对应文档。 @@ -31,7 +29,7 @@ Paddle Lite是飞桨轻量化推理引擎,为手机、IOT端提供高效推理 |Android|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv7.gcc.c++_shared.with_extra.with_cv.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv8.gcc.c++_shared.with_extra.with_cv.tar.gz)| |IOS|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv7.with_cv.with_extra.with_log.tiny_publish.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv8.with_cv.with_extra.with_log.tiny_publish.tar.gz)| - 注:1. 上述预测库为PaddleLite 2.8分支编译得到,有关PaddleLite 2.8 详细信息可参考[链接](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8)。 + 注:1. 上述预测库为PaddleLite 2.8分支编译得到,有关PaddleLite 2.8 详细信息可参考[链接](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8) 。 - 2. [推荐]编译Paddle-Lite得到预测库,Paddle-Lite的编译方式如下: ``` @@ -87,10 +85,7 @@ Paddle-Lite 提供了多种策略来自动优化原始的模型,其中包括 |模型版本|模型简介|模型大小|检测模型|文本方向分类模型|识别模型|Paddle-Lite版本| |---|---|---|---|---|---|---| -|V2.0|超轻量中文OCR 移动端模型|8.1M|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_opt.nb)|v2.8| -|V2.0(slim)|超轻量中文OCR 移动端模型|3.5M|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_prune_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_quant_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_quant_opt.nb)|v2.8| - -注意:V2.0 3.0M 轻量模型是使用PaddleSlim优化后的,需要配合Paddle-Lite最新预测库使用。 +|V2.0|超轻量中文OCR 移动端模型|7.8M|[下载地址](https://paddleocr.bj.bcebos.com/dygraph_v2.0/lite/ch_ppocr_mobile_v2.0_det_infer_nb.nb)|[下载地址](https://paddleocr.bj.bcebos.com/dygraph_v2.0/lite/ch_ppocr_mobile_v2.0_cls_infer_nb.nb)|[下载地址](https://paddleocr.bj.bcebos.com/dygraph_v2.0/lite/ch_ppocr_mobile_v2.0_rec_infer_nb.nb)|v2.8| 如果直接使用上述表格中的模型进行部署,可略过下述步骤,直接阅读 [2.2节](#2.2与手机联调)。 @@ -128,12 +123,16 @@ cd build.opt/lite/api/ ``` # 【推荐】 下载PaddleOCR V2.0版本的中英文 inference模型 -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_slim_infer.tar && tar xf ch_ppocr_mobile_v1.1_det_prune_infer.tar -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_slim_infer.tar && tar xf ch_ppocr_mobile_v1.1_rec_quant_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_slim_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_slim_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_cls_slim_infer.tar # 转换V2.0检测模型 -./opt --model_file=./ch_ppocr_mobile_v1.1_det_prune_infer/model --param_file=./ch_ppocr_mobile_v1.1_det_prune_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_det_prune_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_det_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer # 转换V2.0识别模型 -./opt --model_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/model --param_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_rec_quant_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_rec_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +# 转换V2.0方向分类器模型 +./opt --model_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_cls_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer + ``` 转换成功后,当前目录下会多出`.nb`结尾的文件,即是转换成功的模型文件。 @@ -186,21 +185,23 @@ wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_s ``` 准备测试图像,以`PaddleOCR/doc/imgs/11.jpg`为例,将测试的图像复制到`demo/cxx/ocr/debug/`文件夹下。 - 准备lite opt工具优化后的模型文件,比如使用`ch_ppocr_mobile_v1.1_det_prune_opt.nb,ch_ppocr_mobile_v1.1_rec_quant_opt.nb, ch_ppocr_mobile_cls_quant_opt.nb`,模型文件放置在`demo/cxx/ocr/debug/`文件夹下。 + 准备lite opt工具优化后的模型文件,比如使用`ch_ppocr_mobile_v2.0_det_slim_opt.nb,ch_ppocr_mobile_v2.0_rec_slim_opt.nb, ch_ppocr_mobile_v2.0_cls_slim_opt.nb`,模型文件放置在`demo/cxx/ocr/debug/`文件夹下。 执行完成后,ocr文件夹下将有如下文件格式: ``` demo/cxx/ocr/ |-- debug/ -| |--ch_ppocr_mobile_v1.1_det_prune_opt.nb 优化后的检测模型文件 -| |--ch_ppocr_mobile_v1.1_rec_quant_opt.nb 优化后的识别模型文件 -| |--ch_ppocr_mobile_cls_quant_opt.nb 优化后的文字方向分类器模型文件 +| |--ch_ppocr_mobile_v2.0_det_slim_opt.nb 优化后的检测模型文件 +| |--ch_ppocr_mobile_v2.0_rec_slim_opt.nb 优化后的识别模型文件 +| |--ch_ppocr_mobile_v2.0_cls_slim_opt.nb 优化后的文字方向分类器模型文件 | |--11.jpg 待测试图像 | |--ppocr_keys_v1.txt 中文字典文件 | |--libpaddle_light_api_shared.so C++预测库文件 -| |--config.txt DB-CRNN超参数配置 -|-- config.txt DB-CRNN超参数配置 +| |--config.txt 超参数配置 +|-- config.txt 超参数配置 +|-- cls_process.cc 方向分类器的预处理和后处理文件 +|-- cls_process.h |-- crnn_process.cc 识别模型CRNN的预处理和后处理文件 |-- crnn_process.h |-- db_post_process.cc 检测模型DB的后处理文件 @@ -219,6 +220,7 @@ ic15_dict.txt # 英文字典 dict/japan_dict.txt # 日语字典 dict/korean_dict.txt # 韩语字典 ppocr_keys_v1.txt # 中文字典 +... ``` 2. `config.txt` 包含了检测器、分类器的超参数,如下: @@ -246,7 +248,7 @@ use_direction_classify 0 # 是否使用方向分类器,0表示不使用,1 adb shell cd /data/local/tmp/debug export LD_LIBRARY_PATH=${PWD}:$LD_LIBRARY_PATH - ./ocr_db_crnn ch_ppocr_mobile_v1.1_det_prune_opt.nb ch_ppocr_mobile_v1.1_rec_quant_opt.nb ch_ppocr_mobile_cls_quant_opt.nb ./11.jpg ppocr_keys_v1.txt + ./ocr_db_crnn ch_ppocr_mobile_v2.0_det_slim_opt.nbb ch_ppocr_mobile_v2.0_rec_slim_opt.nb ch_ppocr_mobile_v2.0_cls_slim_opt.nb ./11.jpg ppocr_keys_v1.txt ``` 如果对代码做了修改,则需要重新编译并push到手机上。 diff --git a/deploy/lite/readme_en.md b/deploy/lite/readme_en.md index 58f6a574..9e683f11 100644 --- a/deploy/lite/readme_en.md +++ b/deploy/lite/readme_en.md @@ -1,46 +1,55 @@ - # Tutorial of PaddleOCR Mobile deployment -This tutorial will introduce how to use [paddle-lite](https://github.com/PaddlePaddle/Paddle-Lite) to deploy paddleOCR ultra-lightweight Chinese and English detection models on mobile phones. +This tutorial will introduce how to use [Paddle Lite](https://github.com/PaddlePaddle/Paddle-Lite) to deploy paddleOCR ultra-lightweight Chinese and English detection models on mobile phones. -paddle-lite is a lightweight inference engine for PaddlePaddle. -It provides efficient inference capabilities for mobile phones and IoTs, -and extensively integrates cross-platform hardware to provide lightweight -deployment solutions for end-side deployment issues. +paddle-lite is a lightweight inference engine for PaddlePaddle. It provides efficient inference capabilities for mobile phones and IoTs, and extensively integrates cross-platform hardware to provide lightweight deployment solutions for end-side deployment issues. ## 1. Preparation +### 运行准备 + - Computer (for Compiling Paddle Lite) - Mobile phone (arm7 or arm8) -***Note: PaddleOCR lite deployment currently does not support dynamic graph models, only models saved with static graph. The static branch of PaddleOCR is `develop`.*** +### 1.1 Prepare the cross-compilation environment +The cross-compilation environment is used to compile C++ demos of Paddle Lite and PaddleOCR. +Supports multiple development environments. + +For the compilation process of different development environments, please refer to the corresponding documents. -## 2. Build PaddleLite library 1. [Docker](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#docker) 2. [Linux](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#linux) 3. [MAC OS](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#mac-os) -## 3. Prepare prebuild library for android and ios +### 1.2 Prepare Paddle-Lite library -### 3.1 Download prebuild library -|Platform|Prebuild library Download Link| -|---|---| -|Android|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv7.gcc.c++_shared.with_extra.with_cv.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv8.gcc.c++_shared.with_extra.with_cv.tar.gz)| -|IOS|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv7.with_cv.with_extra.with_log.tiny_publish.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv8.with_cv.with_extra.with_log.tiny_publish.tar.gz)| +There are two ways to obtain the Paddle-Lite library: +- 1. Download directly, the download link of the Paddle-Lite library is as follows: -note: The above pre-build inference library is compiled from the PaddleLite `release/v2.8` branch. For more information about PaddleLite 2.8, please refer to [link](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8). + | Platform | Paddle-Lite library download link | + |---|---| + |Android|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv7.gcc.c++_shared.with_extra.with_cv.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv8.gcc.c++_shared.with_extra.with_cv.tar.gz)| + |IOS|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv7.with_cv.with_extra.with_log.tiny_publish.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv8.with_cv.with_extra.with_log.tiny_publish.tar.gz)| -### 3.2 Compile prebuild library (Recommended) + Note: 1. The above Paddle-Lite library is compiled from the Paddle-Lite 2.8 branch. For more information about Paddle-Lite 2.8, please refer to [link](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8). + +- 2. [Recommended] Compile Paddle-Lite to get the prediction library. The compilation method of Paddle-Lite is as follows: ``` git clone https://github.com/PaddlePaddle/Paddle-Lite.git cd Paddle-Lite -# checkout to Paddle-Lite release/v2.8 branch +# Switch to Paddle-Lite release/v2.8 stable branch git checkout release/v2.8 ./lite/tools/build_android.sh --arch=armv8 --with_cv=ON --with_extra=ON ``` -The structure of the prediction library is as follows: +Note: When compiling Paddle-Lite to obtain the Paddle-Lite library, you need to turn on the two options `--with_cv=ON --with_extra=ON`, `--arch` means the `arm` version, here is designated as armv8, +More compilation commands refer to the introduction [link](https://paddle-lite.readthedocs.io/zh/latest/user_guides/Compile/Android.html#id2) 。 + +After directly downloading the Paddle-Lite library and decompressing it, you can get the `inference_lite_lib.android.armv8/` folder, and the Paddle-Lite library obtained by compiling Paddle-Lite is located +`Paddle-Lite/build.lite.android.armv8.gcc/inference_lite_lib.android.armv8/` folder. + +The structure of the prediction library is as follows: ``` inference_lite_lib.android.armv8/ |-- cxx C++ prebuild library @@ -52,103 +61,117 @@ inference_lite_lib.android.armv8/ | | |-- paddle_use_kernels.h | | |-- paddle_use_ops.h | | `-- paddle_use_passes.h -| `-- lib +| `-- lib C++ library | |-- libpaddle_api_light_bundled.a C++ static library | `-- libpaddle_light_api_shared.so C++ dynamic library -|-- java Java predict library +|-- java Java library | |-- jar | | `-- PaddlePredictor.jar | |-- so | | `-- libpaddle_lite_jni.so | `-- src -|-- demo C++ and java demo -| |-- cxx -| `-- java +|-- demo C++ and Java demo +| |-- cxx C++ demo +| `-- java Java demo ``` +## 2 Run -## 4. Inference Model Optimization +### 2.1 Inference Model Optimization Paddle Lite provides a variety of strategies to automatically optimize the original training model, including quantization, sub-graph fusion, hybrid scheduling, Kernel optimization and so on. In order to make the optimization process more convenient and easy to use, Paddle Lite provide opt tools to automatically complete the optimization steps and output a lightweight, optimal executable model. -If you have prepared the model file ending in `.nb`, you can skip this step. +If you have prepared the model file ending in .nb, you can skip this step. -The following table also provides a series of models that can be deployed on mobile phones to recognize Chinese. -You can directly download the optimized model. +The following table also provides a series of models that can be deployed on mobile phones to recognize Chinese. You can directly download the optimized model. -| Version | Introduction | Model size | Detection model | Text Direction model | Recognition model | Paddle Lite branch | -| --- | --- | --- | --- | --- | --- | --- | -| V1.1 | extra-lightweight chinese OCR optimized model | 8.1M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_opt.nb) | develop | -| [slim] V1.1 | extra-lightweight chinese OCR optimized model | 3.5M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_prune_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_quant_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_quant_opt.nb) | develop | -| V1.0 | lightweight Chinese OCR optimized model | 8.6M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.0_det_opt.nb) | - | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.0_rec_opt.nb) | develop | +|Version|Introduction|Model size|Detection model|Text Direction model|Recognition model|Paddle-Lite branch| +|---|---|---|---|---|---|---| +|V2.0|extra-lightweight chinese OCR optimized model|7.8M|[download link](https://paddleocr.bj.bcebos.com/dygraph_v2.0/lite/ch_ppocr_mobile_v2.0_det_infer_nb.nb)|[download lin](https://paddleocr.bj.bcebos.com/dygraph_v2.0/lite/ch_ppocr_mobile_v2.0_cls_infer_nb.nb)|[download lin](https://paddleocr.bj.bcebos.com/dygraph_v2.0/lite/ch_ppocr_mobile_v2.0_rec_infer_nb.nb)|v2.8| + +If you directly use the model in the above table for deployment, you can skip the following steps and directly read [Section 2.2](#2.2 Run optimized model on Phone). If the model to be deployed is not in the above table, you need to follow the steps below to obtain the optimized model. +The `opt` tool can be obtained by compiling Paddle Lite. ``` git clone https://github.com/PaddlePaddle/Paddle-Lite.git cd Paddle-Lite -git checkout release/v2.7 +git checkout release/v2.8 ./lite/tools/build.sh build_optimize_tool ``` -The `opt` tool can be obtained by compiling Paddle Lite. +After the compilation is complete, the opt file is located under build.opt/lite/api/, You can view the operating options and usage of opt in the following ways: -After the compilation is complete, the opt file is located under `build.opt/lite/api/`. - -The `opt` can optimize the inference model saved by paddle.io.save_inference_model to get the model that the paddlelite API can use. - -The usage of opt is as follows: ``` -# 【Recommend】V1.1 is better than V1.0. steps for convert V1.1 model to nb file are as follows -wget https://paddleocr.bj.bcebos.com/20-09-22/mobile-slim/det/ch_ppocr_mobile_v1.1_det_prune_infer.tar && tar xf ch_ppocr_mobile_v1.1_det_prune_infer.tar -wget https://paddleocr.bj.bcebos.com/20-09-22/mobile-slim/rec/ch_ppocr_mobile_v1.1_rec_quant_infer.tar && tar xf ch_ppocr_mobile_v1.1_rec_quant_infer.tar +cd build.opt/lite/api/ +./opt +``` -./opt --model_file=./ch_ppocr_mobile_v1.1_det_prune_infer/model --param_file=./ch_ppocr_mobile_v1.1_det_prune_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_det_prune_opt --valid_targets=arm -./opt --model_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/model --param_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_rec_quant_opt --valid_targets=arm +|Options|Description| +|---|---| +|--model_dir|The path of the PaddlePaddle model to be optimized (non-combined form)| +|--model_file|The network structure file path of the PaddlePaddle model (combined form) to be optimized| +|--param_file|The weight file path of the PaddlePaddle model (combined form) to be optimized| +|--optimize_out_type|Output model type, currently supports two types: protobuf and naive_buffer, among which naive_buffer is a more lightweight serialization/deserialization implementation. If you need to perform model prediction on the mobile side, please set this option to naive_buffer. The default is protobuf| +|--optimize_out|The output path of the optimized model| +|--valid_targets|The executable backend of the model, the default is arm. Currently it supports x86, arm, opencl, npu, xpu, multiple backends can be specified at the same time (separated by spaces), and Model Optimize Tool will automatically select the best method. If you need to support Huawei NPU (DaVinci architecture NPU equipped with Kirin 810/990 Soc), it should be set to npu, arm| +|--record_tailoring_info|When using the function of cutting library files according to the model, set this option to true to record the kernel and OP information contained in the optimized model. The default is false| -# or use V1.0 model -wget https://paddleocr.bj.bcebos.com/ch_models/ch_det_mv3_db_infer.tar && tar xf ch_det_mv3_db_infer.tar -wget https://paddleocr.bj.bcebos.com/ch_models/ch_rec_mv3_crnn_infer.tar && tar xf ch_rec_mv3_crnn_infer.tar +`--model_dir` is suitable for the non-combined mode of the model to be optimized, and the inference model of PaddleOCR is the combined mode, that is, the model structure and model parameters are stored in a single file. -./opt --model_file=./ch_det_mv3_db/model --param_file=./ch_det_mv3_db/params --optimize_out_type=naive_buffer --optimize_out=./ch_det_mv3_db_opt --valid_targets=arm -./opt --model_file=./ch_rec_mv3_crnn/model --param_file=./ch_rec_mv3_crnn/params --optimize_out_type=naive_buffer --optimize_out=./ch_rec_mv3_crnn_opt --valid_targets=arm +The following takes the ultra-lightweight Chinese model of PaddleOCR as an example to introduce the use of the compiled opt file to complete the conversion of the inference model to the Paddle-Lite optimized model + +``` +# [Recommendation] Download the Chinese and English inference model of PaddleOCR V2.0 +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_slim_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_slim_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_cls_slim_infer.tar +# Convert V2.0 detection model +./opt --model_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_det_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +# 转换V2.0识别模型 +# Convert V2.0 recognition model +./opt --model_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_rec_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +# Convert V2.0 angle classifier model +./opt --model_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_cls_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer ``` -When the above code command is completed, there will be two more files `.nb` in the current directory, which is the converted model file. +After the conversion is successful, there will be more files ending with `.nb` in the current directory, which is the successfully converted model file. -## 5. Run optimized model on Phone + +### 2.2 Run optimized model on Phone -1. Prepare an Android phone with arm8. If the compiled prediction library and opt file are armv7, you need an arm7 phone and modify ARM_ABI = arm7 in the Makefile. +Some preparatory work is required first. + 1. Prepare an Android phone with arm8. If the compiled prediction library and opt file are armv7, you need an arm7 phone and modify ARM_ABI = arm7 in the Makefile. + 2. Make sure the phone is connected to the computer, open the USB debugging option of the phone, and select the file transfer mode. + 3. Install the adb tool on the computer. -2. Make sure the phone is connected to the computer, open the USB debugging option of the phone, and select the file transfer mode. - -3. Install the adb tool on the computer. - 3.1 Install ADB for MAC + 3.1. Install ADB for MAC: ``` brew cask install android-platform-tools ``` - 3.2 Install ADB for Linux + 3.2. Install ADB for Linux ``` sudo apt update sudo apt install -y wget adb ``` - 3.3 Install ADB for windows - [Download Link](https://developer.android.com/studio) + 3.3. Install ADB for windows + + To install on win, you need to go to Google's Android platform to download the adb package for installation:[link](https://developer.android.com/studio) Verify whether adb is installed successfully + ``` + adb devices ``` - $ adb devices - - List of devices attached - 744be294 device + If there is device output, it means the installation is successful。 + ``` + List of devices attached + 744be294 device ``` - If there is `device` output, it means the installation was successful. - -4. Prepare optimized models, prediction library files, test images and dictionary files used. - -``` + 4. Prepare optimized models, prediction library files, test images and dictionary files used. + ``` git clone https://github.com/PaddlePaddle/PaddleOCR.git cd PaddleOCR/deploy/lite/ # run prepare.sh @@ -162,39 +185,35 @@ When the above code command is completed, there will be two more files `.nb` in cd inference_lite_lib.android.armv8/demo/cxx/ocr/ cp ../../../cxx/lib/libpaddle_light_api_shared.so ./debug/ + ``` -``` - -Prepare the test image, taking `PaddleOCR/doc/imgs/11.jpg` as an example, copy the image file to the `demo/cxx/ocr/debug/` folder. -Prepare the model files optimized by the lite opt tool, `ch_det_mv3_db_opt.nb, ch_rec_mv3_crnn_opt.nb`, -and place them under the `demo/cxx/ocr/debug/` folder. - +Prepare the test image, taking PaddleOCR/doc/imgs/11.jpg as an example, copy the image file to the demo/cxx/ocr/debug/ folder. Prepare the model files optimized by the lite opt tool, ch_det_mv3_db_opt.nb, ch_rec_mv3_crnn_opt.nb, and place them under the demo/cxx/ocr/debug/ folder. The structure of the OCR demo is as follows after the above command is executed: + ``` demo/cxx/ocr/ |-- debug/ -| |--ch_ppocr_mobile_v1.1_det_prune_opt.nb Detection model -| |--ch_ppocr_mobile_v1.1_rec_quant_opt.nb Recognition model -| |--ch_ppocr_mobile_cls_quant_opt.nb Text direction classification model +| |--ch_ppocr_mobile_v2.0_det_slim_opt.nb Detection model +| |--ch_ppocr_mobile_v2.0_rec_slim_opt.nb Recognition model +| |--ch_ppocr_mobile_v2.0_cls_slim_opt.nb Text direction classification model | |--11.jpg Image for OCR | |--ppocr_keys_v1.txt Dictionary file | |--libpaddle_light_api_shared.so C++ .so file | |--config.txt Config file -|-- config.txt -|-- crnn_process.cc +|-- config.txt Config file +|-- cls_process.cc Pre-processing and post-processing files for the angle classifier +|-- cls_process.h +|-- crnn_process.cc Pre-processing and post-processing files for the CRNN model |-- crnn_process.h -|-- db_post_process.cc +|-- db_post_process.cc Pre-processing and post-processing files for the DB model |-- db_post_process.h |-- Makefile -|-- ocr_db_crnn.cc - +|-- ocr_db_crnn.cc C++ main code ``` -#### Note: -1. ppocr_keys_v1.txt is a Chinese dictionary file. -If the nb model is used for English recognition or other language recognition, dictionary file should be replaced with a dictionary of the corresponding language. -PaddleOCR provides a variety of dictionaries under ppocr/utils/, including: +#### 注意: +1. `ppocr_keys_v1.txt` is a Chinese dictionary file. If the nb model is used for English recognition or other language recognition, dictionary file should be replaced with a dictionary of the corresponding language. PaddleOCR provides a variety of dictionaries under ppocr/utils/, including: ``` dict/french_dict.txt # french dict/german_dict.txt # german @@ -204,7 +223,7 @@ dict/korean_dict.txt # korean ppocr_keys_v1.txt # chinese ``` -2. `config.txt` of the detector and classifier, as shown below: +2. `config.txt` of the detector and classifier, as shown below: ``` max_side_len 960 # Limit the maximum image height and width to 960 det_db_thresh 0.3 # Used to filter the binarized image of DB prediction, setting 0.-0.3 has no obvious effect on the result @@ -213,19 +232,26 @@ det_db_unclip_ratio 1.6 # Indicates the compactness of the text box, the small use_direction_classify 0 # Whether to use the direction classifier, 0 means not to use, 1 means to use ``` -5. Run Model on phone + 5. Run Model on phone -``` -cd inference_lite_lib.android.armv8/demo/cxx/ocr/ -make -j -mv ocr_db_crnn ./debug/ -adb push debug /data/local/tmp/ -adb shell -cd /data/local/tmp/debug -export LD_LIBRARY_PATH=/data/local/tmp/debug:$LD_LIBRARY_PATH -# run model - ./ocr_db_crnn ch_ppocr_mobile_v1.1_det_prune_opt.nb ch_ppocr_mobile_v1.1_rec_quant_opt.nb ch_ppocr_mobile_cls_quant_opt.nb ./11.jpg ppocr_keys_v1.txt -``` +After the above steps are completed, you can use adb to push the file to the phone to run, the steps are as follows: + + ``` + # Execute the compilation and get the executable file ocr_db_crnn + # The use of ocr_db_crnn is: + # ./ocr_db_crnn Detection model file Orientation classifier model file Recognition model file Test image path Dictionary file path + make -j + # Move the compiled executable file to the debug folder + mv ocr_db_crnn ./debug/ + # Push the debug folder to the phone + adb push debug /data/local/tmp/ + adb shell + cd /data/local/tmp/debug + export LD_LIBRARY_PATH=${PWD}:$LD_LIBRARY_PATH + ./ocr_db_crnn ch_ppocr_mobile_v2.0_det_slim_opt.nbb ch_ppocr_mobile_v2.0_rec_slim_opt.nb ch_ppocr_mobile_v2.0_cls_slim_opt.nb ./11.jpg ppocr_keys_v1.txt + ``` + +If you modify the code, you need to recompile and push to the phone. The outputs are as follows: @@ -233,14 +259,17 @@ The outputs are as follows: + ## FAQ Q1: What if I want to change the model, do I need to run it again according to the process? + A1: If you have performed the above steps, you only need to replace the .nb model file to complete the model replacement. Q2: How to test with another picture? -A2: Replace the .jpg test image under `./debug` with the image you want to test, and run `adb push` to push new image to the phone. + +A2: Replace the .jpg test image under ./debug with the image you want to test, and run adb push to push new image to the phone. Q3: How to package it into the mobile APP? -A3: This demo aims to provide the core algorithm part that can run OCR on mobile phones. Further, -PaddleOCR/deploy/android_demo is an example of encapsulating this demo into a mobile app for reference. + +A3: This demo aims to provide the core algorithm part that can run OCR on mobile phones. Further, PaddleOCR/deploy/android_demo is an example of encapsulating this demo into a mobile app for reference. From 49d5dccaf5b30e78535cd09319b1b321daddc445 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Mon, 1 Feb 2021 12:59:22 +0800 Subject: [PATCH 03/66] delete clipper --- deploy/lite/clipper.cpp | 4394 --------------------------------------- deploy/lite/clipper.hpp | 423 ---- 2 files changed, 4817 deletions(-) delete mode 100644 deploy/lite/clipper.cpp delete mode 100644 deploy/lite/clipper.hpp diff --git a/deploy/lite/clipper.cpp b/deploy/lite/clipper.cpp deleted file mode 100644 index 176d8654..00000000 --- a/deploy/lite/clipper.cpp +++ /dev/null @@ -1,4394 +0,0 @@ -// 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. - -/******************************************************************************* -* * -* 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 "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/lite/clipper.hpp b/deploy/lite/clipper.hpp deleted file mode 100644 index 384a6cf4..00000000 --- a/deploy/lite/clipper.hpp +++ /dev/null @@ -1,423 +0,0 @@ -/******************************************************************************* -* * -* 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 From 98ea07798402b21bd0493623b3bc2e0d64b1e007 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Mon, 1 Feb 2021 13:44:53 +0800 Subject: [PATCH 04/66] add det_db_use_dilate config --- deploy/lite/config.txt | 3 ++- deploy/lite/ocr_db_crnn.cc | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/deploy/lite/config.txt b/deploy/lite/config.txt index 670b2ff0..23f0171d 100644 --- a/deploy/lite/config.txt +++ b/deploy/lite/config.txt @@ -2,4 +2,5 @@ max_side_len 960 det_db_thresh 0.3 det_db_box_thresh 0.5 det_db_unclip_ratio 1.6 -use_direction_classify 0 \ No newline at end of file +det_db_use_dilate 0 +use_direction_classify 1 \ No newline at end of file diff --git a/deploy/lite/ocr_db_crnn.cc b/deploy/lite/ocr_db_crnn.cc index 200d3464..6cacfc82 100644 --- a/deploy/lite/ocr_db_crnn.cc +++ b/deploy/lite/ocr_db_crnn.cc @@ -229,6 +229,7 @@ RunDetModel(std::shared_ptr predictor, cv::Mat img, std::map Config) { // Read img int max_side_len = int(Config["max_side_len"]); + int det_db_use_dilate = int(Config["det_db_use_dilate"]); cv::Mat srcimg; img.copyTo(srcimg); @@ -275,10 +276,14 @@ RunDetModel(std::shared_ptr predictor, cv::Mat img, const double maxvalue = 255; cv::Mat bit_map; cv::threshold(cbuf_map, bit_map, threshold, maxvalue, cv::THRESH_BINARY); - cv::Mat dilation_map; - cv::Mat dila_ele = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2)); - cv::dilate(bit_map, dilation_map, dila_ele); - auto boxes = BoxesFromBitmap(pred_map, dilation_map, Config); + if (det_db_use_dilate == 1) { + cv::Mat dilation_map; + cv::Mat dila_ele = + cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2)); + cv::dilate(bit_map, dilation_map, dila_ele); + bit_map = dilation_map; + } + auto boxes = BoxesFromBitmap(pred_map, bit_map, Config); std::vector>> filter_boxes = FilterTagDetRes(boxes, ratio_hw[0], ratio_hw[1], srcimg); @@ -375,8 +380,7 @@ int main(int argc, char **argv) { auto charactor_dict = ReadDict(dict_path); charactor_dict.insert(charactor_dict.begin(), "#"); // blank char for ctc charactor_dict.push_back(" "); -std: - cout << charactor_dict[0] << " " << charactor_dict[1] << std::endl; + cv::Mat srcimg = cv::imread(img_path, cv::IMREAD_COLOR); auto boxes = RunDetModel(det_predictor, srcimg, Config); From 6fc43d4b0469a8e05f76c41c631668be986ae131 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Mon, 1 Feb 2021 13:45:55 +0800 Subject: [PATCH 05/66] change model to inference modem --- deploy/lite/readme.md | 22 +++++++++++----------- deploy/lite/readme_en.md | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/deploy/lite/readme.md b/deploy/lite/readme.md index d09a7e88..5f12ffdb 100644 --- a/deploy/lite/readme.md +++ b/deploy/lite/readme.md @@ -123,15 +123,15 @@ cd build.opt/lite/api/ ``` # 【推荐】 下载PaddleOCR V2.0版本的中英文 inference模型 -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_slim_infer.tar -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_slim_infer.tar -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_cls_slim_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar && tar xf ch_ppocr_mobile_v2.0_cls_infer.tar # 转换V2.0检测模型 -./opt --model_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_det_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_det_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_det_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_det_opt --valid_targets=arm --optimize_out_type=naive_buffer # 转换V2.0识别模型 -./opt --model_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_rec_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_rec_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_rec_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_rec_opt --valid_targets=arm --optimize_out_type=naive_buffer # 转换V2.0方向分类器模型 -./opt --model_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_cls_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_cls_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_cls_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_cls_opt --valid_targets=arm --optimize_out_type=naive_buffer ``` @@ -185,16 +185,16 @@ wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_s ``` 准备测试图像,以`PaddleOCR/doc/imgs/11.jpg`为例,将测试的图像复制到`demo/cxx/ocr/debug/`文件夹下。 - 准备lite opt工具优化后的模型文件,比如使用`ch_ppocr_mobile_v2.0_det_slim_opt.nb,ch_ppocr_mobile_v2.0_rec_slim_opt.nb, ch_ppocr_mobile_v2.0_cls_slim_opt.nb`,模型文件放置在`demo/cxx/ocr/debug/`文件夹下。 + 准备lite opt工具优化后的模型文件,比如使用`ch_ppocr_mobile_v2.0_det_opt.nb,ch_ppocr_mobile_v2.0_rec_opt.nb, ch_ppocr_mobile_v2.0_cls_opt.nb`,模型文件放置在`demo/cxx/ocr/debug/`文件夹下。 执行完成后,ocr文件夹下将有如下文件格式: ``` demo/cxx/ocr/ |-- debug/ -| |--ch_ppocr_mobile_v2.0_det_slim_opt.nb 优化后的检测模型文件 -| |--ch_ppocr_mobile_v2.0_rec_slim_opt.nb 优化后的识别模型文件 -| |--ch_ppocr_mobile_v2.0_cls_slim_opt.nb 优化后的文字方向分类器模型文件 +| |--ch_ppocr_mobile_v2.0_det_opt.nb 优化后的检测模型文件 +| |--ch_ppocr_mobile_v2.0_rec_opt.nb 优化后的识别模型文件 +| |--ch_ppocr_mobile_v2.0_cls_opt.nb 优化后的文字方向分类器模型文件 | |--11.jpg 待测试图像 | |--ppocr_keys_v1.txt 中文字典文件 | |--libpaddle_light_api_shared.so C++预测库文件 @@ -248,7 +248,7 @@ use_direction_classify 0 # 是否使用方向分类器,0表示不使用,1 adb shell cd /data/local/tmp/debug export LD_LIBRARY_PATH=${PWD}:$LD_LIBRARY_PATH - ./ocr_db_crnn ch_ppocr_mobile_v2.0_det_slim_opt.nbb ch_ppocr_mobile_v2.0_rec_slim_opt.nb ch_ppocr_mobile_v2.0_cls_slim_opt.nb ./11.jpg ppocr_keys_v1.txt + ./ocr_db_crnn ch_ppocr_mobile_v2.0_det_opt.nbb ch_ppocr_mobile_v2.0_rec_opt.nb ch_ppocr_mobile_v2.0_cls_opt.nb ./11.jpg ppocr_keys_v1.txt ``` 如果对代码做了修改,则需要重新编译并push到手机上。 diff --git a/deploy/lite/readme_en.md b/deploy/lite/readme_en.md index 9e683f11..34512fca 100644 --- a/deploy/lite/readme_en.md +++ b/deploy/lite/readme_en.md @@ -124,16 +124,16 @@ The following takes the ultra-lightweight Chinese model of PaddleOCR as an examp ``` # [Recommendation] Download the Chinese and English inference model of PaddleOCR V2.0 -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_slim_infer.tar -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_slim_infer.tar -wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_slim_infer.tar && tar xf ch_ppocr_mobile_v2.0_cls_slim_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_infer.tar && tar xf ch_ppocr_mobile_v2.0_det_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_rec_infer.tar && tar xf ch_ppocr_mobile_v2.0_rec_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar && tar xf ch_ppocr_mobile_v2.0_cls_infer.tar # Convert V2.0 detection model -./opt --model_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_det_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_det_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_det_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_det_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_det_opt --valid_targets=arm --optimize_out_type=naive_buffer # 转换V2.0识别模型 # Convert V2.0 recognition model -./opt --model_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_rec_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_rec_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_rec_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_rec_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_rec_opt --valid_targets=arm --optimize_out_type=naive_buffer # Convert V2.0 angle classifier model -./opt --model_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_cls_slim_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_cls_slim_opt --valid_targets=arm --optimize_out_type=naive_buffer +./opt --model_file=./ch_ppocr_mobile_v2.0_cls_infer/inference.pdmodel --param_file=./ch_ppocr_mobile_v2.0_cls_infer/inference.pdiparams --optimize_out=./ch_ppocr_mobile_v2.0_cls_opt --valid_targets=arm --optimize_out_type=naive_buffer ``` @@ -194,9 +194,9 @@ The structure of the OCR demo is as follows after the above command is executed: ``` demo/cxx/ocr/ |-- debug/ -| |--ch_ppocr_mobile_v2.0_det_slim_opt.nb Detection model -| |--ch_ppocr_mobile_v2.0_rec_slim_opt.nb Recognition model -| |--ch_ppocr_mobile_v2.0_cls_slim_opt.nb Text direction classification model +| |--ch_ppocr_mobile_v2.0_det_opt.nb Detection model +| |--ch_ppocr_mobile_v2.0_rec_opt.nb Recognition model +| |--ch_ppocr_mobile_v2.0_cls_opt.nb Text direction classification model | |--11.jpg Image for OCR | |--ppocr_keys_v1.txt Dictionary file | |--libpaddle_light_api_shared.so C++ .so file @@ -248,7 +248,7 @@ After the above steps are completed, you can use adb to push the file to the pho adb shell cd /data/local/tmp/debug export LD_LIBRARY_PATH=${PWD}:$LD_LIBRARY_PATH - ./ocr_db_crnn ch_ppocr_mobile_v2.0_det_slim_opt.nbb ch_ppocr_mobile_v2.0_rec_slim_opt.nb ch_ppocr_mobile_v2.0_cls_slim_opt.nb ./11.jpg ppocr_keys_v1.txt + ./ocr_db_crnn ch_ppocr_mobile_v2.0_det_opt.nbb ch_ppocr_mobile_v2.0_rec_opt.nb ch_ppocr_mobile_v2.0_cls_opt.nb ./11.jpg ppocr_keys_v1.txt ``` If you modify the code, you need to recompile and push to the phone. From af1ac7c2d2d8113f76b17c7f7fab0da89a04dee8 Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Fri, 5 Feb 2021 12:12:16 +0800 Subject: [PATCH 06/66] fix index bug --- deploy/cpp_infer/src/ocr_rec.cpp | 2 +- deploy/lite/ocr_db_crnn.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/cpp_infer/src/ocr_rec.cpp b/deploy/cpp_infer/src/ocr_rec.cpp index fa1e44ff..76873dad 100644 --- a/deploy/cpp_infer/src/ocr_rec.cpp +++ b/deploy/cpp_infer/src/ocr_rec.cpp @@ -76,7 +76,7 @@ void CRNNRecognizer::Run(std::vector>> boxes, float(*std::max_element(&predict_batch[n * predict_shape[2]], &predict_batch[(n + 1) * predict_shape[2]])); - if (argmax_idx > 0 && (!(i > 0 && argmax_idx == last_index))) { + if (argmax_idx > 0 && (!(n > 0 && argmax_idx == last_index))) { score += max_value; count += 1; str_res.push_back(label_list_[argmax_idx]); diff --git a/deploy/lite/ocr_db_crnn.cc b/deploy/lite/ocr_db_crnn.cc index 6cacfc82..cbf4ece7 100644 --- a/deploy/lite/ocr_db_crnn.cc +++ b/deploy/lite/ocr_db_crnn.cc @@ -211,7 +211,7 @@ void RunRecModel(std::vector>> boxes, cv::Mat img, max_value = float(*std::max_element(&predict_batch[n * predict_shape[2]], &predict_batch[(n + 1) * predict_shape[2]])); - if (argmax_idx > 0 && (!(i > 0 && argmax_idx == last_index))) { + if (argmax_idx > 0 && (!(n > 0 && argmax_idx == last_index))) { score += max_value; count += 1; str_res += charactor_dict[argmax_idx]; From c3bb6b5228d0ae607783fca1a839b39c3b28e72f Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Fri, 5 Feb 2021 12:27:24 +0800 Subject: [PATCH 07/66] Android demo init --- deploy/android_demo/.gitignore | 9 + deploy/android_demo/README.md | 26 + deploy/android_demo/app/.gitignore | 1 + deploy/android_demo/app/build.gradle | 94 + deploy/android_demo/app/proguard-rules.pro | 21 + .../demo/ocr/ExampleInstrumentedTest.java | 26 + .../app/src/main/AndroidManifest.xml | 39 + .../app/src/main/assets/images/180.jpg | Bin 0 -> 202534 bytes .../app/src/main/assets/images/270.jpg | Bin 0 -> 174767 bytes .../app/src/main/assets/images/5.jpg | Bin 0 -> 63736 bytes .../app/src/main/assets/images/90.jpg | Bin 0 -> 62546 bytes .../src/main/assets/labels/ppocr_keys_v1.txt | 6623 +++++++++++++++++ .../app/src/main/cpp/CMakeLists.txt | 117 + deploy/android_demo/app/src/main/cpp/common.h | 37 + .../android_demo/app/src/main/cpp/native.cpp | 114 + deploy/android_demo/app/src/main/cpp/native.h | 137 + .../app/src/main/cpp/ocr_clipper.cpp | 4380 +++++++++++ .../app/src/main/cpp/ocr_clipper.hpp | 544 ++ .../app/src/main/cpp/ocr_cls_process.cpp | 46 + .../app/src/main/cpp/ocr_cls_process.h | 23 + .../app/src/main/cpp/ocr_crnn_process.cpp | 142 + .../app/src/main/cpp/ocr_crnn_process.h | 20 + .../app/src/main/cpp/ocr_db_post_process.cpp | 342 + .../app/src/main/cpp/ocr_db_post_process.h | 13 + .../app/src/main/cpp/ocr_ppredictor.cpp | 261 + .../app/src/main/cpp/ocr_ppredictor.h | 122 + .../app/src/main/cpp/ppredictor.cpp | 65 + .../app/src/main/cpp/ppredictor.h | 63 + .../app/src/main/cpp/predictor_input.cpp | 28 + .../app/src/main/cpp/predictor_input.h | 26 + .../app/src/main/cpp/predictor_output.cpp | 26 + .../app/src/main/cpp/predictor_output.h | 31 + .../app/src/main/cpp/preprocess.cpp | 82 + .../app/src/main/cpp/preprocess.h | 12 + .../demo/ocr/AppCompatPreferenceActivity.java | 128 + .../paddle/lite/demo/ocr/MainActivity.java | 473 ++ .../paddle/lite/demo/ocr/MiniActivity.java | 157 + .../lite/demo/ocr/OCRPredictorNative.java | 102 + .../paddle/lite/demo/ocr/OcrResultModel.java | 52 + .../baidu/paddle/lite/demo/ocr/Predictor.java | 356 + .../lite/demo/ocr/SettingsActivity.java | 201 + .../com/baidu/paddle/lite/demo/ocr/Utils.java | 159 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 + .../app/src/main/res/layout/activity_main.xml | 99 + .../app/src/main/res/layout/activity_mini.xml | 46 + .../src/main/res/menu/menu_action_options.xml | 21 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes .../app/src/main/res/values/arrays.xml | 39 + .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 26 + .../app/src/main/res/values/styles.xml | 25 + .../app/src/main/res/xml/file_paths.xml | 4 + .../app/src/main/res/xml/settings.xml | 75 + .../paddle/lite/demo/ocr/ExampleUnitTest.java | 17 + deploy/android_demo/build.gradle | 27 + deploy/android_demo/gradle.properties | 15 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + deploy/android_demo/gradlew | 172 + deploy/android_demo/gradlew.bat | 84 + deploy/android_demo/settings.gradle | 1 + 73 files changed, 15975 insertions(+) create mode 100644 deploy/android_demo/.gitignore create mode 100644 deploy/android_demo/README.md create mode 100644 deploy/android_demo/app/.gitignore create mode 100644 deploy/android_demo/app/build.gradle create mode 100644 deploy/android_demo/app/proguard-rules.pro create mode 100644 deploy/android_demo/app/src/androidTest/java/com/baidu/paddle/lite/demo/ocr/ExampleInstrumentedTest.java create mode 100644 deploy/android_demo/app/src/main/AndroidManifest.xml create mode 100644 deploy/android_demo/app/src/main/assets/images/180.jpg create mode 100644 deploy/android_demo/app/src/main/assets/images/270.jpg create mode 100644 deploy/android_demo/app/src/main/assets/images/5.jpg create mode 100644 deploy/android_demo/app/src/main/assets/images/90.jpg create mode 100644 deploy/android_demo/app/src/main/assets/labels/ppocr_keys_v1.txt create mode 100644 deploy/android_demo/app/src/main/cpp/CMakeLists.txt create mode 100644 deploy/android_demo/app/src/main/cpp/common.h create mode 100644 deploy/android_demo/app/src/main/cpp/native.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/native.h create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_clipper.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_clipper.hpp create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_cls_process.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_cls_process.h create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_crnn_process.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_crnn_process.h create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_db_post_process.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_db_post_process.h create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_ppredictor.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/ocr_ppredictor.h create mode 100644 deploy/android_demo/app/src/main/cpp/ppredictor.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/ppredictor.h create mode 100644 deploy/android_demo/app/src/main/cpp/predictor_input.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/predictor_input.h create mode 100644 deploy/android_demo/app/src/main/cpp/predictor_output.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/predictor_output.h create mode 100644 deploy/android_demo/app/src/main/cpp/preprocess.cpp create mode 100644 deploy/android_demo/app/src/main/cpp/preprocess.h create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/AppCompatPreferenceActivity.java create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MainActivity.java create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MiniActivity.java create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OCRPredictorNative.java create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OcrResultModel.java create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Predictor.java create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/SettingsActivity.java create mode 100644 deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Utils.java create mode 100644 deploy/android_demo/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 deploy/android_demo/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 deploy/android_demo/app/src/main/res/layout/activity_main.xml create mode 100644 deploy/android_demo/app/src/main/res/layout/activity_mini.xml create mode 100644 deploy/android_demo/app/src/main/res/menu/menu_action_options.xml create mode 100644 deploy/android_demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 deploy/android_demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 deploy/android_demo/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 deploy/android_demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 deploy/android_demo/app/src/main/res/values/arrays.xml create mode 100644 deploy/android_demo/app/src/main/res/values/colors.xml create mode 100644 deploy/android_demo/app/src/main/res/values/strings.xml create mode 100644 deploy/android_demo/app/src/main/res/values/styles.xml create mode 100644 deploy/android_demo/app/src/main/res/xml/file_paths.xml create mode 100644 deploy/android_demo/app/src/main/res/xml/settings.xml create mode 100644 deploy/android_demo/app/src/test/java/com/baidu/paddle/lite/demo/ocr/ExampleUnitTest.java create mode 100644 deploy/android_demo/build.gradle create mode 100644 deploy/android_demo/gradle.properties create mode 100644 deploy/android_demo/gradle/wrapper/gradle-wrapper.jar create mode 100644 deploy/android_demo/gradle/wrapper/gradle-wrapper.properties create mode 100644 deploy/android_demo/gradlew create mode 100644 deploy/android_demo/gradlew.bat create mode 100644 deploy/android_demo/settings.gradle diff --git a/deploy/android_demo/.gitignore b/deploy/android_demo/.gitignore new file mode 100644 index 00000000..93dcb293 --- /dev/null +++ b/deploy/android_demo/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/* +.DS_Store +/build +/captures +.externalNativeBuild + diff --git a/deploy/android_demo/README.md b/deploy/android_demo/README.md new file mode 100644 index 00000000..285f7a84 --- /dev/null +++ b/deploy/android_demo/README.md @@ -0,0 +1,26 @@ +# 如何快速测试 +### 1. 安装最新版本的Android Studio +可以从https://developer.android.com/studio 下载。本Demo使用是4.0版本Android Studio编写。 + +### 2. 按照NDK 20 以上版本 +Demo测试的时候使用的是NDK 20b版本,20版本以上均可以支持编译成功。 + +如果您是初学者,可以用以下方式安装和测试NDK编译环境。 +点击 File -> New ->New Project, 新建 "Native C++" project + +### 3. 导入项目 +点击 File->New->Import Project..., 然后跟着Android Studio的引导导入 + + +# 获得更多支持 +前往[端计算模型生成平台EasyEdge](https://ai.baidu.com/easyedge/app/open_source_demo?referrerUrl=paddlelite),获得更多开发支持: + +- Demo APP:可使用手机扫码安装,方便手机端快速体验文字识别 +- SDK:模型被封装为适配不同芯片硬件和操作系统SDK,包括完善的接口,方便进行二次开发 + + +# FAQ: +Q1: 更新1.1版本的模型后,demo报错? + + +A1. 如果要更换V1.1 版本的模型,请更新模型的同时,更新预测库文件,建议使用[PaddleLite 2.6.3](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.6.3)版本的预测库文件,OCR移动端部署参考[教程](../lite/readme.md)。 diff --git a/deploy/android_demo/app/.gitignore b/deploy/android_demo/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/deploy/android_demo/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/deploy/android_demo/app/build.gradle b/deploy/android_demo/app/build.gradle new file mode 100644 index 00000000..494247a3 --- /dev/null +++ b/deploy/android_demo/app/build.gradle @@ -0,0 +1,94 @@ +import java.security.MessageDigest + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + defaultConfig { + applicationId "com.baidu.paddle.lite.demo.ocr" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + cppFlags "-std=c++11 -frtti -fexceptions -Wno-format" + arguments '-DANDROID_PLATFORM=android-23', '-DANDROID_STL=c++_shared' ,"-DANDROID_ARM_NEON=TRUE" + } + } + ndk { + // abiFilters "arm64-v8a", "armeabi-v7a" + abiFilters "arm64-v8a", "armeabi-v7a" + ldLibs "jnigraphics" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version "3.10.2" + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} + +def archives = [ + [ + 'src' : 'https://paddlelite-demo.bj.bcebos.com/libs/android/paddle_lite_libs_v2_6_1.tar.gz', + 'dest': 'PaddleLite' + ], + [ + 'src' : 'https://paddlelite-demo.bj.bcebos.com/libs/android/opencv-4.2.0-android-sdk.tar.gz', + 'dest': 'OpenCV' + ], + [ + 'src' : 'https://paddleocr.bj.bcebos.com/deploy/lite/ocr_v1_for_cpu.tar.gz', + 'dest' : 'src/main/assets/models' + ] +] + +task downloadAndExtractArchives(type: DefaultTask) { + doFirst { + println "Downloading and extracting archives including libs and models" + } + doLast { + // Prepare cache folder for archives + String cachePath = "cache" + if (!file("${cachePath}").exists()) { + mkdir "${cachePath}" + } + archives.eachWithIndex { archive, index -> + MessageDigest messageDigest = MessageDigest.getInstance('MD5') + messageDigest.update(archive.src.bytes) + String cacheName = new BigInteger(1, messageDigest.digest()).toString(32) + // Download the target archive if not exists + boolean copyFiles = !file("${archive.dest}").exists() + if (!file("${cachePath}/${cacheName}.tar.gz").exists()) { + ant.get(src: archive.src, dest: file("${cachePath}/${cacheName}.tar.gz")) + copyFiles = true; // force to copy files from the latest archive files + } + // Extract the target archive if its dest path does not exists + if (copyFiles) { + copy { + from tarTree("${cachePath}/${cacheName}.tar.gz") + into "${archive.dest}" + } + } + } + } +} +preBuild.dependsOn downloadAndExtractArchives \ No newline at end of file diff --git a/deploy/android_demo/app/proguard-rules.pro b/deploy/android_demo/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/deploy/android_demo/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/deploy/android_demo/app/src/androidTest/java/com/baidu/paddle/lite/demo/ocr/ExampleInstrumentedTest.java b/deploy/android_demo/app/src/androidTest/java/com/baidu/paddle/lite/demo/ocr/ExampleInstrumentedTest.java new file mode 100644 index 00000000..77b179da --- /dev/null +++ b/deploy/android_demo/app/src/androidTest/java/com/baidu/paddle/lite/demo/ocr/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.baidu.paddle.lite.demo", appContext.getPackageName()); + } +} diff --git a/deploy/android_demo/app/src/main/AndroidManifest.xml b/deploy/android_demo/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..54482b1d --- /dev/null +++ b/deploy/android_demo/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/assets/images/180.jpg b/deploy/android_demo/app/src/main/assets/images/180.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84cf4c79ef14769d01b0b0e9667387bd16b3e6e7 GIT binary patch literal 202534 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�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 at%8auq%t}vqO>z4qKYdsK%$B$1OM4xajF*p literal 0 HcmV?d00001 diff --git a/deploy/android_demo/app/src/main/assets/images/270.jpg b/deploy/android_demo/app/src/main/assets/images/270.jpg new file mode 100644 index 0000000000000000000000000000000000000000..568739043b7779425b0abeb4459dbb485caed847 GIT binary patch literal 174767 zcmdSAXIN7~_b(c{G?CtkNR=ueAP7W7dJ~l1M0%IriGqOiCLkb0dMASP-mCQ9J4jEc z0YbRj_dUyi$;_Iy*3A4?nb~)2d;&rOeEfUF_XvspUBEXIVj_}%KmHN< zZ&PeM92`6%0(^r1RQZ4PxN8HE-^2cd^AHE?5eS2Y zVPSg#Hx4;29;*;O#Y;^BGZ)H7!k_O^y^7DOXeVS7(Lzvva2+F}VSl>Df&3@hePUhOdr}V1l?t7#kvC_M*p^=qg4|zgw}T; z+#d2xXuCuU&K+niYYckD(0r6CcwM~HD9IO#1x0bxqAA(U!3g!(&tQ~Il_^F(7a(Q` zZ5M{_GJ~-ph%p<0mI;!}&XLw~j)ndXbieuz6l4J%m%!jc|4Xspf9l?3O^3UYQ;CHV zXaNUWnBRfMB?58&+l1yj45181acb@Z4DO)F??9oEa(AFjyUR^WX*nsE@yJT2Y;Q1H zvK2!Zc?VKlN09+lg8x-9aD}$>LRP7g6Cx7jan5SsMLG2}*RG4ogCQpN!)VPC>FGRHc}6b}YVEDQdi5qz zY6%!*3ED2wx|*)UNsA#wBBTOjsBG>)=xBX$U<}oqQCyrC_dLr%TK9L(>P-tvN{z_cL;+j%VyF@^3c>Wz5y5vYoW* zfC77e4QC6^iL9lO$st%WAs5lE^E(HS8+OCqD$D@n{)6k@RYxf(xBdQ*edkA`wt7|Z z%TL>Nu@v4;isYMVPZoSvb)Oa(fNgIN&RA85A-`HS{TB!h{BGE7#mN^(ZaDo#QxhSL ziEU{iK5_n(`#&C>zZ)E^!Cyw>zqeMRA0oDWA>@&O`rAC2al1WRzTX;vFb=>d@CC zN@ug;O_w=tOj4~g!O={_r5THCe}X~~umnq?tKVT~F&dv%P&Q9ZF>?7Z6h|zYlDg#> zO_{j*><-lMzZU6I7v*dGErOc^MvyLbIcleyd|51Y%~)V+N9XpyD>44$I*2!&`uiHf zwCl-*h}1ZMPyih!>RvP@Z1u&pKkGjTD^>$^^ACosURDA4RkEr9IamCLUF8G&8-m^T zhOo+IZ(c-UH-_!w} zhjSWPCXO!`tXgUb;PXKcDo_;XyC2ZbhCo~ZIk`%o01NW93~91MfMD6GfT zUl^gbbk(dvDmgqneBxYVT&wuurOd9wk#sg_cam5PASO{ePC8@H(To zTYGI2{rn+_%b^Jy*c~WVdkP>Dj{azN2V%c`Wr||zy#ra7L(d1E4_Gv9RNey z&3nfjnG_O?H`ecdm%oRc1gK~NYGU90-)i#ukD7d;?T?|WDeR{}`8)~_P~U&cFKCgf z2yg-jN>q*!Z81F;1Ev`EQ@07>B#F}lpLbA%vMGwAe;;TygTet?x&4P)4GaPBjN-^* z0@k!>APBma{%`b~WjcE#qb7Kogg&2@hwi_#x7YnfvQFcoDHg)bLd=DxaKjL~vs-}? zYOqgw2aq-Ozc6&9iZej`C&0H69}I$~G+!mZMl=718mSDQ{ZJjV*y5p+D;RA%hJH6K zv%h;$81S*{4)oVU{MCh<)D4FDInch&1z4k|tM`DlE|CpA;m5%cz66SE2`*s>HPcUQ zn7`ZMfPt;v6+){`T@(8`-)yy_8os0DC%12=iy=g2wwcC&uaFC{VE`Q$lK}y%?#DkS zaw9Q>5P(vR!yTxny5)bDH}hzGW%#~pNpO2%=PmPa4|=)v(yrnqmYK zUxWa3MzS0RbPcQvl=}*rVh|Hw@_(0B?>*iK(7)C%*m>8afK<^%T)%u|i)j6^xCsH5 zy-s}XCz0t1T}^}`RM_r7&n8h=zz{gg{+su`VHm=H^QI}$aFP|!=*lNQN~`WWP>6K4 zS)lsL-zME+TL6=Ki2W*5Lrc zVF*`f59QXDL|)u}2_$`4K=$<_SvSocLgd~Zq?k5FzhTo|KCPPI86WjyCKVWWZRiL1 zEWz6ONQgfr;tq7dhLTw8hiR!qtDh>P4D;uu?m)ZrC$+?uGA7GnXT8)%cc5y=nMlzk zsIkob$+dCx>t(K}zq&di_wvL!NqDnZfBgmF#d1+^0LW57dq|5kIhhBPzAQE+CFg;w zIp`)(QY^ioPE7s zY3F2_{`518=xPGykQKE3S?TmTV6kVpR@)-zm7-JDtYXT?ge@h+_FQEEoN2NDYm*o4 zl#}eJLyzm=`knpJ9f)uQX;oRh^!!D0^<-9xN-CT@KF>z8;L&WkYC6`R`x+9%D|UPl zl`?Foq#Jxpo1}A$L&hEG12lL#*;6+hWM~>D`95aX_{s{KM7+knunu6v8q;>^s!)~YIHGkoj6R13*MY^CDpDo_Nb%XHb0n2^lyz@KUVoUw>x#f9Mx@hy#CQ!qY1~)W zyC7y8;=co#Lr|Poh)Oj-V1}Ylf(4QG{d`+N$7}fCPkrn%KP(PH=Gy4h#bVAASD`2_ z0QHVG)FvF!03*0al%+>W|CUMbeHdaxc}_~ix{s{pNyQ;(VDAsqi++UPR2F5RLAC2^ z)Zi3snQJGco-pWhzog`0GNHB`fJfYBR57q;(RCQI<{?YlKt_h`0;1u##k`SkaeBOb zF8+zQCp6><<4^P!>#s5_Puk>-rafO;_|TLAeeVJ)gW6{b2M}q@!dIPPYd1FvDK~6u zp(!M^_7XRV9~MDB(Z!N9H-Ex5%S|@~(G`^JT~m4 zS`!&S61VIU+gB<~4>Z!{ zCU9zdA1>*RoRm>*ieEmnuV%n9b1j0q*3Wy& zQbpe=hbLs_~`2m^(se4e3(CAFLUT)4^oI}CeTS=a7(0oel zS(g@gUgDvh@P-X4^-X(mK;^MT4_RodrRm(;HdxiK2@El2a2X%fxE71&O*DPeEeQ92 z7Q{DC{Afh*n-0g#YdG601~c}o4gToAWu^)+8vA8>R6H~f{b)win5?tGatI#oR6Rm! zKN9Hi+FPn8nfcWTE6I~c7rr2h79&^oah{+UU7T)V)JP|U(4#d>MheNiR2wDbwLvQW zt{!xsBVR@OHspQd>8CSH%pZJBaU`&M6M@CI_+&+Wxqw(mEU+(= z2K9N(nJI7zj3)(N18k`shHt|0P`wDzsx)~hn4jdIS6LTb)>T|yp~O~&Ey&uzS3JT- zdFW-rJ1OPD6&;AKl_$+0?e+MCwiG1kh?CCNI>OyQe~aw5L5! zOP(_pivl}VepiQPWZUZ08f0|CIx>Xq zbJ$NLh}0%c9Ju@+i5dOiP56%A@*Bxg;DgxL8Eqo9MWh~8ahH`JKfRS_mRzmwsGkC;Vd8@=$fL{lA z|3Ugd4oFnypQ}202LgOmOaur)_CI=R(9Qxtgk-?~*Z2fRS)D;Ja&Tz&Cn+2Z^FLSc zHxQB9`2(I{r+5~S`{4)w4~*Kc{QXG=RQVscRX|HZ|C1b3{6F2f%WH0&|DUrTmT&Z= z2eh8zhF*kI>*(KV>>KU8Yi?uD%#ZM##9Z#nQ%;VWAIko@C{bLtR*odU=9lNRyr~Dv zzO1qwHoRH!iqPaOSiZI6X6Gs@iX-HHQY^52{?y_+O&4?@Pr*sWL*jZ=3xze6Sd=eE zDM+GpKBS*a;TttYYN(|qgKzchQLr{+>ayUBxt@HwH7kzQ%)Bqddb!1-kHm=4WyyT? zOsY_d%hia{dFjT8iG5n>9mv~{j)x=yfmMrB{kHBS+gcym&hP5pLa=`AO#DkmC(&*l z>)*;r1CrlDAwM!{%Upe@Yd8D4^U*^_Nb!hq&{$5ysRk|T7xtzi{)()LqXD|0A~Z{U zbdS%YM5PEjcZYu|+I?DSr>DG`)LSfl-IUs%VN%jG%zdY+xjs(L+8$qi2jZc`Oi%^XBvZXtnOVk{>-9%k9#hfz|m88OmK7JhEg+n-2`XUW-ZhJ+vRozPF zlhq#l@SJYnq`qu-?EA?;4x(KAZAHT=okKa%FKK0M4VD@7qgosbO&Jxph@B&}5WC;E z`P2QH=3uQ~HW=>>#%55t^}C59+~}{ZtVZKuEuOEeWA=Psk1nP}ZpHg(`9@44$YPo_ z5F18n=5mt^%ib&O_z$05_8RPa!0*rhIyLx1AD}K|)YH5+T1#Mub>i|_J>5NHooCYa z!{d`7uxH=^xEdB3C^W0^(>rZ@dB)&#>(uiQ%0TJ;<-;d?x%UkV@&aG&8r0;hk?Jt3 zC^p&K7jkI9=^w^Pp70*SXKyaL$7Hy7Yg+J-o;eKrJYVG8cn=MOB$;3a*QNK(4`=vd zKMxBlnZcwXUoH=|25*#cY}BWu6_1U?zoMnf|00YA*O!l%ap!tp&TW6D6zW3k#94+PPgfOoOi<5}P(wh#53_(Ppc@?x0(b3Yn|j>T_@#nQpx|-+{PV z&OMEw19a`Si?G+N@e~PB%4DnPgPe@8-KrmJn?;N~>Z>UJ_zlB_^r2KB1ltx}r62@> zIGrv~7$q#NuznGO%TNO=YR)zIqI}GO>JgJvRLRn8$327R2h#hqH)MJ{BvWpYHQtwi zyj^a-*trlw>96^9gGry>JK$SP6_lE@NU$v@KJv*+9c(J^4Em$QRs>f-h_?+9$jVv0 zQ9gf;=Qo?l)NL-L;{pj7tV<#Nw)rN?w8?uWtOajRq?^e_4!mt&gV;G3^LP}(M^}mb zermG2)_QMux)StA^Y<@J7?}w6q_GS zAK+5DC-iB3w5yaBw`fn~Zve09TVCd!hq?vIag~QQ--2i12B*dW?@a$-$FRSDwrlG5 zdC+FdTf-9P6zwEdWBK)>n$(zC_u%~@ILWY|nN(-2?dB0u$G)6^Zf>c4ZDdDVk)W`I zAhYS#ju5rmD_!*2`sF8$kBvhfwMyG_N_60p?y$98quS6H!bNP&9?gzFU`IN?g<(BD z?>smBzrGk4aO>uNO~495^9ahkbYsOm<#m6$>Bc3i&+ww2V5Cj-y^uyzFS7XJ58ED3 z*JroO5_@61cpPjvgviTh8uHSwBKt~}{hc3xL#eial z!sJrojUPRcmbd?b`rf8zs7SDsl9o!)qlG|$=jqzYmdJ@$hD*lMK}2ncD~RJTG_*mQ z1k1aNsEz?F5I8QG7Xs@!lx3e4G$IbHU2FaZUic;gc}6XMXm=}_yA9)7OtVjx z`lS;`{e;tnPoc4JRG?G6)XnteAt7$(Mqb(F?EA!nmgnOcOci);4&Qf>kPHzFktJO2 zr@!E2RWjmoLExO5<+&Aj-%OKpU7D~BoaAKw$FH_ylWTQDwD-G-G0lL!McS)_xe#I3 zz=G3K9QwqrOPWZBa`xZytSJwpyCJEfWjEq0Ljj1lY5~fEPt;cTX*I;RR~qh3;$%h@ zB)(CxD_O6u`yB^QfA2DKX|ZBJWt4^+)L#2VakPh->OCIwJ?5|*QI>){+i;7Ek&7)H z%PvRa^(7_`H@dBeYo~t9A;(X{8(E52A(9Y_1nYGvF4Ex3*k+aWpo@9yi_$lF)r(JZ zg?_~~Tz#se@Kp-J-ZH!`9wp1fNZFLQO zsaC4xKF?hyxTvt;S^6k=M}xWd+YZ-R6*r$Jj`}+lx(&A3eTq%Ls3wx^` zhj|oKE9SE6;=VvIV;wE$jMH4l@VRG#}^#NH<`-XhzJg;r` zqyr9nip2Z-s)b!0)Grv_`as$e)lgzc__1iSSVxDkH&{}S;cszY{K9P}=Qqw>c5I55 zZ)9wSeMj+=V?A>01Ml0GAD!K($!e&?9vGbl$@;~#M=Z%5$Rc$7Y>tDMx@;5C%@jket-C3d z`mXL76MhK`_!cI39!oth=TWljIS&gJjaGOwTD9Ao`MV0))$ilQxi$N@;=5xI=8#N) zw>eC625p_4W2cK@M{Fv&R0fmEB8^r>2mN+MDUR4j4yE%>49=B%CHr zJ>s+ArGxC7KRh5ID-kLc_#+*^EhBuJ+~jAA80^|d>~}Jhx|zy)3!L5DfvB!}wvMGL zuni?XB(UF)8V^$yrs(t|ZyjuUe8aJiCwff0bQEr{05%S#FwD*IMLqw?gJyWi(c z)$D1-G;+H5{?j#Asj6Y#y_=1KzwKe7r!WMns=tMY5EtamOq<1>4{-uG;W#Lym} z5(Fy!ttnTR!N}L6rz(bi9GRC&euWooA&Y%SIa?1Cyzuwjbm=OuG$g_YFkUP6_rRe@gDxI9fB`l5v zLAc>{qY{(3-_1gWe4Nr1?+t9SsZI(QU{`deXh(2ey$N2rEMG$#%D-E$BV$$mCQYUZ zWHiIkLwqiHYcXy!KyZn5MbtU>+mLPIC-U;s@`VcV2qil$DQowQT6sDTuAq^@;Fnzo5QZUIv+)K12mK2r=Fm2qoiPEWG} zF}|QXE4(g|7|C}GWcTK@m8w{ee4-(dyvcnUMU+GS!y8~Gr+HWn?XhXPr zBtKN1-(wT&wXhRAZ!67v&^azt=-LIjkc9Qa^$427u%_)V1er{JVHRsgqeLUU%VK=QKr_`s+ zZ}L~AC?uHzc)GO6Ct~y7^`DEft1kNYp8hQA{E_At41Rk(nS8=_JIeDLKc#rPAQZy; z^QY$urowAooZ{u?ty6Wv_P-)}Q?NRZ z(X5eU(xlVN7#D4>?|eS@6*klppwQ_;Q)6~>+(7DfiM@dPG%TwrDbEAXKHb_UAk~{m z9&-wS&~~O&-kHRPo>&Nlk3R6nNUuoiMjzJNM4ZXAb+~T2`E(Wt=@tGd^=IgNjp@3Z zL4J)c@yF(tl&Zb_QeRtn9vW}LI;xsP*I?xSFQ(MWqaDbGs*fz_V!$O{aNA!fmn;dxnqy_1xuufvDr#{2hk9+B%rF)bh5}W4(IpA&!>|e%CevP-#S-~d61k1R= z;GDpXg?Z1gTSB9$c}_d$rZk4Bj@hb_~xSbp~x+Lwc?z%$6GAx!?4iDGSrN8@$Nugh8MWZ7Z&Bu z#h;s=0?@E(gV&H1Va-MJJqCo?C>Q1#(eux4>e~0 zijx>g7o&?Do3^cYlnW7S8C-6&dGA)8t7a`o*u!F9@up8#+2rbc6W^$8TzWM%e4K(R zp}~ylIm)rKOnuAgbEaQBm3Drq_g=W0UVt%%msV~HFv@#MMZ3S1a4X1b2VHZ`^My{zoYiFioBUALipst2@3O&iq^wK>y29(FuuRs6``+p1sp#t-@a*IwN@|6@NZx-k{!CfJk&1JE-aSHO z`bpbkw2;=^UkrOwy(WKm8s1(90V#@M@4!^0x!Q1$l6q1L&+Jj$dh#9>7H7nxyyu18 z*0LPYxk z6vbMoeBu%B=V@|C75j67 z>+M5*-Pk%K&OruUnnmBKv@k_&F-)tdn$ZSZp#Y@JuDUP%S$1lWyLQ*XS&B)yk9qNl0R>rR2_U!7n19F#i}$} zQ8HHib+$3_uky@bgXPqA^g6NOOkF&Ckfj^Dlk9sBdgkITFf~RL9#Kx`|I)UqD?xF7 z;G@*E$4wT4a1QLEX6CiI>quTFwTyWVmLYlZr#5czOH*M;k8>o0XEiu%z%JpoM#8w7 zkmX@=X@P4&@|Ah~B0l&~`#Z#@l?COj10<!O8(Y$q4uH`l{E>E{2Nn0P(FTpsf1&* z(K@7kHZtF7gw$@{)HMFvP~9Ce7O_6Q%o&~i%|o8?>&xRIOCHtW*(Ooe8*`tp_W9!< z(uh;|I=6bVXM-R=C5-p-UH~rh7WJ@LQX6YbR-x9gH{w&Owr23oZD~VhckVKN1 zI@W=WbVkrruR1$V(D!FVhver}ryt3hJ$({RZ((tz_nhQ4S+iz6QnUwNYCN*Rm?3&`UevL8{=K5hXPr!-##5Y%39(NHPvGVh$%6UQ?2-myw`6D(^V zzF%Q1$R6AvyDvQF;`eMh!rtAyHn?rcvq{sd(kEfm_hgMFWVdLgXL_I;;TT#k5L~=~ z3IyLXYOdO68!SHh{3zV>n9J5Iqw_Y?hPxF$xbEjD#v6;{tu>-KCE!7&pHts*^QhGB z>NG0dIH?ACw&9^rE?40FR-*x_^BI9W=a(DEJBNp5nN}O`Nu$#SE{=G+9pcU5=iWwGyJNyVCfVb6m<$dzVIrji;j38Cz+u zpH4Er*ZHiwZThy=17zVHq=Dycpwb%2u>3>kXu2)EWI;tdbE6N(a5ip$k$dxyq%kOsZDg= zpWWNgYYUO|uQfk_jQ!J}MLU^1rSsj(jT_N#*D$q+6-_mU;Q@)*uX&2OzngvVr-KLi zx6P$$`$v5LVwV`&Vc4gtoGDRkz1OA}B%gCzBUaQ6L#E!{PDi*1P&x-RUcOs?Hsx=Y zW4znHrU4o$E$iN1xWr6FEZY8xsmxgQfhm`O!!gX>_FdBuEccRqHLzymFP~B=X=}TS ztD4t?PoLx9nyRa4uY_T6Y7jb+WMP&__1|UPOyz1vt;J=FhxM1#%MrmKm(USvsh?IR z8VJ}bnzr?Uhgl9PP+I4lt$FImS`2A4gqC0r*}3VOoOgspt@O;y=N(5rb(HPUO@pS` z=Q8uU<-*Zwu4R>qWZ9qWdcWi%SH7}NuA}PLV#&T9mUsES9QPn-Ff1{)omvf;Xr@0251L4dhCA^bWF0Q%FjWwq6Ajp^^vJ zUXyp`?#*1Bh{fX4ki;_=a9M;ibjSJZb+Qj#q!gze7qtlr<$6Q!6C-EATUIe|G!ma$CwzIr87wk$@y+v`B4S6TQ6hT2WFlm;t1{Uzq7Dp z!*eCmjHxbr$f8e2o%o0ub*OChD?GJyp){TkiBKR>cx$-w8`||2v9q4nlHnu75ZFR9 zzdP5W2<$c`JNX5uSqu_d&Udnn&PcDT$;dj5&Kc)wIfI`WC4Jj5$C)K9fnAQ2xvA4K z#WtHIOT& z{S*8DxXGlI^f&tU71A=RW%ZEwNo(o&%`?@{_zpLg8n%zL!xdIt)m$Vs4*3y&`ba4g zib~|08;g@GmvB}yI+???(=~%g7+>lXQ!DK=H&>SU#KWt%`0vo2$oy|hTUD;u?EO|F zSh1!kvOJ2dYf9s$xbP%y*C%afR2IGwRvJb5|A|)O>$%dTTP3~O-8qYW3jXx0jF)Dy zyV_tY=2`6Zo|a(mJt-Xy)ZVkpv+&N*6RRl86@FlPyn6Ptu5*D(N(Iipc}PcZYOrIS z4tC$z)iP2v`rYS!Jsq$#&i*dFH^|ZXijd4YBE=J3Z>w0A;(cA;)+ZpQ` zG_$s1N8?5UTPCS+D&$C)VL0^|yT=GpzKh|cWNK5ofhzJ2L@wn+#ag!Zof#HT>l&Cu z+uM?C@-+QhN8f|B$Sev7(XAGBMlhdKbvShoPMuND#5){OF5Lf_7Ir z{feL66erER0~rh>89%QDyv4y7vEPA4dFKk~%ESIle%qhiw${FRuuy7R2@iTSFQZ0H zmip8@r!9}ZV+P~ktbCr2KX5OH-TNiN(p+adKP}F1*#WglC zTRrA`ZDw)tvg2!)dUpZ*W#uBJS$g~Ck7(A!Bc8w{{)mq#*vx)TK)Aj2PTC{ zulIR_uI2g)sfmp)N#eYuUuzVDB6d9I#L%8 z_$;Dg^E@OYO6ZBXA)5N)aSvnvfXsJ|sslx)vd|*oQ+(OX#AMCOzXVG^2v&b)^O7~r zFS2JrzYo+7hT__?vEs;__dd4sc%`^1rL@*<1c3@K5Dzh}c+BiM$D>dzjG; zzaFwBq2SBgQ&TTn$(p2fer$d(xyA^4!#q_8JI{E(5X8#)ID`w>VgGHak+jXDGWSk~R;C{;C`DEwrIBrH zv=__g*`3r_SUTGz@=?-2<$5O1HrlyJuNP|W!ra|dm`|S_{c&(#t+DRc#Jhgizdr-E zdLOlfzAd0!YtB#)d^-a9SlyXbq9`AXy+)U^9ZoEw1ZL2=lW|Dr*QD%J_vizE0aMy4wfdIc`UX79S{2SJx z&Tmj`c!-CaLf!3;brOiDvI|W4bjab`Bs(tbyv*cNSK<4U&$}ib<$X?q28!WenWF$+ zNqDN>Ldv)LpA?P^@)C?UQw@1*mt>ag_P2YDM%{QXT_}CiGPCOvmxET-*5dGg)=|&O z7Yw+w?@R+{-$;Q&m$DcFSVtpwYnhFuj>ROGHxQuJ8(b@@TKfbCVZEb*g*t1~LBj z_I~&a#HVa+;zySca#v&Syl{XR&{mM0v+Z&n&1rWSSF>XNo-WRbQ zZOUFysCet+!M(r5*6_U(>cyMp=MTK75VEl(xE64f#cojWl|t$4XWCmj4&F)n4^iEW zg11i}RzL5Q!4B&%H)yU!*3=jE7TE=z9$XLYq}7nzpNycOy?ihDM}2QpV1VY0+B)8Z zziok?WXtyo1Cg2(*GJ(3B;hG4aSP!Kw^NRt(|J?p{|@ObM>^+u^BAjWG}pO6{RP%* z?C~I;Z=zvR;MZ2gHDcE*;pHB1LjPANXO5VJw$1`04JgSk`v?7)+Cf6W?_WF@b*W{N zF;o>3PJv+m$5U0`&q?vIm|N005cOiN;QDQW!|s3zxi}gJz9xy)I}nAjJ*^2}U8tj# zf3JC=UFFkPZ$i8bdwPYOw6$JsJRd1jwVHx^@XL1cW-sn~!YuR-Jo+qR9ZnPrJs;w@ zK2YMwkW3qzKtlxT*M^??TaGtD3hgBWcXKlt$;6h}->g~HEMt+pDqK_Qh*ef0W2%io zOB8tjUJaR!_aj|ezb#i&CyZx!(9WTigU5M0*Jk;sNs^8RDtS%h8vO_J<$wYhT%>hIMu>m<8(PU|#o#F1n# z$m)6TPowFSU~A-7Q|%b4@lB;U#~7yDkh_5F;!Wcx3s+e&pDULk?FlricY}0t`Uc}{ zlV+i;K3XW}fJh=XKZ_|tj2#p<%I^5~$% zLylZ|X6zqt2((w5p}mPFc4OWJofndhxssVfcpD8qmW?$+tE%m^#9h z=GAe1+3B|=FsR!2`WKMpXZ4tRSH8h#gJbz}hahYFx4%-r#*#@#dkfwEGWavixuI^t z1&)-f0ehhr@O-mEFK;6^nn+_m}yoDCN$Xjns(rkml_*pa~Wa618tH_o*jTf%F}l z8AXzgJ~o#8BzCi_WuZkp4HQBJXSL@V8SIvMlr%>+6*&yRn%=C5a4xS1iuTK-BStR1 z)FNVgV5Huyx;FwzY=A3bV3i~N!wpWzl|fv1Du42Y@6$(y2FQK1!i-03xR(i)a~MOm zjo7@FT7h@Zg^dnl?e1nTo%2(u8^uG+x$VdX1gGHZM4OihdsWR8+<_x)yh9~PCDpuL zCyCteahzq);l)o9=Wtq68os9shOiJBrcpt`J5bf+UUI|XWX0p(Y$B2e#tWVPKP~OH z8_Q4%?a33~gWbBvwq`AUbh!#V=%=KqS?4b-*XFSqxlmywE zeWYHNDfzjif{);7GIHQWCLNZ~v|Zq@NK|^eDGP>ZS1gd;@S=7+uYU4pOYO8HnQqzx zy)I}IjQ4)eC-FZJ%4)K&Itx(C$@KT-DKePS+3d9Y&WZFQ`OvUSYwF43F)`D;S| zwP*65ig<$Wg?ZVrL_b3S=cmjPW!lEoOC!UAOz9a3o5wW~`c>s6Xj|ibkQ-QnTlr}K zUQsCwS`4reku?3(P!Q{}@`WY^LQ_^o7EyJPJfkP+gmgNJ-pXw^O74ntiy0bnpJfTE zYb8L%0B;MSase_7cNQ)*H zNP{vfy)>@@YbtFe&QKk0jE*(=GJtYVDu=c{M@?p$Hq(-?HH}J~=PkEXQUBrZQk7)r zZwz{&-}yi~+JT;MPvl%w1o3oCU;sUxx1No4Ob@Ip2$ySZvaz)|>4qMD}eb z$NOqcYl*~Z%2Bh2P-(sZ_M855gX5NkP)q9?bjIQ5E7T@SwdhKv+=qe_1QF{V;K<~Q z>j>Khiv%^-@elV7<9>)=4hQ}%@4PTatsw|HX(zmS*Wn#E&%;gD<*TRDM1S-Y{0vw< z1S{8X#DOrmo{3s;Ds!w^!#R}bL=V=+N$PH6rn5^%)V(0tf7cKn%Xp}5 z{R+lcDx43kT{hGt$-+#joL3IVI`GOiPpp*8nW;}FV|}kJW!sjGLIN$@oN@@V;Dah; zB;M&t7A@mu_hQ_L`M!HVG=~BzZukNxg;2}$D{CvU0@|X@k<4WiaTPGlYc(0pMvr5i2hN|Tk6BE8+l+SzrLr0#YOFoKOAH8|edl3Qyu8Eh!)1^$3G)f4TMa04X=l9h097p71~&wc<_i z8~d+F)_D3nVR7kxxR{4VPRE*0F&&H0r1d2EwqIg!igywemiM3{`iVxNj(e^Fc9&n-<4m}}!0LQ;-X^qq zxSYYA9#qQM>)(eY>xS;Ods&J-cT zsNV*{pYK4RyvMFD#H4vIxX1H>)4f(?c!u<$f6pvQi~~P-^)EO3vcH^&K(x-vcKOy+ z-#kWfo@zIoawRJB7|RQ5ZyuZU2r@j?vVdfGX(@k{a(L^%of^*cabKfWk@jfyR?f29 zOK|9v$bOHO99sM^fLH%jFISg?C1mG&aeLY5Nhh*c)9$F%r~f+bb&~ZRNaqdUip>_G z6g(~bUm)-kH&jF@LzH@7QIX6HgCF?Q7ol{u4Eg0~M4}gZl-FB|6>}X@^TT4&EqC>0 z_z{8e>y0$klZWdtHmtblwgCG-U1qGh?*i^?qz*(~S^2womsI5P2%VK?y!ct$GYTO( z#t8U}Px_OWG&gaNSlxm4Hs9@@!^P|3gRY#qZq@wrd885{iXBthHm5IoCtoD^V;LRQ ztkTnYEKTYv1a#F?5AC-Fp2lvJD|H>pTSSS(@jicBYxt`jdFo#B_78!qwrXR`B}CI` zTrAhsgm;3ZX4%M&n5CfEP165lH^r8lQJHDeCDnWs!fA8Z-e1*vh5%;`ilb{Ula?ey{09_mcCNgwNn2;aEFvqXo?yE0iPQq;#Y zRhG1rPOPlRSqXl^2bzh2cx4Oi)nMPGTELO*n5R*}%zGF+x3N)EfgDsframuR(F@|x zD_eR+XgDK14^ zD^Yjprbj#O8TT7>vjmfFg*W8Q^i$x-rIJ(Lqk9QdN>3V-ps{TAM}9M3{Q-=N;$JuH3E*R$5NnhkS%tDt&)!)3nW!~)l1bpyhmw)`Pkbn7 zw4BSX@ec=S-l$QkmfXUEw1YR~le(&!f-6$PdygH@`TsAb&O4mV_YM10OI1koqk@*&BSld&MeUK=dykm4_e`v!NX#I7-~5i_eUIb)GM2VI!P;`7u_a`|0qY17FO+Sv0*cRP&pxPQck})@{)VLJY$_l8=^X zZY_URiCJ%O)TwKIP%Sg|#05>U|5eBox=e>8NV1<1GT0uxa;rKV6`zwrVA<~Vr!&>S zjD8?Ll(cLQ^Bkt^-bzO&)Ol$Rl{j+Ng9bs_AI~MfjiKFuZ1gkxHs6U)q)J{8E z$Z2yL1m=1=FUdD}Hh3Kl4mC-Sn#}l86KYrHR!TG_MGu@Naow23FTVr=Hqi4fTBiA2 z@zj2^IpL+zd!*|)DXu{u7iNoApu2qpi<~7WI{^^;Irk$8F&BxiWVhyOnM^us|42Gk zGfQ+aQ(Y#<`?AxE46#Az7fEZ^`dIy$Gv@ zta18nJlolLV79Vpzp>Q!gSY?IUN{X(sHjA0mLSo_xK+8_7jS?aR53jc&83Rb?K zbT>S(Op&HC*`}Teawg`+AmapYIcy+FM@ZFELFHMlI?T!6fxMaI@x*93*nIx0t?nOP6_b zu)0BZT}y1zb4%5c*@Gy6>d%RLJBOtdd*=hmmmN8y|DwJSDA1tz`_8R9O(%O z3P3nZPHeGjhTD8iQ_Xa&z{dgjv-l|D-b@)*HPAxwRxOsaX+w~r|CSZIP(S&s*48s$ z@UydoxaZf9m$#?VY_Gq$hA={PF~~4^qkM?EN$guNj80?FRxqVZ%Y!SXX+fYk(|%`b z`8BqWNVTG89YGlYe+;z{kX2YoiA?bi@($!TF9%OW`WTbcE2GO*i*b=AShXCe2x)Q% zYAMzs`5@8yFfkK%SL0_S5^BE`B$RB6zIL-P0CHm^&kNz6Kmmb2u!m}YgfGz_*E+81 zUF$kxYT+!FV9DcXZ}gY?0JJ>VXDuDzSWa(y7K!h7 z-@TImE#HS^9UIld1@{*_f|=zSmI>uRJoT+E{0Se@x50FT{kwWEC%xI zvZ%*HX*P~%|DJo=utw)K#6L~_r&djxfR_1Izg#&>M#OYM-KNMU6$(|~$Oc=?y0zM1 z#%^i$-6XMUUxIiLPex0Z{;ljhj&uJa<(Vp18om<~Ob&IOZPmU8?XzST7Wo&sU{%pp zl!qD2`I$CZwNjSosH&MhIM|FK0I6m|mt2NG zo5Fw!&?DShm(9akts{EKj^)&;o1~0AqwMH7&3e@mvwtUb#AN_*npnJ@s%AWx!2WbU zTh|)45-{5>BTbQfb>u$3z$zl2*f}=3Sc0Qa$0j7M#|tatqS2;jD}LScT_s@iAb2wU z&Q-s$p^5yLZ=NkNvD!FKlqZ%$H;yIfu+F>0)%ebcT60UbLatt$Ilnh2&&kcPm}VJ) zRAS)7H5Anq>_pCW>A84Ox=wDddK?gYjb%@y2+#`titmf=>Lc>LWr*wo)p~^0j?v?e z*88?frhGNuS5HDMeg*jZC0o2Pa;H%rA=lSf)ci0D>kX1a-g09$zu`Hse%ZhFHr~c) zAkNsH3ERdK-msrww2dNVer`+Qme{$zv3oe`K5J$SA{ODun8Vk8E%|NV^%%LWmUWp# zq%VAKA@5g+SHMZFgCbelYotCyf%UYwOyk?}8GDPShu)SAs~PGyQdUtJ z^R9Z2=#PYT(dHysvtULb?zRILo!9}a~@G8 z$IV1V26=7NQB-qI?ynUbC;C^$8y2b}w*FsEQ?z>uj^tdcM(GdYdvE`}yl!))jtZeG zZzXeZs|sWDg+)VbMw{ zZ>yHraDcVF3Q#Tyr<- zJzZ}ZOhj)T?I5fu_}AOJW4ZGX`)ke9VXc;k1WH|lr0rKHn}epy zDO?XJe-iNhf8HE#R>4oH)&djmL4@NDgZXg13y|UN;Fps;7>8L{t65Y>hast1 zlbJDD$@gZS ze17u&E%?sY-u`QTS?oY`Sk_Kqpz+oS4a3O;*if~Qtua)T?1P(I1<-;l?47L{F%`1a|6D>3r@6pJDRB;B&Ei|oBt#7tvP%kwGH0V z`g_;U`#qJuvh-h>jWbX>;`Pl}Xb{UFTKFp+Jz#+)s<|o-+9D{Fs%fNnRF(fd(4D?d zOku3>d0xzp*n{VJVK4Jyo)i7wiv-is1)}mzN4VF47-?izCQH;5VPgN=-Y^#E8Pv*1zo z=QNdt(@!-W3DFP&y*leZ)QeUr7Q+2k*~V+#s1|%esCN&!9Q*o)k2gOoT{NE&WPu;u zSugOsXH*#cflBXZyWw!Yly`YQ2x_wcwf+U{aM%`4KHL(a0%thR|;!td?5Ul@K)R zOS2SH4=&hjhj2TmE1@6@e(WL}kL)JT(AL*zy_RG0cqMA3)|wL6K-F`^Bhqr1IyPvD z=%!v$-awCL#Ad6Z1vND6+G<-I%{adYVGOg%t`I9WD1>XOC%cZp#y-ZQ9&zyELvXQc z2V*PUV>j-EHu`?XD0?U)KG1}s3~JImmsL(jOd<3)ZzYv;d2Y1*&fLz7Q}W0MflROB zkCdMxBRG%stkg(FzU=NApO72ksAM*2y4wBxXkCx3y?MO%wDnW{yua;$%%d!ecaf-9 zQ@@-7^kA?5_&)g)^U`qLe&66yf0(P9UDkWD5yS$iX4N7`I3?;SJ#|076)tdtT=bDs z$h!_Pdi>dFSo@!&+JqAm(n%LQ*}<^XlaW=8L>*B-PU{t*AJS_`PAch&YGk(*2BKPUpeXgHGyGRMX)DSfj|ytKNgzL?|Vu@`s-x;{~o5U@8}+^f&4&d2klfBl6ePLARUByw@E1eUg1 z2_2axSFcT>@i(|~n4pW?A#KDy(bO;M3}g}_v&7H54<5xU-ZbKwf4&axD&Y@!(;B}f zg(oAFGjRN4K8xWF$vN{$YlRp|=4tOs6Ep#Ttpmj1J$x=wW&87{T5U%}EQU_%0Xa*? z4e}z`kUo}<6!ro%rPu7v0Uu98>I8+ryOziO=W@S^RuX15X2!gQcE*ukMJspyeYprI))GOX`*`epf#aPWBTW<8wE8i8Kq8pKCU`tp zIO3RW0{C>mjahHArhT|ssN3t8&!thwx=s+dat7pn#PWC1IIE}U%D6~wzWgH5e6_7i zjm4~6SU#zXlVt0%!CUXiHQVw~2l={vIrRd94X*+-kG7i;ZU~LPPU=kkGxDD8=a~fB z9Ixr&clSeBu;f-77`^rd-hhagH+26wtM6AmS#hryyCnOMU*<)|M1 zNT}5cb1WQdLvbcuS3!$$+;}tdg!$h9t47RvBWF(LiS~wr)ph^;hh|fWfrLL-f2&5k z!b}$%Mo166vv%47ZaR=_lxu52Ijd+0w|=)4YadNB{P51F|A^X59C1x~D@5_q+@B64 zvPw_bvLXs|?+)_(owuJX?n;+s$w69F&Dfhn_>Scpep~8$@?#Oi7k#}wu7MB`)dbvW z0x>LT@_#cSX{2AZ<3TBsjGKs|2RKtV?jBqj%$qpD<=@0%tM+H>f1Lv@ud?fdsn@vP{JJVk3suU)^A+A=gEU<;`we)svM}0G` z#p92W(zQ~zgxuFpjEBZ||Zo|xpfjRB`+FIF0AUWI!<5d&;QkvT?wG7po}^2zxGHx8c9A zRxQ;{(Lw0T#adu1Dt=;lO3il+v>XN+9tLa3>}=|OPby2c#&Zr@B>#g;={LvA4WnfN zWD6>2a^yKI^6<2!SnV>mMez=Spj9WhbG)PKc&w1@=?K-?4NLY(VHuRRI(qK3|W!8sYw;Fi?W0J|=e{pPob;#Lg&|sfl0eRms=BF&e1@U|l}F$p&7xPwzdi=<#_~Fo zY>!DCgwUPtQZ+n%W-MbPLy|&&a?uY)#7p`$zv{Sapwxn;*!$;mQZcgT_}(K!dDZ&k z5E0SEooqo@;yHL!avxS_2~~vW4|22smoW>$qE%Zx?Ad|%H$y!;(oAkm=sF(&8APh9 z5DR^V%G8!QI^1k(x1cbLKIFQ6krhsyb@5K~qWG2*Z-o)=Lfq|00181W=aiEA=u<7OzjyLBCW2id6G@f>5 z)b@OMz5_HHK2E0GZ}nkfs^HfRz1mL44(sFQ82g3kSSfCv3BWr~n%gs7hmnhST2R{Yy6P+g!bm`8L<;R(8{FC8hEsdS+RUaZvRQ&zQB2;F}~*R;!NV2~F0Gm1Tx)cnR)&2*k?!*_V>HmmTafY28OC(;TI% zjdT;I#+19I$Oo3az;M~Mh!7WeBnu4xVl4I|mD;Ubune372!C~M^0u9wLDSHoqZJnC zCozFb?o9*U#W-vi=CF6nX~!pN_}mDs#E{_gIZb&`?di1Ma}&SWH?!xoYr3w*+}9J9OE(J-&V}QQ z)@WUUpeejk{t`~NbGURtw^$PpRhVYx%-&Bc%5@B_r*mkxihLO4d?)3wW*ruH<=%lA zR>q}s87y?ye_C!PEiMWC6`CF|_1RpKzq?-M3CrJKk;dcllrDMByZ!I<4BEog&cB_9r-%ll#Lnywf zdc{?nvn%+;B#?IxDC-g1+;^B*Jg))dZo>-k#)Et$jn_=S#gp6{tkuU7u#z8QU?=4@{;U7enyWd(E zi6cpz-TAFo*Lr|7Noz_|J}Y8c77MNW{8~(?xMdxr+_d_MhUR8`5q<6-LmaFOvfpF< za9Un#T{tox?dHw|TfoZ+{_~6W%nBKH`K_*WY5bL?FM0PHB-7AvGO8-ZqRXEPQlIR^ z=g8@-cZ2`Oo-&39VT$7Ad5ghE8@e(2xTHCQ>5Prn)oGeB3qAvii{oVRKZeJ&mt1Rs zKn!oMaMUW#O}1|bv1^+CE<>VZVFn^{8>T&-o19mESSSN4ExB~Qvz?DZJc2CsTQlHb z1Z!9v<0qqtWLAm8SbG=x(Zb&Zf2{GbX2)G>)PBzCSPY0k+$NkvvuRE|+tpbj$eRD2 z8$c`Luf^cA`9eR%(FX({hSHKn4m~C!471b4kVA?Cqgu2mMF)X{zvj8#2v&R#n$~aP zD_f2uE$@glv0?D_c+ej#3Ng^0tjm?pt?gSHpM zoN-PCZu{1pH8^|B@qS;9^xVkrzp^&j<*t(B;^9NQV;tpX?t!GEq#p%MabQ)9?pM42 zDd*gGOkL{T1;rIa9`L*EK6ZWi2xLdJWCo~l>NO313USn0(x%p%de)eBz;G_xR-+_N zJnSgLY2T!n_+EQsYq8Yx#8^GDw&MX5GA++|t%VoGABGdyi7DE)eH73$u=D!nwvw*UOtE1;r;=-6CT zY(HZ-n=!aMH#kKMCX=hYtM%O2GW4Yqn|gs0^dKDftm+%5gunF? zyHHv4^8RM#Qy}gG4-xOlnb2V~U(X23bNZJ$i

v*j23LYz+!Co%n^FUIOcerN3N} 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

6H&%(Q1lRf~46*KcYqJgWl2 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

|_(0CBde&02(%AEd~xP!lw;@FH|HK zSPiYzQs<}FjDu*5tuC2=zt~@@@(Dsj&$m0F7uUKL%?51TpZEXSnvtpG+hd4?HeiyY zW!-c85&tO31i#=)R1~Nwp?xGZW&N#(c^VJygR;2c(<{OKdrBgRbsyhF$eM=NCT)oI zebxJ8Uyse%!_|gHM0q1k;T-csdiOQzz|)GE%vTw+s5drdLEdoHqHWCKxwgeX~gm8WIfP3ja?qK)wPG>k_GSC@*o3ARxv-WpqvaHBDF=~(gelt zL_y+2eQEWE{v<6dv)f{Ga}^RhO*MPhxYN{`M5ph%;&=WP%^4K;HlOjR(=kN6F}QBA zT#bF#gVWIo@}9INYk0&BMbMdYx-LL+;e$65vDOfLhYm7s64DYE_^H-sfoRV#8F4O$ zg>s(@H>xIZoLjtLKP+@759sYRf6{P7Y4qr-+0)Lh81GT$$q~Bd1%{$csE-PB4VOJR z@v-mW1f@YSE+K&SD`bH?B?z4U!aNiHtY7A3CqZkT`qMPc z!vWsk`b_pR7dcD%>*R9{vpt1jFO2}67jzmEy5npDmC0c z_@E-!C5FPD`NJG4O7Y_z7_UXVZfC2sdYIsk@$OjbS?A}^b``KHA4s!LUg^_)Swi%h zE6=Sw6JDMEW-p96GBYUBVs2@Cn~v654B|>&Z4RZo@A{qJ@Rh-=q;IXjWBnhEOPvEr zn!K_H9FpQHi0tMh2ELqaf;9;AW3uWn!Z8Gi@gma?6qC-rsimv83*+CGu584X1genM z)q^iBE*{(NU7UnG$mS8q@peq;(17R5C{TQGb~rD%1?6su{#CA8*F+~B;#Vui2L=bR z(%l-{{wmKri0MmGxx{UDY78D$?;#l)A}5Be_iqfKC)Lv~gF3KnNzw%!iS^Woxq*e? zIR^+IoG}vlJ!Ze#x#GPzI*-okFvUc{IUKc0@m%H0g=6N@U#KYbg&5J9kzu@p~O!jWh_&z}j>1JW_ZzC0XJ^5jX z4hz)UJQdtWd({);n9{o0PQFS{j~A55fzQ9L?8(ntwA@ik!roc=x9whf{c`$96J(_W>1R(3I6wm;)n;s7z9scbx@Bvrbwko?P)7Pjg4J3zpr zB!ra{2W^rQ2u3q|Jh!LAg?j?&a(_&u+N#6Ms5%{YWWG2#2}@5ucwb2Yn{y`vI?MIk zoR+sp?(o$-!5mdyw0lNnO<$Qk3}LO6-ZbCZdgk*I%MWL_Q!TXLWCVW=o@UQp{f~MZG($xYb;gsyBzH zW8E(j+IShIWCn)@du)t`sd-35J>3$BM*pcK z(zJbE@- zyg043J3rURkq_{b7p``y#yI7R5`pUj(QGeF&lW8{;^x}#55Lxj&L9nbJZB!OC_$tdOmG(lUkNoN_L)ZCE z7entKZ~2V(gI~AJ1LCmn!}g2ir94xp=V4ndQL_VlX>IRXkL~C_01?b)qi5%GL#ChL ZHIzwIB}}HarH=oPFXcae6{^2W{{>!MYrg;h literal 0 HcmV?d00001 diff --git a/deploy/android_demo/app/src/main/assets/images/5.jpg b/deploy/android_demo/app/src/main/assets/images/5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8517e125c5e660f1bc603a844f946c68086e4616 GIT binary patch literal 63736 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 @{rU7nFH)Mwr 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

+ * This technique can be used with an {@link android.app.Activity} class, not just + * {@link PreferenceActivity}. + */ +public abstract class AppCompatPreferenceActivity extends PreferenceActivity { + private AppCompatDelegate mDelegate; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); + } + + public ActionBar getSupportActionBar() { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) { + getDelegate().setSupportActionBar(toolbar); + } + + @Override + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + getDelegate().setContentView(layoutResID); + } + + @Override + public void setContentView(View view) { + getDelegate().setContentView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().addContentView(view, params); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onTitleChanged(CharSequence title, int color) { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override + protected void onStop() { + super.onStop(); + getDelegate().onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + getDelegate().onDestroy(); + } + + public void invalidateOptionsMenu() { + getDelegate().invalidateOptionsMenu(); + } + + private AppCompatDelegate getDelegate() { + if (mDelegate == null) { + mDelegate = AppCompatDelegate.create(this, null); + } + return mDelegate; + } +} diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MainActivity.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MainActivity.java new file mode 100644 index 00000000..afb261dc --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MainActivity.java @@ -0,0 +1,473 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.Manifest; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.preference.PreferenceManager; +import android.provider.MediaStore; +import android.text.method.ScrollingMovementMethod; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class MainActivity extends AppCompatActivity { + private static final String TAG = MainActivity.class.getSimpleName(); + public static final int OPEN_GALLERY_REQUEST_CODE = 0; + public static final int TAKE_PHOTO_REQUEST_CODE = 1; + + public static final int REQUEST_LOAD_MODEL = 0; + public static final int REQUEST_RUN_MODEL = 1; + public static final int RESPONSE_LOAD_MODEL_SUCCESSED = 0; + public static final int RESPONSE_LOAD_MODEL_FAILED = 1; + public static final int RESPONSE_RUN_MODEL_SUCCESSED = 2; + public static final int RESPONSE_RUN_MODEL_FAILED = 3; + + protected ProgressDialog pbLoadModel = null; + protected ProgressDialog pbRunModel = null; + + protected Handler receiver = null; // Receive messages from worker thread + protected Handler sender = null; // Send command to worker thread + protected HandlerThread worker = null; // Worker thread to load&run model + + // UI components of object detection + protected TextView tvInputSetting; + protected ImageView ivInputImage; + protected TextView tvOutputResult; + protected TextView tvInferenceTime; + + // Model settings of object detection + protected String modelPath = ""; + protected String labelPath = ""; + protected String imagePath = ""; + protected int cpuThreadNum = 1; + protected String cpuPowerMode = ""; + protected String inputColorFormat = ""; + protected long[] inputShape = new long[]{}; + protected float[] inputMean = new float[]{}; + protected float[] inputStd = new float[]{}; + protected float scoreThreshold = 0.1f; + private String currentPhotoPath; + + protected Predictor predictor = new Predictor(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Clear all setting items to avoid app crashing due to the incorrect settings + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.commit(); + + // Prepare the worker thread for mode loading and inference + receiver = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case RESPONSE_LOAD_MODEL_SUCCESSED: + pbLoadModel.dismiss(); + onLoadModelSuccessed(); + break; + case RESPONSE_LOAD_MODEL_FAILED: + pbLoadModel.dismiss(); + Toast.makeText(MainActivity.this, "Load model failed!", Toast.LENGTH_SHORT).show(); + onLoadModelFailed(); + break; + case RESPONSE_RUN_MODEL_SUCCESSED: + pbRunModel.dismiss(); + onRunModelSuccessed(); + break; + case RESPONSE_RUN_MODEL_FAILED: + pbRunModel.dismiss(); + Toast.makeText(MainActivity.this, "Run model failed!", Toast.LENGTH_SHORT).show(); + onRunModelFailed(); + break; + default: + break; + } + } + }; + + worker = new HandlerThread("Predictor Worker"); + worker.start(); + sender = new Handler(worker.getLooper()) { + public void handleMessage(Message msg) { + switch (msg.what) { + case REQUEST_LOAD_MODEL: + // Load model and reload test image + if (onLoadModel()) { + receiver.sendEmptyMessage(RESPONSE_LOAD_MODEL_SUCCESSED); + } else { + receiver.sendEmptyMessage(RESPONSE_LOAD_MODEL_FAILED); + } + break; + case REQUEST_RUN_MODEL: + // Run model if model is loaded + if (onRunModel()) { + receiver.sendEmptyMessage(RESPONSE_RUN_MODEL_SUCCESSED); + } else { + receiver.sendEmptyMessage(RESPONSE_RUN_MODEL_FAILED); + } + break; + default: + break; + } + } + }; + + // Setup the UI components + tvInputSetting = findViewById(R.id.tv_input_setting); + ivInputImage = findViewById(R.id.iv_input_image); + tvInferenceTime = findViewById(R.id.tv_inference_time); + tvOutputResult = findViewById(R.id.tv_output_result); + tvInputSetting.setMovementMethod(ScrollingMovementMethod.getInstance()); + tvOutputResult.setMovementMethod(ScrollingMovementMethod.getInstance()); + } + + @Override + protected void onResume() { + super.onResume(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean settingsChanged = false; + String model_path = sharedPreferences.getString(getString(R.string.MODEL_PATH_KEY), + getString(R.string.MODEL_PATH_DEFAULT)); + String label_path = sharedPreferences.getString(getString(R.string.LABEL_PATH_KEY), + getString(R.string.LABEL_PATH_DEFAULT)); + String image_path = sharedPreferences.getString(getString(R.string.IMAGE_PATH_KEY), + getString(R.string.IMAGE_PATH_DEFAULT)); + settingsChanged |= !model_path.equalsIgnoreCase(modelPath); + settingsChanged |= !label_path.equalsIgnoreCase(labelPath); + settingsChanged |= !image_path.equalsIgnoreCase(imagePath); + int cpu_thread_num = Integer.parseInt(sharedPreferences.getString(getString(R.string.CPU_THREAD_NUM_KEY), + getString(R.string.CPU_THREAD_NUM_DEFAULT))); + settingsChanged |= cpu_thread_num != cpuThreadNum; + String cpu_power_mode = + sharedPreferences.getString(getString(R.string.CPU_POWER_MODE_KEY), + getString(R.string.CPU_POWER_MODE_DEFAULT)); + settingsChanged |= !cpu_power_mode.equalsIgnoreCase(cpuPowerMode); + String input_color_format = + sharedPreferences.getString(getString(R.string.INPUT_COLOR_FORMAT_KEY), + getString(R.string.INPUT_COLOR_FORMAT_DEFAULT)); + settingsChanged |= !input_color_format.equalsIgnoreCase(inputColorFormat); + long[] input_shape = + Utils.parseLongsFromString(sharedPreferences.getString(getString(R.string.INPUT_SHAPE_KEY), + getString(R.string.INPUT_SHAPE_DEFAULT)), ","); + float[] input_mean = + Utils.parseFloatsFromString(sharedPreferences.getString(getString(R.string.INPUT_MEAN_KEY), + getString(R.string.INPUT_MEAN_DEFAULT)), ","); + float[] input_std = + Utils.parseFloatsFromString(sharedPreferences.getString(getString(R.string.INPUT_STD_KEY) + , getString(R.string.INPUT_STD_DEFAULT)), ","); + settingsChanged |= input_shape.length != inputShape.length; + settingsChanged |= input_mean.length != inputMean.length; + settingsChanged |= input_std.length != inputStd.length; + if (!settingsChanged) { + for (int i = 0; i < input_shape.length; i++) { + settingsChanged |= input_shape[i] != inputShape[i]; + } + for (int i = 0; i < input_mean.length; i++) { + settingsChanged |= input_mean[i] != inputMean[i]; + } + for (int i = 0; i < input_std.length; i++) { + settingsChanged |= input_std[i] != inputStd[i]; + } + } + float score_threshold = + Float.parseFloat(sharedPreferences.getString(getString(R.string.SCORE_THRESHOLD_KEY), + getString(R.string.SCORE_THRESHOLD_DEFAULT))); + settingsChanged |= scoreThreshold != score_threshold; + if (settingsChanged) { + modelPath = model_path; + labelPath = label_path; + imagePath = image_path; + cpuThreadNum = cpu_thread_num; + cpuPowerMode = cpu_power_mode; + inputColorFormat = input_color_format; + inputShape = input_shape; + inputMean = input_mean; + inputStd = input_std; + scoreThreshold = score_threshold; + // Update UI + tvInputSetting.setText("Model: " + modelPath.substring(modelPath.lastIndexOf("/") + 1) + "\n" + "CPU" + + " Thread Num: " + Integer.toString(cpuThreadNum) + "\n" + "CPU Power Mode: " + cpuPowerMode); + tvInputSetting.scrollTo(0, 0); + // Reload model if configure has been changed + loadModel(); + } + } + + public void loadModel() { + pbLoadModel = ProgressDialog.show(this, "", "Loading model...", false, false); + sender.sendEmptyMessage(REQUEST_LOAD_MODEL); + } + + public void runModel() { + pbRunModel = ProgressDialog.show(this, "", "Running model...", false, false); + sender.sendEmptyMessage(REQUEST_RUN_MODEL); + } + + public boolean onLoadModel() { + return predictor.init(MainActivity.this, modelPath, labelPath, cpuThreadNum, + cpuPowerMode, + inputColorFormat, + inputShape, inputMean, + inputStd, scoreThreshold); + } + + public boolean onRunModel() { + return predictor.isLoaded() && predictor.runModel(); + } + + public void onLoadModelSuccessed() { + // Load test image from path and run model + try { + if (imagePath.isEmpty()) { + return; + } + Bitmap image = null; + // Read test image file from custom path if the first character of mode path is '/', otherwise read test + // image file from assets + if (!imagePath.substring(0, 1).equals("/")) { + InputStream imageStream = getAssets().open(imagePath); + image = BitmapFactory.decodeStream(imageStream); + } else { + if (!new File(imagePath).exists()) { + return; + } + image = BitmapFactory.decodeFile(imagePath); + } + if (image != null && predictor.isLoaded()) { + predictor.setInputImage(image); + runModel(); + } + } catch (IOException e) { + Toast.makeText(MainActivity.this, "Load image failed!", Toast.LENGTH_SHORT).show(); + e.printStackTrace(); + } + } + + public void onLoadModelFailed() { + } + + public void onRunModelSuccessed() { + // Obtain results and update UI + tvInferenceTime.setText("Inference time: " + predictor.inferenceTime() + " ms"); + Bitmap outputImage = predictor.outputImage(); + if (outputImage != null) { + ivInputImage.setImageBitmap(outputImage); + } + tvOutputResult.setText(predictor.outputResult()); + tvOutputResult.scrollTo(0, 0); + } + + public void onRunModelFailed() { + } + + public void onImageChanged(Bitmap image) { + // Rerun model if users pick test image from gallery or camera + if (image != null && predictor.isLoaded()) { + predictor.setInputImage(image); + runModel(); + } + } + + public void onSettingsClicked() { + startActivity(new Intent(MainActivity.this, SettingsActivity.class)); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_action_options, menu); + return true; + } + + public boolean onPrepareOptionsMenu(Menu menu) { + boolean isLoaded = predictor.isLoaded(); + menu.findItem(R.id.open_gallery).setEnabled(isLoaded); + menu.findItem(R.id.take_photo).setEnabled(isLoaded); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.open_gallery: + if (requestAllPermissions()) { + openGallery(); + } + break; + case R.id.take_photo: + if (requestAllPermissions()) { + takePhoto(); + } + break; + case R.id.settings: + if (requestAllPermissions()) { + // Make sure we have SDCard r&w permissions to load model from SDCard + onSettingsClicked(); + } + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show(); + } + } + + private boolean requestAllPermissions() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, + Manifest.permission.CAMERA) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.CAMERA}, + 0); + return false; + } + return true; + } + + private void openGallery() { + Intent intent = new Intent(Intent.ACTION_PICK, null); + intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*"); + startActivityForResult(intent, OPEN_GALLERY_REQUEST_CODE); + } + + private void takePhoto() { + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + // Ensure that there's a camera activity to handle the intent + if (takePictureIntent.resolveActivity(getPackageManager()) != null) { + // Create the File where the photo should go + File photoFile = null; + try { + photoFile = createImageFile(); + } catch (IOException ex) { + Log.e("MainActitity", ex.getMessage(), ex); + Toast.makeText(MainActivity.this, + "Create Camera temp file failed: " + ex.getMessage(), Toast.LENGTH_SHORT).show(); + } + // Continue only if the File was successfully created + if (photoFile != null) { + Log.i(TAG, "FILEPATH " + getExternalFilesDir("Pictures").getAbsolutePath()); + Uri photoURI = FileProvider.getUriForFile(this, + "com.baidu.paddle.lite.demo.ocr.fileprovider", + photoFile); + currentPhotoPath = photoFile.getAbsolutePath(); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); + startActivityForResult(takePictureIntent, TAKE_PHOTO_REQUEST_CODE); + Log.i(TAG, "startActivityForResult finished"); + } + } + + } + + private File createImageFile() throws IOException { + // Create an image file name + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + File image = File.createTempFile( + imageFileName, /* prefix */ + ".bmp", /* suffix */ + storageDir /* directory */ + ); + + return image; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + switch (requestCode) { + case OPEN_GALLERY_REQUEST_CODE: + if (data == null) { + break; + } + try { + ContentResolver resolver = getContentResolver(); + Uri uri = data.getData(); + Bitmap image = MediaStore.Images.Media.getBitmap(resolver, uri); + String[] proj = {MediaStore.Images.Media.DATA}; + Cursor cursor = managedQuery(uri, proj, null, null, null); + cursor.moveToFirst(); + onImageChanged(image); + } catch (IOException e) { + Log.e(TAG, e.toString()); + } + break; + case TAKE_PHOTO_REQUEST_CODE: + if (currentPhotoPath != null) { + ExifInterface exif = null; + try { + exif = new ExifInterface(currentPhotoPath); + } catch (IOException e) { + e.printStackTrace(); + } + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED); + Log.i(TAG, "rotation " + orientation); + Bitmap image = BitmapFactory.decodeFile(currentPhotoPath); + image = Utils.rotateBitmap(image, orientation); + onImageChanged(image); + } else { + Log.e(TAG, "currentPhotoPath is null"); + } + break; + default: + break; + } + } + } + + @Override + protected void onDestroy() { + if (predictor != null) { + predictor.releaseModel(); + } + worker.quit(); + super.onDestroy(); + } +} diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MiniActivity.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MiniActivity.java new file mode 100644 index 00000000..c25c6fce --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/MiniActivity.java @@ -0,0 +1,157 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import java.io.IOException; +import java.io.InputStream; + +public class MiniActivity extends AppCompatActivity { + + + public static final int REQUEST_LOAD_MODEL = 0; + public static final int REQUEST_RUN_MODEL = 1; + public static final int REQUEST_UNLOAD_MODEL = 2; + public static final int RESPONSE_LOAD_MODEL_SUCCESSED = 0; + public static final int RESPONSE_LOAD_MODEL_FAILED = 1; + public static final int RESPONSE_RUN_MODEL_SUCCESSED = 2; + public static final int RESPONSE_RUN_MODEL_FAILED = 3; + + private static final String TAG = "MiniActivity"; + + protected Handler receiver = null; // Receive messages from worker thread + protected Handler sender = null; // Send command to worker thread + protected HandlerThread worker = null; // Worker thread to load&run model + protected volatile Predictor predictor = null; + + private String assetModelDirPath = "models/ocr_v2_for_cpu"; + private String assetlabelFilePath = "labels/ppocr_keys_v1.txt"; + + private Button button; + private ImageView imageView; // image result + private TextView textView; // text result + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_mini); + + Log.i(TAG, "SHOW in Logcat"); + + // Prepare the worker thread for mode loading and inference + worker = new HandlerThread("Predictor Worker"); + worker.start(); + sender = new Handler(worker.getLooper()) { + public void handleMessage(Message msg) { + switch (msg.what) { + case REQUEST_LOAD_MODEL: + // Load model and reload test image + if (!onLoadModel()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(MiniActivity.this, "Load model failed!", Toast.LENGTH_SHORT).show(); + } + }); + } + break; + case REQUEST_RUN_MODEL: + // Run model if model is loaded + final boolean isSuccessed = onRunModel(); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (isSuccessed){ + onRunModelSuccessed(); + }else{ + Toast.makeText(MiniActivity.this, "Run model failed!", Toast.LENGTH_SHORT).show(); + } + } + }); + break; + } + } + }; + sender.sendEmptyMessage(REQUEST_LOAD_MODEL); // corresponding to REQUEST_LOAD_MODEL, to call onLoadModel() + + imageView = findViewById(R.id.imageView); + textView = findViewById(R.id.sample_text); + button = findViewById(R.id.button); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + sender.sendEmptyMessage(REQUEST_RUN_MODEL); + } + }); + + + } + + @Override + protected void onDestroy() { + onUnloadModel(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + worker.quitSafely(); + } else { + worker.quit(); + } + super.onDestroy(); + } + + /** + * call in onCreate, model init + * + * @return + */ + private boolean onLoadModel() { + if (predictor == null) { + predictor = new Predictor(); + } + return predictor.init(this, assetModelDirPath, assetlabelFilePath); + } + + /** + * init engine + * call in onCreate + * + * @return + */ + private boolean onRunModel() { + try { + String assetImagePath = "images/5.jpg"; + InputStream imageStream = getAssets().open(assetImagePath); + Bitmap image = BitmapFactory.decodeStream(imageStream); + // Input is Bitmap + predictor.setInputImage(image); + return predictor.isLoaded() && predictor.runModel(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + private void onRunModelSuccessed() { + Log.i(TAG, "onRunModelSuccessed"); + textView.setText(predictor.outputResult); + imageView.setImageBitmap(predictor.outputImage); + } + + private void onUnloadModel() { + if (predictor != null) { + predictor.releaseModel(); + } + } +} diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OCRPredictorNative.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OCRPredictorNative.java new file mode 100644 index 00000000..1fa419e3 --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OCRPredictorNative.java @@ -0,0 +1,102 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.graphics.Bitmap; +import android.util.Log; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +public class OCRPredictorNative { + + private static final AtomicBoolean isSOLoaded = new AtomicBoolean(); + + public static void loadLibrary() throws RuntimeException { + if (!isSOLoaded.get() && isSOLoaded.compareAndSet(false, true)) { + try { + System.loadLibrary("Native"); + } catch (Throwable e) { + RuntimeException exception = new RuntimeException( + "Load libNative.so failed, please check it exists in apk file.", e); + throw exception; + } + } + } + + private Config config; + + private long nativePointer = 0; + + public OCRPredictorNative(Config config) { + this.config = config; + loadLibrary(); + nativePointer = init(config.detModelFilename, config.recModelFilename,config.clsModelFilename, + config.cpuThreadNum, config.cpuPower); + Log.i("OCRPredictorNative", "load success " + nativePointer); + + } + + + public ArrayList runImage(float[] inputData, int width, int height, int channels, Bitmap originalImage) { + Log.i("OCRPredictorNative", "begin to run image " + inputData.length + " " + width + " " + height); + float[] dims = new float[]{1, channels, height, width}; + float[] rawResults = forward(nativePointer, inputData, dims, originalImage); + ArrayList results = postprocess(rawResults); + return results; + } + + public static class Config { + public int cpuThreadNum; + public String cpuPower; + public String detModelFilename; + public String recModelFilename; + public String clsModelFilename; + + } + + public void destory(){ + if (nativePointer > 0) { + release(nativePointer); + nativePointer = 0; + } + } + + protected native long init(String detModelPath, String recModelPath,String clsModelPath, int threadNum, String cpuMode); + + protected native float[] forward(long pointer, float[] buf, float[] ddims, Bitmap originalImage); + + protected native void release(long pointer); + + private ArrayList postprocess(float[] raw) { + ArrayList results = new ArrayList(); + int begin = 0; + + while (begin < raw.length) { + int point_num = Math.round(raw[begin]); + int word_num = Math.round(raw[begin + 1]); + OcrResultModel model = parse(raw, begin + 2, point_num, word_num); + begin += 2 + 1 + point_num * 2 + word_num; + results.add(model); + } + + return results; + } + + private OcrResultModel parse(float[] raw, int begin, int pointNum, int wordNum) { + int current = begin; + OcrResultModel model = new OcrResultModel(); + model.setConfidence(raw[current]); + current++; + for (int i = 0; i < pointNum; i++) { + model.addPoints(Math.round(raw[current + i * 2]), Math.round(raw[current + i * 2 + 1])); + } + current += (pointNum * 2); + for (int i = 0; i < wordNum; i++) { + int index = Math.round(raw[current + i]); + model.addWordIndex(index); + } + Log.i("OCRPredictorNative", "word finished " + wordNum); + return model; + } + + +} diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OcrResultModel.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OcrResultModel.java new file mode 100644 index 00000000..9494574e --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/OcrResultModel.java @@ -0,0 +1,52 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.graphics.Point; + +import java.util.ArrayList; +import java.util.List; + +public class OcrResultModel { + private List points; + private List wordIndex; + private String label; + private float confidence; + + public OcrResultModel() { + super(); + points = new ArrayList<>(); + wordIndex = new ArrayList<>(); + } + + public void addPoints(int x, int y) { + Point point = new Point(x, y); + points.add(point); + } + + public void addWordIndex(int index) { + wordIndex.add(index); + } + + public List getPoints() { + return points; + } + + public List getWordIndex() { + return wordIndex; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public float getConfidence() { + return confidence; + } + + public void setConfidence(float confidence) { + this.confidence = confidence; + } +} diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Predictor.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Predictor.java new file mode 100644 index 00000000..9cad590a --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Predictor.java @@ -0,0 +1,356 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.util.Log; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Vector; + +import static android.graphics.Color.*; + +public class Predictor { + private static final String TAG = Predictor.class.getSimpleName(); + public boolean isLoaded = false; + public int warmupIterNum = 1; + public int inferIterNum = 1; + public int cpuThreadNum = 4; + public String cpuPowerMode = "LITE_POWER_HIGH"; + public String modelPath = ""; + public String modelName = ""; + protected OCRPredictorNative paddlePredictor = null; + protected float inferenceTime = 0; + // Only for object detection + protected Vector wordLabels = new Vector(); + protected String inputColorFormat = "BGR"; + protected long[] inputShape = new long[]{1, 3, 960}; + protected float[] inputMean = new float[]{0.485f, 0.456f, 0.406f}; + protected float[] inputStd = new float[]{1.0f / 0.229f, 1.0f / 0.224f, 1.0f / 0.225f}; + protected float scoreThreshold = 0.1f; + protected Bitmap inputImage = null; + protected Bitmap outputImage = null; + protected volatile String outputResult = ""; + protected float preprocessTime = 0; + protected float postprocessTime = 0; + + + public Predictor() { + } + + public boolean init(Context appCtx, String modelPath, String labelPath) { + isLoaded = loadModel(appCtx, modelPath, cpuThreadNum, cpuPowerMode); + if (!isLoaded) { + return false; + } + isLoaded = loadLabel(appCtx, labelPath); + return isLoaded; + } + + + public boolean init(Context appCtx, String modelPath, String labelPath, int cpuThreadNum, String cpuPowerMode, + String inputColorFormat, + long[] inputShape, float[] inputMean, + float[] inputStd, float scoreThreshold) { + if (inputShape.length != 3) { + Log.e(TAG, "Size of input shape should be: 3"); + return false; + } + if (inputMean.length != inputShape[1]) { + Log.e(TAG, "Size of input mean should be: " + Long.toString(inputShape[1])); + return false; + } + if (inputStd.length != inputShape[1]) { + Log.e(TAG, "Size of input std should be: " + Long.toString(inputShape[1])); + return false; + } + if (inputShape[0] != 1) { + Log.e(TAG, "Only one batch is supported in the image classification demo, you can use any batch size in " + + "your Apps!"); + return false; + } + if (inputShape[1] != 1 && inputShape[1] != 3) { + Log.e(TAG, "Only one/three channels are supported in the image classification demo, you can use any " + + "channel size in your Apps!"); + return false; + } + if (!inputColorFormat.equalsIgnoreCase("BGR")) { + Log.e(TAG, "Only BGR color format is supported."); + return false; + } + boolean isLoaded = init(appCtx, modelPath, labelPath); + if (!isLoaded) { + return false; + } + this.inputColorFormat = inputColorFormat; + this.inputShape = inputShape; + this.inputMean = inputMean; + this.inputStd = inputStd; + this.scoreThreshold = scoreThreshold; + return true; + } + + protected boolean loadModel(Context appCtx, String modelPath, int cpuThreadNum, String cpuPowerMode) { + // Release model if exists + releaseModel(); + + // Load model + if (modelPath.isEmpty()) { + return false; + } + String realPath = modelPath; + if (!modelPath.substring(0, 1).equals("/")) { + // Read model files from custom path if the first character of mode path is '/' + // otherwise copy model to cache from assets + realPath = appCtx.getCacheDir() + "/" + modelPath; + Utils.copyDirectoryFromAssets(appCtx, modelPath, realPath); + } + if (realPath.isEmpty()) { + return false; + } + + OCRPredictorNative.Config config = new OCRPredictorNative.Config(); + config.cpuThreadNum = cpuThreadNum; + config.detModelFilename = realPath + File.separator + "ch_det_mv3_db_opt.nb"; + config.recModelFilename = realPath + File.separator + "ch_rec_mv3_crnn_opt.nb"; + config.clsModelFilename = realPath + File.separator + "ch_cls_mv3_opt.nb"; + Log.e("Predictor", "model path" + config.detModelFilename + " ; " + config.recModelFilename + ";" + config.clsModelFilename); + config.cpuPower = cpuPowerMode; + paddlePredictor = new OCRPredictorNative(config); + + this.cpuThreadNum = cpuThreadNum; + this.cpuPowerMode = cpuPowerMode; + this.modelPath = realPath; + this.modelName = realPath.substring(realPath.lastIndexOf("/") + 1); + return true; + } + + public void releaseModel() { + if (paddlePredictor != null) { + paddlePredictor.destory(); + paddlePredictor = null; + } + isLoaded = false; + cpuThreadNum = 1; + cpuPowerMode = "LITE_POWER_HIGH"; + modelPath = ""; + modelName = ""; + } + + protected boolean loadLabel(Context appCtx, String labelPath) { + wordLabels.clear(); + // Load word labels from file + try { + InputStream assetsInputStream = appCtx.getAssets().open(labelPath); + int available = assetsInputStream.available(); + byte[] lines = new byte[available]; + assetsInputStream.read(lines); + assetsInputStream.close(); + String words = new String(lines); + String[] contents = words.split("\n"); + for (String content : contents) { + wordLabels.add(content); + } + Log.i(TAG, "Word label size: " + wordLabels.size()); + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + return false; + } + return true; + } + + + public boolean runModel() { + if (inputImage == null || !isLoaded()) { + return false; + } + + // Pre-process image, and feed input tensor with pre-processed data + + Bitmap scaleImage = Utils.resizeWithStep(inputImage, Long.valueOf(inputShape[2]).intValue(), 32); + + Date start = new Date(); + int channels = (int) inputShape[1]; + int width = scaleImage.getWidth(); + int height = scaleImage.getHeight(); + float[] inputData = new float[channels * width * height]; + if (channels == 3) { + int[] channelIdx = null; + if (inputColorFormat.equalsIgnoreCase("RGB")) { + channelIdx = new int[]{0, 1, 2}; + } else if (inputColorFormat.equalsIgnoreCase("BGR")) { + channelIdx = new int[]{2, 1, 0}; + } else { + Log.i(TAG, "Unknown color format " + inputColorFormat + ", only RGB and BGR color format is " + + "supported!"); + return false; + } + int[] channelStride = new int[]{width * height, width * height * 2}; + int p = scaleImage.getPixel(scaleImage.getWidth() - 1, scaleImage.getHeight() - 1); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int color = scaleImage.getPixel(x, y); + float[] rgb = new float[]{(float) red(color) / 255.0f, (float) green(color) / 255.0f, + (float) blue(color) / 255.0f}; + inputData[y * width + x] = (rgb[channelIdx[0]] - inputMean[0]) / inputStd[0]; + inputData[y * width + x + channelStride[0]] = (rgb[channelIdx[1]] - inputMean[1]) / inputStd[1]; + inputData[y * width + x + channelStride[1]] = (rgb[channelIdx[2]] - inputMean[2]) / inputStd[2]; + + } + } + } else if (channels == 1) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int color = inputImage.getPixel(x, y); + float gray = (float) (red(color) + green(color) + blue(color)) / 3.0f / 255.0f; + inputData[y * width + x] = (gray - inputMean[0]) / inputStd[0]; + } + } + } else { + Log.i(TAG, "Unsupported channel size " + Integer.toString(channels) + ", only channel 1 and 3 is " + + "supported!"); + return false; + } + float[] pixels = inputData; + Log.i(TAG, "pixels " + pixels[0] + " " + pixels[1] + " " + pixels[2] + " " + pixels[3] + + " " + pixels[pixels.length / 2] + " " + pixels[pixels.length / 2 + 1] + " " + pixels[pixels.length - 2] + " " + pixels[pixels.length - 1]); + Date end = new Date(); + preprocessTime = (float) (end.getTime() - start.getTime()); + + // Warm up + for (int i = 0; i < warmupIterNum; i++) { + paddlePredictor.runImage(inputData, width, height, channels, inputImage); + } + warmupIterNum = 0; // do not need warm + // Run inference + start = new Date(); + ArrayList results = paddlePredictor.runImage(inputData, width, height, channels, inputImage); + end = new Date(); + inferenceTime = (end.getTime() - start.getTime()) / (float) inferIterNum; + + results = postprocess(results); + Log.i(TAG, "[stat] Preprocess Time: " + preprocessTime + + " ; Inference Time: " + inferenceTime + " ;Box Size " + results.size()); + drawResults(results); + + return true; + } + + + public boolean isLoaded() { + return paddlePredictor != null && isLoaded; + } + + public String modelPath() { + return modelPath; + } + + public String modelName() { + return modelName; + } + + public int cpuThreadNum() { + return cpuThreadNum; + } + + public String cpuPowerMode() { + return cpuPowerMode; + } + + public float inferenceTime() { + return inferenceTime; + } + + public Bitmap inputImage() { + return inputImage; + } + + public Bitmap outputImage() { + return outputImage; + } + + public String outputResult() { + return outputResult; + } + + public float preprocessTime() { + return preprocessTime; + } + + public float postprocessTime() { + return postprocessTime; + } + + + public void setInputImage(Bitmap image) { + if (image == null) { + return; + } + this.inputImage = image.copy(Bitmap.Config.ARGB_8888, true); + } + + private ArrayList postprocess(ArrayList results) { + for (OcrResultModel r : results) { + StringBuffer word = new StringBuffer(); + for (int index : r.getWordIndex()) { + if (index >= 0 && index < wordLabels.size()) { + word.append(wordLabels.get(index)); + } else { + Log.e(TAG, "Word index is not in label list:" + index); + word.append("×"); + } + } + r.setLabel(word.toString()); + } + return results; + } + + private void drawResults(ArrayList results) { + StringBuffer outputResultSb = new StringBuffer(""); + for (int i = 0; i < results.size(); i++) { + OcrResultModel result = results.get(i); + StringBuilder sb = new StringBuilder(""); + sb.append(result.getLabel()); + sb.append(" ").append(result.getConfidence()); + sb.append("; Points: "); + for (Point p : result.getPoints()) { + sb.append("(").append(p.x).append(",").append(p.y).append(") "); + } + Log.i(TAG, sb.toString()); // show LOG in Logcat panel + outputResultSb.append(i + 1).append(": ").append(result.getLabel()).append("\n"); + } + outputResult = outputResultSb.toString(); + outputImage = inputImage; + Canvas canvas = new Canvas(outputImage); + Paint paintFillAlpha = new Paint(); + paintFillAlpha.setStyle(Paint.Style.FILL); + paintFillAlpha.setColor(Color.parseColor("#3B85F5")); + paintFillAlpha.setAlpha(50); + + Paint paint = new Paint(); + paint.setColor(Color.parseColor("#3B85F5")); + paint.setStrokeWidth(5); + paint.setStyle(Paint.Style.STROKE); + + for (OcrResultModel result : results) { + Path path = new Path(); + List points = result.getPoints(); + path.moveTo(points.get(0).x, points.get(0).y); + for (int i = points.size() - 1; i >= 0; i--) { + Point p = points.get(i); + path.lineTo(p.x, p.y); + } + canvas.drawPath(path, paint); + canvas.drawPath(path, paintFillAlpha); + } + } + +} diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/SettingsActivity.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/SettingsActivity.java new file mode 100644 index 00000000..b3653ccc --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/SettingsActivity.java @@ -0,0 +1,201 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; + +import androidx.appcompat.app.ActionBar; + +import java.util.ArrayList; +import java.util.List; + + +public class SettingsActivity extends AppCompatPreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + ListPreference lpChoosePreInstalledModel = null; + CheckBoxPreference cbEnableCustomSettings = null; + EditTextPreference etModelPath = null; + EditTextPreference etLabelPath = null; + EditTextPreference etImagePath = null; + ListPreference lpCPUThreadNum = null; + ListPreference lpCPUPowerMode = null; + ListPreference lpInputColorFormat = null; + EditTextPreference etInputShape = null; + EditTextPreference etInputMean = null; + EditTextPreference etInputStd = null; + EditTextPreference etScoreThreshold = null; + + List preInstalledModelPaths = null; + List preInstalledLabelPaths = null; + List preInstalledImagePaths = null; + List preInstalledInputShapes = null; + List preInstalledCPUThreadNums = null; + List preInstalledCPUPowerModes = null; + List preInstalledInputColorFormats = null; + List preInstalledInputMeans = null; + List preInstalledInputStds = null; + List preInstalledScoreThresholds = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings); + ActionBar supportActionBar = getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + + // Initialized pre-installed models + preInstalledModelPaths = new ArrayList(); + preInstalledLabelPaths = new ArrayList(); + preInstalledImagePaths = new ArrayList(); + preInstalledInputShapes = new ArrayList(); + preInstalledCPUThreadNums = new ArrayList(); + preInstalledCPUPowerModes = new ArrayList(); + preInstalledInputColorFormats = new ArrayList(); + preInstalledInputMeans = new ArrayList(); + preInstalledInputStds = new ArrayList(); + preInstalledScoreThresholds = new ArrayList(); + // Add ssd_mobilenet_v1_pascalvoc_for_cpu + preInstalledModelPaths.add(getString(R.string.MODEL_PATH_DEFAULT)); + preInstalledLabelPaths.add(getString(R.string.LABEL_PATH_DEFAULT)); + preInstalledImagePaths.add(getString(R.string.IMAGE_PATH_DEFAULT)); + preInstalledCPUThreadNums.add(getString(R.string.CPU_THREAD_NUM_DEFAULT)); + preInstalledCPUPowerModes.add(getString(R.string.CPU_POWER_MODE_DEFAULT)); + preInstalledInputColorFormats.add(getString(R.string.INPUT_COLOR_FORMAT_DEFAULT)); + preInstalledInputShapes.add(getString(R.string.INPUT_SHAPE_DEFAULT)); + preInstalledInputMeans.add(getString(R.string.INPUT_MEAN_DEFAULT)); + preInstalledInputStds.add(getString(R.string.INPUT_STD_DEFAULT)); + preInstalledScoreThresholds.add(getString(R.string.SCORE_THRESHOLD_DEFAULT)); + + // Setup UI components + lpChoosePreInstalledModel = + (ListPreference) findPreference(getString(R.string.CHOOSE_PRE_INSTALLED_MODEL_KEY)); + String[] preInstalledModelNames = new String[preInstalledModelPaths.size()]; + for (int i = 0; i < preInstalledModelPaths.size(); i++) { + preInstalledModelNames[i] = + preInstalledModelPaths.get(i).substring(preInstalledModelPaths.get(i).lastIndexOf("/") + 1); + } + lpChoosePreInstalledModel.setEntries(preInstalledModelNames); + lpChoosePreInstalledModel.setEntryValues(preInstalledModelPaths.toArray(new String[preInstalledModelPaths.size()])); + cbEnableCustomSettings = + (CheckBoxPreference) findPreference(getString(R.string.ENABLE_CUSTOM_SETTINGS_KEY)); + etModelPath = (EditTextPreference) findPreference(getString(R.string.MODEL_PATH_KEY)); + etModelPath.setTitle("Model Path (SDCard: " + Utils.getSDCardDirectory() + ")"); + etLabelPath = (EditTextPreference) findPreference(getString(R.string.LABEL_PATH_KEY)); + etImagePath = (EditTextPreference) findPreference(getString(R.string.IMAGE_PATH_KEY)); + lpCPUThreadNum = + (ListPreference) findPreference(getString(R.string.CPU_THREAD_NUM_KEY)); + lpCPUPowerMode = + (ListPreference) findPreference(getString(R.string.CPU_POWER_MODE_KEY)); + lpInputColorFormat = + (ListPreference) findPreference(getString(R.string.INPUT_COLOR_FORMAT_KEY)); + etInputShape = (EditTextPreference) findPreference(getString(R.string.INPUT_SHAPE_KEY)); + etInputMean = (EditTextPreference) findPreference(getString(R.string.INPUT_MEAN_KEY)); + etInputStd = (EditTextPreference) findPreference(getString(R.string.INPUT_STD_KEY)); + etScoreThreshold = (EditTextPreference) findPreference(getString(R.string.SCORE_THRESHOLD_KEY)); + } + + private void reloadPreferenceAndUpdateUI() { + SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); + boolean enableCustomSettings = + sharedPreferences.getBoolean(getString(R.string.ENABLE_CUSTOM_SETTINGS_KEY), false); + String modelPath = sharedPreferences.getString(getString(R.string.CHOOSE_PRE_INSTALLED_MODEL_KEY), + getString(R.string.MODEL_PATH_DEFAULT)); + int modelIdx = lpChoosePreInstalledModel.findIndexOfValue(modelPath); + if (modelIdx >= 0 && modelIdx < preInstalledModelPaths.size()) { + if (!enableCustomSettings) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(getString(R.string.MODEL_PATH_KEY), preInstalledModelPaths.get(modelIdx)); + editor.putString(getString(R.string.LABEL_PATH_KEY), preInstalledLabelPaths.get(modelIdx)); + editor.putString(getString(R.string.IMAGE_PATH_KEY), preInstalledImagePaths.get(modelIdx)); + editor.putString(getString(R.string.CPU_THREAD_NUM_KEY), preInstalledCPUThreadNums.get(modelIdx)); + editor.putString(getString(R.string.CPU_POWER_MODE_KEY), preInstalledCPUPowerModes.get(modelIdx)); + editor.putString(getString(R.string.INPUT_COLOR_FORMAT_KEY), + preInstalledInputColorFormats.get(modelIdx)); + editor.putString(getString(R.string.INPUT_SHAPE_KEY), preInstalledInputShapes.get(modelIdx)); + editor.putString(getString(R.string.INPUT_MEAN_KEY), preInstalledInputMeans.get(modelIdx)); + editor.putString(getString(R.string.INPUT_STD_KEY), preInstalledInputStds.get(modelIdx)); + editor.putString(getString(R.string.SCORE_THRESHOLD_KEY), + preInstalledScoreThresholds.get(modelIdx)); + editor.commit(); + } + lpChoosePreInstalledModel.setSummary(modelPath); + } + cbEnableCustomSettings.setChecked(enableCustomSettings); + etModelPath.setEnabled(enableCustomSettings); + etLabelPath.setEnabled(enableCustomSettings); + etImagePath.setEnabled(enableCustomSettings); + lpCPUThreadNum.setEnabled(enableCustomSettings); + lpCPUPowerMode.setEnabled(enableCustomSettings); + lpInputColorFormat.setEnabled(enableCustomSettings); + etInputShape.setEnabled(enableCustomSettings); + etInputMean.setEnabled(enableCustomSettings); + etInputStd.setEnabled(enableCustomSettings); + etScoreThreshold.setEnabled(enableCustomSettings); + modelPath = sharedPreferences.getString(getString(R.string.MODEL_PATH_KEY), + getString(R.string.MODEL_PATH_DEFAULT)); + String labelPath = sharedPreferences.getString(getString(R.string.LABEL_PATH_KEY), + getString(R.string.LABEL_PATH_DEFAULT)); + String imagePath = sharedPreferences.getString(getString(R.string.IMAGE_PATH_KEY), + getString(R.string.IMAGE_PATH_DEFAULT)); + String cpuThreadNum = sharedPreferences.getString(getString(R.string.CPU_THREAD_NUM_KEY), + getString(R.string.CPU_THREAD_NUM_DEFAULT)); + String cpuPowerMode = sharedPreferences.getString(getString(R.string.CPU_POWER_MODE_KEY), + getString(R.string.CPU_POWER_MODE_DEFAULT)); + String inputColorFormat = sharedPreferences.getString(getString(R.string.INPUT_COLOR_FORMAT_KEY), + getString(R.string.INPUT_COLOR_FORMAT_DEFAULT)); + String inputShape = sharedPreferences.getString(getString(R.string.INPUT_SHAPE_KEY), + getString(R.string.INPUT_SHAPE_DEFAULT)); + String inputMean = sharedPreferences.getString(getString(R.string.INPUT_MEAN_KEY), + getString(R.string.INPUT_MEAN_DEFAULT)); + String inputStd = sharedPreferences.getString(getString(R.string.INPUT_STD_KEY), + getString(R.string.INPUT_STD_DEFAULT)); + String scoreThreshold = sharedPreferences.getString(getString(R.string.SCORE_THRESHOLD_KEY), + getString(R.string.SCORE_THRESHOLD_DEFAULT)); + etModelPath.setSummary(modelPath); + etModelPath.setText(modelPath); + etLabelPath.setSummary(labelPath); + etLabelPath.setText(labelPath); + etImagePath.setSummary(imagePath); + etImagePath.setText(imagePath); + lpCPUThreadNum.setValue(cpuThreadNum); + lpCPUThreadNum.setSummary(cpuThreadNum); + lpCPUPowerMode.setValue(cpuPowerMode); + lpCPUPowerMode.setSummary(cpuPowerMode); + lpInputColorFormat.setValue(inputColorFormat); + lpInputColorFormat.setSummary(inputColorFormat); + etInputShape.setSummary(inputShape); + etInputShape.setText(inputShape); + etInputMean.setSummary(inputMean); + etInputMean.setText(inputMean); + etInputStd.setSummary(inputStd); + etInputStd.setText(inputStd); + etScoreThreshold.setText(scoreThreshold); + etScoreThreshold.setSummary(scoreThreshold); + } + + @Override + protected void onResume() { + super.onResume(); + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + reloadPreferenceAndUpdateUI(); + } + + @Override + protected void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(getString(R.string.CHOOSE_PRE_INSTALLED_MODEL_KEY))) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(getString(R.string.ENABLE_CUSTOM_SETTINGS_KEY), false); + editor.commit(); + } + reloadPreferenceAndUpdateUI(); + } +} diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Utils.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Utils.java new file mode 100644 index 00000000..ef468057 --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/Utils.java @@ -0,0 +1,159 @@ +package com.baidu.paddle.lite.demo.ocr; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.media.ExifInterface; +import android.os.Environment; + +import java.io.*; + +public class Utils { + private static final String TAG = Utils.class.getSimpleName(); + + public static void copyFileFromAssets(Context appCtx, String srcPath, String dstPath) { + if (srcPath.isEmpty() || dstPath.isEmpty()) { + return; + } + InputStream is = null; + OutputStream os = null; + try { + is = new BufferedInputStream(appCtx.getAssets().open(srcPath)); + os = new BufferedOutputStream(new FileOutputStream(new File(dstPath))); + byte[] buffer = new byte[1024]; + int length = 0; + while ((length = is.read(buffer)) != -1) { + os.write(buffer, 0, length); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + os.close(); + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public static void copyDirectoryFromAssets(Context appCtx, String srcDir, String dstDir) { + if (srcDir.isEmpty() || dstDir.isEmpty()) { + return; + } + try { + if (!new File(dstDir).exists()) { + new File(dstDir).mkdirs(); + } + for (String fileName : appCtx.getAssets().list(srcDir)) { + String srcSubPath = srcDir + File.separator + fileName; + String dstSubPath = dstDir + File.separator + fileName; + if (new File(srcSubPath).isDirectory()) { + copyDirectoryFromAssets(appCtx, srcSubPath, dstSubPath); + } else { + copyFileFromAssets(appCtx, srcSubPath, dstSubPath); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static float[] parseFloatsFromString(String string, String delimiter) { + String[] pieces = string.trim().toLowerCase().split(delimiter); + float[] floats = new float[pieces.length]; + for (int i = 0; i < pieces.length; i++) { + floats[i] = Float.parseFloat(pieces[i].trim()); + } + return floats; + } + + public static long[] parseLongsFromString(String string, String delimiter) { + String[] pieces = string.trim().toLowerCase().split(delimiter); + long[] longs = new long[pieces.length]; + for (int i = 0; i < pieces.length; i++) { + longs[i] = Long.parseLong(pieces[i].trim()); + } + return longs; + } + + public static String getSDCardDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath(); + } + + public static boolean isSupportedNPU() { + return false; + // String hardware = android.os.Build.HARDWARE; + // return hardware.equalsIgnoreCase("kirin810") || hardware.equalsIgnoreCase("kirin990"); + } + + public static Bitmap resizeWithStep(Bitmap bitmap, int maxLength, int step) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int maxWH = Math.max(width, height); + float ratio = 1; + int newWidth = width; + int newHeight = height; + if (maxWH > maxLength) { + ratio = maxLength * 1.0f / maxWH; + newWidth = (int) Math.floor(ratio * width); + newHeight = (int) Math.floor(ratio * height); + } + + newWidth = newWidth - newWidth % step; + if (newWidth == 0) { + newWidth = step; + } + newHeight = newHeight - newHeight % step; + if (newHeight == 0) { + newHeight = step; + } + return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); + } + + public static Bitmap rotateBitmap(Bitmap bitmap, int orientation) { + + Matrix matrix = new Matrix(); + switch (orientation) { + case ExifInterface.ORIENTATION_NORMAL: + return bitmap; + case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: + matrix.setScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_180: + matrix.setRotate(180); + break; + case ExifInterface.ORIENTATION_FLIP_VERTICAL: + matrix.setRotate(180); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_TRANSPOSE: + matrix.setRotate(90); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_90: + matrix.setRotate(90); + break; + case ExifInterface.ORIENTATION_TRANSVERSE: + matrix.setRotate(-90); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_270: + matrix.setRotate(-90); + break; + default: + return bitmap; + } + try { + Bitmap bmRotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + bitmap.recycle(); + return bmRotated; + } + catch (OutOfMemoryError e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/deploy/android_demo/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/deploy/android_demo/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..1f6bb290 --- /dev/null +++ b/deploy/android_demo/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/deploy/android_demo/app/src/main/res/drawable/ic_launcher_background.xml b/deploy/android_demo/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..0d025f9b --- /dev/null +++ b/deploy/android_demo/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deploy/android_demo/app/src/main/res/layout/activity_main.xml b/deploy/android_demo/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..adc70e20 --- /dev/null +++ b/deploy/android_demo/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/res/layout/activity_mini.xml b/deploy/android_demo/app/src/main/res/layout/activity_mini.xml new file mode 100644 index 00000000..ec4622ae --- /dev/null +++ b/deploy/android_demo/app/src/main/res/layout/activity_mini.xml @@ -0,0 +1,46 @@ + + + + + + + + +

*c& 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 literal 0 HcmV?d00001 diff --git a/deploy/android_demo/app/src/main/assets/images/90.jpg b/deploy/android_demo/app/src/main/assets/images/90.jpg new file mode 100644 index 0000000000000000000000000000000000000000..49e949aa9cc14e3afc507c5806c87d9894c2dcb9 GIT binary patch literal 62546 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 +: +] +, +, +骑 +刈 +全 +消 +昏 +傈 +安 +久 +钟 +嗅 +不 +影 +处 +驽 +蜿 +资 +关 +椤 +地 +瘸 +专 +问 +忖 +票 +嫉 +炎 +韵 +要 +月 +田 +节 +陂 +鄙 +捌 +备 +拳 +伺 +眼 +网 +盎 +大 +傍 +心 +东 +愉 +汇 +蹿 +科 +每 +业 +里 +航 +晏 +字 +平 +录 +先 +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/android_demo/app/src/main/cpp/CMakeLists.txt b/deploy/android_demo/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..742786ad --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,117 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.4.1) + +# Creates and names a library, sets it as either STATIC or SHARED, and provides +# the relative paths to its source code. You can define multiple libraries, and +# CMake builds them for you. Gradle automatically packages shared libraries with +# your APK. + +set(PaddleLite_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../PaddleLite") +include_directories(${PaddleLite_DIR}/cxx/include) + +set(OpenCV_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../OpenCV/sdk/native/jni") +message(STATUS "opencv dir: ${OpenCV_DIR}") +find_package(OpenCV REQUIRED) +message(STATUS "OpenCV libraries: ${OpenCV_LIBS}") +include_directories(${OpenCV_INCLUDE_DIRS}) +aux_source_directory(. SOURCES) +set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -ffast-math -Ofast -Os" + ) +set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -fdata-sections -ffunction-sections" + ) +set(CMAKE_SHARED_LINKER_FLAGS + "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections -Wl,-z,nocopyreloc") + +add_library( + # Sets the name of the library. + Native + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + ${SOURCES}) + +find_library( + # Sets the name of the path variable. + log-lib + # Specifies the name of the NDK library that you want CMake to locate. + log) + +add_library( + # Sets the name of the library. + paddle_light_api_shared + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + IMPORTED) + +set_target_properties( + # Specifies the target library. + paddle_light_api_shared + # Specifies the parameter you want to define. + PROPERTIES + IMPORTED_LOCATION + ${PaddleLite_DIR}/cxx/libs/${ANDROID_ABI}/libpaddle_light_api_shared.so + # Provides the path to the library you want to import. +) + + +# Specifies libraries CMake should link to your target library. You can link +# multiple libraries, such as libraries you define in this build script, +# prebuilt third-party libraries, or system libraries. + +target_link_libraries( + # Specifies the target library. + Native + paddle_light_api_shared + ${OpenCV_LIBS} + GLESv2 + EGL + jnigraphics + ${log-lib} +) + +add_custom_command( + TARGET Native + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy + ${PaddleLite_DIR}/cxx/libs/${ANDROID_ABI}/libc++_shared.so + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libc++_shared.so) + +add_custom_command( + TARGET Native + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy + ${PaddleLite_DIR}/cxx/libs/${ANDROID_ABI}/libpaddle_light_api_shared.so + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libpaddle_light_api_shared.so) + +add_custom_command( + TARGET Native + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy + ${PaddleLite_DIR}/cxx/libs/${ANDROID_ABI}/libhiai.so + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libhiai.so) + +add_custom_command( + TARGET Native + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy + ${PaddleLite_DIR}/cxx/libs/${ANDROID_ABI}/libhiai_ir.so + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libhiai_ir.so) + +add_custom_command( + TARGET Native + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy + ${PaddleLite_DIR}/cxx/libs/${ANDROID_ABI}/libhiai_ir_build.so + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libhiai_ir_build.so) \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/common.h b/deploy/android_demo/app/src/main/cpp/common.h new file mode 100644 index 00000000..fc474078 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/common.h @@ -0,0 +1,37 @@ +// +// Created by fu on 4/25/18. +// + +#pragma once +#import +#import + +#ifdef __ANDROID__ + +#include + +#define LOG_TAG "OCR_NDK" + +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#else +#include +#define LOGI(format, ...) \ + fprintf(stdout, "[" LOG_TAG "]" format "\n", ##__VA_ARGS__) +#define LOGW(format, ...) \ + fprintf(stdout, "[" LOG_TAG "]" format "\n", ##__VA_ARGS__) +#define LOGE(format, ...) \ + fprintf(stderr, "[" LOG_TAG "]Error: " format "\n", ##__VA_ARGS__) +#endif + +enum RETURN_CODE { RETURN_OK = 0 }; + +enum NET_TYPE { NET_OCR = 900100, NET_OCR_INTERNAL = 991008 }; + +template inline T product(const std::vector &vec) { + if (vec.empty()) { + return 0; + } + return std::accumulate(vec.begin(), vec.end(), 1, std::multiplies()); +} diff --git a/deploy/android_demo/app/src/main/cpp/native.cpp b/deploy/android_demo/app/src/main/cpp/native.cpp new file mode 100644 index 00000000..963c5246 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/native.cpp @@ -0,0 +1,114 @@ +// +// Created by fujiayi on 2020/7/5. +// + +#include "native.h" +#include "ocr_ppredictor.h" +#include +#include +#include + +static paddle::lite_api::PowerMode str_to_cpu_mode(const std::string &cpu_mode); + +extern "C" JNIEXPORT jlong JNICALL +Java_com_baidu_paddle_lite_demo_ocr_OCRPredictorNative_init( + JNIEnv *env, jobject thiz, jstring j_det_model_path, + jstring j_rec_model_path, jstring j_cls_model_path, jint j_thread_num, + jstring j_cpu_mode) { + std::string det_model_path = jstring_to_cpp_string(env, j_det_model_path); + std::string rec_model_path = jstring_to_cpp_string(env, j_rec_model_path); + std::string cls_model_path = jstring_to_cpp_string(env, j_cls_model_path); + int thread_num = j_thread_num; + std::string cpu_mode = jstring_to_cpp_string(env, j_cpu_mode); + ppredictor::OCR_Config conf; + conf.thread_num = thread_num; + conf.mode = str_to_cpu_mode(cpu_mode); + ppredictor::OCR_PPredictor *orc_predictor = + new ppredictor::OCR_PPredictor{conf}; + orc_predictor->init_from_file(det_model_path, rec_model_path, cls_model_path); + return reinterpret_cast(orc_predictor); +} + +/** + * "LITE_POWER_HIGH" convert to paddle::lite_api::LITE_POWER_HIGH + * @param cpu_mode + * @return + */ +static paddle::lite_api::PowerMode +str_to_cpu_mode(const std::string &cpu_mode) { + static std::map cpu_mode_map{ + {"LITE_POWER_HIGH", paddle::lite_api::LITE_POWER_HIGH}, + {"LITE_POWER_LOW", paddle::lite_api::LITE_POWER_HIGH}, + {"LITE_POWER_FULL", paddle::lite_api::LITE_POWER_FULL}, + {"LITE_POWER_NO_BIND", paddle::lite_api::LITE_POWER_NO_BIND}, + {"LITE_POWER_RAND_HIGH", paddle::lite_api::LITE_POWER_RAND_HIGH}, + {"LITE_POWER_RAND_LOW", paddle::lite_api::LITE_POWER_RAND_LOW}}; + std::string upper_key; + std::transform(cpu_mode.cbegin(), cpu_mode.cend(), upper_key.begin(), + ::toupper); + auto index = cpu_mode_map.find(upper_key); + if (index == cpu_mode_map.end()) { + LOGE("cpu_mode not found %s", upper_key.c_str()); + return paddle::lite_api::LITE_POWER_HIGH; + } else { + return index->second; + } +} + +extern "C" JNIEXPORT jfloatArray JNICALL +Java_com_baidu_paddle_lite_demo_ocr_OCRPredictorNative_forward( + JNIEnv *env, jobject thiz, jlong java_pointer, jfloatArray buf, + jfloatArray ddims, jobject original_image) { + LOGI("begin to run native forward"); + if (java_pointer == 0) { + LOGE("JAVA pointer is NULL"); + return cpp_array_to_jfloatarray(env, nullptr, 0); + } + cv::Mat origin = bitmap_to_cv_mat(env, original_image); + if (origin.size == 0) { + LOGE("origin bitmap cannot convert to CV Mat"); + return cpp_array_to_jfloatarray(env, nullptr, 0); + } + ppredictor::OCR_PPredictor *ppredictor = + (ppredictor::OCR_PPredictor *)java_pointer; + std::vector dims_float_arr = jfloatarray_to_float_vector(env, ddims); + std::vector dims_arr; + dims_arr.resize(dims_float_arr.size()); + std::copy(dims_float_arr.cbegin(), dims_float_arr.cend(), dims_arr.begin()); + + // 这里值有点大,就不调用jfloatarray_to_float_vector了 + int64_t buf_len = (int64_t)env->GetArrayLength(buf); + jfloat *buf_data = env->GetFloatArrayElements(buf, JNI_FALSE); + float *data = (jfloat *)buf_data; + std::vector results = + ppredictor->infer_ocr(dims_arr, data, buf_len, NET_OCR, origin); + LOGI("infer_ocr finished with boxes %ld", results.size()); + // 这里将std::vector 序列化成 + // float数组,传输到java层再反序列化 + std::vector float_arr; + for (const ppredictor::OCRPredictResult &r : results) { + float_arr.push_back(r.points.size()); + float_arr.push_back(r.word_index.size()); + float_arr.push_back(r.score); + for (const std::vector &point : r.points) { + float_arr.push_back(point.at(0)); + float_arr.push_back(point.at(1)); + } + for (int index : r.word_index) { + float_arr.push_back(index); + } + } + return cpp_array_to_jfloatarray(env, float_arr.data(), float_arr.size()); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_baidu_paddle_lite_demo_ocr_OCRPredictorNative_release( + JNIEnv *env, jobject thiz, jlong java_pointer) { + if (java_pointer == 0) { + LOGE("JAVA pointer is NULL"); + return; + } + ppredictor::OCR_PPredictor *ppredictor = + (ppredictor::OCR_PPredictor *)java_pointer; + delete ppredictor; +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/native.h b/deploy/android_demo/app/src/main/cpp/native.h new file mode 100644 index 00000000..9b8e4e40 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/native.h @@ -0,0 +1,137 @@ +// +// Created by fujiayi on 2020/7/5. +// + +#pragma once + +#include "common.h" +#include +#include +#include +#include +#include + +inline std::string jstring_to_cpp_string(JNIEnv *env, jstring jstr) { + // In java, a unicode char will be encoded using 2 bytes (utf16). + // so jstring will contain characters utf16. std::string in c++ is + // essentially a string of bytes, not characters, so if we want to + // pass jstring from JNI to c++, we have convert utf16 to bytes. + if (!jstr) { + return ""; + } + const jclass stringClass = env->GetObjectClass(jstr); + const jmethodID getBytes = + env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B"); + const jbyteArray stringJbytes = (jbyteArray)env->CallObjectMethod( + jstr, getBytes, env->NewStringUTF("UTF-8")); + + size_t length = (size_t)env->GetArrayLength(stringJbytes); + jbyte *pBytes = env->GetByteArrayElements(stringJbytes, NULL); + + std::string ret = std::string(reinterpret_cast(pBytes), length); + env->ReleaseByteArrayElements(stringJbytes, pBytes, JNI_ABORT); + + env->DeleteLocalRef(stringJbytes); + env->DeleteLocalRef(stringClass); + return ret; +} + +inline jstring cpp_string_to_jstring(JNIEnv *env, std::string str) { + auto *data = str.c_str(); + jclass strClass = env->FindClass("java/lang/String"); + jmethodID strClassInitMethodID = + env->GetMethodID(strClass, "", "([BLjava/lang/String;)V"); + + jbyteArray bytes = env->NewByteArray(strlen(data)); + env->SetByteArrayRegion(bytes, 0, strlen(data), + reinterpret_cast(data)); + + jstring encoding = env->NewStringUTF("UTF-8"); + jstring res = (jstring)( + env->NewObject(strClass, strClassInitMethodID, bytes, encoding)); + + env->DeleteLocalRef(strClass); + env->DeleteLocalRef(encoding); + env->DeleteLocalRef(bytes); + + return res; +} + +inline jfloatArray cpp_array_to_jfloatarray(JNIEnv *env, const float *buf, + int64_t len) { + if (len == 0) { + return env->NewFloatArray(0); + } + jfloatArray result = env->NewFloatArray(len); + env->SetFloatArrayRegion(result, 0, len, buf); + return result; +} + +inline jintArray cpp_array_to_jintarray(JNIEnv *env, const int *buf, + int64_t len) { + jintArray result = env->NewIntArray(len); + env->SetIntArrayRegion(result, 0, len, buf); + return result; +} + +inline jbyteArray cpp_array_to_jbytearray(JNIEnv *env, const int8_t *buf, + int64_t len) { + jbyteArray result = env->NewByteArray(len); + env->SetByteArrayRegion(result, 0, len, buf); + return result; +} + +inline jlongArray int64_vector_to_jlongarray(JNIEnv *env, + const std::vector &vec) { + jlongArray result = env->NewLongArray(vec.size()); + jlong *buf = new jlong[vec.size()]; + for (size_t i = 0; i < vec.size(); ++i) { + buf[i] = (jlong)vec[i]; + } + env->SetLongArrayRegion(result, 0, vec.size(), buf); + delete[] buf; + return result; +} + +inline std::vector jlongarray_to_int64_vector(JNIEnv *env, + jlongArray data) { + int data_size = env->GetArrayLength(data); + jlong *data_ptr = env->GetLongArrayElements(data, nullptr); + std::vector data_vec(data_ptr, data_ptr + data_size); + env->ReleaseLongArrayElements(data, data_ptr, 0); + return data_vec; +} + +inline std::vector jfloatarray_to_float_vector(JNIEnv *env, + jfloatArray data) { + int data_size = env->GetArrayLength(data); + jfloat *data_ptr = env->GetFloatArrayElements(data, nullptr); + std::vector data_vec(data_ptr, data_ptr + data_size); + env->ReleaseFloatArrayElements(data, data_ptr, 0); + return data_vec; +} + +inline cv::Mat bitmap_to_cv_mat(JNIEnv *env, jobject bitmap) { + AndroidBitmapInfo info; + int result = AndroidBitmap_getInfo(env, bitmap, &info); + if (result != ANDROID_BITMAP_RESULT_SUCCESS) { + LOGE("AndroidBitmap_getInfo failed, result: %d", result); + return cv::Mat{}; + } + if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { + LOGE("Bitmap format is not RGBA_8888 !"); + return cv::Mat{}; + } + unsigned char *srcData = NULL; + AndroidBitmap_lockPixels(env, bitmap, (void **)&srcData); + cv::Mat mat = cv::Mat::zeros(info.height, info.width, CV_8UC4); + memcpy(mat.data, srcData, info.height * info.width * 4); + AndroidBitmap_unlockPixels(env, bitmap); + cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR); + /** + if (!cv::imwrite("/sdcard/1/copy.jpg", mat)){ + LOGE("Write image failed " ); + } + */ + return mat; +} diff --git a/deploy/android_demo/app/src/main/cpp/ocr_clipper.cpp b/deploy/android_demo/app/src/main/cpp/ocr_clipper.cpp new file mode 100644 index 00000000..4a531fcf --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_clipper.cpp @@ -0,0 +1,4380 @@ +/******************************************************************************* +* * +* 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/android_demo/app/src/main/cpp/ocr_clipper.hpp b/deploy/android_demo/app/src/main/cpp/ocr_clipper.hpp new file mode 100644 index 00000000..60af2bb7 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_clipper.hpp @@ -0,0 +1,544 @@ +/******************************************************************************* +* * +* 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/android_demo/app/src/main/cpp/ocr_cls_process.cpp b/deploy/android_demo/app/src/main/cpp/ocr_cls_process.cpp new file mode 100644 index 00000000..e7de9b0b --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_cls_process.cpp @@ -0,0 +1,46 @@ +// 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_cls_process.h" +#include +#include +#include +#include +#include +#include + +const std::vector CLS_IMAGE_SHAPE = {3, 48, 192}; + +cv::Mat cls_resize_img(const cv::Mat &img) { + int imgC = CLS_IMAGE_SHAPE[0]; + int imgW = CLS_IMAGE_SHAPE[2]; + int imgH = CLS_IMAGE_SHAPE[1]; + + float ratio = float(img.cols) / float(img.rows); + int resize_w = 0; + 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); + + if (resize_w < imgW) { + cv::copyMakeBorder(resize_img, resize_img, 0, 0, 0, int(imgW - resize_w), + cv::BORDER_CONSTANT, {0, 0, 0}); + } + return resize_img; +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/ocr_cls_process.h b/deploy/android_demo/app/src/main/cpp/ocr_cls_process.h new file mode 100644 index 00000000..1c30ee10 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_cls_process.h @@ -0,0 +1,23 @@ +// 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. + +#pragma once + +#include "common.h" +#include +#include + +extern const std::vector CLS_IMAGE_SHAPE; + +cv::Mat cls_resize_img(const cv::Mat &img); \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/ocr_crnn_process.cpp b/deploy/android_demo/app/src/main/cpp/ocr_crnn_process.cpp new file mode 100644 index 00000000..44c34a28 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_crnn_process.cpp @@ -0,0 +1,142 @@ +// 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 = 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 = 0; + 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 = 0; + 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/android_demo/app/src/main/cpp/ocr_crnn_process.h b/deploy/android_demo/app/src/main/cpp/ocr_crnn_process.h new file mode 100644 index 00000000..0346afe4 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_crnn_process.h @@ -0,0 +1,20 @@ +// +// Created by fujiayi on 2020/7/3. +// +#pragma once + +#include "common.h" +#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/android_demo/app/src/main/cpp/ocr_db_post_process.cpp b/deploy/android_demo/app/src/main/cpp/ocr_db_post_process.cpp new file mode 100644 index 00000000..9816ea4a --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_db_post_process.cpp @@ -0,0 +1,342 @@ +// 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_clipper.hpp" +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include +#include +#include + +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.emplace_back(std::move(a)); + } + boxes.emplace_back(std::move(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; +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/ocr_db_post_process.h b/deploy/android_demo/app/src/main/cpp/ocr_db_post_process.h new file mode 100644 index 00000000..327da36c --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_db_post_process.h @@ -0,0 +1,13 @@ +// +// Created by fujiayi on 2020/7/2. +// +#pragma once +#include +#include + +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/android_demo/app/src/main/cpp/ocr_ppredictor.cpp b/deploy/android_demo/app/src/main/cpp/ocr_ppredictor.cpp new file mode 100644 index 00000000..c68456e1 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_ppredictor.cpp @@ -0,0 +1,261 @@ +// +// Created by fujiayi on 2020/7/1. +// + +#include "ocr_ppredictor.h" +#include "common.h" +#include "ocr_cls_process.h" +#include "ocr_crnn_process.h" +#include "ocr_db_post_process.h" +#include "preprocess.h" + +namespace ppredictor { + +OCR_PPredictor::OCR_PPredictor(const OCR_Config &config) : _config(config) {} + +int OCR_PPredictor::init(const std::string &det_model_content, + const std::string &rec_model_content, + const std::string &cls_model_content) { + _det_predictor = std::unique_ptr( + new PPredictor{_config.thread_num, NET_OCR, _config.mode}); + _det_predictor->init_nb(det_model_content); + + _rec_predictor = std::unique_ptr( + new PPredictor{_config.thread_num, NET_OCR_INTERNAL, _config.mode}); + _rec_predictor->init_nb(rec_model_content); + + _cls_predictor = std::unique_ptr( + new PPredictor{_config.thread_num, NET_OCR_INTERNAL, _config.mode}); + _cls_predictor->init_nb(cls_model_content); + return RETURN_OK; +} + +int OCR_PPredictor::init_from_file(const std::string &det_model_path, + const std::string &rec_model_path, + const std::string &cls_model_path) { + _det_predictor = std::unique_ptr( + new PPredictor{_config.thread_num, NET_OCR, _config.mode}); + _det_predictor->init_from_file(det_model_path); + + _rec_predictor = std::unique_ptr( + new PPredictor{_config.thread_num, NET_OCR_INTERNAL, _config.mode}); + _rec_predictor->init_from_file(rec_model_path); + + _cls_predictor = std::unique_ptr( + new PPredictor{_config.thread_num, NET_OCR_INTERNAL, _config.mode}); + _cls_predictor->init_from_file(cls_model_path); + return RETURN_OK; +} +/** + * for debug use, show result of First Step + * @param filter_boxes + * @param boxes + * @param srcimg + */ +static void +visual_img(const std::vector>> &filter_boxes, + const std::vector>> &boxes, + const cv::Mat &srcimg) { + // visualization + 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])); + } + } + + cv::Mat img_vis; + srcimg.copyTo(img_vis); + for (int n = 0; n < boxes.size(); n++) { + const cv::Point *ppt[1] = {rook_points[n]}; + int npt[] = {4}; + cv::polylines(img_vis, ppt, npt, 1, 1, CV_RGB(0, 255, 0), 2, 8, 0); + } + // 调试用,自行替换需要修改的路径 + cv::imwrite("/sdcard/1/vis.png", img_vis); +} + +std::vector +OCR_PPredictor::infer_ocr(const std::vector &dims, + const float *input_data, int input_len, int net_flag, + cv::Mat &origin) { + PredictorInput input = _det_predictor->get_first_input(); + input.set_dims(dims); + input.set_data(input_data, input_len); + std::vector results = _det_predictor->infer(); + PredictorOutput &res = results.at(0); + std::vector>> filtered_box = calc_filtered_boxes( + res.get_float_data(), res.get_size(), (int)dims[2], (int)dims[3], origin); + LOGI("Filter_box size %ld", filtered_box.size()); + return infer_rec(filtered_box, origin); +} + +std::vector OCR_PPredictor::infer_rec( + const std::vector>> &boxes, + const cv::Mat &origin_img) { + std::vector mean = {0.5f, 0.5f, 0.5f}; + std::vector scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + std::vector dims = {1, 3, 0, 0}; + std::vector ocr_results; + + PredictorInput input = _rec_predictor->get_first_input(); + for (auto bp = boxes.crbegin(); bp != boxes.crend(); ++bp) { + const std::vector> &box = *bp; + cv::Mat crop_img = get_rotate_crop_image(origin_img, box); + crop_img = infer_cls(crop_img); + + float wh_ratio = float(crop_img.cols) / float(crop_img.rows); + cv::Mat input_image = crnn_resize_img(crop_img, wh_ratio); + input_image.convertTo(input_image, CV_32FC3, 1 / 255.0f); + const float *dimg = reinterpret_cast(input_image.data); + int input_size = input_image.rows * input_image.cols; + + dims[2] = input_image.rows; + dims[3] = input_image.cols; + input.set_dims(dims); + + neon_mean_scale(dimg, input.get_mutable_float_data(), input_size, mean, + scale); + + std::vector results = _rec_predictor->infer(); + const float *predict_batch = results.at(0).get_float_data(); + const std::vector predict_shape = results.at(0).get_shape(); + + OCRPredictResult res; + + // ctc decode + int argmax_idx; + int last_index = 0; + float score = 0.f; + int count = 0; + float max_value = 0.0f; + + for (int n = 0; n < predict_shape[1]; n++) { + argmax_idx = int(argmax(&predict_batch[n * predict_shape[2]], + &predict_batch[(n + 1) * predict_shape[2]])); + max_value = + float(*std::max_element(&predict_batch[n * predict_shape[2]], + &predict_batch[(n + 1) * predict_shape[2]])); + if (argmax_idx > 0 && (!(n > 0 && argmax_idx == last_index))) { + score += max_value; + count += 1; + res.word_index.push_back(argmax_idx); + } + last_index = argmax_idx; + } + score /= count; + if (res.word_index.empty()) { + continue; + } + res.score = score; + res.points = box; + ocr_results.emplace_back(std::move(res)); + } + LOGI("ocr_results finished %lu", ocr_results.size()); + return ocr_results; +} + +cv::Mat OCR_PPredictor::infer_cls(const cv::Mat &img, float thresh) { + std::vector mean = {0.5f, 0.5f, 0.5f}; + std::vector scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + std::vector dims = {1, 3, 0, 0}; + std::vector ocr_results; + + PredictorInput input = _cls_predictor->get_first_input(); + + cv::Mat input_image = cls_resize_img(img); + input_image.convertTo(input_image, CV_32FC3, 1 / 255.0f); + const float *dimg = reinterpret_cast(input_image.data); + int input_size = input_image.rows * input_image.cols; + + dims[2] = input_image.rows; + dims[3] = input_image.cols; + input.set_dims(dims); + + neon_mean_scale(dimg, input.get_mutable_float_data(), input_size, mean, + scale); + + std::vector results = _cls_predictor->infer(); + + const float *scores = results.at(0).get_float_data(); + float score = 0; + int label = 0; + for (int64_t i = 0; i < results.at(0).get_size(); i++) { + LOGI("output scores [%f]", scores[i]); + if (scores[i] > score) { + score = scores[i]; + label = i; + } + } + cv::Mat srcimg; + img.copyTo(srcimg); + if (label % 2 == 1 && score > thresh) { + cv::rotate(srcimg, srcimg, 1); + } + return srcimg; +} + +std::vector>> +OCR_PPredictor::calc_filtered_boxes(const float *pred, int pred_size, + int output_height, int output_width, + const cv::Mat &origin) { + const double threshold = 0.3; + const double maxvalue = 1; + + cv::Mat pred_map = cv::Mat::zeros(output_height, output_width, CV_32F); + memcpy(pred_map.data, pred, pred_size * sizeof(float)); + cv::Mat cbuf_map; + pred_map.convertTo(cbuf_map, CV_8UC1); + + cv::Mat bit_map; + cv::threshold(cbuf_map, bit_map, threshold, maxvalue, cv::THRESH_BINARY); + + std::vector>> boxes = + boxes_from_bitmap(pred_map, bit_map); + float ratio_h = output_height * 1.0f / origin.rows; + float ratio_w = output_width * 1.0f / origin.cols; + std::vector>> filter_boxes = + filter_tag_det_res(boxes, ratio_h, ratio_w, origin); + return filter_boxes; +} + +std::vector +OCR_PPredictor::postprocess_rec_word_index(const PredictorOutput &res) { + const int *rec_idx = res.get_int_data(); + const std::vector> rec_idx_lod = res.get_lod(); + + std::vector pred_idx; + for (int n = int(rec_idx_lod[0][0]); n < int(rec_idx_lod[0][1] * 2); n += 2) { + pred_idx.emplace_back(rec_idx[n]); + } + return pred_idx; +} + +float OCR_PPredictor::postprocess_rec_score(const PredictorOutput &res) { + const float *predict_batch = res.get_float_data(); + const std::vector predict_shape = res.get_shape(); + const std::vector> predict_lod = res.get_lod(); + int blank = predict_shape[1]; + float score = 0.f; + int count = 0; + for (int n = predict_lod[0][0]; n < predict_lod[0][1] - 1; n++) { + int argmax_idx = argmax(predict_batch + n * predict_shape[1], + predict_batch + (n + 1) * predict_shape[1]); + float max_value = predict_batch[n * predict_shape[1] + argmax_idx]; + if (blank - 1 - argmax_idx > 1e-5) { + score += max_value; + count += 1; + } + } + if (count == 0) { + LOGE("calc score count 0"); + } else { + score /= count; + } + LOGI("calc score: %f", score); + return score; +} + +NET_TYPE OCR_PPredictor::get_net_flag() const { return NET_OCR; } +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/ocr_ppredictor.h b/deploy/android_demo/app/src/main/cpp/ocr_ppredictor.h new file mode 100644 index 00000000..588f25cb --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ocr_ppredictor.h @@ -0,0 +1,122 @@ +// +// Created by fujiayi on 2020/7/1. +// + +#pragma once + +#include "ppredictor.h" +#include +#include +#include + +namespace ppredictor { + +/** + * Config + */ +struct OCR_Config { + int thread_num = 4; // Thread num + paddle::lite_api::PowerMode mode = + paddle::lite_api::LITE_POWER_HIGH; // PaddleLite Mode +}; + +/** + * PolyGone Result + */ +struct OCRPredictResult { + std::vector word_index; + std::vector> points; + float score; +}; + +/** + * OCR there are 2 models + * 1. First model(det),select polygones to show where are the texts + * 2. crop from the origin images, use these polygones to infer + */ +class OCR_PPredictor : public PPredictor_Interface { +public: + OCR_PPredictor(const OCR_Config &config); + + virtual ~OCR_PPredictor() {} + + /** + * 初始化二个模型的Predictor + * @param det_model_content + * @param rec_model_content + * @return + */ + int init(const std::string &det_model_content, + const std::string &rec_model_content, + const std::string &cls_model_content); + int init_from_file(const std::string &det_model_path, + const std::string &rec_model_path, + const std::string &cls_model_path); + /** + * Return OCR result + * @param dims + * @param input_data + * @param input_len + * @param net_flag + * @param origin + * @return + */ + virtual std::vector + infer_ocr(const std::vector &dims, const float *input_data, + int input_len, int net_flag, cv::Mat &origin); + + virtual NET_TYPE get_net_flag() const; + +private: + /** + * calcul Polygone from the result image of first model + * @param pred + * @param output_height + * @param output_width + * @param origin + * @return + */ + std::vector>> + calc_filtered_boxes(const float *pred, int pred_size, int output_height, + int output_width, const cv::Mat &origin); + + /** + * infer for second model + * + * @param boxes + * @param origin + * @return + */ + std::vector + infer_rec(const std::vector>> &boxes, + const cv::Mat &origin); + + /** + * infer for cls model + * + * @param boxes + * @param origin + * @return + */ + cv::Mat infer_cls(const cv::Mat &origin, float thresh = 0.9); + + /** + * Postprocess or sencod model to extract text + * @param res + * @return + */ + std::vector postprocess_rec_word_index(const PredictorOutput &res); + + /** + * calculate confidence of second model text result + * @param res + * @return + */ + float postprocess_rec_score(const PredictorOutput &res); + + std::unique_ptr _det_predictor; + std::unique_ptr _rec_predictor; + std::unique_ptr _cls_predictor; + OCR_Config _config; +}; +} diff --git a/deploy/android_demo/app/src/main/cpp/ppredictor.cpp b/deploy/android_demo/app/src/main/cpp/ppredictor.cpp new file mode 100644 index 00000000..dcbc7691 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ppredictor.cpp @@ -0,0 +1,65 @@ +#include "ppredictor.h" +#include "common.h" + +namespace ppredictor { +PPredictor::PPredictor(int thread_num, int net_flag, + paddle::lite_api::PowerMode mode) + : _thread_num(thread_num), _net_flag(net_flag), _mode(mode) {} + +int PPredictor::init_nb(const std::string &model_content) { + paddle::lite_api::MobileConfig config; + config.set_model_from_buffer(model_content); + return _init(config); +} + +int PPredictor::init_from_file(const std::string &model_content) { + paddle::lite_api::MobileConfig config; + config.set_model_from_file(model_content); + return _init(config); +} + +template int PPredictor::_init(ConfigT &config) { + config.set_threads(_thread_num); + config.set_power_mode(_mode); + _predictor = paddle::lite_api::CreatePaddlePredictor(config); + LOGI("paddle instance created"); + return RETURN_OK; +} + +PredictorInput PPredictor::get_input(int index) { + PredictorInput input{_predictor->GetInput(index), index, _net_flag}; + _is_input_get = true; + return input; +} + +std::vector PPredictor::get_inputs(int num) { + std::vector results; + for (int i = 0; i < num; i++) { + results.emplace_back(get_input(i)); + } + return results; +} + +PredictorInput PPredictor::get_first_input() { return get_input(0); } + +std::vector PPredictor::infer() { + LOGI("infer Run start %d", _net_flag); + std::vector results; + if (!_is_input_get) { + return results; + } + _predictor->Run(); + LOGI("infer Run end"); + + for (int i = 0; i < _predictor->GetOutputNames().size(); i++) { + std::unique_ptr output_tensor = + _predictor->GetOutput(i); + LOGI("output tensor[%d] size %ld", i, product(output_tensor->shape())); + PredictorOutput result{std::move(output_tensor), i, _net_flag}; + results.emplace_back(std::move(result)); + } + return results; +} + +NET_TYPE PPredictor::get_net_flag() const { return (NET_TYPE)_net_flag; } +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/ppredictor.h b/deploy/android_demo/app/src/main/cpp/ppredictor.h new file mode 100644 index 00000000..836861aa --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/ppredictor.h @@ -0,0 +1,63 @@ +#pragma once + +#include "paddle_api.h" +#include "predictor_input.h" +#include "predictor_output.h" + +namespace ppredictor { + +/** + * PaddleLite Preditor Common Interface + */ +class PPredictor_Interface { +public: + virtual ~PPredictor_Interface() {} + + virtual NET_TYPE get_net_flag() const = 0; +}; + +/** + * Common Predictor + */ +class PPredictor : public PPredictor_Interface { +public: + PPredictor( + int thread_num, int net_flag = 0, + paddle::lite_api::PowerMode mode = paddle::lite_api::LITE_POWER_HIGH); + + virtual ~PPredictor() {} + + /** + * init paddlitelite opt model,nb format ,or use ini_paddle + * @param model_content + * @return 0 + */ + virtual int init_nb(const std::string &model_content); + + virtual int init_from_file(const std::string &model_content); + + std::vector infer(); + + std::shared_ptr get_predictor() { + return _predictor; + } + + virtual std::vector get_inputs(int num); + + virtual PredictorInput get_input(int index); + + virtual PredictorInput get_first_input(); + + virtual NET_TYPE get_net_flag() const; + +protected: + template int _init(ConfigT &config); + +private: + int _thread_num; + paddle::lite_api::PowerMode _mode; + std::shared_ptr _predictor; + bool _is_input_get = false; + int _net_flag; +}; +} diff --git a/deploy/android_demo/app/src/main/cpp/predictor_input.cpp b/deploy/android_demo/app/src/main/cpp/predictor_input.cpp new file mode 100644 index 00000000..f0b4bf8a --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/predictor_input.cpp @@ -0,0 +1,28 @@ +#include "predictor_input.h" + +namespace ppredictor { + +void PredictorInput::set_dims(std::vector dims) { + // yolov3 + if (_net_flag == 101 && _index == 1) { + _tensor->Resize({1, 2}); + _tensor->mutable_data()[0] = (int)dims.at(2); + _tensor->mutable_data()[1] = (int)dims.at(3); + } else { + _tensor->Resize(dims); + } + _is_dims_set = true; +} + +float *PredictorInput::get_mutable_float_data() { + if (!_is_dims_set) { + LOGE("PredictorInput::set_dims is not called"); + } + return _tensor->mutable_data(); +} + +void PredictorInput::set_data(const float *input_data, int input_float_len) { + float *input_raw_data = get_mutable_float_data(); + memcpy(input_raw_data, input_data, input_float_len * sizeof(float)); +} +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/predictor_input.h b/deploy/android_demo/app/src/main/cpp/predictor_input.h new file mode 100644 index 00000000..f3fd6cfe --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/predictor_input.h @@ -0,0 +1,26 @@ +#pragma once + +#include "common.h" +#include +#include + +namespace ppredictor { +class PredictorInput { +public: + PredictorInput(std::unique_ptr &&tensor, int index, + int net_flag) + : _tensor(std::move(tensor)), _index(index), _net_flag(net_flag) {} + + void set_dims(std::vector dims); + + float *get_mutable_float_data(); + + void set_data(const float *input_data, int input_float_len); + +private: + std::unique_ptr _tensor; + bool _is_dims_set = false; + int _index; + int _net_flag; +}; +} diff --git a/deploy/android_demo/app/src/main/cpp/predictor_output.cpp b/deploy/android_demo/app/src/main/cpp/predictor_output.cpp new file mode 100644 index 00000000..e9cfdbc3 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/predictor_output.cpp @@ -0,0 +1,26 @@ +#include "predictor_output.h" +namespace ppredictor { +const float *PredictorOutput::get_float_data() const { + return _tensor->data(); +} + +const int *PredictorOutput::get_int_data() const { + return _tensor->data(); +} + +const std::vector> PredictorOutput::get_lod() const { + return _tensor->lod(); +} + +int64_t PredictorOutput::get_size() const { + if (_net_flag == NET_OCR) { + return _tensor->shape().at(2) * _tensor->shape().at(3); + } else { + return product(_tensor->shape()); + } +} + +const std::vector PredictorOutput::get_shape() const { + return _tensor->shape(); +} +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/predictor_output.h b/deploy/android_demo/app/src/main/cpp/predictor_output.h new file mode 100644 index 00000000..8e8c9ba0 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/predictor_output.h @@ -0,0 +1,31 @@ +#pragma once + +#include "common.h" +#include +#include + +namespace ppredictor { +class PredictorOutput { +public: + PredictorOutput() {} + PredictorOutput(std::unique_ptr &&tensor, + int index, int net_flag) + : _tensor(std::move(tensor)), _index(index), _net_flag(net_flag) {} + + const float *get_float_data() const; + const int *get_int_data() const; + int64_t get_size() const; + const std::vector> get_lod() const; + const std::vector get_shape() const; + + std::vector data; // return float, or use data_int + std::vector data_int; // several layers return int ,or use data + std::vector shape; // PaddleLite output shape + std::vector> lod; // PaddleLite output lod + +private: + std::unique_ptr _tensor; + int _index; + int _net_flag; +}; +} diff --git a/deploy/android_demo/app/src/main/cpp/preprocess.cpp b/deploy/android_demo/app/src/main/cpp/preprocess.cpp new file mode 100644 index 00000000..e99b2cd1 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/preprocess.cpp @@ -0,0 +1,82 @@ +#include "preprocess.h" +#include + +cv::Mat bitmap_to_cv_mat(JNIEnv *env, jobject bitmap) { + AndroidBitmapInfo info; + int result = AndroidBitmap_getInfo(env, bitmap, &info); + if (result != ANDROID_BITMAP_RESULT_SUCCESS) { + LOGE("AndroidBitmap_getInfo failed, result: %d", result); + return cv::Mat{}; + } + if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { + LOGE("Bitmap format is not RGBA_8888 !"); + return cv::Mat{}; + } + unsigned char *srcData = NULL; + AndroidBitmap_lockPixels(env, bitmap, (void **)&srcData); + cv::Mat mat = cv::Mat::zeros(info.height, info.width, CV_8UC4); + memcpy(mat.data, srcData, info.height * info.width * 4); + AndroidBitmap_unlockPixels(env, bitmap); + cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR); + /** + if (!cv::imwrite("/sdcard/1/copy.jpg", mat)){ + LOGE("Write image failed " ); + } + */ + + return mat; +} + +cv::Mat resize_img(const cv::Mat &img, int height, int width) { + if (img.rows == height && img.cols == width) { + return img; + } + cv::Mat new_img; + cv::resize(img, new_img, cv::Size(height, width)); + return new_img; +} + +// fill tensor with mean and scale and trans layout: nhwc -> nchw, neon speed up +void neon_mean_scale(const float *din, float *dout, int size, + const std::vector &mean, + const std::vector &scale) { + if (mean.size() != 3 || scale.size() != 3) { + LOGE("[ERROR] mean or scale size must equal to 3"); + return; + } + + 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(scale[0]); + float32x4_t vscale1 = vdupq_n_f32(scale[1]); + float32x4_t vscale2 = vdupq_n_f32(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]; + } +} \ No newline at end of file diff --git a/deploy/android_demo/app/src/main/cpp/preprocess.h b/deploy/android_demo/app/src/main/cpp/preprocess.h new file mode 100644 index 00000000..79091527 --- /dev/null +++ b/deploy/android_demo/app/src/main/cpp/preprocess.h @@ -0,0 +1,12 @@ +#pragma once + +#include "common.h" +#include +#include +cv::Mat bitmap_to_cv_mat(JNIEnv *env, jobject bitmap); + +cv::Mat resize_img(const cv::Mat &img, int height, int width); + +void neon_mean_scale(const float *din, float *dout, int size, + const std::vector &mean, + const std::vector &scale); diff --git a/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/AppCompatPreferenceActivity.java b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/AppCompatPreferenceActivity.java new file mode 100644 index 00000000..49af0afe --- /dev/null +++ b/deploy/android_demo/app/src/main/java/com/baidu/paddle/lite/demo/ocr/AppCompatPreferenceActivity.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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. + */ + +package com.baidu.paddle.lite.demo.ocr; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; + +/** + * A {@link PreferenceActivity} which implements and proxies the necessary calls + * to be used with AppCompat. + *