├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── bannerplain.png ├── harbour-textractor.desktop ├── harbour-textractor.png ├── harbour-textractor.pro ├── qml ├── cover │ └── CoverPage.qml ├── harbour-textractor.qml ├── images │ ├── circular218.png │ └── crosshair.png └── pages │ ├── About.qml │ ├── CameraPage.qml │ ├── CornerPoint.qml │ ├── CroppingPage.qml │ ├── DownloadDialog.qml │ ├── DownloadPage.qml │ ├── EditPage.qml │ ├── FilePickerDialog.qml │ ├── HintsPage.qml │ ├── LanguageDialog.qml │ ├── MainPage.qml │ ├── PageSelectPage.qml │ ├── ResultsPage.qml │ └── Settings.qml ├── rpm ├── harbour-textractor.changes.in └── harbour-textractor.spec ├── src ├── PDFThumbnailProvider.cpp ├── PDFThumbnailProvider.h ├── cameramodecontrol.cpp ├── cameramodecontrol.h ├── downloadmanager.cpp ├── downloadmanager.h ├── harbour-textractor.cpp ├── imageprocessor.cpp ├── imageprocessor.h ├── pdfhandler.cpp ├── pdfhandler.h ├── settings.cpp ├── settings.h ├── tesseractapi.cpp └── tesseractapi.h └── translations └── harbour-textractor.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | # Compiled Dynamic libraries 6 | *.so 7 | *.dylib 8 | # Compiled Static libraries 9 | *.lai 10 | *.la 11 | *.a 12 | # Qt 13 | *.pro.user 14 | *.pro.user.* 15 | moc_*.cpp 16 | qrc_*.cpp 17 | Makefile 18 | *-build-* 19 | *.autosave -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/folderlistmodel"] 2 | path = lib/folderlistmodel 3 | url = https://github.com/skvark/qt-folderlistmodel.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Olli-Pekka Heinisuo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Textractor 2 | ========== 3 | 4 | Textractor is an OCR application for Sailfish OS. Main features: 5 | 6 | OCR can be run on: 7 | - an image taken with the app 8 | - an image selected from the device 9 | - a PDF file (one or multiple pages) 10 | 11 | Cropping is supported in any reasonable quadrilateral arrangement and perspective correction is applied for the selection. User has access to advanced image preprocessing settings. 12 | 13 | Found text can be edited or copied to clipboard. As SFOS is a true multitasking OS, the whole OCR process can be run on background while user can use the device for other purposes at the same time. 14 | 15 | Documentation and Help 16 | ---------------------- 17 | 18 | [Textractor Documentation](http://skvark.github.io/Textractor/) 19 | 20 | Environment and building 21 | ------------------------ 22 | 23 | To be able to build this, follow this Gist to setup the environment correctly: https://gist.github.com/skvark/49a2f1904192b6db311a 24 | 25 | In short: 26 | 27 | Add my repositories containing Tesseract OCR and Leptonica to the build machine targets. 28 | 29 | Preprocessing 30 | ------------- 31 | 32 | Tesseract OCR is just plain engine so Leptonica is used for preprocessing the image. 33 | 34 | Currently following steps will be done before the image is passed to the engine for recognition: 35 | 36 | 1. Image is first opened using QImage, dpi is set to 300, image is rotated according to device angle and the image is saved in jpg format. 37 | 2. Load the jpg image with Leptonica and convert the 32 bpp image to gray 8 bpp image 38 | 3. Unsharp mask 39 | 4. Local background normalization with Otsu's algorithm 40 | 5. Skew angle detection and rotation (Leptonica decides if the image needs to be rotated) 41 | 42 | After those steps the image is passed to the Tesseract. 43 | 44 | Test image and result 45 | --------------------- 46 | 47 | Original: 48 | 49 | ![preview0](http://relativity.fi/textextractor/original.jpg) 50 | 51 | Preprocessed 52 | 53 | ![preview01](http://relativity.fi/textextractor/preprocessed.jpg) 54 | 55 | Extracted text: 56 | 57 | ```` 58 | This is a lot of 12 point text to test the 59 | ocr code and see if it works on all types 60 | of file format. 61 | 62 | The quick brown dog jumped over the 63 | lazy fox. The quick brown dog jumped 64 | over the lazy fox. The quick brown dog 65 | jumped over the lazy fox. The quick 66 | brown dog jumped over the lazy fox. 67 | 68 | 69 | 70 | 71 | 72 | 73 | D R I N K COFFEE 74 | L Do Stupid Faster 75 | With More Energy 76 | ```` 77 | -------------------------------------------------------------------------------- /bannerplain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skvark/Textractor/1275b4fd75f886efbce3e3b4ca5f01adf9fe7d22/bannerplain.png -------------------------------------------------------------------------------- /harbour-textractor.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | X-Nemo-Application-Type=silica-qt5 4 | Icon=harbour-textractor 5 | Exec=harbour-textractor 6 | Name=Textractor 7 | -------------------------------------------------------------------------------- /harbour-textractor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skvark/Textractor/1275b4fd75f886efbce3e3b4ca5f01adf9fe7d22/harbour-textractor.png -------------------------------------------------------------------------------- /harbour-textractor.pro: -------------------------------------------------------------------------------- 1 | # NOTICE: 2 | # 3 | # Application name defined in TARGET has a corresponding QML filename. 4 | # If name defined in TARGET is changed, the following needs to be done 5 | # to match new name: 6 | # - corresponding QML filename must be changed 7 | # - desktop icon filename must be changed 8 | # - desktop filename must be changed 9 | # - icon definition filename in desktop file must be changed 10 | # - translation filenames have to be changed 11 | 12 | # The name of your application 13 | TARGET = harbour-textractor 14 | 15 | CONFIG += sailfishapp 16 | CONFIG += c++11 17 | QT += multimedia network core-private qml-private quick 18 | 19 | INCLUDEPATH += src/ 20 | INCLUDEPATH += lib/ 21 | 22 | LIBS += -ltesseract -llept -lexif -lpoppler-qt5 23 | 24 | QMAKE_RPATHDIR += /usr/share/harbour-textractor/lib/ 25 | 26 | SOURCES += lib/folderlistmodel/qquickfolderlistmodel.cpp \ 27 | lib/folderlistmodel/fileinfothread.cpp \ 28 | src/pdfhandler.cpp \ 29 | src/PDFThumbnailProvider.cpp 30 | HEADERS += lib/folderlistmodel/qquickfolderlistmodel.h \ 31 | lib/folderlistmodel/fileproperty_p.h \ 32 | lib/folderlistmodel/fileinfothread_p.h \ 33 | src/pdfhandler.h \ 34 | src/PDFThumbnailProvider.h 35 | 36 | DEFINES += APP_VERSION=\\\"$$VERSION\\\" 37 | 38 | SOURCES += \ 39 | src/tesseractapi.cpp \ 40 | src/imageprocessor.cpp \ 41 | src/harbour-textractor.cpp \ 42 | src/settings.cpp \ 43 | src/cameramodecontrol.cpp \ 44 | src/downloadmanager.cpp 45 | 46 | OTHER_FILES += qml/harbour-textractor.qml \ 47 | qml/cover/CoverPage.qml \ 48 | translations/*.ts \ 49 | rpm/harbour-textractor.spec \ 50 | rpm/harbour-textractor.changes.in \ 51 | harbour-textractor.desktop \ 52 | README.md \ 53 | qml/pages/EditPage.qml \ 54 | qml/pages/HintsPage.qml \ 55 | qml/pages/Settings.qml \ 56 | qml/pages/LanguageDialog.qml \ 57 | qml/pages/DownloadDialog.qml \ 58 | qml/pages/CameraPage.qml \ 59 | qml/pages/ResultsPage.qml \ 60 | qml/pages/MainPage.qml \ 61 | qml/pages/DownloadPage.qml \ 62 | qml/pages/About.qml \ 63 | qml/pages/CroppingPage.qml \ 64 | qml/pages/CornerPoint.qml \ 65 | qml/pages/FilePickerDialog.qml \ 66 | qml/pages/PageSelectPage.qml 67 | 68 | # to disable building translations every time, comment out the 69 | # following CONFIG line 70 | # CONFIG += sailfishapp_i18n 71 | 72 | HEADERS += \ 73 | src/tesseractapi.h \ 74 | src/imageprocessor.h \ 75 | src/settings.h \ 76 | src/cameramodecontrol.h \ 77 | src/downloadmanager.h 78 | 79 | -------------------------------------------------------------------------------- /qml/cover/CoverPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | CoverBackground { 5 | 6 | Label { 7 | id: label 8 | text: "Textractor" 9 | width: parent.width 10 | anchors.top: parent.top 11 | anchors.leftMargin: Theme.paddingSmall 12 | anchors.rightMargin: Theme.paddingSmall 13 | anchors.topMargin: Theme.paddingLarge 14 | horizontalAlignment: Text.AlignHCenter 15 | } 16 | 17 | Label { 18 | id: label2 19 | text: "" 20 | anchors.top: label.bottom 21 | width: parent.width 22 | anchors.leftMargin: Theme.paddingSmall 23 | anchors.rightMargin: Theme.paddingSmall 24 | anchors.topMargin: Theme.paddingLarge 25 | horizontalAlignment: Text.AlignHCenter 26 | wrapMode: Text.Wrap 27 | } 28 | 29 | Label { 30 | id: label3 31 | anchors.top: label2.bottom 32 | width: parent.width 33 | anchors.leftMargin: Theme.paddingSmall 34 | anchors.rightMargin: Theme.paddingSmall 35 | anchors.topMargin: Theme.paddingLarge 36 | anchors.horizontalCenter: parent.horizontalCenter 37 | text: "" 38 | wrapMode: Text.Wrap 39 | font.pixelSize: Theme.fontSizeExtraLarge 40 | horizontalAlignment: Text.AlignHCenter 41 | } 42 | 43 | Connections { 44 | target: tesseractAPI 45 | 46 | onAnalyzed: { 47 | label.text = "Textractor" 48 | label2.text = "Idle" 49 | label3.text = "" 50 | } 51 | 52 | onStateChanged: { 53 | label2.text = state; 54 | } 55 | 56 | onPercentageChanged: { 57 | label3.text = percentage.toString() + " %"; 58 | } 59 | } 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /qml/harbour-textractor.qml: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2013 Jolla Ltd. 3 | Contact: Thomas Perl 4 | All rights reserved. 5 | 6 | You may use this file under the terms of BSD license as follows: 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of the Jolla Ltd nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | import QtQuick 2.0 32 | import Sailfish.Silica 1.0 33 | import "pages" 34 | 35 | ApplicationWindow 36 | { 37 | initialPage: Component { MainPage { } } 38 | cover: Qt.resolvedUrl("cover/CoverPage.qml") 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /qml/images/circular218.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skvark/Textractor/1275b4fd75f886efbce3e3b4ca5f01adf9fe7d22/qml/images/circular218.png -------------------------------------------------------------------------------- /qml/images/crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skvark/Textractor/1275b4fd75f886efbce3e3b4ca5f01adf9fe7d22/qml/images/crosshair.png -------------------------------------------------------------------------------- /qml/pages/About.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | id: page 6 | 7 | SilicaFlickable { 8 | anchors.fill: parent 9 | contentHeight: column.height 10 | 11 | Column { 12 | id: column 13 | anchors.top: parent.top 14 | anchors.left: parent.left 15 | anchors.right: parent.right 16 | anchors.leftMargin: Theme.paddingLarge 17 | anchors.rightMargin: Theme.paddingLarge 18 | anchors.topMargin: Theme.paddingLarge * 2 19 | height: childrenRect.height 20 | 21 | Label { 22 | width: parent.width 23 | wrapMode: Text.Wrap 24 | font.pixelSize: Theme.fontSizeSmall 25 | color: Theme.primaryColor 26 | verticalAlignment: Text.AlignVCenter 27 | horizontalAlignment: Text.AlignHCenter 28 | textFormat: Text.RichText; 29 | onLinkActivated: Qt.openUrlExternally(link) 30 | text: "

