Skip to content

Commit

Permalink
Merge pull request #37 from klep/klep/libscanline
Browse files Browse the repository at this point in the history
Refactor core functionality into libscanline
  • Loading branch information
klep authored Mar 12, 2022
2 parents aa06a54 + b87b335 commit 4849cc8
Show file tree
Hide file tree
Showing 18 changed files with 870 additions and 549 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Xcode
.DS_Store
scanline.xcodeproj/xcshareddata/
build/
derived_data/
*.pbxuser
!default.pbxuser
*.mode1v3
Expand Down
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,31 @@ You can see all of scanline's options by typing:
scanline -help
```

## Swift Rewrite

In December, 2017, the Swift rewrite of scanline was merged into `master`. Let me know if you experience any new issues.

## Installing scanline

You can download a signed, notarized installer from:

https://github.com/klep/scanline/blob/master/scanline-1.0.1.pkg?raw=true
https://github.com/klep/scanline/blob/master/scanline-2.0.pkg?raw=true

## Building Your Own Installer

The bundled installer is signed and notarized by Boat Launch, Inc., a company founded by the author and maintainer of scanline. This is provided merely a convenience, and you are welcome to build and sign your own installer if you wish.

I used the instructions at https://scriptingosx.com/2019/09/notarize-a-command-line-tool/
I used the instructions at https://scriptingosx.com/2021/07/notarize-a-command-line-tool-with-notarytool/

Note that, of course, you'll need to set your own Team / Bundle ID / Certificate

## libscanline

In early 2022, scanline was refactored to separate out the core functionality from the command line interface. libscanline is a macOS framework that can be embedded in any application that wants to easily support the functionality of scanline.

To build libscanline:

xcodebuild clean build -project scanline.xcodeproj -scheme libscanline -configuration Release -sdk macosx11.3 -derivedDataPath derived_data BUILD_LIBRARY_FOR_DISTRIBUTION=YES

The project is structured so that the command line tool is a separate target that also includes all of the source files from libscanline. Ideally, it would simply embed libscanline, but that would require making the command line tool part of an app bundle, or dynamically linking to libscanline.


## Contributing to scanline

If you're interested in making a change, fix, or enhancement to scanline, please do! I'd appreciate a heads up on any bigger changes, and I'm happy to review any PRs.
Expand Down
24 changes: 24 additions & 0 deletions libscanline/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2022 Scott J. Kleper. All rights reserved.</string>
</dict>
</plist>
8 changes: 4 additions & 4 deletions scanline/Logger.swift → libscanline/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@

import Foundation

class Logger: NSObject {
public class Logger: NSObject {
let configuration: ScanConfiguration

init(configuration: ScanConfiguration) {
public init(configuration: ScanConfiguration) {
self.configuration = configuration
super.init()
}

func verbose(_ message: String) {
public func verbose(_ message: String) {
guard configuration.config[ScanlineConfigOptionVerbose] != nil else { return }
print(message)
}

func log(_ message: String) {
public func log(_ message: String) {
print(message)
}
}
12 changes: 8 additions & 4 deletions scanline/ScanConfiguration.h → libscanline/ScanConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

#define SKLog(FORMAT, ...) printf("%s\n", [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);

NS_ASSUME_NONNULL_BEGIN

static NSString * const ScanlineConfigOptionDuplex = @"duplex";
static NSString * const ScanlineConfigOptionBatch = @"batch";
static NSString * const ScanlineConfigOptionList = @"list";
Expand All @@ -34,12 +36,14 @@ static NSString * const ScanlineConfigOptionExactName = @"exactname";
@property (strong, nonatomic) NSMutableArray *tags;
@property (strong, nonatomic) NSMutableDictionary *config;

- (id)init;
- (id)initWithArguments:(NSArray *)inArguments;
- (id)initWithArguments:(NSArray *)inArguments configFilePath:(NSString *)configFilePath;
- (nonnull id)init;
- (nonnull id)initWithArguments:(nonnull NSArray *)inArguments;
- (nonnull id)initWithArguments:(nonnull NSArray *)inArguments configFilePath:(NSString *)configFilePath;

+ (NSDictionary*)configOptions;
+ (nonnull NSDictionary *)configOptions;

@end

extern BOOL verboseLogging;

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//

#import "ScanConfiguration.h"
#import "scanline-Swift.h"

BOOL debugLogging = NO;

Expand Down Expand Up @@ -217,7 +216,7 @@ - (void)loadConfigurationFromArguments:(NSArray*)inArguments
} else {
NSDictionary *configDetails = [ScanConfiguration configOptions][canonicalKey];
if ([(NSString *)configDetails[@"type"] isEqualToString:@"string"]) {
if (i < [inArguments count] && [inArguments objectAtIndex:i+1] != nil) {
if (i+1 < [inArguments count]) {
NSString *value = [inArguments objectAtIndex:++i];
self.config[canonicalKey] = value;
} else {
Expand Down
162 changes: 162 additions & 0 deletions libscanline/ScanlineOutputProcessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
//
// ScanlineOutputProcessor.swift
// libscanline
//
// Created by Scott J. Kleper on 5/13/21.
// Copyright © 2021 Scott J. Kleper. All rights reserved.
//

import Foundation
import AppKit
import Quartz

public class ScanlineOutputProcessor {
let logger: Logger
let configuration: ScanConfiguration
let urls: [URL]

public init(urls: [URL], configuration: ScanConfiguration, logger: Logger) {
self.urls = urls
self.configuration = configuration
self.logger = logger
}

public func process() -> Bool {
let wantsPDF = configuration.config[ScanlineConfigOptionJPEG] == nil && configuration.config[ScanlineConfigOptionTIFF] == nil
if !wantsPDF {
for url in urls {
outputAndTag(url: url)
}
} else {
// Combine into a single PDF
if let combinedURL = combine(urls: urls) {
outputAndTag(url: combinedURL)
} else {
logger.log("Error while creating PDF")
return false
}
}

return true
}

public func combine(urls: [URL]) -> URL? {
let document = PDFDocument()

for url in urls {
if let page = PDFPage(image: NSImage(byReferencing: url)) {
document.insert(page, at: document.pageCount)
}
}

let tempFilePath = "\(NSTemporaryDirectory())/scan.pdf"
document.write(toFile: tempFilePath)

return URL(fileURLWithPath: tempFilePath)
}

public func outputAndTag(url: URL) {
let gregorian = NSCalendar(calendarIdentifier: .gregorian)!
let dateComponents = gregorian.components([.year, .hour, .minute, .second], from: Date())

let outputRootDirectory = configuration.config[ScanlineConfigOptionDir] as! String
var path = outputRootDirectory

// If there's a tag, move the file to the first tag location
if configuration.tags.count > 0 {
path = "\(path)/\(configuration.tags[0])/\(dateComponents.year!.fld())"
}

logger.verbose("Output path: \(path)")

do {
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
} catch {
logger.log("Error while creating directory \(path)")
return
}

let destinationFileExtension: String
if configuration.config[ScanlineConfigOptionTIFF] != nil {
destinationFileExtension = "tif"
} else if configuration.config[ScanlineConfigOptionJPEG] != nil {
destinationFileExtension = "jpg"
} else {
destinationFileExtension = "pdf"
}

let destinationFileRoot: String = { () -> String in
if let fileName = self.configuration.config[ScanlineConfigOptionName] {
return "\(path)/\(fileName)"
}
return "\(path)/scan_\(dateComponents.hour!.f02ld())\(dateComponents.minute!.f02ld())\(dateComponents.second!.f02ld())"
}()

var destinationFilePath = "\(destinationFileRoot).\(destinationFileExtension)"
var i = 0
while FileManager.default.fileExists(atPath: destinationFilePath) {
destinationFilePath = "\(destinationFileRoot).\(i).\(destinationFileExtension)"
i += 1
}

logger.verbose("About to copy \(url.absoluteString) to \(destinationFilePath)")

let destinationURL = URL(fileURLWithPath: destinationFilePath)
do {
try FileManager.default.copyItem(at: url, to: destinationURL)
} catch {
logger.log("Error while copying file to \(destinationURL.absoluteString)")
return
}

// Alias to all other tag locations
// todo: this is super repetitive with above...
if configuration.tags.count > 1 {
for tag in configuration.tags.subarray(with: NSMakeRange(1, configuration.tags.count - 1)) {
logger.verbose("Aliasing to tag \(tag)")
let aliasDirPath = "\(outputRootDirectory)/\(tag)/\(dateComponents.year!.fld())"
do {
try FileManager.default.createDirectory(atPath: aliasDirPath, withIntermediateDirectories: true, attributes: nil)
} catch {
logger.log("Error while creating directory \(aliasDirPath)")
return
}
let aliasFileRoot = { () -> String in
if let name = configuration.config[ScanlineConfigOptionName] {
return "\(aliasDirPath)/\(name)"
}
return "\(aliasDirPath)/scan_\(dateComponents.hour!.f02ld())\(dateComponents.minute!.f02ld())\(dateComponents.second!.f02ld())"
}()
var aliasFilePath = "\(aliasFileRoot).\(destinationFileExtension)"
var i = 0
while FileManager.default.fileExists(atPath: aliasFilePath) {
aliasFilePath = "\(aliasFileRoot).\(i).\(destinationFileExtension)"
i += 1
}
logger.verbose("Aliasing to \(aliasFilePath)")
do {
try FileManager.default.createSymbolicLink(atPath: aliasFilePath, withDestinationPath: destinationFilePath)
} catch {
logger.log("Error while creating alias at \(aliasFilePath)")
return
}
}
}

if configuration.config[ScanlineConfigOptionOpen] != nil {
logger.verbose("Opening file at \(destinationFilePath)")
NSWorkspace.shared.openFile(destinationFilePath)
}
}
}

fileprivate extension Int {
// format to 2 decimal places
func f02ld() -> String {
return String(format: "%02ld", self)
}

func fld() -> String {
return String(format: "%ld", self)
}
}
38 changes: 26 additions & 12 deletions scanline/ScannerBrowser.swift → libscanline/ScannerBrowser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@
import Foundation
import ImageCaptureCore

protocol ScannerBrowserDelegate: class {
public protocol ScannerBrowserDelegate: AnyObject {
func scannerBrowser(_ scannerBrowser: ScannerBrowser, didFinishBrowsingWithScanner scanner: ICScannerDevice?)
func scannerBrowser(_ scannerBrowser: ScannerBrowser, didUpdateAvailableScanners availableScanners: [String])
}

class ScannerBrowser: NSObject, ICDeviceBrowserDelegate {
public class ScannerBrowser: NSObject, ICDeviceBrowserDelegate {
let logger: Logger
let deviceBrowser = ICDeviceBrowser()
var selectedScanner: ICScannerDevice?
let configuration: ScanConfiguration
public var configuration: ScanConfiguration
var searching: Bool
var availableScannerNames: [String] = []

weak var delegate: ScannerBrowserDelegate?
public weak var delegate: ScannerBrowserDelegate?

init(configuration: ScanConfiguration, logger: Logger) {
public init(configuration: ScanConfiguration, logger: Logger) {
self.configuration = configuration
self.logger = logger
self.searching = false
Expand All @@ -38,8 +40,8 @@ class ScannerBrowser: NSObject, ICDeviceBrowserDelegate {
deviceBrowser.browsedDeviceTypeMask = mask!
}

func browse() {
logger.verbose("Searching for available scanners")
public func browse() {
logger.verbose("Browsing for scanners.")
searching = true

if configuration.config[ScanlineConfigOptionList] != nil {
Expand All @@ -48,14 +50,15 @@ class ScannerBrowser: NSObject, ICDeviceBrowserDelegate {
deviceBrowser.start()
}

func stopBrowsing() {
public func stopBrowsing() {
guard searching else { return }

logger.verbose("Done searching for scanners")

delegate?.scannerBrowser(self, didFinishBrowsingWithScanner: selectedScanner)
searching = false
}

func deviceMatchesSpecified(device: ICScannerDevice) -> Bool {
private func deviceMatchesSpecified(device: ICScannerDevice) -> Bool {
// If no name was specified, this is perforce an exact match
guard let desiredName = configuration.config[ScanlineConfigOptionScanner] as? String else { return configuration.config[ScanlineConfigOptionList] == nil }
guard let deviceName = device.name else { return false }
Expand All @@ -73,20 +76,31 @@ class ScannerBrowser: NSObject, ICDeviceBrowserDelegate {
return false
}

func deviceBrowser(_ browser: ICDeviceBrowser, didAdd device: ICDevice, moreComing: Bool) {
public func deviceBrowser(_ browser: ICDeviceBrowser, didAdd device: ICDevice, moreComing: Bool) {
logger.verbose("Added device: \(device)")
if configuration.config[ScanlineConfigOptionList] != nil {
logger.log("* \(device.name ?? "[Nameless Device]")")
}

guard let scannerDevice = device as? ICScannerDevice else { return }

if let scannerName = scannerDevice.name {
availableScannerNames.append(scannerName)
delegate?.scannerBrowser(self, didUpdateAvailableScanners: availableScannerNames)
}

if deviceMatchesSpecified(device: scannerDevice) {
selectedScanner = scannerDevice
stopBrowsing()
}
}

func deviceBrowser(_ browser: ICDeviceBrowser, didRemove device: ICDevice, moreGoing: Bool) {
public func deviceBrowser(_ browser: ICDeviceBrowser, didRemove device: ICDevice, moreGoing: Bool) {
logger.verbose("Removed device: \(device)")
guard let _ = device as? ICScannerDevice, let scannerName = device.name else { return }

availableScannerNames.removeAll(where: { $0 == scannerName })
delegate?.scannerBrowser(self, didUpdateAvailableScanners: availableScannerNames)
}
}

Loading

0 comments on commit 4849cc8

Please sign in to comment.