Textractor

v"+APP_VERSION+"

" + 31 | 32 | "" + 33 | "" + 34 | 35 | "Created by
Olli-Pekka Heinisuo

" + 36 | "Icon and cover image by
Janne Peltonen

" + 37 | 38 | "Textractor is an OCR (optical character recognition) application.

" + 39 | "Textractor uses Tesseract OCR engine to perform actual OCR and " + 40 | "Leptonica image processing library for general image manipulation.


" + 41 | 42 | "Tesseract OCR version
" + tesseractAPI.tesseractVersion() + "
" + 43 | 44 | "Leptonica version
" + tesseractAPI.leptonicaVersion() + "

" + 45 | 46 | "This software is released under MIT license.
" + 47 | "You can get the code and contribute at:
\n" + 48 | "
" + 49 | "GitHub \\ Textractor"; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /qml/pages/CameraPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import Sailfish.Silica 1.0 3 | import QtMultimedia 5.0 4 | import QtSensors 5.0 5 | import harbour.textractor.cameramodecontrol 1.0 6 | 7 | 8 | Page { 9 | id: root 10 | 11 | property bool cropReady: false; 12 | allowedOrientations: Orientation.All 13 | 14 | Component.onCompleted: { 15 | root._clickablePageIndicators = false 16 | internal.complete = true 17 | } 18 | 19 | Component.onDestruction: { 20 | if(Camera.UnloadedState != camera.cameraState) { 21 | camera.cameraState = Camera.UnloadedState 22 | } 23 | } 24 | 25 | onStatusChanged: { 26 | if (status === PageStatus.Activating && internal.complete && !cropReady) { 27 | camera.cameraState = Camera.ActiveState; 28 | } 29 | if (status === PageStatus.Active && cropReady) { 30 | cropReady = false; 31 | backNavigation = true; 32 | pageStack.push(Qt.resolvedUrl("ResultsPage.qml"), { loading: true }); 33 | } 34 | } 35 | 36 | QtObject { 37 | id: internal 38 | 39 | property bool complete: false 40 | property bool unload: false 41 | 42 | function reload() { 43 | if(complete) { 44 | unload = true; 45 | } 46 | } 47 | } 48 | 49 | Item { 50 | id: viewfinderClip 51 | anchors.fill: parent 52 | clip: true 53 | 54 | VideoOutput { 55 | 56 | id: viewfinder 57 | property bool mirror: cameraModeControl.device == "secondary" 58 | anchors.centerIn: parent 59 | 60 | height: root.isPortrait ? root.height : root.width 61 | width: root.isPortrait ? root.width : root.height 62 | 63 | visible: !imagePreview.visible 64 | 65 | rotation: -root.rotation 66 | scale: contentRect.width / contentRect.height < width / height 67 | ? width / contentRect.width : height / contentRect.height 68 | 69 | source: camera 70 | 71 | } 72 | 73 | BusyIndicator { 74 | id: busyind 75 | anchors.centerIn: parent 76 | size: BusyIndicatorSize.Large 77 | running: cropReady; 78 | } 79 | 80 | } 81 | 82 | Image { 83 | id: imagePreview 84 | 85 | anchors.fill: parent 86 | 87 | visible: Image.Ready == status 88 | 89 | fillMode: Image.PreserveAspectFit 90 | smooth: true 91 | } 92 | 93 | CameraModeControl { 94 | id: cameraModeControl 95 | camera: camera 96 | onDeviceChanged: { 97 | internal.reload() 98 | } 99 | } 100 | 101 | Timer { 102 | id: reloadTimer 103 | interval: 10 104 | running: internal.unload && Camera.UnloadedStatus == camera.cameraStatus 105 | 106 | onTriggered: { 107 | internal.unload = false 108 | } 109 | } 110 | 111 | onPageContainerChanged: { 112 | 113 | } 114 | 115 | Camera { 116 | id: camera 117 | 118 | captureMode: Camera.CaptureStillImage 119 | cameraState: internal.complete && !internal.unload ? Camera.ActiveState : Camera.UnloadedState 120 | 121 | exposure.exposureMode: Camera.ExposureAuto 122 | 123 | focus.focusMode: Camera.FocusContinuous 124 | focus.focusPointMode: Camera.FocusPointAuto 125 | 126 | flash.mode: Camera.FlashOff 127 | 128 | imageCapture { 129 | resolution: "primary" == cameraModeControl.device 130 | ? cameraModeControl.primaryResolution : cameraModeControl.secondaryResolution 131 | 132 | onImageCaptured: { 133 | imagePreview.source = preview 134 | } 135 | 136 | onImageSaved: { 137 | camera.cameraState = Camera.UnloadedState 138 | 139 | if(orientationModes[orientationMode] === "auto") { 140 | tesseractAPI.prepareForCropping(path, picRotation, false); 141 | } else { 142 | if(orientationModes[orientationMode] === "landscape") { 143 | // landscape is the default orientation 144 | tesseractAPI.prepareForCropping(path, 0, false); 145 | } else if (orientationModes[orientationMode] === "portrait") { 146 | // top edge of the device is pointing up, rotate the image 90 degrees clockwise 147 | tesseractAPI.prepareForCropping(path, 90, false); 148 | } 149 | } 150 | var dialog = pageStack.push(Qt.resolvedUrl("CroppingPage.qml"), { loading: true }); 151 | dialog.accepted.connect(function() { 152 | root.backNavigation = false; 153 | cropReady = true; 154 | }); 155 | } 156 | } 157 | 158 | onCameraStatusChanged: { 159 | if(Camera.LoadedStatus == cameraStatus) { 160 | camera.exposure.setAutoAperture() 161 | camera.exposure.setAutoIsoSensitivity() 162 | camera.exposure.setAutoShutterSpeed() 163 | } 164 | } 165 | } 166 | 167 | OrientationSensor { 168 | id: sensor 169 | active: true 170 | property int rotationAngle: _getOrientation(reading.orientation) 171 | function _getOrientation(value) { 172 | switch (value) { 173 | case 1: 174 | return 90 175 | case 2: 176 | return -90 177 | case 3: 178 | return 180 179 | case 4: 180 | return 0 181 | default: 182 | return 0 183 | } 184 | } 185 | } 186 | 187 | property int picRotation; 188 | property int orientationMode: 0; 189 | property var orientationModes: ["auto", "landscape", "portrait"]; 190 | 191 | IconButton { 192 | id: captureButton 193 | 194 | anchors.bottom: parent.bottom 195 | anchors.right: parent.right 196 | anchors.rightMargin: root.isLandscape ? 20 : (Screen.width / 3 - width / 3) 197 | anchors.bottomMargin: 20 198 | 199 | enabled: Camera.ActiveState == camera.cameraState && Camera.ActiveStatus == camera.cameraStatus 200 | 201 | icon.source: "image://theme/icon-camera-shutter-release" 202 | 203 | onClicked: { 204 | picRotation = sensor.rotationAngle; 205 | camera.imageCapture.capture(); 206 | } 207 | } 208 | 209 | IconButton { 210 | 211 | id: orientationButton 212 | anchors.bottom: parent.bottom 213 | anchors.right: parent.right 214 | width: 200 215 | height: captureButton.height 216 | anchors.rightMargin: root.isLandscape ? (Screen.width / 2 - width / 2) + 100 : Screen.width - 150 217 | enabled: Camera.ActiveState == camera.cameraState && Camera.ActiveStatus == camera.cameraStatus 218 | anchors.bottomMargin: 20 219 | 220 | icon.source: if(orientationModes[orientationMode] !== "auto") { 221 | return "image://theme/icon-camera-backcamera" 222 | } else { 223 | return "" 224 | } 225 | 226 | icon.scale: if(orientationModes[orientationMode] === "auto") { 227 | return 0.5; 228 | } else { 229 | return 1.0; 230 | } 231 | 232 | icon.rotation: if(orientationModes[orientationMode] === "landscape") { 233 | return -90; 234 | } else { 235 | return 0; 236 | } 237 | 238 | icon.anchors.left: orientationButton.left 239 | 240 | Text { 241 | text: "Orientation: " + orientationModes[orientationMode]; 242 | anchors.left: parent.left 243 | height: parent.height 244 | verticalAlignment: Text.AlignVCenter 245 | anchors.leftMargin: 130 246 | color: Theme.primaryColor 247 | font.pixelSize: Theme.fontSizeExtraSmall 248 | } 249 | 250 | onClicked: { 251 | ++orientationMode; 252 | if(orientationMode > 2) { 253 | orientationMode = 0; 254 | } 255 | } 256 | } 257 | 258 | Connections { 259 | target: Qt.application 260 | onActiveChanged: 261 | if(!Qt.application.active) { 262 | camera.cameraState = Camera.UnloadedState; 263 | } else if (Qt.application.active) { 264 | camera.cameraState = Camera.ActiveState; 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /qml/pages/CornerPoint.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Rectangle { 5 | 6 | id: corner 7 | width: 40; 8 | height: 40; 9 | Drag.active: area.drag.active 10 | radius: width / 2 11 | border.width: 3 12 | border.color: "blue" 13 | color: "transparent" 14 | 15 | MouseArea { 16 | 17 | id: area 18 | x: -50 19 | y: -50 20 | width: parent.width * 4 21 | height: parent.height * 4 22 | drag.axis: Drag.XandYAxis 23 | drag.target: parent 24 | drag.minimumX: (parent.parent.width - parent.parent.paintedWidth - parent.width) / 2 25 | drag.maximumX: (parent.parent.width + parent.parent.paintedWidth - parent.width) / 2 26 | drag.minimumY: (parent.parent.height - parent.parent.paintedHeight - parent.height) / 2 27 | drag.maximumY: (parent.parent.height + parent.parent.paintedHeight - parent.height) / 2 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /qml/pages/CroppingPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Dialog { 5 | 6 | id: cropDialog 7 | property bool loading; 8 | property var cropPoints: {"topLeft": Qt.point(0, 0)}; 9 | property string curPoint: ""; 10 | 11 | Component.onCompleted: { 12 | curPoint = topLeft.objectName 13 | if(tesseractAPI.getRotated()) { 14 | cropView.source = tesseractAPI.getRotatedPath(); 15 | tesseractAPI.setRotated(false); 16 | loading = false; 17 | busyind.running = false; 18 | } 19 | } 20 | 21 | DialogHeader { 22 | id: header 23 | acceptText: "Analyze" 24 | cancelText: "Cancel" 25 | } 26 | 27 | onAccepted: { 28 | tesseractAPI.analyze(cropView.source, cropPoints); 29 | } 30 | 31 | SilicaFlickable { 32 | 33 | id: container 34 | anchors.top: header.bottom; 35 | anchors.left: parent.left 36 | anchors.right: parent.right 37 | anchors.leftMargin: Theme.paddingLarge 38 | anchors.rightMargin: Theme.paddingLarge 39 | 40 | Rectangle { 41 | id: mask 42 | anchors.top: parent.top 43 | anchors.horizontalCenter: parent.horizontalCenter 44 | anchors.leftMargin: 540 / 2 - width / 2 45 | anchors.topMargin: -zoomArea.height - Theme.paddingLarge 46 | width: zoomArea.width 47 | height: zoomArea.height 48 | border.width: 1 49 | border.color: Theme.secondaryHighlightColor 50 | clip: true 51 | color: Theme.secondaryColor 52 | } 53 | 54 | Item { 55 | 56 | id: zoomArea 57 | width: 110 58 | height: 110 59 | anchors.fill: mask 60 | clip: true 61 | 62 | Image { 63 | id: zoom 64 | cache: false 65 | width: 5 * cropView.paintedWidth 66 | height: 5 * cropView.paintedHeight 67 | x: 0 68 | y: 0 69 | source: cropView.source 70 | } 71 | 72 | Image { 73 | id: crosshair 74 | anchors.fill: parent 75 | source: "../images/circular218.png" 76 | } 77 | 78 | } 79 | 80 | BusyIndicator { 81 | id: busyind 82 | anchors.centerIn: parent 83 | size: BusyIndicatorSize.Large 84 | anchors.verticalCenterOffset: 300 85 | running: true; 86 | } 87 | 88 | ViewPlaceholder { 89 | id: pholder 90 | anchors.centerIn: parent 91 | anchors.verticalCenterOffset: 450 92 | enabled: loading 93 | text: qsTr("Preparing image...") 94 | } 95 | 96 | Label { 97 | id: info 98 | width: parent.width 99 | wrapMode: Text.Wrap 100 | font.pixelSize: Theme.fontSizeExtraSmall 101 | color: Theme.primaryColor 102 | textFormat: Text.RichText; 103 | onLinkActivated: Qt.openUrlExternally(link) 104 | text: "Drag the corner points to crop the image before recognition. Moving: " + curPoint 105 | } 106 | 107 | Image { 108 | id: cropView 109 | anchors.top: info.bottom 110 | anchors.left: parent.left 111 | anchors.topMargin: Theme.paddingMedium * 2 112 | width: parent.width 113 | height: cropDialog.height - header.height - info.height - Theme.paddingMedium * 4 114 | fillMode: Image.PreserveAspectFit 115 | cache: false 116 | 117 | CornerPoint { 118 | 119 | id: topLeft 120 | objectName: "topLeft" 121 | visible: !loading 122 | x: (parent.width - parent.paintedWidth) / 2 - this.width / 2 123 | y: (parent.height - parent.paintedHeight) / 2 - this.height / 2 124 | 125 | onXChanged: { 126 | zoom.x = cornerXRelativeToImg(x, topLeft); 127 | addCorner(topLeft); 128 | canvas.requestPaint(); 129 | curPoint = objectName 130 | } 131 | onYChanged: { 132 | zoom.y = cornerYRelativeToImg(y, topLeft); 133 | addCorner(topLeft); 134 | canvas.requestPaint(); 135 | curPoint = objectName 136 | } 137 | 138 | } 139 | 140 | CornerPoint { 141 | id: topRight 142 | objectName: "topRight" 143 | visible: !loading 144 | x: (parent.width - parent.paintedWidth) / 2 + parent.paintedWidth - this.width / 2 145 | y: (parent.height - parent.paintedHeight) / 2 - this.height / 2 146 | 147 | onXChanged: { 148 | zoom.x = cornerXRelativeToImg(x, topLeft); 149 | addCorner(topRight); 150 | canvas.requestPaint(); 151 | curPoint = objectName 152 | } 153 | onYChanged: { 154 | zoom.y = cornerYRelativeToImg(y, topLeft); 155 | addCorner(topRight); 156 | canvas.requestPaint(); 157 | curPoint = objectName 158 | } 159 | 160 | } 161 | 162 | CornerPoint { 163 | id: bottomLeft 164 | objectName: "bottomLeft" 165 | visible: !loading 166 | x: (parent.width - parent.paintedWidth) / 2 - this.width / 2 167 | y: (parent.height - parent.paintedHeight) / 2 + parent.paintedHeight - this.height / 2 168 | 169 | onXChanged: { 170 | zoom.x = cornerXRelativeToImg(x, topLeft); 171 | addCorner(bottomLeft); 172 | canvas.requestPaint(); 173 | curPoint = objectName 174 | } 175 | onYChanged: { 176 | zoom.y = cornerYRelativeToImg(y, topLeft); 177 | addCorner(bottomLeft); 178 | canvas.requestPaint(); 179 | curPoint = objectName 180 | } 181 | 182 | } 183 | 184 | CornerPoint { 185 | id: bottomRight 186 | objectName: "bottomRight" 187 | visible: !loading 188 | x: (parent.width - parent.paintedWidth) / 2 + parent.paintedWidth - this.width / 2 189 | y: (parent.height - parent.paintedHeight) / 2 + parent.paintedHeight - this.height / 2 190 | 191 | onXChanged: { 192 | zoom.x = cornerXRelativeToImg(x, topLeft); 193 | addCorner(bottomRight); 194 | canvas.requestPaint(); 195 | curPoint = objectName 196 | } 197 | onYChanged: { 198 | zoom.y = cornerYRelativeToImg(y, topLeft); 199 | addCorner(bottomRight); 200 | canvas.requestPaint(); 201 | curPoint = objectName 202 | } 203 | 204 | } 205 | 206 | Canvas { 207 | id: canvas 208 | anchors.fill: parent 209 | z: 10 210 | 211 | onPaint: { 212 | var context = getContext("2d"); 213 | 214 | var offset = topLeft.width / 2; 215 | 216 | context.reset() 217 | context.beginPath(); 218 | context.lineWidth = 2; 219 | context.moveTo(topLeft.x + offset, topLeft.y + offset); 220 | context.strokeStyle = "#87CEFA" 221 | 222 | context.lineTo(topRight.x + offset, topRight.y + offset); 223 | context.lineTo(bottomRight.x + offset, bottomRight.y + offset); 224 | context.lineTo(bottomLeft.x + offset, bottomLeft.y + offset); 225 | context.lineTo(topLeft.x + offset, topLeft.y + offset); 226 | context.closePath(); 227 | context.stroke(); 228 | } 229 | } 230 | } 231 | } 232 | 233 | Connections { 234 | target: tesseractAPI 235 | 236 | onRotated: { 237 | busyind.running = false; 238 | forwardNavigation: true; 239 | loading = false; 240 | cropView.source = path; 241 | } 242 | } 243 | 244 | function cornerXRelativeToImg(x, corner) { 245 | var transferred = (x - (cropView.width - cropView.paintedWidth) / 2 + corner.width / 2) * -5 + zoomArea.width / 2; 246 | return transferred; 247 | } 248 | 249 | function cornerYRelativeToImg(y, corner) { 250 | return (y - (cropView.height - cropView.paintedHeight) / 2 + corner.height / 2) * -5 + zoomArea.height / 2; 251 | } 252 | 253 | function addCorner(corner) { 254 | var offsetx = corner.width / 2; 255 | var offsety = corner.height / 2; 256 | var xScale = cropView.sourceSize.width / cropView.paintedWidth; 257 | var yScale = cropView.sourceSize.height / cropView.paintedHeight; 258 | cropPoints[corner.objectName] = Qt.point(xScale * (corner.x - (cropView.width - cropView.paintedWidth) / 2 + offsetx), 259 | yScale * (corner.y - (cropView.height - cropView.paintedHeight) / 2 + offsety)); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /qml/pages/DownloadDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.textractor.settingsmanager 1.0 4 | 5 | Dialog { 6 | 7 | id: downloadDialog 8 | property string language; 9 | 10 | DialogHeader { 11 | id: header 12 | acceptText: "Download" 13 | cancelText: "Cancel" 14 | } 15 | 16 | Column { 17 | id: column 18 | anchors.top: header.bottom; 19 | anchors.topMargin: 50; 20 | anchors.left: parent.left 21 | anchors.right: parent.right 22 | anchors.leftMargin: Theme.paddingLarge 23 | anchors.rightMargin: Theme.paddingLarge 24 | 25 | Label { 26 | width: parent.width 27 | height: 800 28 | wrapMode: Text.Wrap 29 | font.pixelSize: Theme.fontSizeLarge 30 | color: Theme.primaryColor 31 | textFormat: Text.RichText; 32 | onLinkActivated: Qt.openUrlExternally(link) 33 | text: "Language data for " + language + " hasn't been yet downloaded.

" + 34 | "Do you want to download the language data?" 35 | 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /qml/pages/DownloadPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | 6 | id: page 7 | property string language; 8 | 9 | Component.onCompleted: { 10 | tesseractAPI.downloadLanguage(language); 11 | } 12 | 13 | Timer { 14 | id: timer 15 | interval: 100; 16 | running: false; 17 | repeat: false; 18 | onTriggered: pageStack.pop(); 19 | } 20 | 21 | backNavigation: false; 22 | 23 | BusyIndicator { 24 | id: busyind 25 | anchors.centerIn: page 26 | size: BusyIndicatorSize.Large 27 | running: false; 28 | } 29 | 30 | SilicaFlickable { 31 | id: flickable 32 | anchors.fill: parent 33 | 34 | Column { 35 | id: headerContainer 36 | width: parent.width 37 | 38 | PageHeader { 39 | title: qsTr("Downloading language") 40 | } 41 | } 42 | 43 | ProgressBar { 44 | id: bar 45 | anchors.top: headerContainer.bottom; 46 | anchors.topMargin: 50; 47 | width: parent.width 48 | anchors.leftMargin: Theme.paddingMedium; 49 | anchors.rightMargin: Theme.paddingMedium; 50 | minimumValue: 0 51 | maximumValue: 100 52 | value: 0 53 | valueText: value + "%" 54 | label: "Downloading " + language + " ..." 55 | } 56 | 57 | } 58 | 59 | Connections { 60 | target: tesseractAPI 61 | 62 | onLanguageReady: { 63 | busyind.running = false; 64 | page.backNavigation = true; 65 | tesseractAPI.settings.setLanguage(language); 66 | timer.start(); 67 | } 68 | 69 | onProgressStatus: { 70 | var percentage = Math.round((downloaded / total) * 100); 71 | bar.value = percentage; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /qml/pages/EditPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | 5 | Page { 6 | id: page 7 | property string editText; 8 | 9 | Component.onCompleted: { 10 | area.text = editText; 11 | } 12 | 13 | SilicaFlickable { 14 | 15 | anchors.fill: parent 16 | 17 | PullDownMenu { 18 | MenuItem { 19 | text: qsTr("Copy Text to Clipboard"); 20 | onClicked: { 21 | Clipboard.text = area.text; 22 | } 23 | } 24 | } 25 | 26 | Column { 27 | 28 | PageHeader { 29 | title: qsTr("Edit text") 30 | } 31 | 32 | id: column 33 | anchors.left: parent.left 34 | anchors.right: parent.right 35 | anchors.leftMargin: Theme.paddingLarge 36 | anchors.rightMargin: Theme.paddingLarge 37 | anchors.topMargin: Theme.paddingLarge 38 | 39 | TextArea { 40 | id: area 41 | width: parent.width 42 | height: 450 43 | wrapMode: Text.Wrap 44 | anchors.topMargin: Theme.paddingLarge 45 | font.pixelSize: Theme.fontSizeSmall 46 | color: Theme.primaryColor 47 | text: editText 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /qml/pages/FilePickerDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.textractor.folderlistmodel 1.0 4 | 5 | Dialog { 6 | 7 | id: filePicker 8 | property url selectedFile: ""; 9 | property string currentFolder: ""; 10 | property int lastSelected: -1; 11 | canAccept: false 12 | 13 | Component.onCompleted: { 14 | folderModel.folder = tesseractAPI.homePath(); 15 | currentFolder = tesseractAPI.homePath(); 16 | } 17 | 18 | SilicaFlickable { 19 | 20 | id: flickable 21 | anchors.fill: parent 22 | contentHeight: childrenRect.height 23 | 24 | PullDownMenu { 25 | id: menu 26 | 27 | MenuItem { 28 | text: qsTr("Sort by Name"); 29 | onClicked: { 30 | folderModel.sortField = FolderListModel.Name 31 | } 32 | } 33 | 34 | MenuItem { 35 | text: qsTr("Sort by Type"); 36 | onClicked: { 37 | folderModel.sortField = FolderListModel.Type 38 | } 39 | } 40 | 41 | MenuItem { 42 | text: qsTr("Sort by Last Modified"); 43 | onClicked: { 44 | folderModel.sortField = FolderListModel.Time 45 | } 46 | } 47 | } 48 | 49 | PageHeader { 50 | id: headertext 51 | title: "Pick a File" 52 | } 53 | 54 | BackgroundItem { 55 | 56 | id: parentFolder 57 | width: parent.width 58 | height: Theme.itemSizeSmall 59 | anchors.top: headertext.bottom 60 | anchors.left: parent.left 61 | anchors.right: parent.right 62 | 63 | Image { 64 | fillMode: Image.Pad 65 | horizontalAlignment: Image.AlignHCenter 66 | verticalAlignment: Image.AlignVCenter 67 | anchors.leftMargin: Theme.paddingLarge 68 | id: folderup 69 | anchors.left: parent.left 70 | anchors.top: parent.top 71 | anchors.bottom: parent.bottom 72 | rotation: -90 73 | source: "image://theme/icon-m-page-up" 74 | visible: currentFolder != "/" 75 | } 76 | 77 | Text { 78 | width: parent.width - folderup.width - Theme.paddingLarge * 3 79 | anchors.top: parent.top 80 | anchors.left: folderup.right 81 | anchors.right: parent.right 82 | height: parent.height 83 | wrapMode: Text.Wrap 84 | font.pixelSize: 25 85 | color: Theme.primaryColor 86 | verticalAlignment: Text.AlignVCenter 87 | anchors.leftMargin: Theme.paddingSmall 88 | anchors.rightMargin: Theme.paddingSmall 89 | text: currentFolder 90 | } 91 | 92 | onClicked: { 93 | var folder = String(folderModel.parentFolder).replace("file://", ""); 94 | if(folder !== "") { 95 | currentFolder = folder; 96 | folderModel.folder = folderModel.parentFolder; 97 | } 98 | } 99 | } 100 | 101 | SilicaListView { 102 | 103 | id: fileList 104 | anchors.left: parent.left 105 | anchors.right: parent.right 106 | anchors.top: parentFolder.bottom 107 | anchors.topMargin: Theme.paddingLarge 108 | model: folderModel 109 | height: filePicker.height - headertext.height - parentFolder.height - Theme.paddingLarge 110 | clip: true 111 | 112 | FolderListModel { 113 | id: folderModel 114 | showOnlyReadable: true 115 | nameFilters: ["*.pdf"] 116 | } 117 | 118 | delegate: BackgroundItem { 119 | 120 | id: fileDelegate 121 | width: parent.width 122 | height: Theme.itemSizeSmall 123 | anchors.left: parent.left 124 | anchors.right: parent.right 125 | 126 | Image { 127 | fillMode: Image.Pad 128 | horizontalAlignment: Image.AlignHCenter 129 | verticalAlignment: Image.AlignVCenter 130 | id: foldericon 131 | anchors.left: parent.left 132 | anchors.top: parent.top 133 | anchors.leftMargin: Theme.paddingLarge 134 | source: "image://theme/icon-m-folder" 135 | visible: fileIsDir 136 | 137 | } 138 | 139 | Label { 140 | id: namelabel 141 | anchors { 142 | left: if(fileIsDir) { 143 | foldericon.right 144 | } else { 145 | parent.left 146 | } 147 | right: parent.right 148 | leftMargin: Theme.paddingLarge 149 | rightMargin: Theme.paddingLarge 150 | topMargin: 15 151 | } 152 | textFormat: Text.RichText 153 | text: fileName 154 | } 155 | 156 | Label { 157 | id: sizelabel 158 | anchors { 159 | left: if(fileIsDir) { 160 | foldericon.right 161 | } else { 162 | parent.left 163 | } 164 | right: parent.right 165 | top: namelabel.bottom 166 | leftMargin: Theme.paddingLarge 167 | rightMargin: Theme.paddingLarge 168 | } 169 | 170 | font.pixelSize: 20 171 | textFormat: Text.RichText 172 | text: parseInt(fileSize) / 1000 + " kB, " + fileModified 173 | color: Theme.rgba(Theme.secondaryColor, 0.5) 174 | } 175 | 176 | onClicked: { 177 | if(folderModel.isFolder(index)) { 178 | lastSelected = -1; 179 | folderModel.folder = fileURL; 180 | currentFolder = String(fileURL).replace("file://", ""); 181 | } else { 182 | canAccept = true; 183 | selectedFile = fileURL; 184 | filePicker.accept(); 185 | } 186 | } 187 | } 188 | 189 | VerticalScrollDecorator { 190 | flickable: fileList 191 | 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /qml/pages/HintsPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | 6 | id: page 7 | 8 | SilicaFlickable { 9 | 10 | id: info 11 | anchors.fill: parent 12 | contentHeight: header.height 13 | + generalsection.height 14 | + takingpics.height 15 | + section2.height 16 | + section2text.height 17 | + section3.height 18 | + section3text.height 19 | + 120 20 | 21 | PageHeader { 22 | id: header 23 | title: qsTr("Usage Hints") 24 | } 25 | 26 | SectionHeader { 27 | id: generalsection 28 | text: qsTr("Taking a Good Picture") 29 | height: 50; 30 | anchors.top: header.bottom 31 | } 32 | 33 | Label { 34 | id: takingpics 35 | width: parent.width 36 | wrapMode: Text.Wrap 37 | font.pixelSize: Theme.fontSizeSmall 38 | color: Theme.primaryColor 39 | anchors.top: generalsection.bottom 40 | anchors.left: parent.left 41 | anchors.right: parent.right 42 | anchors.leftMargin: Theme.paddingLarge 43 | anchors.rightMargin: Theme.paddingLarge 44 | textFormat: Text.RichText; 45 | onLinkActivated: Qt.openUrlExternally(link) 46 | text: "To get the best results you should follow a couple of simple guidelines when taking pictures:" + 47 | "