update dependencies
This commit is contained in:
parent
3a3a7e5a63
commit
785dccdf9c
35 changed files with 2489 additions and 424 deletions
|
|
@ -9,11 +9,11 @@ introduction_screen: 4a90e557630b28834479ed9c64a9d2d0185d8e48
|
|||
libsignal_protocol_dart: 618f0c0b49534245a640a31d204265440cbac9ee
|
||||
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
|
||||
mutex: 84ca903a3ac863735e3228c75a212133621f680f
|
||||
no_screenshot: 57b4a072e9193b4fa1257a6f1acb13ef307625e7
|
||||
no_screenshot: 9ca2a492ff12e5179583a1fa015bf0843382b866
|
||||
optional: 71c638891ce4f2aff35c7387727989f31f9d877d
|
||||
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
||||
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
||||
qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
|
||||
qr: 5fa01fcccd6121b906dc7df4fffa9fa22ca94f75
|
||||
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
||||
restart_app: 12339f63bf8e9631e619c4f9f6b4e013fa324715
|
||||
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
package com.flutterplaza.no_screenshot
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager.LayoutParams
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.NonNull
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
|
|
@ -17,6 +22,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
|||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.concurrent.Executors
|
||||
import org.json.JSONObject
|
||||
|
||||
const val SCREENSHOT_ON_CONST = "screenshotOn"
|
||||
|
|
@ -28,72 +34,122 @@ const val STOP_SCREENSHOT_LISTENING_CONST = "stopScreenshotListening"
|
|||
const val SCREENSHOT_PATH = "screenshot_path"
|
||||
const val PREF_KEY_SCREENSHOT = "is_screenshot_on"
|
||||
const val SCREENSHOT_TAKEN = "was_screenshot_taken"
|
||||
const val SET_IMAGE_CONST = "toggleScreenshotWithImage"
|
||||
const val PREF_KEY_IMAGE_OVERLAY = "is_image_overlay_mode_enabled"
|
||||
const val IS_SCREEN_RECORDING = "is_screen_recording"
|
||||
const val START_SCREEN_RECORDING_LISTENING_CONST = "startScreenRecordingListening"
|
||||
const val STOP_SCREEN_RECORDING_LISTENING_CONST = "stopScreenRecordingListening"
|
||||
const val SCREENSHOT_METHOD_CHANNEL = "com.flutterplaza.no_screenshot_methods"
|
||||
const val SCREENSHOT_EVENT_CHANNEL = "com.flutterplaza.no_screenshot_streams"
|
||||
|
||||
class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, EventChannel.StreamHandler {
|
||||
class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware,
|
||||
EventChannel.StreamHandler {
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
private lateinit var eventChannel: EventChannel
|
||||
private lateinit var context: Context
|
||||
private var activity: Activity? = null
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
private var screenshotObserver: ContentObserver? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private var lastSharedPreferencesState: String = ""
|
||||
private var hasSharedPreferencesChanged: Boolean = false
|
||||
private var isImageOverlayModeEnabled: Boolean = false
|
||||
private var overlayImageView: ImageView? = null
|
||||
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
|
||||
private var isScreenRecording: Boolean = false
|
||||
private var isRecordingListening: Boolean = false
|
||||
private var screenCaptureCallback: Any? = null
|
||||
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_METHOD_CHANNEL)
|
||||
methodChannel =
|
||||
MethodChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_METHOD_CHANNEL)
|
||||
methodChannel.setMethodCallHandler(this)
|
||||
|
||||
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_EVENT_CHANNEL)
|
||||
eventChannel.setStreamHandler(this)
|
||||
|
||||
initScreenshotObserver()
|
||||
registerLifecycleCallbacks()
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
screenshotObserver?.let { context.contentResolver.unregisterContentObserver(it) }
|
||||
unregisterLifecycleCallbacks()
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
restoreScreenshotState()
|
||||
if (isRecordingListening) {
|
||||
registerScreenCaptureCallback()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
unregisterScreenCaptureCallback()
|
||||
removeImageOverlay()
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {}
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
restoreScreenshotState()
|
||||
if (isRecordingListening) {
|
||||
registerScreenCaptureCallback()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {}
|
||||
override fun onDetachedFromActivity() {
|
||||
unregisterScreenCaptureCallback()
|
||||
removeImageOverlay()
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
SCREENSHOT_ON_CONST -> {
|
||||
result.success(screenshotOn().also { updateSharedPreferencesState("") })
|
||||
}
|
||||
|
||||
SCREENSHOT_OFF_CONST -> {
|
||||
result.success(screenshotOff().also { updateSharedPreferencesState("") })
|
||||
}
|
||||
|
||||
TOGGLE_SCREENSHOT_CONST -> {
|
||||
toggleScreenshot()
|
||||
result.success(true.also { updateSharedPreferencesState("") })
|
||||
}
|
||||
|
||||
START_SCREENSHOT_LISTENING_CONST -> {
|
||||
startListening()
|
||||
result.success("Listening started")
|
||||
}
|
||||
|
||||
STOP_SCREENSHOT_LISTENING_CONST -> {
|
||||
stopListening()
|
||||
result.success("Listening stopped".also { updateSharedPreferencesState("") })
|
||||
}
|
||||
|
||||
SET_IMAGE_CONST -> {
|
||||
result.success(toggleScreenshotWithImage())
|
||||
}
|
||||
|
||||
START_SCREEN_RECORDING_LISTENING_CONST -> {
|
||||
startRecordingListening()
|
||||
result.success("Recording listening started")
|
||||
}
|
||||
|
||||
STOP_SCREEN_RECORDING_LISTENING_CONST -> {
|
||||
stopRecordingListening()
|
||||
result.success("Recording listening stopped".also { updateSharedPreferencesState("") })
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
|
@ -108,12 +164,137 @@ class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activ
|
|||
eventSink = null
|
||||
}
|
||||
|
||||
private fun registerLifecycleCallbacks() {
|
||||
val app = context as? Application ?: return
|
||||
lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityPaused(act: Activity) {
|
||||
if (act == activity && isImageOverlayModeEnabled) {
|
||||
act.window?.clearFlags(LayoutParams.FLAG_SECURE)
|
||||
showImageOverlay(act)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResumed(act: Activity) {
|
||||
if (act == activity && isImageOverlayModeEnabled) {
|
||||
removeImageOverlay()
|
||||
act.window?.addFlags(LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(act: Activity, savedInstanceState: Bundle?) {}
|
||||
override fun onActivityStarted(act: Activity) {}
|
||||
override fun onActivityStopped(act: Activity) {}
|
||||
override fun onActivitySaveInstanceState(act: Activity, outState: Bundle) {
|
||||
if (act == activity && isImageOverlayModeEnabled) {
|
||||
showImageOverlay(act)
|
||||
}
|
||||
}
|
||||
override fun onActivityDestroyed(act: Activity) {}
|
||||
}
|
||||
app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
|
||||
}
|
||||
|
||||
private fun unregisterLifecycleCallbacks() {
|
||||
val app = context as? Application ?: return
|
||||
lifecycleCallbacks?.let { app.unregisterActivityLifecycleCallbacks(it) }
|
||||
lifecycleCallbacks = null
|
||||
}
|
||||
|
||||
private fun showImageOverlay(activity: Activity) {
|
||||
if (overlayImageView != null) return
|
||||
val resId = activity.resources.getIdentifier("image", "drawable", activity.packageName)
|
||||
if (resId == 0) return
|
||||
activity.runOnUiThread {
|
||||
val imageView = ImageView(activity).apply {
|
||||
setImageResource(resId)
|
||||
scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
(activity.window.decorView as? ViewGroup)?.addView(imageView)
|
||||
overlayImageView = imageView
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeImageOverlay() {
|
||||
val imageView = overlayImageView ?: return
|
||||
val act = activity
|
||||
if (act != null) {
|
||||
act.runOnUiThread {
|
||||
(imageView.parent as? ViewGroup)?.removeView(imageView)
|
||||
overlayImageView = null
|
||||
}
|
||||
} else {
|
||||
(imageView.parent as? ViewGroup)?.removeView(imageView)
|
||||
overlayImageView = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleScreenshotWithImage(): Boolean {
|
||||
isImageOverlayModeEnabled = !preferences.getBoolean(PREF_KEY_IMAGE_OVERLAY, false)
|
||||
saveImageOverlayState(isImageOverlayModeEnabled)
|
||||
|
||||
if (isImageOverlayModeEnabled) {
|
||||
screenshotOff()
|
||||
} else {
|
||||
screenshotOn()
|
||||
removeImageOverlay()
|
||||
}
|
||||
updateSharedPreferencesState("")
|
||||
return isImageOverlayModeEnabled
|
||||
}
|
||||
|
||||
// ── Screen Recording Detection ─────────────────────────────────────
|
||||
|
||||
private fun startRecordingListening() {
|
||||
if (isRecordingListening) return
|
||||
isRecordingListening = true
|
||||
registerScreenCaptureCallback()
|
||||
updateSharedPreferencesState("")
|
||||
}
|
||||
|
||||
private fun stopRecordingListening() {
|
||||
if (!isRecordingListening) return
|
||||
isRecordingListening = false
|
||||
unregisterScreenCaptureCallback()
|
||||
isScreenRecording = false
|
||||
updateSharedPreferencesState("")
|
||||
}
|
||||
|
||||
private fun registerScreenCaptureCallback() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 34) {
|
||||
val act = activity ?: return
|
||||
if (screenCaptureCallback != null) return
|
||||
|
||||
val callback = Activity.ScreenCaptureCallback {
|
||||
isScreenRecording = true
|
||||
updateSharedPreferencesState("")
|
||||
}
|
||||
act.registerScreenCaptureCallback(act.mainExecutor, callback)
|
||||
screenCaptureCallback = callback
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterScreenCaptureCallback() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 34) {
|
||||
val act = activity ?: return
|
||||
val callback = screenCaptureCallback as? Activity.ScreenCaptureCallback ?: return
|
||||
act.unregisterScreenCaptureCallback(callback)
|
||||
screenCaptureCallback = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun initScreenshotObserver() {
|
||||
screenshotObserver = object : ContentObserver(Handler()) {
|
||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||
super.onChange(selfChange, uri)
|
||||
uri?.let {
|
||||
if (it.toString().contains(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) {
|
||||
if (it.toString()
|
||||
.contains(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
|
||||
) {
|
||||
Log.d("ScreenshotProtection", "Screenshot detected")
|
||||
updateSharedPreferencesState(it.path ?: "")
|
||||
}
|
||||
|
|
@ -124,7 +305,11 @@ class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activ
|
|||
|
||||
private fun startListening() {
|
||||
screenshotObserver?.let {
|
||||
context.contentResolver.registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, it)
|
||||
context.contentResolver.registerContentObserver(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
true,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,28 +344,50 @@ class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activ
|
|||
}
|
||||
|
||||
private fun saveScreenshotState(isSecure: Boolean) {
|
||||
preferences.edit().putBoolean(PREF_KEY_SCREENSHOT, isSecure).apply()
|
||||
Executors.newSingleThreadExecutor().execute {
|
||||
preferences.edit().putBoolean(PREF_KEY_SCREENSHOT, isSecure).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveImageOverlayState(enabled: Boolean) {
|
||||
Executors.newSingleThreadExecutor().execute {
|
||||
preferences.edit().putBoolean(PREF_KEY_IMAGE_OVERLAY, enabled).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreScreenshotState() {
|
||||
val isSecure = preferences.getBoolean(PREF_KEY_SCREENSHOT, false)
|
||||
if (isSecure) {
|
||||
screenshotOff()
|
||||
} else {
|
||||
screenshotOn()
|
||||
Executors.newSingleThreadExecutor().execute {
|
||||
val isSecure = preferences.getBoolean(PREF_KEY_SCREENSHOT, false)
|
||||
val overlayEnabled = preferences.getBoolean(PREF_KEY_IMAGE_OVERLAY, false)
|
||||
isImageOverlayModeEnabled = overlayEnabled
|
||||
|
||||
activity?.runOnUiThread {
|
||||
if (isImageOverlayModeEnabled || isSecure) {
|
||||
screenshotOff()
|
||||
} else {
|
||||
screenshotOn()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSharedPreferencesState(screenshotData: String) {
|
||||
val jsonString = convertMapToJsonString(mapOf(
|
||||
PREF_KEY_SCREENSHOT to preferences.getBoolean(PREF_KEY_SCREENSHOT, false),
|
||||
SCREENSHOT_PATH to screenshotData,
|
||||
SCREENSHOT_TAKEN to screenshotData.isNotEmpty()
|
||||
))
|
||||
if (lastSharedPreferencesState != jsonString) {
|
||||
hasSharedPreferencesChanged = true
|
||||
lastSharedPreferencesState = jsonString
|
||||
}
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
val isSecure =
|
||||
(activity?.window?.attributes?.flags ?: 0) and LayoutParams.FLAG_SECURE != 0
|
||||
val jsonString = convertMapToJsonString(
|
||||
mapOf(
|
||||
PREF_KEY_SCREENSHOT to isSecure,
|
||||
SCREENSHOT_PATH to screenshotData,
|
||||
SCREENSHOT_TAKEN to screenshotData.isNotEmpty(),
|
||||
IS_SCREEN_RECORDING to isScreenRecording
|
||||
)
|
||||
)
|
||||
if (lastSharedPreferencesState != jsonString) {
|
||||
hasSharedPreferencesChanged = true
|
||||
lastSharedPreferencesState = jsonString
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
private fun convertMapToJsonString(map: Map<String, Any>): String {
|
||||
|
|
|
|||
|
|
@ -1,30 +1,35 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
import ScreenProtectorKit
|
||||
|
||||
public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
|
||||
private var screenProtectorKit: ScreenProtectorKit? = nil
|
||||
private var screenPrevent = UITextField()
|
||||
private var screenImage: UIImageView? = nil
|
||||
private weak var attachedWindow: UIWindow? = nil
|
||||
private static var methodChannel: FlutterMethodChannel? = nil
|
||||
private static var eventChannel: FlutterEventChannel? = nil
|
||||
private static var preventScreenShot: Bool = false
|
||||
private var eventSink: FlutterEventSink? = nil
|
||||
private var lastSharedPreferencesState: String = ""
|
||||
private var hasSharedPreferencesChanged: Bool = false
|
||||
private var isImageOverlayModeEnabled: Bool = false
|
||||
private var isScreenRecording: Bool = false
|
||||
private var isRecordingListening: Bool = false
|
||||
|
||||
private static let ENABLESCREENSHOT = false
|
||||
private static let DISABLESCREENSHOT = true
|
||||
|
||||
private static let preventScreenShotKey = "preventScreenShot"
|
||||
private static let imageOverlayModeKey = "imageOverlayMode"
|
||||
private static let methodChannelName = "com.flutterplaza.no_screenshot_methods"
|
||||
private static let eventChannelName = "com.flutterplaza.no_screenshot_streams"
|
||||
private static let screenshotPathPlaceholder = "screenshot_path_placeholder"
|
||||
|
||||
init(screenProtectorKit: ScreenProtectorKit) {
|
||||
self.screenProtectorKit = screenProtectorKit
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// Restore the saved state from UserDefaults
|
||||
var fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
|
||||
let fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
|
||||
isImageOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.imageOverlayModeKey)
|
||||
updateScreenshotState(isScreenshotBlocked: fetchVal)
|
||||
}
|
||||
|
||||
|
|
@ -32,30 +37,111 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
methodChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: registrar.messenger())
|
||||
eventChannel = FlutterEventChannel(name: eventChannelName, binaryMessenger: registrar.messenger())
|
||||
|
||||
let window = UIApplication.shared.delegate?.window
|
||||
let screenProtectorKit = ScreenProtectorKit(window: window as? UIWindow)
|
||||
screenProtectorKit.configurePreventionScreenshot()
|
||||
let instance = IOSNoScreenshotPlugin()
|
||||
|
||||
let instance = IOSNoScreenshotPlugin(screenProtectorKit: screenProtectorKit)
|
||||
registrar.addMethodCallDelegate(instance, channel: methodChannel!)
|
||||
eventChannel?.setStreamHandler(instance)
|
||||
registrar.addApplicationDelegate(instance)
|
||||
}
|
||||
|
||||
// MARK: - Inline Screenshot Prevention (replaces ScreenProtectorKit)
|
||||
|
||||
private func configurePreventionScreenshot(window: UIWindow) {
|
||||
guard let rootLayer = window.layer.superlayer else { return }
|
||||
guard screenPrevent.layer.superlayer == nil else { return }
|
||||
|
||||
screenPrevent.semanticContentAttribute = .forceLeftToRight // RTL fix
|
||||
screenPrevent.textAlignment = .left // RTL fix
|
||||
|
||||
// Briefly add to the window so UIKit creates the text field's
|
||||
// internal sublayer hierarchy, then force a layout pass and
|
||||
// immediately remove so screenPrevent is NOT a subview of window.
|
||||
// This avoids a circular view-hierarchy that causes EXC_BAD_ACCESS
|
||||
// (stack overflow in _collectExistingTraitCollectionsForTraitTracking)
|
||||
// on iOS 26+.
|
||||
window.addSubview(screenPrevent)
|
||||
screenPrevent.layoutIfNeeded()
|
||||
screenPrevent.removeFromSuperview()
|
||||
|
||||
// Keep the layer at the origin so reparenting window.layer
|
||||
// does not shift the app content.
|
||||
screenPrevent.layer.frame = .zero
|
||||
|
||||
rootLayer.addSublayer(screenPrevent.layer)
|
||||
if #available(iOS 17.0, *) {
|
||||
screenPrevent.layer.sublayers?.last?.addSublayer(window.layer)
|
||||
} else {
|
||||
screenPrevent.layer.sublayers?.first?.addSublayer(window.layer)
|
||||
}
|
||||
}
|
||||
|
||||
private func enablePreventScreenshot() {
|
||||
screenPrevent.isSecureTextEntry = true
|
||||
}
|
||||
|
||||
private func disablePreventScreenshot() {
|
||||
screenPrevent.isSecureTextEntry = false
|
||||
}
|
||||
|
||||
private func enableImageScreen(named: String) {
|
||||
guard let window = attachedWindow else { return }
|
||||
let imageView = UIImageView(frame: UIScreen.main.bounds)
|
||||
imageView.image = UIImage(named: named)
|
||||
imageView.isUserInteractionEnabled = false
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
window.addSubview(imageView)
|
||||
screenImage = imageView
|
||||
}
|
||||
|
||||
private func disableImageScreen() {
|
||||
screenImage?.removeFromSuperview()
|
||||
screenImage = nil
|
||||
}
|
||||
|
||||
// MARK: - App Lifecycle
|
||||
//
|
||||
// Image overlay lifecycle is intentionally handled in exactly two places:
|
||||
// SHOW: applicationWillResignActive (app is about to lose focus)
|
||||
// HIDE: applicationDidBecomeActive (app is fully interactive again)
|
||||
//
|
||||
// willResignActive always fires before didEnterBackground, and
|
||||
// didBecomeActive always fires after willEnterForeground, so a single
|
||||
// show/hide pair covers both the app-switcher peek and the full
|
||||
// background → foreground round-trip without double-showing the image.
|
||||
|
||||
public func applicationWillResignActive(_ application: UIApplication) {
|
||||
persistState()
|
||||
|
||||
if isImageOverlayModeEnabled {
|
||||
// Temporarily lift screenshot prevention so the overlay image is
|
||||
// visible in the app switcher (otherwise the secure text field
|
||||
// would show a blank screen).
|
||||
disablePreventScreenshot()
|
||||
enableImageScreen(named: "image")
|
||||
}
|
||||
}
|
||||
|
||||
public func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Remove the image overlay FIRST.
|
||||
if isImageOverlayModeEnabled {
|
||||
disableImageScreen()
|
||||
}
|
||||
|
||||
// Now restore screenshot protection (and re-attach the window if it
|
||||
// changed while the app was in the background).
|
||||
fetchPersistedState()
|
||||
}
|
||||
|
||||
public func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
fetchPersistedState()
|
||||
// Image overlay removal is handled in applicationDidBecomeActive
|
||||
// which always fires after this callback.
|
||||
}
|
||||
|
||||
public func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
persistState()
|
||||
// Image overlay was already shown in applicationWillResignActive
|
||||
// which always fires before this callback.
|
||||
}
|
||||
|
||||
public func applicationWillTerminate(_ application: UIApplication) {
|
||||
|
|
@ -65,15 +151,17 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
func persistState() {
|
||||
// Persist the state when changed
|
||||
UserDefaults.standard.set(IOSNoScreenshotPlugin.preventScreenShot, forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
|
||||
print("Persisted state: \(IOSNoScreenshotPlugin.preventScreenShot)")
|
||||
UserDefaults.standard.set(isImageOverlayModeEnabled, forKey: IOSNoScreenshotPlugin.imageOverlayModeKey)
|
||||
print("Persisted state: \(IOSNoScreenshotPlugin.preventScreenShot), imageOverlay: \(isImageOverlayModeEnabled)")
|
||||
updateSharedPreferencesState("")
|
||||
}
|
||||
|
||||
func fetchPersistedState() {
|
||||
// Restore the saved state from UserDefaults
|
||||
var fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey) ? IOSNoScreenshotPlugin.DISABLESCREENSHOT :IOSNoScreenshotPlugin.ENABLESCREENSHOT
|
||||
let fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey) ? IOSNoScreenshotPlugin.DISABLESCREENSHOT : IOSNoScreenshotPlugin.ENABLESCREENSHOT
|
||||
isImageOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.imageOverlayModeKey)
|
||||
updateScreenshotState(isScreenshotBlocked: fetchVal)
|
||||
print("Fetched state: \(IOSNoScreenshotPlugin.preventScreenShot)")
|
||||
print("Fetched state: \(IOSNoScreenshotPlugin.preventScreenShot), imageOverlay: \(isImageOverlayModeEnabled)")
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
|
|
@ -84,8 +172,11 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
case "screenshotOn":
|
||||
shotOn()
|
||||
result(true)
|
||||
case "toggleScreenshotWithImage":
|
||||
let isActive = toggleScreenshotWithImage()
|
||||
result(isActive)
|
||||
case "toggleScreenshot":
|
||||
IOSNoScreenshotPlugin.preventScreenShot ? shotOn(): shotOff()
|
||||
IOSNoScreenshotPlugin.preventScreenShot ? shotOn() : shotOff()
|
||||
result(true)
|
||||
case "startScreenshotListening":
|
||||
startListening()
|
||||
|
|
@ -93,6 +184,12 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
case "stopScreenshotListening":
|
||||
stopListening()
|
||||
result("Listening stopped")
|
||||
case "startScreenRecordingListening":
|
||||
startRecordingListening()
|
||||
result("Recording listening started")
|
||||
case "stopScreenRecordingListening":
|
||||
stopRecordingListening()
|
||||
result("Recording listening stopped")
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
|
@ -100,16 +197,35 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
|
||||
private func shotOff() {
|
||||
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
|
||||
screenProtectorKit?.enabledPreventScreenshot()
|
||||
enablePreventScreenshot()
|
||||
persistState()
|
||||
}
|
||||
|
||||
private func shotOn() {
|
||||
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
|
||||
screenProtectorKit?.disablePreventScreenshot()
|
||||
disablePreventScreenshot()
|
||||
persistState()
|
||||
}
|
||||
|
||||
private func toggleScreenshotWithImage() -> Bool {
|
||||
// Toggle the image overlay mode state
|
||||
isImageOverlayModeEnabled.toggle()
|
||||
|
||||
if isImageOverlayModeEnabled {
|
||||
// Mode is now active (true) - screenshot prevention should be ON (screenshots blocked)
|
||||
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
|
||||
enablePreventScreenshot()
|
||||
} else {
|
||||
// Mode is now inactive (false) - screenshot prevention should be OFF (screenshots allowed)
|
||||
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
|
||||
disablePreventScreenshot()
|
||||
disableImageScreen()
|
||||
}
|
||||
|
||||
persistState()
|
||||
return isImageOverlayModeEnabled
|
||||
}
|
||||
|
||||
private func startListening() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(screenshotDetected), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
||||
persistState()
|
||||
|
|
@ -120,16 +236,60 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
persistState()
|
||||
}
|
||||
|
||||
// MARK: - Screen Recording Detection
|
||||
|
||||
private func startRecordingListening() {
|
||||
guard !isRecordingListening else { return }
|
||||
isRecordingListening = true
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(screenCapturedDidChange),
|
||||
name: UIScreen.capturedDidChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
// Check initial state
|
||||
isScreenRecording = UIScreen.main.isCaptured
|
||||
}
|
||||
|
||||
updateSharedPreferencesState("")
|
||||
}
|
||||
|
||||
private func stopRecordingListening() {
|
||||
guard isRecordingListening else { return }
|
||||
isRecordingListening = false
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: UIScreen.capturedDidChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
isScreenRecording = false
|
||||
updateSharedPreferencesState("")
|
||||
}
|
||||
|
||||
@objc private func screenCapturedDidChange() {
|
||||
if #available(iOS 11.0, *) {
|
||||
isScreenRecording = UIScreen.main.isCaptured
|
||||
}
|
||||
updateSharedPreferencesState("")
|
||||
}
|
||||
|
||||
@objc private func screenshotDetected() {
|
||||
print("Screenshot detected")
|
||||
updateSharedPreferencesState(IOSNoScreenshotPlugin.screenshotPathPlaceholder)
|
||||
}
|
||||
|
||||
private func updateScreenshotState(isScreenshotBlocked: Bool) {
|
||||
attachWindowIfNeeded()
|
||||
if isScreenshotBlocked {
|
||||
screenProtectorKit?.enabledPreventScreenshot()
|
||||
enablePreventScreenshot()
|
||||
} else {
|
||||
screenProtectorKit?.disablePreventScreenshot()
|
||||
disablePreventScreenshot()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +297,8 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
let map: [String: Any] = [
|
||||
"is_screenshot_on": IOSNoScreenshotPlugin.preventScreenShot,
|
||||
"screenshot_path": screenshotData,
|
||||
"was_screenshot_taken": !screenshotData.isEmpty
|
||||
"was_screenshot_taken": !screenshotData.isEmpty,
|
||||
"is_screen_recording": isScreenRecording
|
||||
]
|
||||
let jsonString = convertMapToJsonString(map)
|
||||
if lastSharedPreferencesState != jsonString {
|
||||
|
|
@ -176,7 +337,47 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
|||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
screenProtectorKit?.removeAllObserver()
|
||||
private func attachWindowIfNeeded() {
|
||||
var activeWindow: UIWindow?
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
if let windowScene = UIApplication.shared.connectedScenes
|
||||
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
|
||||
let active = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
||||
activeWindow = active
|
||||
}
|
||||
} else {
|
||||
activeWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
|
||||
}
|
||||
|
||||
guard let window = activeWindow else {
|
||||
print("❗️No active window found.")
|
||||
return
|
||||
}
|
||||
|
||||
// Skip re-configuration if already attached to this window.
|
||||
if window === attachedWindow {
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up old state before re-attaching to a new window.
|
||||
if isImageOverlayModeEnabled {
|
||||
disableImageScreen()
|
||||
}
|
||||
disablePreventScreenshot()
|
||||
|
||||
// Undo previous layer reparenting: move the old window's layer
|
||||
// back to the root layer and detach the text field's layer.
|
||||
if let oldWindow = attachedWindow,
|
||||
let rootLayer = screenPrevent.layer.superlayer {
|
||||
rootLayer.addSublayer(oldWindow.layer)
|
||||
screenPrevent.layer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
// Use a fresh UITextField to avoid stale layer state.
|
||||
screenPrevent = UITextField()
|
||||
|
||||
configurePreventionScreenshot(window: window)
|
||||
self.attachedWindow = window
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ A new Flutter plugin project.
|
|||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
# Updated the dependency version to remove the wildcard and use a specific version range
|
||||
s.dependency 'ScreenProtectorKit', '~> 1.3.1'
|
||||
s.platform = :ios, '10.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
const screenShotOnConst = "screenshotOn";
|
||||
const screenShotOffConst = "screenshotOff";
|
||||
const screenSetImage = "toggleScreenshotWithImage";
|
||||
const toggleScreenShotConst = "toggleScreenshot";
|
||||
const startScreenshotListeningConst = 'startScreenshotListening';
|
||||
const stopScreenshotListeningConst = 'stopScreenshotListening';
|
||||
const startScreenRecordingListeningConst = 'startScreenRecordingListening';
|
||||
const stopScreenRecordingListeningConst = 'stopScreenRecordingListening';
|
||||
const screenshotMethodChannel = "com.flutterplaza.no_screenshot_methods";
|
||||
const screenshotEventChannel = "com.flutterplaza.no_screenshot_streams";
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ class NoScreenshot implements NoScreenshotPlatform {
|
|||
return _instancePlatform.screenshotOn();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> toggleScreenshotWithImage() {
|
||||
return _instancePlatform.toggleScreenshotWithImage();
|
||||
}
|
||||
|
||||
/// Return `true` if screenshot capabilities has been
|
||||
/// successfully toggle from it previous state and `false` if the attempt
|
||||
/// to toggle failed.
|
||||
|
|
@ -61,6 +66,18 @@ class NoScreenshot implements NoScreenshotPlatform {
|
|||
return _instancePlatform.stopScreenshotListening();
|
||||
}
|
||||
|
||||
/// Start listening to screen recording activities
|
||||
@override
|
||||
Future<void> startScreenRecordingListening() {
|
||||
return _instancePlatform.startScreenRecordingListening();
|
||||
}
|
||||
|
||||
/// Stop listening to screen recording activities
|
||||
@override
|
||||
Future<void> stopScreenRecordingListening() {
|
||||
return _instancePlatform.stopScreenRecordingListening();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ class MethodChannelNoScreenshot extends NoScreenshotPlatform {
|
|||
return result ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> toggleScreenshotWithImage() async {
|
||||
final result = await methodChannel.invokeMethod<bool>(screenSetImage);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startScreenshotListening() {
|
||||
return methodChannel.invokeMethod<void>(startScreenshotListeningConst);
|
||||
|
|
@ -49,4 +55,14 @@ class MethodChannelNoScreenshot extends NoScreenshotPlatform {
|
|||
Future<void> stopScreenshotListening() {
|
||||
return methodChannel.invokeMethod<void>(stopScreenshotListeningConst);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startScreenRecordingListening() {
|
||||
return methodChannel.invokeMethod<void>(startScreenRecordingListeningConst);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopScreenRecordingListening() {
|
||||
return methodChannel.invokeMethod<void>(stopScreenRecordingListeningConst);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ abstract class NoScreenshotPlatform extends PlatformInterface {
|
|||
throw UnimplementedError('screenshotOn() has not been implemented.');
|
||||
}
|
||||
|
||||
/// Return `true` if screenshot capabilities has been
|
||||
/// successfully enabled or is currently enabled and `false` otherwise.
|
||||
/// throw `UnmimplementedError` if not implement
|
||||
Future<bool> toggleScreenshotWithImage() {
|
||||
throw UnimplementedError(
|
||||
'toggleScreenshotWithImage() has not been implemented.');
|
||||
}
|
||||
|
||||
/// Return `true` if screenshot capabilities has been
|
||||
/// successfully toggle from it previous state and `false` if the attempt
|
||||
/// to toggle failed.
|
||||
|
|
@ -65,4 +73,16 @@ abstract class NoScreenshotPlatform extends PlatformInterface {
|
|||
throw UnimplementedError(
|
||||
'stopScreenshotListening has not been implemented.');
|
||||
}
|
||||
|
||||
/// Start listening to screen recording activities
|
||||
Future<void> startScreenRecordingListening() {
|
||||
throw UnimplementedError(
|
||||
'startScreenRecordingListening has not been implemented.');
|
||||
}
|
||||
|
||||
/// Stop listening to screen recording activities
|
||||
Future<void> stopScreenRecordingListening() {
|
||||
throw UnimplementedError(
|
||||
'stopScreenRecordingListening has not been implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
class ScreenshotSnapshot {
|
||||
/// File path of the captured screenshot.
|
||||
///
|
||||
/// Only available on **macOS** (via Spotlight / `NSMetadataQuery`) and
|
||||
/// **Linux** (via GFileMonitor / inotify).
|
||||
/// On Android and iOS the OS does not expose the screenshot file path —
|
||||
/// this field will contain a placeholder string.
|
||||
/// Use [wasScreenshotTaken] to detect screenshot events on all platforms.
|
||||
final String screenshotPath;
|
||||
|
||||
final bool isScreenshotProtectionOn;
|
||||
final bool wasScreenshotTaken;
|
||||
final bool isScreenRecording;
|
||||
|
||||
ScreenshotSnapshot({
|
||||
required this.screenshotPath,
|
||||
required this.isScreenshotProtectionOn,
|
||||
required this.wasScreenshotTaken,
|
||||
this.isScreenRecording = false,
|
||||
});
|
||||
|
||||
factory ScreenshotSnapshot.fromMap(Map<String, dynamic> map) {
|
||||
|
|
@ -14,6 +24,7 @@ class ScreenshotSnapshot {
|
|||
screenshotPath: map['screenshot_path'] as String? ?? '',
|
||||
isScreenshotProtectionOn: map['is_screenshot_on'] as bool? ?? false,
|
||||
wasScreenshotTaken: map['was_screenshot_taken'] as bool? ?? false,
|
||||
isScreenRecording: map['is_screen_recording'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -22,12 +33,13 @@ class ScreenshotSnapshot {
|
|||
'screenshot_path': screenshotPath,
|
||||
'is_screenshot_on': isScreenshotProtectionOn,
|
||||
'was_screenshot_taken': wasScreenshotTaken,
|
||||
'is_screen_recording': isScreenRecording,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ScreenshotSnapshot(\nscreenshotPath: $screenshotPath, \nisScreenshotProtectionOn: $isScreenshotProtectionOn, \nwasScreenshotTaken: $wasScreenshotTaken\n)';
|
||||
return 'ScreenshotSnapshot(\nscreenshotPath: $screenshotPath, \nisScreenshotProtectionOn: $isScreenshotProtectionOn, \nwasScreenshotTaken: $wasScreenshotTaken, \nisScreenRecording: $isScreenRecording\n)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -37,13 +49,15 @@ class ScreenshotSnapshot {
|
|||
return other is ScreenshotSnapshot &&
|
||||
other.screenshotPath == screenshotPath &&
|
||||
other.isScreenshotProtectionOn == isScreenshotProtectionOn &&
|
||||
other.wasScreenshotTaken == wasScreenshotTaken;
|
||||
other.wasScreenshotTaken == wasScreenshotTaken &&
|
||||
other.isScreenRecording == isScreenRecording;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return screenshotPath.hashCode ^
|
||||
isScreenshotProtectionOn.hashCode ^
|
||||
wasScreenshotTaken.hashCode;
|
||||
wasScreenshotTaken.hashCode ^
|
||||
isScreenRecording.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
name: no_screenshot
|
||||
description: Flutter plugin to enable, disable, toggle or stream screenshot activities in your application.
|
||||
version: 0.3.2
|
||||
description: Flutter plugin to enable, disable, toggle or stream screenshot and screen recording activities in your application.
|
||||
version: 0.4.0
|
||||
homepage: https://flutterplaza.com
|
||||
repository: https://github.com/FlutterPlaza/no_screenshot/releases/tag/v0.3.2
|
||||
repository: https://github.com/FlutterPlaza/no_screenshot/releases/tag/v0.4.0
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
|
@ -30,3 +30,5 @@ flutter:
|
|||
pluginClass: NoScreenshotPlugin
|
||||
macos:
|
||||
pluginClass: MacOSNoScreenshotPlugin
|
||||
linux:
|
||||
pluginClass: NoScreenshotPlugin
|
||||
|
|
|
|||
|
|
@ -83,6 +83,87 @@ void main() {
|
|||
await platform.stopScreenshotListening();
|
||||
expect(true, true); // Add more specific expectations if needed
|
||||
});
|
||||
|
||||
test('toggleScreenshotWithImage', () async {
|
||||
const bool expected = true;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
if (methodCall.method == screenSetImage) {
|
||||
return expected;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
final result = await platform.toggleScreenshotWithImage();
|
||||
expect(result, expected);
|
||||
});
|
||||
|
||||
test('toggleScreenshotWithImage returns false when channel returns null',
|
||||
() async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
return null;
|
||||
});
|
||||
|
||||
final result = await platform.toggleScreenshotWithImage();
|
||||
expect(result, false);
|
||||
});
|
||||
|
||||
test('screenshotOn returns false when channel returns null', () async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
return null;
|
||||
});
|
||||
|
||||
final result = await platform.screenshotOn();
|
||||
expect(result, false);
|
||||
});
|
||||
|
||||
test('screenshotOff returns false when channel returns null', () async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
return null;
|
||||
});
|
||||
|
||||
final result = await platform.screenshotOff();
|
||||
expect(result, false);
|
||||
});
|
||||
|
||||
test('toggleScreenshot returns false when channel returns null', () async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
return null;
|
||||
});
|
||||
|
||||
final result = await platform.toggleScreenshot();
|
||||
expect(result, false);
|
||||
});
|
||||
|
||||
test('startScreenRecordingListening', () async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
if (methodCall.method == startScreenRecordingListeningConst) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
await platform.startScreenRecordingListening();
|
||||
expect(true, true);
|
||||
});
|
||||
|
||||
test('stopScreenRecordingListening', () async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
if (methodCall.method == stopScreenRecordingListeningConst) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
await platform.stopScreenRecordingListening();
|
||||
expect(true, true);
|
||||
});
|
||||
});
|
||||
|
||||
group('ScreenshotSnapshot', () {
|
||||
|
|
@ -96,6 +177,31 @@ void main() {
|
|||
expect(snapshot.screenshotPath, '/example/path');
|
||||
expect(snapshot.isScreenshotProtectionOn, true);
|
||||
expect(snapshot.wasScreenshotTaken, true);
|
||||
expect(snapshot.isScreenRecording, false);
|
||||
});
|
||||
|
||||
test('fromMap with is_screen_recording', () {
|
||||
final map = {
|
||||
'screenshot_path': '/example/path',
|
||||
'is_screenshot_on': true,
|
||||
'was_screenshot_taken': false,
|
||||
'is_screen_recording': true,
|
||||
};
|
||||
final snapshot = ScreenshotSnapshot.fromMap(map);
|
||||
expect(snapshot.screenshotPath, '/example/path');
|
||||
expect(snapshot.isScreenshotProtectionOn, true);
|
||||
expect(snapshot.wasScreenshotTaken, false);
|
||||
expect(snapshot.isScreenRecording, true);
|
||||
});
|
||||
|
||||
test('fromMap without is_screen_recording defaults to false', () {
|
||||
final map = {
|
||||
'screenshot_path': '/example/path',
|
||||
'is_screenshot_on': true,
|
||||
'was_screenshot_taken': true,
|
||||
};
|
||||
final snapshot = ScreenshotSnapshot.fromMap(map);
|
||||
expect(snapshot.isScreenRecording, false);
|
||||
});
|
||||
|
||||
test('toMap', () {
|
||||
|
|
@ -103,11 +209,23 @@ void main() {
|
|||
screenshotPath: '/example/path',
|
||||
isScreenshotProtectionOn: true,
|
||||
wasScreenshotTaken: true,
|
||||
isScreenRecording: true,
|
||||
);
|
||||
final map = snapshot.toMap();
|
||||
expect(map['screenshot_path'], '/example/path');
|
||||
expect(map['is_screenshot_on'], true);
|
||||
expect(map['was_screenshot_taken'], true);
|
||||
expect(map['is_screen_recording'], true);
|
||||
});
|
||||
|
||||
test('toMap with default isScreenRecording', () {
|
||||
final snapshot = ScreenshotSnapshot(
|
||||
screenshotPath: '/example/path',
|
||||
isScreenshotProtectionOn: true,
|
||||
wasScreenshotTaken: true,
|
||||
);
|
||||
final map = snapshot.toMap();
|
||||
expect(map['is_screen_recording'], false);
|
||||
});
|
||||
|
||||
test('equality operator', () {
|
||||
|
|
@ -126,9 +244,16 @@ void main() {
|
|||
isScreenshotProtectionOn: false,
|
||||
wasScreenshotTaken: false,
|
||||
);
|
||||
final snapshot4 = ScreenshotSnapshot(
|
||||
screenshotPath: '/example/path',
|
||||
isScreenshotProtectionOn: true,
|
||||
wasScreenshotTaken: true,
|
||||
isScreenRecording: true,
|
||||
);
|
||||
|
||||
expect(snapshot1 == snapshot2, true);
|
||||
expect(snapshot1 == snapshot3, false);
|
||||
expect(snapshot1 == snapshot4, false);
|
||||
});
|
||||
|
||||
test('hashCode', () {
|
||||
|
|
@ -152,6 +277,28 @@ void main() {
|
|||
expect(snapshot1.hashCode, isNot(snapshot3.hashCode));
|
||||
});
|
||||
|
||||
test('fromMap with empty map uses defaults', () {
|
||||
final snapshot = ScreenshotSnapshot.fromMap({});
|
||||
expect(snapshot.screenshotPath, '');
|
||||
expect(snapshot.isScreenshotProtectionOn, false);
|
||||
expect(snapshot.wasScreenshotTaken, false);
|
||||
expect(snapshot.isScreenRecording, false);
|
||||
});
|
||||
|
||||
test('fromMap with null values uses defaults', () {
|
||||
final map = <String, dynamic>{
|
||||
'screenshot_path': null,
|
||||
'is_screenshot_on': null,
|
||||
'was_screenshot_taken': null,
|
||||
'is_screen_recording': null,
|
||||
};
|
||||
final snapshot = ScreenshotSnapshot.fromMap(map);
|
||||
expect(snapshot.screenshotPath, '');
|
||||
expect(snapshot.isScreenshotProtectionOn, false);
|
||||
expect(snapshot.wasScreenshotTaken, false);
|
||||
expect(snapshot.isScreenRecording, false);
|
||||
});
|
||||
|
||||
test('toString', () {
|
||||
final snapshot = ScreenshotSnapshot(
|
||||
screenshotPath: '/example/path',
|
||||
|
|
@ -160,7 +307,19 @@ void main() {
|
|||
);
|
||||
final string = snapshot.toString();
|
||||
expect(string,
|
||||
'ScreenshotSnapshot(\nscreenshotPath: /example/path, \nisScreenshotProtectionOn: true, \nwasScreenshotTaken: true\n)');
|
||||
'ScreenshotSnapshot(\nscreenshotPath: /example/path, \nisScreenshotProtectionOn: true, \nwasScreenshotTaken: true, \nisScreenRecording: false\n)');
|
||||
});
|
||||
|
||||
test('toString with isScreenRecording true', () {
|
||||
final snapshot = ScreenshotSnapshot(
|
||||
screenshotPath: '/example/path',
|
||||
isScreenshotProtectionOn: true,
|
||||
wasScreenshotTaken: true,
|
||||
isScreenRecording: true,
|
||||
);
|
||||
final string = snapshot.toString();
|
||||
expect(string,
|
||||
'ScreenshotSnapshot(\nscreenshotPath: /example/path, \nisScreenshotProtectionOn: true, \nwasScreenshotTaken: true, \nisScreenRecording: true\n)');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import 'package:no_screenshot/no_screenshot_method_channel.dart';
|
|||
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
|
||||
import 'package:no_screenshot/screenshot_snapshot.dart';
|
||||
|
||||
/// A minimal subclass that does NOT override toggleScreenshotWithImage,
|
||||
/// so we can verify the base class throws UnimplementedError.
|
||||
class BaseNoScreenshotPlatform extends NoScreenshotPlatform {}
|
||||
|
||||
class MockNoScreenshotPlatform extends NoScreenshotPlatform {
|
||||
@override
|
||||
Future<bool> screenshotOff() async {
|
||||
|
|
@ -29,10 +33,25 @@ class MockNoScreenshotPlatform extends NoScreenshotPlatform {
|
|||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> toggleScreenshotWithImage() async {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopScreenshotListening() async {
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startScreenRecordingListening() async {
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopScreenRecordingListening() async {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
|
@ -71,5 +90,86 @@ void main() {
|
|||
() async {
|
||||
expect(platform.stopScreenshotListening(), completes);
|
||||
});
|
||||
|
||||
test('toggleScreenshotWithImage should return true when called', () async {
|
||||
expect(await platform.toggleScreenshotWithImage(), isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'base NoScreenshotPlatform.toggleScreenshotWithImage() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.toggleScreenshotWithImage(),
|
||||
throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test('base NoScreenshotPlatform.screenshotOff() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.screenshotOff(), throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test('base NoScreenshotPlatform.screenshotOn() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.screenshotOn(), throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test(
|
||||
'base NoScreenshotPlatform.toggleScreenshot() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.toggleScreenshot(), throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test('base NoScreenshotPlatform.screenshotStream throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.screenshotStream, throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test(
|
||||
'base NoScreenshotPlatform.startScreenshotListening() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.startScreenshotListening(),
|
||||
throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test(
|
||||
'base NoScreenshotPlatform.stopScreenshotListening() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.stopScreenshotListening(),
|
||||
throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test(
|
||||
'startScreenRecordingListening should not throw UnimplementedError when called',
|
||||
() async {
|
||||
expect(platform.startScreenRecordingListening(), completes);
|
||||
});
|
||||
|
||||
test(
|
||||
'stopScreenRecordingListening should not throw UnimplementedError when called',
|
||||
() async {
|
||||
expect(platform.stopScreenRecordingListening(), completes);
|
||||
});
|
||||
|
||||
test(
|
||||
'base NoScreenshotPlatform.startScreenRecordingListening() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.startScreenRecordingListening(),
|
||||
throwsUnimplementedError);
|
||||
});
|
||||
|
||||
test(
|
||||
'base NoScreenshotPlatform.stopScreenRecordingListening() throws UnimplementedError',
|
||||
() {
|
||||
final basePlatform = BaseNoScreenshotPlatform();
|
||||
expect(() => basePlatform.stopScreenRecordingListening(),
|
||||
throwsUnimplementedError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ class MockNoScreenshotPlatform
|
|||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> toggleScreenshotWithImage() async {
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> toggleScreenshot() async {
|
||||
// Mock implementation or return a fixed value
|
||||
|
|
@ -38,6 +43,16 @@ class MockNoScreenshotPlatform
|
|||
Future<void> stopScreenshotListening() {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startScreenRecordingListening() {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopScreenRecordingListening() {
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
|
@ -86,10 +101,27 @@ void main() {
|
|||
expect(NoScreenshot.instance.stopScreenshotListening(), completes);
|
||||
});
|
||||
|
||||
test('toggleScreenshotWithImage', () async {
|
||||
expect(await NoScreenshot.instance.toggleScreenshotWithImage(), true);
|
||||
});
|
||||
|
||||
test('NoScreenshot equality operator', () {
|
||||
final instance1 = NoScreenshot.instance;
|
||||
final instance2 = NoScreenshot.instance;
|
||||
|
||||
expect(instance1 == instance2, true, reason: 'Instances should be equal');
|
||||
});
|
||||
|
||||
test('NoScreenshot hashCode consistency', () {
|
||||
final instance1 = NoScreenshot.instance;
|
||||
final instance2 = NoScreenshot.instance;
|
||||
|
||||
expect(instance1.hashCode, instance2.hashCode);
|
||||
});
|
||||
|
||||
test('deprecated constructor still works', () {
|
||||
// ignore: deprecated_member_use
|
||||
final instance = NoScreenshot();
|
||||
expect(instance, isA<NoScreenshot>());
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
export 'src/bit_buffer.dart';
|
||||
export 'src/byte.dart';
|
||||
export 'src/eci.dart';
|
||||
export 'src/ecivalue.dart';
|
||||
export 'src/error_correct_level.dart';
|
||||
export 'src/input_too_long_exception.dart';
|
||||
export 'src/mode.dart';
|
||||
export 'src/qr_code.dart';
|
||||
export 'src/qr_image.dart';
|
||||
|
|
|
|||
|
|
@ -1,34 +1,78 @@
|
|||
import 'dart:collection';
|
||||
|
||||
class QrBitBuffer extends Object with ListMixin<bool> {
|
||||
final List<int> _buffer;
|
||||
/// A growable sequence of bits.
|
||||
///
|
||||
/// Used internally to construct the data bit stream for a QR code.
|
||||
class QrBitBuffer extends Iterable<bool> {
|
||||
final _buffer = <int>[];
|
||||
int _length = 0;
|
||||
|
||||
QrBitBuffer() : _buffer = <int>[];
|
||||
|
||||
@override
|
||||
void operator []=(int index, bool value) =>
|
||||
throw UnsupportedError('cannot change');
|
||||
|
||||
@override
|
||||
bool operator [](int index) {
|
||||
final bufIndex = index ~/ 8;
|
||||
return ((_buffer[bufIndex] >> (7 - index % 8)) & 1) == 1;
|
||||
}
|
||||
QrBitBuffer();
|
||||
|
||||
@override
|
||||
int get length => _length;
|
||||
|
||||
@override
|
||||
set length(int value) => throw UnsupportedError('Cannot change');
|
||||
Iterator<bool> get iterator => _QrBitBufferIterator(this);
|
||||
|
||||
bool operator [](int index) {
|
||||
final bufIndex = index ~/ 8;
|
||||
return ((_buffer[bufIndex] >> (7 - index % 8)) & 1) == 1;
|
||||
}
|
||||
|
||||
int getByte(int index) => _buffer[index];
|
||||
|
||||
void put(int number, int length) {
|
||||
for (var i = 0; i < length; i++) {
|
||||
final bit = ((number >> (length - i - 1)) & 1) == 1;
|
||||
putBit(bit);
|
||||
if (length == 0) return;
|
||||
|
||||
var bitIndex = _length;
|
||||
final endBitIndex = bitIndex + length;
|
||||
|
||||
// Ensure capacity
|
||||
final neededBytes = (endBitIndex + 7) >> 3; // (endBitIndex + 7) ~/ 8
|
||||
while (_buffer.length < neededBytes) {
|
||||
_buffer.add(0);
|
||||
}
|
||||
|
||||
// Optimization for byte-aligned writes of 8 bits (common case)
|
||||
if (length == 8 && (bitIndex & 7) == 0 && number >= 0 && number <= 255) {
|
||||
_buffer[bitIndex >> 3] = number;
|
||||
_length = endBitIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic chunked write
|
||||
var bitsLeft = length;
|
||||
|
||||
while (bitsLeft > 0) {
|
||||
final bufIndex = bitIndex >> 3;
|
||||
final leftBitIndex = bitIndex & 7;
|
||||
final available = 8 - leftBitIndex;
|
||||
final bitsToWrite = bitsLeft < available ? bitsLeft : available;
|
||||
|
||||
// Extract the 'bitsToWrite' most significant bits from 'number'
|
||||
// Shift number right to move target bits to bottom
|
||||
// Mask them
|
||||
// Then allocate them to the byte buffer
|
||||
|
||||
final shift = bitsLeft - bitsToWrite;
|
||||
final bits = (number >> shift) & ((1 << bitsToWrite) - 1);
|
||||
|
||||
// Setup position in byte.
|
||||
// We want to write 'bits' starting at 'leftBitIndex'.
|
||||
// So we shift 'bits' left by (available - bitsToWrite)?
|
||||
// No, `leftBitIndex` is 0-7. 0 is MSB (0x80).
|
||||
// If leftBitIndex is 0, we write starting at 0x80.
|
||||
// If bitsToWrite is 8, we write 0xFF.
|
||||
// If 4 bits, we write 0xF0.
|
||||
// formula: bits << (8 - leftBitIndex - bitsToWrite)
|
||||
|
||||
final posShift = 8 - leftBitIndex - bitsToWrite;
|
||||
_buffer[bufIndex] |= bits << posShift;
|
||||
|
||||
bitsLeft -= bitsToWrite;
|
||||
bitIndex += bitsToWrite;
|
||||
}
|
||||
|
||||
_length = endBitIndex;
|
||||
}
|
||||
|
||||
void putBit(bool bit) {
|
||||
|
|
@ -43,4 +87,28 @@ class QrBitBuffer extends Object with ListMixin<bool> {
|
|||
|
||||
_length++;
|
||||
}
|
||||
|
||||
List<bool> getRange(int start, int end) {
|
||||
final list = <bool>[];
|
||||
for (var i = start; i < end; i++) {
|
||||
list.add(this[i]);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
class _QrBitBufferIterator implements Iterator<bool> {
|
||||
final QrBitBuffer _buffer;
|
||||
int _currentIndex = -1;
|
||||
|
||||
_QrBitBufferIterator(this._buffer);
|
||||
|
||||
@override
|
||||
bool get current => _buffer[_currentIndex];
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
_currentIndex++;
|
||||
return _currentIndex < _buffer.length;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,47 @@ import 'dart:convert';
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'bit_buffer.dart';
|
||||
import 'mode.dart' as qr_mode;
|
||||
import 'eci.dart';
|
||||
import 'mode.dart';
|
||||
|
||||
/// A piece of data to be encoded in a QR code.
|
||||
///
|
||||
/// Use [toDatums] to parse a string into optimal segments.
|
||||
abstract class QrDatum {
|
||||
int get mode;
|
||||
QrMode get mode;
|
||||
int get length;
|
||||
void write(QrBitBuffer buffer);
|
||||
|
||||
/// Parses [data] into a list of [QrDatum] segments, optimizing for the
|
||||
/// most efficient encoding modes (Numeric, Alphanumeric, Byte).
|
||||
///
|
||||
/// Automatically handles UTF-8 characters by using [QrEci] and [QrByte]
|
||||
/// segments if necessary.
|
||||
static List<QrDatum> toDatums(String data) {
|
||||
if (QrNumeric.validationRegex.hasMatch(data)) {
|
||||
return [QrNumeric.fromString(data)];
|
||||
}
|
||||
if (QrAlphaNumeric.validationRegex.hasMatch(data)) {
|
||||
return [QrAlphaNumeric.fromString(data)];
|
||||
}
|
||||
// Default to byte mode for other characters
|
||||
// Check if we need ECI (if there are chars > 255)
|
||||
// Actually, standard ISO-8859-1 is 0-255.
|
||||
// Emojis and other UTF-8 chars will definitely trigger this.
|
||||
final hasNonLatin1 = data.codeUnits.any((c) => c > 255);
|
||||
if (hasNonLatin1) {
|
||||
return [QrEci(26), QrByte(data)]; // UTF-8
|
||||
}
|
||||
return [QrByte(data)];
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents data encoded in Byte mode (8-bit).
|
||||
///
|
||||
/// Supports ISO-8859-1 and UTF-8 (when preceded by an ECI segment).
|
||||
class QrByte implements QrDatum {
|
||||
@override
|
||||
final int mode = qr_mode.mode8bitByte;
|
||||
final QrMode mode = QrMode.byte;
|
||||
final Uint8List _data;
|
||||
|
||||
factory QrByte(String input) =>
|
||||
|
|
@ -34,13 +64,20 @@ class QrByte implements QrDatum {
|
|||
}
|
||||
}
|
||||
|
||||
/// Encodes numbers (0-9) 10 bits per 3 digits.
|
||||
/// Encodes numeric data (digits 0-9).
|
||||
///
|
||||
/// Compresses 3 digits into 10 bits.
|
||||
/// Most efficient mode for decimal numbers.
|
||||
class QrNumeric implements QrDatum {
|
||||
static final RegExp validationRegex = RegExp(r'^[0-9]+$');
|
||||
|
||||
factory QrNumeric.fromString(String numberString) {
|
||||
if (!validationRegex.hasMatch(numberString)) {
|
||||
throw ArgumentError('string can only contain digits 0-9');
|
||||
throw ArgumentError.value(
|
||||
numberString,
|
||||
'numberString',
|
||||
'string can only contain digits 0-9',
|
||||
);
|
||||
}
|
||||
final newList = Uint8List(numberString.length);
|
||||
var count = 0;
|
||||
|
|
@ -55,7 +92,7 @@ class QrNumeric implements QrDatum {
|
|||
final Uint8List _data;
|
||||
|
||||
@override
|
||||
final int mode = qr_mode.modeNumber;
|
||||
final QrMode mode = QrMode.numeric;
|
||||
|
||||
@override
|
||||
void write(QrBitBuffer buffer) {
|
||||
|
|
@ -82,7 +119,10 @@ class QrNumeric implements QrDatum {
|
|||
int get length => _data.length;
|
||||
}
|
||||
|
||||
/// Encodes numbers (0-9) 10 bits per 3 digits.
|
||||
/// Encodes alphanumeric data (uppercase letters, digits, and specific symbols).
|
||||
///
|
||||
/// Supported characters: 0-9, A-Z, space, $, %, *, +, -, ., /, :
|
||||
/// Compresses 2 characters into 11 bits.
|
||||
class QrAlphaNumeric implements QrDatum {
|
||||
static const alphaNumTable = r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
||||
// Note: '-' anywhere in this string is a range character.
|
||||
|
|
@ -102,9 +142,10 @@ class QrAlphaNumeric implements QrDatum {
|
|||
|
||||
factory QrAlphaNumeric.fromString(String alphaNumeric) {
|
||||
if (!alphaNumeric.contains(validationRegex)) {
|
||||
throw ArgumentError(
|
||||
'String does not contain valid ALPHA-NUM '
|
||||
'character set: $alphaNumeric',
|
||||
throw ArgumentError.value(
|
||||
alphaNumeric,
|
||||
'alphaNumeric',
|
||||
'String does not contain valid ALPHA-NUM character set',
|
||||
);
|
||||
}
|
||||
return QrAlphaNumeric._(alphaNumeric);
|
||||
|
|
@ -113,7 +154,7 @@ class QrAlphaNumeric implements QrDatum {
|
|||
QrAlphaNumeric._(this._string);
|
||||
|
||||
@override
|
||||
final int mode = qr_mode.modeAlphaNum;
|
||||
final QrMode mode = QrMode.alphaNumeric;
|
||||
|
||||
@override
|
||||
void write(QrBitBuffer buffer) {
|
||||
|
|
|
|||
40
qr/lib/src/eci.dart
Normal file
40
qr/lib/src/eci.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import 'bit_buffer.dart';
|
||||
import 'byte.dart';
|
||||
|
||||
import 'mode.dart';
|
||||
|
||||
/// Extended Channel Interpretation (ECI) mode data.
|
||||
///
|
||||
/// Use this to specify a different character encoding for the following data.
|
||||
class QrEci implements QrDatum {
|
||||
final int value;
|
||||
|
||||
factory QrEci(int value) {
|
||||
if (value < 0 || value > 999999) {
|
||||
throw RangeError.range(value, 0, 999999, 'value');
|
||||
}
|
||||
return QrEci._(value);
|
||||
}
|
||||
|
||||
QrEci._(this.value);
|
||||
|
||||
@override
|
||||
QrMode get mode => QrMode.eci;
|
||||
|
||||
@override
|
||||
int get length => 0; // ECI segments do not have a length field
|
||||
|
||||
@override
|
||||
void write(QrBitBuffer buffer) {
|
||||
if (value < 128) {
|
||||
// 0xxxxxxx
|
||||
buffer.put(value, 8);
|
||||
} else if (value < 16384) {
|
||||
// 10xxxxxx xxxxxxxx
|
||||
buffer.put(0x8000 | value, 16);
|
||||
} else {
|
||||
// 110xxxxx xxxxxxxx xxxxxxxx
|
||||
buffer.put(0xC00000 | value, 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
qr/lib/src/ecivalue.dart
Normal file
87
qr/lib/src/ecivalue.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/// ECI value for QR Codes.
|
||||
///
|
||||
/// This extension type provides constants for common ECI values.
|
||||
///
|
||||
/// See: https://github.com/zxing/zxing/blob/master/core/src/main/java/com/google/zxing/common/CharacterSetECI.java
|
||||
extension type const QrEciValue(int value) implements int {
|
||||
/// ISO-8859-1 (Latin-1). Default encoding.
|
||||
static const iso8859_1 = QrEciValue(3);
|
||||
|
||||
/// ISO-8859-2 (Latin-2).
|
||||
static const iso8859_2 = QrEciValue(4);
|
||||
|
||||
/// ISO-8859-3 (Latin-3).
|
||||
static const iso8859_3 = QrEciValue(5);
|
||||
|
||||
/// ISO-8859-4 (Latin-4).
|
||||
static const iso8859_4 = QrEciValue(6);
|
||||
|
||||
/// ISO-8859-5 (Latin/Cyrillic).
|
||||
static const iso8859_5 = QrEciValue(7);
|
||||
|
||||
/// ISO-8859-6 (Latin/Arabic).
|
||||
static const iso8859_6 = QrEciValue(8);
|
||||
|
||||
/// ISO-8859-7 (Latin/Greek).
|
||||
static const iso8859_7 = QrEciValue(9);
|
||||
|
||||
/// ISO-8859-8 (Latin/Hebrew).
|
||||
static const iso8859_8 = QrEciValue(10);
|
||||
|
||||
/// ISO-8859-9 (Latin-5).
|
||||
static const iso8859_9 = QrEciValue(11);
|
||||
|
||||
/// ISO-8859-10 (Latin-6).
|
||||
static const iso8859_10 = QrEciValue(12);
|
||||
|
||||
/// ISO-8859-11 (Latin/Thai).
|
||||
static const iso8859_11 = QrEciValue(13);
|
||||
|
||||
/// ISO-8859-13 (Latin-7).
|
||||
static const iso8859_13 = QrEciValue(15);
|
||||
|
||||
/// ISO-8859-14 (Latin-8).
|
||||
static const iso8859_14 = QrEciValue(16);
|
||||
|
||||
/// ISO-8859-15 (Latin-9).
|
||||
static const iso8859_15 = QrEciValue(17);
|
||||
|
||||
/// ISO-8859-16 (Latin-10).
|
||||
static const iso8859_16 = QrEciValue(18);
|
||||
|
||||
/// Shift JIS.
|
||||
static const shiftJis = QrEciValue(20);
|
||||
|
||||
/// Windows-1250 (Latin-2).
|
||||
static const windows1250 = QrEciValue(21);
|
||||
|
||||
/// Windows-1251 (Cyrillic).
|
||||
static const windows1251 = QrEciValue(22);
|
||||
|
||||
/// Windows-1252 (Latin-1).
|
||||
static const windows1252 = QrEciValue(23);
|
||||
|
||||
/// Windows-1256 (Arabic).
|
||||
static const windows1256 = QrEciValue(24);
|
||||
|
||||
/// UTF-16 (Big Endian).
|
||||
static const utf16BE = QrEciValue(25);
|
||||
|
||||
/// UTF-8.
|
||||
static const utf8 = QrEciValue(26);
|
||||
|
||||
/// US-ASCII.
|
||||
static const ascii = QrEciValue(27);
|
||||
|
||||
/// Big5.
|
||||
static const big5 = QrEciValue(28);
|
||||
|
||||
/// GB 2312.
|
||||
static const gb2312 = QrEciValue(29);
|
||||
|
||||
/// EUC-KR.
|
||||
static const eucKr = QrEciValue(30);
|
||||
|
||||
/// GBK.
|
||||
static const gbk = QrEciValue(31);
|
||||
}
|
||||
|
|
@ -1,20 +1,27 @@
|
|||
// ignore: avoid_classes_with_only_static_members
|
||||
class QrErrorCorrectLevel {
|
||||
static const int L = 1;
|
||||
static const int M = 0;
|
||||
static const int Q = 3;
|
||||
static const int H = 2;
|
||||
/// QR Code error correction level.
|
||||
///
|
||||
/// Recover capacity:
|
||||
/// * [low] : ~7%
|
||||
/// * [medium] : ~15%
|
||||
/// * [quartile] : ~25%
|
||||
/// * [high] : ~30%
|
||||
enum QrErrorCorrectLevel {
|
||||
// NOTE: the order here MATTERS.
|
||||
// The index maps to the QR standard.
|
||||
|
||||
// thesee *are* in order of lowest to highest quality...I think
|
||||
// all I know for sure: you can create longer messages w/ item N than N+1
|
||||
// I assume this correcsponds to more error correction for N+1
|
||||
static const List<int> levels = [L, M, Q, H];
|
||||
/// Level M (Medium) ~15% error correction.
|
||||
medium(15),
|
||||
|
||||
static String getName(int level) => switch (level) {
|
||||
L => 'Low',
|
||||
M => 'Medium',
|
||||
Q => 'Quartile',
|
||||
H => 'High',
|
||||
_ => throw ArgumentError('level $level not supported'),
|
||||
};
|
||||
/// Level L (Low) ~7% error correction.
|
||||
low(7),
|
||||
|
||||
/// Level H (High) ~30% error correction.
|
||||
high(30),
|
||||
|
||||
/// Level Q (Quartile) ~25% error correction.
|
||||
quartile(25);
|
||||
|
||||
final int recoveryRate;
|
||||
|
||||
const QrErrorCorrectLevel(this.recoveryRate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,25 @@
|
|||
const int pattern000 = 0;
|
||||
const int pattern001 = 1;
|
||||
const int pattern010 = 2;
|
||||
const int pattern011 = 3;
|
||||
const int pattern100 = 4;
|
||||
const int pattern101 = 5;
|
||||
const int pattern110 = 6;
|
||||
const int pattern111 = 7;
|
||||
enum QrMaskPattern {
|
||||
pattern000(_check000),
|
||||
pattern001(_check001),
|
||||
pattern010(_check010),
|
||||
pattern011(_check011),
|
||||
pattern100(_check100),
|
||||
pattern101(_check101),
|
||||
pattern110(_check110),
|
||||
pattern111(_check111);
|
||||
|
||||
final bool Function(int i, int j) _check;
|
||||
|
||||
const QrMaskPattern(this._check);
|
||||
|
||||
bool check(int i, int j) => _check(i, j);
|
||||
}
|
||||
|
||||
bool _check000(int i, int j) => (i + j).isEven;
|
||||
bool _check001(int i, int j) => i.isEven;
|
||||
bool _check010(int i, int j) => j % 3 == 0;
|
||||
bool _check011(int i, int j) => (i + j) % 3 == 0;
|
||||
bool _check100(int i, int j) => ((i ~/ 2) + (j ~/ 3)).isEven;
|
||||
bool _check101(int i, int j) => ((i * j) % 2 + (i * j) % 3) == 0;
|
||||
bool _check110(int i, int j) => (((i * j) % 2) + ((i * j) % 3)).isEven;
|
||||
bool _check111(int i, int j) => (((i * j) % 3) + ((i + j) % 2)).isEven;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import 'dart:typed_data';
|
|||
final Uint8List _logTable = _createLogTable();
|
||||
final Uint8List _expTable = _createExpTable();
|
||||
|
||||
int glog(int n) => (n >= 1) ? _logTable[n] : throw ArgumentError('glog($n)');
|
||||
int glog(int n) =>
|
||||
(n >= 1) ? _logTable[n] : throw ArgumentError.value(n, 'n', 'must be >= 1');
|
||||
|
||||
int gexp(int n) => _expTable[n % 255];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,55 @@
|
|||
const int modeNumber = 1 << 0;
|
||||
const int modeAlphaNum = 1 << 1;
|
||||
const int mode8bitByte = 1 << 2;
|
||||
const int modeKanji = 1 << 3;
|
||||
/// The encoding mode of a QR code segment.
|
||||
enum QrMode {
|
||||
/// Numeric mode (0-9). Most efficient.
|
||||
numeric(1),
|
||||
|
||||
/// Alphanumeric mode (0-9, A-Z, space, %, *, +, -, ., /, :).
|
||||
alphaNumeric(2),
|
||||
|
||||
/// Byte mode (8-bit data).
|
||||
byte(4),
|
||||
|
||||
/// Kanji mode (Shift-JIS).
|
||||
kanji(8),
|
||||
|
||||
/// Extended Channel Interpretation (ECI) mode.
|
||||
eci(7);
|
||||
|
||||
final int value;
|
||||
|
||||
const QrMode(this.value);
|
||||
|
||||
int getLengthBits(int type) {
|
||||
if (this == eci) return 0;
|
||||
if (type < 1 || type > 40) throw RangeError.range(type, 1, 40, 'type');
|
||||
|
||||
if (type < 10) {
|
||||
// 1 - 9
|
||||
return switch (this) {
|
||||
numeric => 10,
|
||||
alphaNumeric => 9,
|
||||
byte => 8,
|
||||
kanji => 8,
|
||||
eci => 0,
|
||||
};
|
||||
} else if (type < 27) {
|
||||
// 10 - 26
|
||||
return switch (this) {
|
||||
numeric => 12,
|
||||
alphaNumeric => 11,
|
||||
byte => 16,
|
||||
kanji => 10,
|
||||
eci => 0,
|
||||
};
|
||||
} else {
|
||||
// 27 - 40
|
||||
return switch (this) {
|
||||
numeric => 14,
|
||||
alphaNumeric => 13,
|
||||
byte => 16,
|
||||
kanji => 12,
|
||||
eci => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,36 +28,75 @@ class QrPolynomial {
|
|||
int get length => _values.length;
|
||||
|
||||
QrPolynomial multiply(QrPolynomial e) {
|
||||
final List<int> foo = Uint8List(length + e.length - 1);
|
||||
final foo = Uint8List(length + e.length - 1);
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
final v1 = _values[i];
|
||||
if (v1 == 0) continue;
|
||||
final log1 = qr_math.glog(v1);
|
||||
for (var j = 0; j < e.length; j++) {
|
||||
foo[i + j] ^= qr_math.gexp(qr_math.glog(this[i]) + qr_math.glog(e[j]));
|
||||
final v2 = e[j];
|
||||
if (v2 == 0) continue;
|
||||
foo[i + j] ^= qr_math.gexp(log1 + qr_math.glog(v2));
|
||||
}
|
||||
}
|
||||
|
||||
return QrPolynomial(foo, 0);
|
||||
return QrPolynomial._internal(foo);
|
||||
}
|
||||
|
||||
QrPolynomial mod(QrPolynomial e) {
|
||||
if (length - e.length < 0) {
|
||||
// ignore: avoid_returning_this
|
||||
return this;
|
||||
}
|
||||
|
||||
final ratio = qr_math.glog(this[0]) - qr_math.glog(e[0]);
|
||||
// Use a copy of _values that we will mutate
|
||||
// We only need the part that will remain after modulo?
|
||||
// Actually, standard polynomial division.
|
||||
// We can work on a copy of `this._values` and zero out leading terms.
|
||||
|
||||
final value = Uint8List(length);
|
||||
final values = Uint8List.fromList(_values);
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
value[i] = this[i];
|
||||
for (var i = 0; i < values.length - e.length + 1; i++) {
|
||||
final v = values[i];
|
||||
if (v == 0) continue;
|
||||
|
||||
final ratio = qr_math.glog(v) - qr_math.glog(e[0]);
|
||||
|
||||
for (var j = 0; j < e.length; j++) {
|
||||
final eVal = e[j];
|
||||
if (eVal == 0) continue;
|
||||
values[i + j] ^= qr_math.gexp(qr_math.glog(eVal) + ratio);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < e.length; i++) {
|
||||
value[i] ^= qr_math.gexp(qr_math.glog(e[i]) + ratio);
|
||||
}
|
||||
// The result is the remainder, which is the last e.length - 1 coefficients?
|
||||
// Wait, the degree of remainder is less than degree of divisor (e).
|
||||
// e.length is e.degree + 1.
|
||||
// So remainder length is e.length - 1.
|
||||
|
||||
// recursive call
|
||||
return QrPolynomial(value, 0).mod(e);
|
||||
// Find where the remainder starts.
|
||||
// In the loop above, we zeroed out terms from 0 to
|
||||
// `values.length - e.length`.
|
||||
// So the remainder starts at values.length - e.length + 1?
|
||||
// No, we iterated i from 0 to diff.
|
||||
// The loop eliminates the term at `i`.
|
||||
// The last `i` is `values.length - e.length`.
|
||||
// After that, the terms from `0` to `values.length - e.length` should be 0.
|
||||
// The remainder is at the end.
|
||||
|
||||
// Note: The original implementation used `offset` to skip leading zeros.
|
||||
// `offset` increased when `values[offset] == 0`.
|
||||
// My loop enforces `values[i]` becomes 0 (arithmetically, though likely not
|
||||
// exactly 0 due to XOR, wait XOR equal things is 0).
|
||||
|
||||
// Let's manually increment offset to match original logic if needed,
|
||||
// or just slice the end.
|
||||
// The remainder should fit in e.length - 1.
|
||||
|
||||
// We can just return the tail.
|
||||
// But we need to handle leading zeros in the result too?
|
||||
// `QrPolynomial` constructor handles leading zeros.
|
||||
|
||||
return QrPolynomial(values.sublist(values.length - e.length + 1), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,66 +5,61 @@ import 'package:meta/meta.dart';
|
|||
|
||||
import 'bit_buffer.dart';
|
||||
import 'byte.dart';
|
||||
import 'eci.dart';
|
||||
import 'error_correct_level.dart';
|
||||
import 'input_too_long_exception.dart';
|
||||
import 'math.dart' as qr_math;
|
||||
import 'mode.dart' as qr_mode;
|
||||
|
||||
import 'polynomial.dart';
|
||||
import 'rs_block.dart';
|
||||
|
||||
class QrCode {
|
||||
final int typeNumber;
|
||||
final int errorCorrectLevel;
|
||||
final QrErrorCorrectLevel errorCorrectLevel;
|
||||
final int moduleCount;
|
||||
List<int>? _dataCache;
|
||||
final _dataList = <QrDatum>[];
|
||||
|
||||
QrCode(this.typeNumber, this.errorCorrectLevel)
|
||||
: moduleCount = typeNumber * 4 + 17 {
|
||||
// The typeNumber is now calculated internally by the factories,
|
||||
// so this check is only needed if QrCode is instantiated directly.
|
||||
// However, the factories ensure a valid typeNumber is passed.
|
||||
// Keeping it for direct instantiation safety.
|
||||
RangeError.checkValueInInterval(typeNumber, 1, 40, 'typeNumber');
|
||||
RangeError.checkValidIndex(
|
||||
errorCorrectLevel,
|
||||
QrErrorCorrectLevel.levels,
|
||||
'errorCorrectLevel',
|
||||
);
|
||||
}
|
||||
|
||||
factory QrCode.fromData({
|
||||
required String data,
|
||||
required int errorCorrectLevel,
|
||||
required QrErrorCorrectLevel errorCorrectLevel,
|
||||
}) {
|
||||
final QrDatum datum;
|
||||
// Automatically determine mode here
|
||||
if (QrNumeric.validationRegex.hasMatch(data)) {
|
||||
// Numeric mode for numbers only
|
||||
datum = QrNumeric.fromString(data);
|
||||
} else if (QrAlphaNumeric.validationRegex.hasMatch(data)) {
|
||||
// Alphanumeric mode for alphanumeric characters only
|
||||
datum = QrAlphaNumeric.fromString(data);
|
||||
} else {
|
||||
// Default to byte mode for other characters
|
||||
datum = QrByte(data);
|
||||
final datumList = QrDatum.toDatums(data);
|
||||
|
||||
final typeNumber = _calculateTypeNumberFromData(
|
||||
errorCorrectLevel,
|
||||
datumList,
|
||||
);
|
||||
|
||||
final qrCode = QrCode(typeNumber, errorCorrectLevel);
|
||||
for (final datum in datumList) {
|
||||
qrCode._addToList(datum);
|
||||
}
|
||||
|
||||
final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, datum);
|
||||
|
||||
final qrCode = QrCode(typeNumber, errorCorrectLevel).._addToList(datum);
|
||||
return qrCode;
|
||||
}
|
||||
|
||||
factory QrCode.fromUint8List({
|
||||
required Uint8List data,
|
||||
required int errorCorrectLevel,
|
||||
required QrErrorCorrectLevel errorCorrectLevel,
|
||||
}) {
|
||||
final typeNumber = _calculateTypeNumberFromData(
|
||||
errorCorrectLevel,
|
||||
QrByte.fromUint8List(data),
|
||||
);
|
||||
return QrCode(typeNumber, errorCorrectLevel)
|
||||
.._addToList(QrByte.fromUint8List(data));
|
||||
final datum = QrByte.fromUint8List(data);
|
||||
final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, [datum]);
|
||||
return QrCode(typeNumber, errorCorrectLevel).._addToList(datum);
|
||||
}
|
||||
|
||||
static int _calculateTotalDataBits(int typeNumber, int errorCorrectLevel) {
|
||||
static int _calculateTotalDataBits(
|
||||
int typeNumber,
|
||||
QrErrorCorrectLevel errorCorrectLevel,
|
||||
) {
|
||||
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
|
||||
var totalDataBits = 0;
|
||||
for (var rsBlock in rsBlocks) {
|
||||
|
|
@ -73,26 +68,35 @@ class QrCode {
|
|||
return totalDataBits;
|
||||
}
|
||||
|
||||
static int _calculateTypeNumberFromData(int errorCorrectLevel, QrDatum data) {
|
||||
static int _calculateTypeNumberFromData(
|
||||
QrErrorCorrectLevel errorCorrectLevel,
|
||||
List<QrDatum> data,
|
||||
) {
|
||||
for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {
|
||||
final totalDataBits = _calculateTotalDataBits(
|
||||
typeNumber,
|
||||
errorCorrectLevel,
|
||||
);
|
||||
|
||||
final buffer = QrBitBuffer()
|
||||
..put(data.mode, 4)
|
||||
..put(data.length, _lengthInBits(data.mode, typeNumber));
|
||||
data.write(buffer);
|
||||
final buffer = QrBitBuffer();
|
||||
for (final datum in data) {
|
||||
buffer
|
||||
..put(datum.mode.value, 4)
|
||||
..put(datum.length, datum.mode.getLengthBits(typeNumber));
|
||||
datum.write(buffer);
|
||||
}
|
||||
|
||||
if (buffer.length <= totalDataBits) return typeNumber;
|
||||
}
|
||||
|
||||
// If we reach here, the data is too long for any QR Code version.
|
||||
final buffer = QrBitBuffer()
|
||||
..put(data.mode, 4)
|
||||
..put(data.length, _lengthInBits(data.mode, 40));
|
||||
data.write(buffer);
|
||||
final buffer = QrBitBuffer();
|
||||
for (final datum in data) {
|
||||
buffer
|
||||
..put(datum.mode.value, 4)
|
||||
..put(datum.length, datum.mode.getLengthBits(40));
|
||||
datum.write(buffer);
|
||||
}
|
||||
|
||||
final maxBits = _calculateTotalDataBits(40, errorCorrectLevel);
|
||||
|
||||
|
|
@ -100,19 +104,9 @@ class QrCode {
|
|||
}
|
||||
|
||||
void addData(String data) {
|
||||
final QrDatum datum;
|
||||
// Automatically determine mode here, just like QrCode.fromData
|
||||
if (QrNumeric.validationRegex.hasMatch(data)) {
|
||||
// Numeric mode for numbers only
|
||||
datum = QrNumeric.fromString(data);
|
||||
} else if (QrAlphaNumeric.validationRegex.hasMatch(data)) {
|
||||
// Alphanumeric mode for alphanumeric characters only
|
||||
datum = QrAlphaNumeric.fromString(data);
|
||||
} else {
|
||||
// Default to byte mode for other characters
|
||||
datum = QrByte(data);
|
||||
for (final datum in QrDatum.toDatums(data)) {
|
||||
_addToList(datum);
|
||||
}
|
||||
_addToList(datum);
|
||||
}
|
||||
|
||||
void addByteData(ByteData data) => _addToList(QrByte.fromByteData(data));
|
||||
|
|
@ -127,6 +121,8 @@ class QrCode {
|
|||
void addAlphaNumeric(String alphaNumeric) =>
|
||||
_addToList(QrAlphaNumeric.fromString(alphaNumeric));
|
||||
|
||||
void addECI(int eciValue) => _addToList(QrEci(eciValue));
|
||||
|
||||
void _addToList(QrDatum data) {
|
||||
_dataList.add(data);
|
||||
_dataCache = null;
|
||||
|
|
@ -142,7 +138,7 @@ const int _pad1 = 0x11;
|
|||
|
||||
List<int> _createData(
|
||||
int typeNumber,
|
||||
int errorCorrectLevel,
|
||||
QrErrorCorrectLevel errorCorrectLevel,
|
||||
List<QrDatum> dataList,
|
||||
) {
|
||||
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
|
||||
|
|
@ -152,8 +148,8 @@ List<int> _createData(
|
|||
for (var i = 0; i < dataList.length; i++) {
|
||||
final data = dataList[i];
|
||||
buffer
|
||||
..put(data.mode, 4)
|
||||
..put(data.length, _lengthInBits(data.mode, typeNumber));
|
||||
..put(data.mode.value, 4)
|
||||
..put(data.length, data.mode.getLengthBits(typeNumber));
|
||||
data.write(buffer);
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +160,10 @@ List<int> _createData(
|
|||
errorCorrectLevel,
|
||||
);
|
||||
|
||||
if (buffer.length > totalDataBits) {
|
||||
throw InputTooLongException(buffer.length, totalDataBits);
|
||||
}
|
||||
|
||||
// HUH?
|
||||
// èIí[ÉRÅ[Éh
|
||||
if (buffer.length + 4 <= totalDataBits) {
|
||||
|
|
@ -244,39 +244,6 @@ List<int> _createBytes(QrBitBuffer buffer, List<QrRsBlock> rsBlocks) {
|
|||
return data;
|
||||
}
|
||||
|
||||
int _lengthInBits(int mode, int type) {
|
||||
if (1 <= type && type < 10) {
|
||||
// 1 - 9
|
||||
return switch (mode) {
|
||||
qr_mode.modeNumber => 10,
|
||||
qr_mode.modeAlphaNum => 9,
|
||||
qr_mode.mode8bitByte => 8,
|
||||
qr_mode.modeKanji => 8,
|
||||
_ => throw ArgumentError('mode:$mode'),
|
||||
};
|
||||
} else if (type < 27) {
|
||||
// 10 - 26
|
||||
return switch (mode) {
|
||||
qr_mode.modeNumber => 12,
|
||||
qr_mode.modeAlphaNum => 11,
|
||||
qr_mode.mode8bitByte => 16,
|
||||
qr_mode.modeKanji => 10,
|
||||
_ => throw ArgumentError('mode:$mode'),
|
||||
};
|
||||
} else if (type < 41) {
|
||||
// 27 - 40
|
||||
return switch (mode) {
|
||||
qr_mode.modeNumber => 14,
|
||||
qr_mode.modeAlphaNum => 13,
|
||||
qr_mode.mode8bitByte => 16,
|
||||
qr_mode.modeKanji => 12,
|
||||
_ => throw ArgumentError('mode:$mode'),
|
||||
};
|
||||
} else {
|
||||
throw ArgumentError('type:$type');
|
||||
}
|
||||
}
|
||||
|
||||
QrPolynomial _errorCorrectPolynomial(int errorCorrectLength) {
|
||||
var a = QrPolynomial([1], 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,72 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'mask_pattern.dart' as qr_mask_pattern;
|
||||
import 'error_correct_level.dart';
|
||||
import 'mask_pattern.dart';
|
||||
import 'qr_code.dart';
|
||||
import 'util.dart' as qr_util;
|
||||
|
||||
/// Renders the encoded data from a [QrCode] in a portable format.
|
||||
class QrImage {
|
||||
static const _pixelUnassigned = 0;
|
||||
static const _pixelLight = 1;
|
||||
static const _pixelDark = 2;
|
||||
|
||||
final int moduleCount;
|
||||
final int typeNumber;
|
||||
final int errorCorrectLevel;
|
||||
final QrErrorCorrectLevel errorCorrectLevel;
|
||||
final int maskPattern;
|
||||
|
||||
final _modules = <List<bool?>>[];
|
||||
final Uint8List _data;
|
||||
|
||||
/// Generates a QrImage with the best mask pattern encoding [qrCode].
|
||||
factory QrImage(QrCode qrCode) {
|
||||
var minLostPoint = 0.0;
|
||||
QrImage? bestImage;
|
||||
// Create a template with invariant patterns
|
||||
final template = QrImage._template(qrCode);
|
||||
final moduleCount = template.moduleCount;
|
||||
final dataSize = moduleCount * moduleCount;
|
||||
|
||||
// Step 1: Clone template to working buffer and place data (no mask)
|
||||
final dataMap = Uint8List(dataSize)..setRange(0, dataSize, template._data);
|
||||
|
||||
// Create a temporary QrImage to use its _placeData method
|
||||
// We pass 0 as maskPattern, but we will modify _placeData to NOT mask.
|
||||
QrImage._fromData(qrCode, 0, dataMap)._placeData(qrCode.dataCache);
|
||||
|
||||
final workingBuffer = Uint8List(dataSize);
|
||||
var minLostPoint = double.maxFinite;
|
||||
var bestMaskPattern = 0;
|
||||
Uint8List? bestData; // We need to store the best result.
|
||||
|
||||
// Step 2: Try all 8 masks
|
||||
for (var i = 0; i < 8; i++) {
|
||||
final testImage = QrImage._test(qrCode, i);
|
||||
// Copy pre-placed data to working buffer
|
||||
workingBuffer.setRange(0, dataSize, dataMap);
|
||||
|
||||
final testImage = QrImage._fromData(qrCode, i, workingBuffer)
|
||||
// Apply mask (XOR)
|
||||
.._applyMask(QrMaskPattern.values[i], template._data);
|
||||
|
||||
final lostPoint = _lostPoint(testImage);
|
||||
|
||||
if (i == 0 || minLostPoint > lostPoint) {
|
||||
if (lostPoint < minLostPoint) {
|
||||
minLostPoint = lostPoint;
|
||||
bestImage = testImage;
|
||||
bestMaskPattern = i;
|
||||
// Copy working buffer to bestData
|
||||
bestData ??= Uint8List(dataSize);
|
||||
bestData.setRange(0, dataSize, workingBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
return QrImage.withMaskPattern(qrCode, bestImage!.maskPattern);
|
||||
final finalImage = QrImage._fromData(qrCode, bestMaskPattern, bestData!)
|
||||
// Final setup with correct format info (not test, so actual pixels)
|
||||
.._setupTypeInfo(bestMaskPattern, false);
|
||||
if (finalImage.typeNumber >= 7) {
|
||||
finalImage._setupTypeNumber(false);
|
||||
}
|
||||
|
||||
return finalImage;
|
||||
}
|
||||
|
||||
/// Generates a specific image for the [qrCode] and [maskPattern].
|
||||
|
|
@ -36,35 +74,75 @@ class QrImage {
|
|||
: assert(maskPattern >= 0 && maskPattern <= 7),
|
||||
moduleCount = qrCode.moduleCount,
|
||||
typeNumber = qrCode.typeNumber,
|
||||
errorCorrectLevel = qrCode.errorCorrectLevel {
|
||||
errorCorrectLevel = qrCode.errorCorrectLevel,
|
||||
_data = Uint8List(qrCode.moduleCount * qrCode.moduleCount) {
|
||||
_makeImpl(maskPattern, qrCode.dataCache, false);
|
||||
}
|
||||
|
||||
QrImage._test(QrCode qrCode, this.maskPattern)
|
||||
/// Internal constructor for template creation
|
||||
QrImage._template(QrCode qrCode)
|
||||
: moduleCount = qrCode.moduleCount,
|
||||
typeNumber = qrCode.typeNumber,
|
||||
errorCorrectLevel = qrCode.errorCorrectLevel {
|
||||
_makeImpl(maskPattern, qrCode.dataCache, true);
|
||||
errorCorrectLevel = qrCode.errorCorrectLevel,
|
||||
maskPattern = 0, // Irrelevant
|
||||
_data = Uint8List(qrCode.moduleCount * qrCode.moduleCount) {
|
||||
// Setup invariant parts with test=true (reserving space)
|
||||
_resetModules();
|
||||
_setupPositionProbePattern(0, 0);
|
||||
_setupPositionProbePattern(moduleCount - 7, 0);
|
||||
_setupPositionProbePattern(0, moduleCount - 7);
|
||||
_setupPositionAdjustPattern();
|
||||
_setupTimingPattern();
|
||||
// Type info and Type number are invariant if test=true (all light)
|
||||
_setupTypeInfo(0, true);
|
||||
if (typeNumber >= 7) {
|
||||
_setupTypeNumber(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal constructor for testing phase
|
||||
QrImage._fromData(QrCode qrCode, this.maskPattern, this._data)
|
||||
: moduleCount = qrCode.moduleCount,
|
||||
typeNumber = qrCode.typeNumber,
|
||||
errorCorrectLevel = qrCode.errorCorrectLevel;
|
||||
|
||||
@visibleForTesting
|
||||
List<List<bool?>> get qrModules => _modules;
|
||||
List<List<bool?>> get qrModules {
|
||||
final list = <List<bool?>>[];
|
||||
for (var r = 0; r < moduleCount; r++) {
|
||||
final row = List<bool?>.filled(moduleCount, null);
|
||||
for (var c = 0; c < moduleCount; c++) {
|
||||
final v = _data[r * moduleCount + c];
|
||||
row[c] = v == _pixelUnassigned ? null : (v == _pixelDark);
|
||||
}
|
||||
list.add(row);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
void _resetModules() {
|
||||
_modules.clear();
|
||||
for (var row = 0; row < moduleCount; row++) {
|
||||
_modules.add(List<bool?>.filled(moduleCount, null));
|
||||
}
|
||||
_data.fillRange(0, _data.length, _pixelUnassigned);
|
||||
}
|
||||
|
||||
bool isDark(int row, int col) {
|
||||
if (row < 0 || moduleCount <= row || col < 0 || moduleCount <= col) {
|
||||
throw ArgumentError('$row , $col');
|
||||
if (row < 0 || moduleCount <= row) {
|
||||
throw RangeError.range(row, 0, moduleCount - 1, 'row');
|
||||
}
|
||||
return _modules[row][col]!;
|
||||
if (col < 0 || moduleCount <= col) {
|
||||
throw RangeError.range(col, 0, moduleCount - 1, 'col');
|
||||
}
|
||||
return _data[row * moduleCount + col] == _pixelDark;
|
||||
}
|
||||
|
||||
void _set(int row, int col, bool value) {
|
||||
_data[row * moduleCount + col] = value ? _pixelDark : _pixelLight;
|
||||
}
|
||||
|
||||
void _makeImpl(int maskPattern, List<int> dataCache, bool test) {
|
||||
// If not testing, we do full setup.
|
||||
// If testing (template), this method is NOT called directly, but manually
|
||||
// in _template.
|
||||
// However, withMaskPattern calls this.
|
||||
_resetModules();
|
||||
_setupPositionProbePattern(0, 0);
|
||||
_setupPositionProbePattern(moduleCount - 7, 0);
|
||||
|
|
@ -80,6 +158,13 @@ class QrImage {
|
|||
_mapData(dataCache, maskPattern);
|
||||
}
|
||||
|
||||
// ... (existing constructors)
|
||||
|
||||
// Refactored _mapData to JUST call _placeData then _applyMask?
|
||||
// No, original _mapData did both.
|
||||
|
||||
// Implemented below...
|
||||
|
||||
void _setupPositionProbePattern(int row, int col) {
|
||||
for (var r = -1; r <= 7; r++) {
|
||||
if (row + r <= -1 || moduleCount <= row + r) continue;
|
||||
|
|
@ -90,9 +175,9 @@ class QrImage {
|
|||
if ((0 <= r && r <= 6 && (c == 0 || c == 6)) ||
|
||||
(0 <= c && c <= 6 && (r == 0 || r == 6)) ||
|
||||
(2 <= r && r <= 4 && 2 <= c && c <= 4)) {
|
||||
_modules[row + r][col + c] = true;
|
||||
_set(row + r, col + c, true);
|
||||
} else {
|
||||
_modules[row + r][col + c] = false;
|
||||
_set(row + r, col + c, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,16 +191,16 @@ class QrImage {
|
|||
final row = pos[i];
|
||||
final col = pos[j];
|
||||
|
||||
if (_modules[row][col] != null) {
|
||||
if (_data[row * moduleCount + col] != _pixelUnassigned) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var r = -2; r <= 2; r++) {
|
||||
for (var c = -2; c <= 2; c++) {
|
||||
if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
|
||||
_modules[row + r][col + c] = true;
|
||||
_set(row + r, col + c, true);
|
||||
} else {
|
||||
_modules[row + r][col + c] = false;
|
||||
_set(row + r, col + c, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,22 +210,22 @@ class QrImage {
|
|||
|
||||
void _setupTimingPattern() {
|
||||
for (var r = 8; r < moduleCount - 8; r++) {
|
||||
if (_modules[r][6] != null) {
|
||||
if (_data[r * moduleCount + 6] != _pixelUnassigned) {
|
||||
continue;
|
||||
}
|
||||
_modules[r][6] = r.isEven;
|
||||
_set(r, 6, r.isEven);
|
||||
}
|
||||
|
||||
for (var c = 8; c < moduleCount - 8; c++) {
|
||||
if (_modules[6][c] != null) {
|
||||
if (_data[6 * moduleCount + c] != _pixelUnassigned) {
|
||||
continue;
|
||||
}
|
||||
_modules[6][c] = c.isEven;
|
||||
_set(6, c, c.isEven);
|
||||
}
|
||||
}
|
||||
|
||||
void _setupTypeInfo(int maskPattern, bool test) {
|
||||
final data = (errorCorrectLevel << 3) | maskPattern;
|
||||
final data = (errorCorrectLevel.index << 3) | maskPattern;
|
||||
final bits = qr_util.bchTypeInfo(data);
|
||||
|
||||
int i;
|
||||
|
|
@ -151,11 +236,11 @@ class QrImage {
|
|||
mod = !test && ((bits >> i) & 1) == 1;
|
||||
|
||||
if (i < 6) {
|
||||
_modules[i][8] = mod;
|
||||
_set(i, 8, mod);
|
||||
} else if (i < 8) {
|
||||
_modules[i + 1][8] = mod;
|
||||
_set(i + 1, 8, mod);
|
||||
} else {
|
||||
_modules[moduleCount - 15 + i][8] = mod;
|
||||
_set(moduleCount - 15 + i, 8, mod);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,16 +249,16 @@ class QrImage {
|
|||
mod = !test && ((bits >> i) & 1) == 1;
|
||||
|
||||
if (i < 8) {
|
||||
_modules[8][moduleCount - i - 1] = mod;
|
||||
_set(8, moduleCount - i - 1, mod);
|
||||
} else if (i < 9) {
|
||||
_modules[8][15 - i - 1 + 1] = mod;
|
||||
_set(8, 15 - i - 1 + 1, mod);
|
||||
} else {
|
||||
_modules[8][15 - i - 1] = mod;
|
||||
_set(8, 15 - i - 1, mod);
|
||||
}
|
||||
}
|
||||
|
||||
// fixed module
|
||||
_modules[moduleCount - 8][8] = !test;
|
||||
_set(moduleCount - 8, 8, !test);
|
||||
}
|
||||
|
||||
void _setupTypeNumber(bool test) {
|
||||
|
|
@ -181,12 +266,12 @@ class QrImage {
|
|||
|
||||
for (var i = 0; i < 18; i++) {
|
||||
final mod = !test && ((bits >> i) & 1) == 1;
|
||||
_modules[i ~/ 3][i % 3 + moduleCount - 8 - 3] = mod;
|
||||
_set(i ~/ 3, i % 3 + moduleCount - 8 - 3, mod);
|
||||
}
|
||||
|
||||
for (var i = 0; i < 18; i++) {
|
||||
final mod = !test && ((bits >> i) & 1) == 1;
|
||||
_modules[i % 3 + moduleCount - 8 - 3][i ~/ 3] = mod;
|
||||
_set(i % 3 + moduleCount - 8 - 3, i ~/ 3, mod);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,20 +286,20 @@ class QrImage {
|
|||
|
||||
for (;;) {
|
||||
for (var c = 0; c < 2; c++) {
|
||||
if (_modules[row][col - c] == null) {
|
||||
if (_data[row * moduleCount + (col - c)] == _pixelUnassigned) {
|
||||
var dark = false;
|
||||
|
||||
if (byteIndex < data.length) {
|
||||
dark = ((data[byteIndex] >> bitIndex) & 1) == 1;
|
||||
}
|
||||
|
||||
final mask = _mask(maskPattern, row, col - c);
|
||||
final mask = QrMaskPattern.values[maskPattern].check(row, col - c);
|
||||
|
||||
if (mask) {
|
||||
dark = !dark;
|
||||
}
|
||||
|
||||
_modules[row][col - c] = dark;
|
||||
_set(row, col - c, dark);
|
||||
bitIndex--;
|
||||
|
||||
if (bitIndex == -1) {
|
||||
|
|
@ -234,49 +319,123 @@ class QrImage {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _mask(int maskPattern, int i, int j) => switch (maskPattern) {
|
||||
qr_mask_pattern.pattern000 => (i + j).isEven,
|
||||
qr_mask_pattern.pattern001 => i.isEven,
|
||||
qr_mask_pattern.pattern010 => j % 3 == 0,
|
||||
qr_mask_pattern.pattern011 => (i + j) % 3 == 0,
|
||||
qr_mask_pattern.pattern100 => ((i ~/ 2) + (j ~/ 3)).isEven,
|
||||
qr_mask_pattern.pattern101 => (i * j) % 2 + (i * j) % 3 == 0,
|
||||
qr_mask_pattern.pattern110 => ((i * j) % 2 + (i * j) % 3).isEven,
|
||||
qr_mask_pattern.pattern111 => ((i * j) % 3 + (i + j) % 2).isEven,
|
||||
_ => throw ArgumentError('bad maskPattern:$maskPattern'),
|
||||
};
|
||||
void _placeData(List<int> data) {
|
||||
var inc = -1;
|
||||
var row = moduleCount - 1;
|
||||
var bitIndex = 7;
|
||||
var byteIndex = 0;
|
||||
|
||||
for (var col = moduleCount - 1; col > 0; col -= 2) {
|
||||
if (col == 6) col--;
|
||||
|
||||
for (;;) {
|
||||
for (var c = 0; c < 2; c++) {
|
||||
if (_data[row * moduleCount + (col - c)] == _pixelUnassigned) {
|
||||
var dark = false;
|
||||
|
||||
if (byteIndex < data.length) {
|
||||
dark = ((data[byteIndex] >> bitIndex) & 1) == 1;
|
||||
}
|
||||
|
||||
_set(row, col - c, dark);
|
||||
bitIndex--;
|
||||
|
||||
if (bitIndex == -1) {
|
||||
byteIndex++;
|
||||
bitIndex = 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row += inc;
|
||||
|
||||
if (row < 0 || moduleCount <= row) {
|
||||
row -= inc;
|
||||
inc = -inc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _applyMask(QrMaskPattern maskPattern, Uint8List templateData) {
|
||||
var inc = -1;
|
||||
var row = moduleCount - 1;
|
||||
|
||||
for (var col = moduleCount - 1; col > 0; col -= 2) {
|
||||
if (col == 6) col--;
|
||||
|
||||
for (;;) {
|
||||
for (var c = 0; c < 2; c++) {
|
||||
if (templateData[row * moduleCount + (col - c)] == _pixelUnassigned) {
|
||||
final mask = maskPattern.check(row, col - c);
|
||||
if (mask) {
|
||||
_data[row * moduleCount + (col - c)] ^= _pixelDark ^ _pixelLight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row += inc;
|
||||
|
||||
if (row < 0 || moduleCount <= row) {
|
||||
row -= inc;
|
||||
inc = -inc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double _lostPoint(QrImage qrImage) {
|
||||
final moduleCount = qrImage.moduleCount;
|
||||
|
||||
final data = qrImage._data;
|
||||
var lostPoint = 0.0;
|
||||
int row, col;
|
||||
|
||||
// LEVEL1
|
||||
for (row = 0; row < moduleCount; row++) {
|
||||
for (col = 0; col < moduleCount; col++) {
|
||||
// Cache data length for faster access (though it's final)
|
||||
// Accessing local vars is faster.
|
||||
|
||||
// Level 1
|
||||
for (var row = 0; row < moduleCount; row++) {
|
||||
for (var col = 0; col < moduleCount; col++) {
|
||||
var sameCount = 0;
|
||||
final dark = qrImage.isDark(row, col);
|
||||
final currentIdx = row * moduleCount + col;
|
||||
final isDark = data[currentIdx] == QrImage._pixelDark;
|
||||
|
||||
for (var r = -1; r <= 1; r++) {
|
||||
if (row + r < 0 || moduleCount <= row + r) {
|
||||
continue;
|
||||
// Check all 8 neighbors
|
||||
// Top row
|
||||
if (row > 0) {
|
||||
final upIdx = currentIdx - moduleCount;
|
||||
if (col > 0 && (data[upIdx - 1] == QrImage._pixelDark) == isDark) {
|
||||
sameCount++;
|
||||
}
|
||||
if ((data[upIdx] == QrImage._pixelDark) == isDark) sameCount++;
|
||||
if (col < moduleCount - 1 &&
|
||||
(data[upIdx + 1] == QrImage._pixelDark) == isDark) {
|
||||
sameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (var c = -1; c <= 1; c++) {
|
||||
if (col + c < 0 || moduleCount <= col + c) {
|
||||
continue;
|
||||
}
|
||||
// Middle row (left/right)
|
||||
if (col > 0 && (data[currentIdx - 1] == QrImage._pixelDark) == isDark) {
|
||||
sameCount++;
|
||||
}
|
||||
if (col < moduleCount - 1 &&
|
||||
(data[currentIdx + 1] == QrImage._pixelDark) == isDark) {
|
||||
sameCount++;
|
||||
}
|
||||
|
||||
if (r == 0 && c == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dark == qrImage.isDark(row + r, col + c)) {
|
||||
sameCount++;
|
||||
}
|
||||
// Bottom row
|
||||
if (row < moduleCount - 1) {
|
||||
final downIdx = currentIdx + moduleCount;
|
||||
if (col > 0 && (data[downIdx - 1] == QrImage._pixelDark) == isDark) {
|
||||
sameCount++;
|
||||
}
|
||||
if ((data[downIdx] == QrImage._pixelDark) == isDark) sameCount++;
|
||||
if (col < moduleCount - 1 &&
|
||||
(data[downIdx + 1] == QrImage._pixelDark) == isDark) {
|
||||
sameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,58 +445,58 @@ double _lostPoint(QrImage qrImage) {
|
|||
}
|
||||
}
|
||||
|
||||
// LEVEL2
|
||||
for (row = 0; row < moduleCount - 1; row++) {
|
||||
for (col = 0; col < moduleCount - 1; col++) {
|
||||
var count = 0;
|
||||
if (qrImage.isDark(row, col)) count++;
|
||||
if (qrImage.isDark(row + 1, col)) count++;
|
||||
if (qrImage.isDark(row, col + 1)) count++;
|
||||
if (qrImage.isDark(row + 1, col + 1)) count++;
|
||||
if (count == 0 || count == 4) {
|
||||
// Level 2: 2x2 blocks of same color
|
||||
for (var row = 0; row < moduleCount - 1; row++) {
|
||||
for (var col = 0; col < moduleCount - 1; col++) {
|
||||
final idx = row * moduleCount + col;
|
||||
final p00 = data[idx];
|
||||
final p01 = data[idx + 1];
|
||||
final p10 = data[idx + moduleCount];
|
||||
final p11 = data[idx + moduleCount + 1];
|
||||
|
||||
if (p00 == p01 && p00 == p10 && p00 == p11) {
|
||||
lostPoint += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LEVEL3
|
||||
for (row = 0; row < moduleCount; row++) {
|
||||
for (col = 0; col < moduleCount - 6; col++) {
|
||||
if (qrImage.isDark(row, col) &&
|
||||
!qrImage.isDark(row, col + 1) &&
|
||||
qrImage.isDark(row, col + 2) &&
|
||||
qrImage.isDark(row, col + 3) &&
|
||||
qrImage.isDark(row, col + 4) &&
|
||||
!qrImage.isDark(row, col + 5) &&
|
||||
qrImage.isDark(row, col + 6)) {
|
||||
// Level 3: 1:1:3:1:1 pattern
|
||||
// Dark, Light, Dark, Dark, Dark, Light, Dark
|
||||
for (var row = 0; row < moduleCount; row++) {
|
||||
for (var col = 0; col < moduleCount - 6; col++) {
|
||||
final idx = row * moduleCount + col;
|
||||
if (data[idx] == QrImage._pixelDark &&
|
||||
data[idx + 1] == QrImage._pixelLight &&
|
||||
data[idx + 2] == QrImage._pixelDark &&
|
||||
data[idx + 3] == QrImage._pixelDark &&
|
||||
data[idx + 4] == QrImage._pixelDark &&
|
||||
data[idx + 5] == QrImage._pixelLight &&
|
||||
data[idx + 6] == QrImage._pixelDark) {
|
||||
lostPoint += 40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (col = 0; col < moduleCount; col++) {
|
||||
for (row = 0; row < moduleCount - 6; row++) {
|
||||
if (qrImage.isDark(row, col) &&
|
||||
!qrImage.isDark(row + 1, col) &&
|
||||
qrImage.isDark(row + 2, col) &&
|
||||
qrImage.isDark(row + 3, col) &&
|
||||
qrImage.isDark(row + 4, col) &&
|
||||
!qrImage.isDark(row + 5, col) &&
|
||||
qrImage.isDark(row + 6, col)) {
|
||||
// Check cols
|
||||
for (var col = 0; col < moduleCount; col++) {
|
||||
for (var row = 0; row < moduleCount - 6; row++) {
|
||||
final idx = row * moduleCount + col;
|
||||
if (data[idx] == QrImage._pixelDark &&
|
||||
data[idx + moduleCount] == QrImage._pixelLight &&
|
||||
data[idx + 2 * moduleCount] == QrImage._pixelDark &&
|
||||
data[idx + 3 * moduleCount] == QrImage._pixelDark &&
|
||||
data[idx + 4 * moduleCount] == QrImage._pixelDark &&
|
||||
data[idx + 5 * moduleCount] == QrImage._pixelLight &&
|
||||
data[idx + 6 * moduleCount] == QrImage._pixelDark) {
|
||||
lostPoint += 40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LEVEL4
|
||||
// Level 4: Dark ratio
|
||||
var darkCount = 0;
|
||||
|
||||
for (col = 0; col < moduleCount; col++) {
|
||||
for (row = 0; row < moduleCount; row++) {
|
||||
if (qrImage.isDark(row, col)) {
|
||||
darkCount++;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
if (data[i] == QrImage._pixelDark) darkCount++;
|
||||
}
|
||||
|
||||
final ratio = (100 * darkCount / moduleCount / moduleCount - 50).abs() / 5;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ class QrRsBlock {
|
|||
|
||||
QrRsBlock._(this.totalCount, this.dataCount);
|
||||
|
||||
static List<QrRsBlock> getRSBlocks(int typeNumber, int errorCorrectLevel) {
|
||||
static List<QrRsBlock> getRSBlocks(
|
||||
int typeNumber,
|
||||
QrErrorCorrectLevel errorCorrectLevel,
|
||||
) {
|
||||
final rsBlock = _getRsBlockTable(typeNumber, errorCorrectLevel);
|
||||
|
||||
final length = rsBlock.length ~/ 3;
|
||||
|
|
@ -29,15 +32,12 @@ class QrRsBlock {
|
|||
|
||||
List<int> _getRsBlockTable(
|
||||
int typeNumber,
|
||||
int errorCorrectLevel,
|
||||
QrErrorCorrectLevel errorCorrectLevel,
|
||||
) => switch (errorCorrectLevel) {
|
||||
QrErrorCorrectLevel.L => _rsBlockTable[(typeNumber - 1) * 4 + 0],
|
||||
QrErrorCorrectLevel.M => _rsBlockTable[(typeNumber - 1) * 4 + 1],
|
||||
QrErrorCorrectLevel.Q => _rsBlockTable[(typeNumber - 1) * 4 + 2],
|
||||
QrErrorCorrectLevel.H => _rsBlockTable[(typeNumber - 1) * 4 + 3],
|
||||
_ => throw ArgumentError(
|
||||
'bad rs block @ typeNumber: $typeNumber/errorCorrectLevel:$errorCorrectLevel',
|
||||
),
|
||||
QrErrorCorrectLevel.low => _rsBlockTable[(typeNumber - 1) * 4 + 0],
|
||||
QrErrorCorrectLevel.medium => _rsBlockTable[(typeNumber - 1) * 4 + 1],
|
||||
QrErrorCorrectLevel.quartile => _rsBlockTable[(typeNumber - 1) * 4 + 2],
|
||||
QrErrorCorrectLevel.high => _rsBlockTable[(typeNumber - 1) * 4 + 3],
|
||||
};
|
||||
|
||||
const List<List<int>> _rsBlockTable = [
|
||||
|
|
|
|||
|
|
@ -12,9 +12,13 @@ dependencies:
|
|||
meta: ^1.7.0
|
||||
|
||||
dev_dependencies:
|
||||
args: ^2.1.0
|
||||
benchmark_harness: ^2.0.0
|
||||
build_runner: ^2.2.1
|
||||
build_web_compilers: ^4.1.4
|
||||
dart_flutter_team_lints: ^3.0.0
|
||||
path: ^1.9.1
|
||||
stream_transform: ^2.0.0
|
||||
test: ^1.21.6
|
||||
test_process: ^2.1.1
|
||||
web: ^1.1.0
|
||||
|
|
|
|||
564
qr/test/eci_test.dart
Normal file
564
qr/test/eci_test.dart
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:qr/qr.dart';
|
||||
import 'package:qr/src/mode.dart' as qr_mode;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('QrEci', () {
|
||||
test('validates value range', () {
|
||||
expect(() => QrEci(-1), throwsArgumentError);
|
||||
expect(() => QrEci(1000000), throwsArgumentError);
|
||||
expect(QrEci(0).value, 0);
|
||||
expect(QrEci(999999).value, 999999);
|
||||
});
|
||||
|
||||
test('constants', () {
|
||||
expect(QrEciValue.iso8859_1, 3);
|
||||
expect(QrEciValue.iso8859_2, 4);
|
||||
expect(QrEciValue.iso8859_3, 5);
|
||||
expect(QrEciValue.iso8859_4, 6);
|
||||
expect(QrEciValue.iso8859_5, 7);
|
||||
expect(QrEciValue.iso8859_6, 8);
|
||||
expect(QrEciValue.iso8859_7, 9);
|
||||
expect(QrEciValue.iso8859_8, 10);
|
||||
expect(QrEciValue.iso8859_9, 11);
|
||||
expect(QrEciValue.iso8859_10, 12);
|
||||
expect(QrEciValue.iso8859_11, 13);
|
||||
expect(QrEciValue.iso8859_13, 15);
|
||||
expect(QrEciValue.iso8859_14, 16);
|
||||
expect(QrEciValue.iso8859_15, 17);
|
||||
expect(QrEciValue.iso8859_16, 18);
|
||||
expect(QrEciValue.shiftJis, 20);
|
||||
expect(QrEciValue.windows1250, 21);
|
||||
expect(QrEciValue.windows1251, 22);
|
||||
expect(QrEciValue.windows1252, 23);
|
||||
expect(QrEciValue.windows1256, 24);
|
||||
expect(QrEciValue.utf16BE, 25);
|
||||
expect(QrEciValue.utf8, 26);
|
||||
expect(QrEciValue.ascii, 27);
|
||||
expect(QrEciValue.big5, 28);
|
||||
expect(QrEciValue.gb2312, 29);
|
||||
expect(QrEciValue.eucKr, 30);
|
||||
expect(QrEciValue.gbk, 31);
|
||||
});
|
||||
|
||||
test('properties', () {
|
||||
final eci = QrEci(123);
|
||||
expect(eci.mode, qr_mode.QrMode.eci);
|
||||
expect(eci.length, 0);
|
||||
});
|
||||
|
||||
test('encodes 0-127 (8 bits)', () {
|
||||
_testEci(0, [0x00]); // 00000000
|
||||
_testEci(65, [0x41]); // 01000001
|
||||
_testEci(127, [0x7F]); // 01111111
|
||||
});
|
||||
|
||||
test('encodes 128-16383 (16 bits)', () {
|
||||
// 128 -> 10 000000 10000000 -> 0x80 0x80
|
||||
_testEci(128, [0x80, 0x80]);
|
||||
// 16383 -> 10 111111 11111111 -> 0xBF 0xFF
|
||||
_testEci(16383, [0xBF, 0xFF]);
|
||||
});
|
||||
|
||||
test('encodes 16384-999999 (24 bits)', () {
|
||||
// 16384 -> 110 00000 01000000 00000000 -> 0xC0 0x40 0x00
|
||||
_testEci(16384, [0xC0, 0x40, 0x00]);
|
||||
// 999999 -> 11110100001000111111 -> 0F 42 3F
|
||||
// 999999 = 0xF423F
|
||||
// 110 01111 01000010 00111111 -> 0xCF 0x42 0x3F
|
||||
_testEci(999999, [0xCF, 0x42, 0x3F]);
|
||||
});
|
||||
});
|
||||
|
||||
test('validates emoji', () {
|
||||
final code = QrCode(1, QrErrorCorrectLevel.low)..addData('🙃');
|
||||
|
||||
// Validate bitstream structure:
|
||||
// Header: Mode 7 (0111) + Value 26 (00011010) + Mode 4 (0100) + Length 4 (00000100)
|
||||
// 0111 0001 1010 0100 0000 0100 -> 0x71 0xA4 0x04
|
||||
// Data: F0 9F 99 83
|
||||
// Terminator: 0000
|
||||
// Padding to byte: 0000 (since 60 bits + 4 bits = 64 bits = 8 bytes)
|
||||
// Pad Bytes: 0xEC, 0x11... (to fill 19 bytes)
|
||||
final expectedData = [
|
||||
0x71, 0xA4, 0x04, // Header
|
||||
0xF0, 0x9F, 0x99, 0x83, // '🙃' in UTF-8
|
||||
0x00, // Terminator + Bit Padding to byte boundary
|
||||
// Padding Codewords (0xEC, 0x11 alternating) to fill 19 bytes capacity
|
||||
0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC,
|
||||
];
|
||||
|
||||
// Verify the full data cache (19 Data Codewords for Version 1-L)
|
||||
expect(code.dataCache.sublist(0, 19), expectedData);
|
||||
|
||||
final image = QrImage(code);
|
||||
expect(image.moduleCount, 21); // Version 1 is 21x21
|
||||
expect(_getModules(image), _expectedEmojiModules);
|
||||
});
|
||||
}
|
||||
|
||||
void _testEci(int value, List<int> expectedBytes) {
|
||||
final buffer = QrBitBuffer();
|
||||
QrEci(value).write(buffer);
|
||||
|
||||
expect(buffer, hasLength(expectedBytes.length * 8));
|
||||
for (var i = 0; i < expectedBytes.length; i++) {
|
||||
expect(buffer.getByte(i), expectedBytes[i], reason: 'Byte $i mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
List<bool> _getModules(QrImage image) {
|
||||
final modules = <bool>[];
|
||||
for (var i = 0; i < image.moduleCount; i++) {
|
||||
for (var j = 0; j < image.moduleCount; j++) {
|
||||
modules.add(image.isDark(i, j));
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
const _expectedEmojiModules = [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true, // Row 0
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true, // Row 1
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true, // Row 2
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true, // Row 3
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true, // Row 4
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true, // Row 5
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true, // Row 6
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false, // Row 7
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false, // Row 8
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false, // Row 9
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false, // Row 10
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false, // Row 11
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false, // Row 12
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false, // Row 13
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false, // Row 14
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true, // Row 15
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true, // Row 16
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false, // Row 17
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false, // Row 18
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true, // Row 19
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false, // Row 20
|
||||
];
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:qr/qr.dart';
|
||||
import 'package:qr/src/byte.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -7,11 +6,11 @@ void main() {
|
|||
final qr = QrAlphaNumeric.fromString(
|
||||
r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:',
|
||||
);
|
||||
expect(qr.mode, 2);
|
||||
expect(qr.mode, QrMode.alphaNumeric);
|
||||
expect(qr.length, 45);
|
||||
final buffer = QrBitBuffer();
|
||||
qr.write(buffer);
|
||||
expect(buffer.length, 248);
|
||||
expect(buffer, hasLength(248));
|
||||
expect(
|
||||
buffer.map<String>((e) => e ? '1' : '0').join(),
|
||||
'00000000001'
|
||||
|
|
@ -42,21 +41,21 @@ void main() {
|
|||
|
||||
test('single alphanumeric', () {
|
||||
final qr = QrAlphaNumeric.fromString(r'$');
|
||||
expect(qr.mode, 2);
|
||||
expect(qr.mode, QrMode.alphaNumeric);
|
||||
expect(qr.length, 1);
|
||||
final buffer = QrBitBuffer();
|
||||
qr.write(buffer);
|
||||
expect(buffer.length, 6);
|
||||
expect(buffer, hasLength(6));
|
||||
expect(buffer.map<String>((e) => e ? '1' : '0').join(), '100101');
|
||||
});
|
||||
|
||||
test('double (even) alphanumeric', () {
|
||||
final qr = QrAlphaNumeric.fromString('3Z');
|
||||
expect(qr.mode, 2);
|
||||
expect(qr.mode, QrMode.alphaNumeric);
|
||||
expect(qr.length, 2);
|
||||
final buffer = QrBitBuffer();
|
||||
qr.write(buffer);
|
||||
expect(buffer.length, 11, reason: 'n*5+1 = 11');
|
||||
expect(buffer, hasLength(11), reason: 'n*5+1 = 11');
|
||||
expect(
|
||||
buffer
|
||||
.getRange(0, 11)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:qr/qr.dart';
|
||||
import 'package:qr/src/byte.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -11,36 +11,32 @@ import 'qr_code_test_data_with_mask.dart';
|
|||
void main() {
|
||||
test('simple', () {
|
||||
for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {
|
||||
for (var quality in QrErrorCorrectLevel.levels) {
|
||||
for (var quality in QrErrorCorrectLevel.values) {
|
||||
final qr = QrImage(QrCode(typeNumber, quality)..addData('shanna!'));
|
||||
final modules = qr.qrModules;
|
||||
for (var i = 0; i < modules.length; i++) {
|
||||
expect(
|
||||
_encodeBoolListToString(modules[i]),
|
||||
qrCodeTestData[typeNumber.toString()][quality.toString()][i],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('fromData', () {
|
||||
for (var quality in QrErrorCorrectLevel.levels) {
|
||||
final qr = QrImage(
|
||||
QrCode.fromData(data: 'shanna!', errorCorrectLevel: quality),
|
||||
);
|
||||
final modules = qr.qrModules;
|
||||
for (var i = 0; i < modules.length; i++) {
|
||||
expect(
|
||||
_encodeBoolListToString(modules[i]),
|
||||
qrCodeTestData['1'][quality.toString()][i],
|
||||
modules.map(_encodeBoolListToString),
|
||||
qrCodeTestData[typeNumber.toString()][quality.index.toString()],
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('fromData', () {
|
||||
for (var quality in QrErrorCorrectLevel.values) {
|
||||
final qr = QrImage(
|
||||
QrCode.fromData(data: 'shanna!', errorCorrectLevel: quality),
|
||||
);
|
||||
final modules = qr.qrModules;
|
||||
expect(
|
||||
modules.map(_encodeBoolListToString),
|
||||
qrCodeTestData['1'][quality.index.toString()],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('fromUint8List', () {
|
||||
for (var quality in QrErrorCorrectLevel.levels) {
|
||||
for (var quality in QrErrorCorrectLevel.values) {
|
||||
final qr = QrImage(
|
||||
QrCode.fromUint8List(
|
||||
data: Uint8List.fromList([115, 104, 97, 110, 110, 97, 33]),
|
||||
|
|
@ -48,67 +44,53 @@ void main() {
|
|||
),
|
||||
);
|
||||
final modules = qr.qrModules;
|
||||
for (var i = 0; i < modules.length; i++) {
|
||||
expect(
|
||||
_encodeBoolListToString(modules[i]),
|
||||
qrCodeTestData['1'][quality.toString()][i],
|
||||
);
|
||||
}
|
||||
expect(
|
||||
modules.map(_encodeBoolListToString),
|
||||
qrCodeTestData['1'][quality.index.toString()],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('WHEN mask pattern is provided, SHOULD make a masked QR Code', () {
|
||||
for (var mask = 0; mask <= 7; mask++) {
|
||||
final qr = QrImage.withMaskPattern(
|
||||
QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
|
||||
QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'),
|
||||
mask,
|
||||
);
|
||||
final modules = qr.qrModules;
|
||||
for (var i = 0; i < modules.length; i++) {
|
||||
expect(
|
||||
_encodeBoolListToString(modules[i]),
|
||||
qrCodeTestDataWithMask[mask.toString()][i],
|
||||
);
|
||||
}
|
||||
expect(
|
||||
modules.map(_encodeBoolListToString),
|
||||
qrCodeTestDataWithMask[mask.toString()],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'''
|
||||
WHEN provided mask pattern is smaller than 0,
|
||||
SHOULD throw an AssertionError
|
||||
''',
|
||||
() {
|
||||
expect(() {
|
||||
QrImage.withMaskPattern(
|
||||
QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
|
||||
-1,
|
||||
);
|
||||
}, throwsA(isA<AssertionError>()));
|
||||
},
|
||||
);
|
||||
test('WHEN provided mask pattern is smaller than 0, '
|
||||
'SHOULD throw an AssertionError', () {
|
||||
expect(() {
|
||||
QrImage.withMaskPattern(
|
||||
QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'),
|
||||
-1,
|
||||
);
|
||||
}, throwsA(isA<AssertionError>()));
|
||||
});
|
||||
|
||||
test(
|
||||
'''
|
||||
WHEN provided mask pattern is bigger than 7,
|
||||
SHOULD throw an AssertionError
|
||||
''',
|
||||
() {
|
||||
expect(() {
|
||||
QrImage.withMaskPattern(
|
||||
QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
|
||||
8,
|
||||
);
|
||||
}, throwsA(isA<AssertionError>()));
|
||||
},
|
||||
);
|
||||
test('WHEN provided mask pattern is bigger than 7, '
|
||||
'SHOULD throw an AssertionError', () {
|
||||
expect(() {
|
||||
QrImage.withMaskPattern(
|
||||
QrCode(1, QrErrorCorrectLevel.high)..addData('shanna!'),
|
||||
8,
|
||||
);
|
||||
}, throwsA(isA<AssertionError>()));
|
||||
});
|
||||
group('QrCode.fromData Automatic Mode Detection', () {
|
||||
// Numeric Mode
|
||||
test('should use Numeric Mode for numbers', () {
|
||||
// 9 numeric chars fit version 1 (H level).
|
||||
final qr = QrCode.fromData(
|
||||
data: '123456789',
|
||||
errorCorrectLevel: QrErrorCorrectLevel.H,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.high,
|
||||
);
|
||||
expect(qr.typeNumber, 1);
|
||||
});
|
||||
|
|
@ -119,7 +101,7 @@ void main() {
|
|||
// version 2 (H level, 16 chars).
|
||||
final qr = QrCode.fromData(
|
||||
data: 'HELLO WORLD A',
|
||||
errorCorrectLevel: QrErrorCorrectLevel.H,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.high,
|
||||
);
|
||||
expect(qr.typeNumber, 2);
|
||||
});
|
||||
|
|
@ -130,7 +112,7 @@ void main() {
|
|||
// '機械学習' (12 bytes) fits version 2 (H level, 16 bytes).
|
||||
final qr = QrCode.fromData(
|
||||
data: '機械学習',
|
||||
errorCorrectLevel: QrErrorCorrectLevel.H,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.high,
|
||||
);
|
||||
expect(qr.typeNumber, 2);
|
||||
});
|
||||
|
|
@ -140,7 +122,7 @@ void main() {
|
|||
// Numeric Mode
|
||||
test('should use Numeric Mode for numbers', () {
|
||||
// 9 numeric characters fit version 1 (H level).
|
||||
final qr = QrCode(1, QrErrorCorrectLevel.H)..addData('123456789');
|
||||
final qr = QrCode(1, QrErrorCorrectLevel.low)..addData('123456789');
|
||||
expect(qr.typeNumber, 1);
|
||||
});
|
||||
|
||||
|
|
@ -148,7 +130,7 @@ void main() {
|
|||
test('should use Alphanumeric Mode', () {
|
||||
// 13 alphanumeric characters exceed version 1 (7 chars) but fit
|
||||
// version 2 (H level, 16 chars).
|
||||
final qr = QrCode(2, QrErrorCorrectLevel.H)..addData('HELLO WORLD A');
|
||||
final qr = QrCode(2, QrErrorCorrectLevel.high)..addData('HELLO WORLD A');
|
||||
expect(qr.typeNumber, 2);
|
||||
});
|
||||
|
||||
|
|
@ -156,7 +138,7 @@ void main() {
|
|||
test('should use Byte Mode for non-alphanumeric characters', () {
|
||||
// Kanji characters are UTF-8 encoded.
|
||||
// '機械学習' (12 bytes) fits version 2 (H level, 16 bytes).
|
||||
final qr = QrCode(2, QrErrorCorrectLevel.H)..addData('機械学習');
|
||||
final qr = QrCode(2, QrErrorCorrectLevel.high)..addData('機械学習');
|
||||
expect(qr.typeNumber, 2);
|
||||
});
|
||||
});
|
||||
|
|
@ -168,7 +150,7 @@ void main() {
|
|||
|
||||
final qrCode = QrCode.fromData(
|
||||
data: largeData,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.L,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.low,
|
||||
);
|
||||
|
||||
expect(qrCode.typeNumber, 40);
|
||||
|
|
@ -182,12 +164,20 @@ void main() {
|
|||
expect(
|
||||
() => QrCode.fromData(
|
||||
data: excessivelyLargeData,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.L,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.low,
|
||||
),
|
||||
throwsA(isA<InputTooLongException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('QrCode.addData size checks', () {
|
||||
test('should throw if data exceeds capacity for fixed version', () {
|
||||
final code = QrCode(1, QrErrorCorrectLevel.low)..addData('|' * 30);
|
||||
|
||||
expect(() => code.dataCache, throwsA(isA<InputTooLongException>()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
String _encodeBoolListToString(List<bool?> source) =>
|
||||
|
|
|
|||
46
qr/test/qr_datum_test.dart
Normal file
46
qr/test/qr_datum_test.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:qr/src/byte.dart';
|
||||
import 'package:qr/src/eci.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('QrDatum.toDatums', () {
|
||||
test('Numeric', () {
|
||||
final datums = QrDatum.toDatums('123456');
|
||||
expect(datums, hasLength(1));
|
||||
expect(datums.first, isA<QrNumeric>());
|
||||
});
|
||||
|
||||
test('AlphaNumeric', () {
|
||||
final datums = QrDatum.toDatums('HELLO WORLD');
|
||||
expect(datums, hasLength(1));
|
||||
expect(datums.first, isA<QrAlphaNumeric>());
|
||||
});
|
||||
|
||||
test('Byte (Latin-1)', () {
|
||||
final datums = QrDatum.toDatums('Hello World!');
|
||||
expect(datums, hasLength(1));
|
||||
expect(datums.first, isA<QrByte>());
|
||||
});
|
||||
|
||||
test('Byte (UTF-8 with ECI)', () {
|
||||
final datums = QrDatum.toDatums('Hello 🌍');
|
||||
expect(datums, hasLength(2));
|
||||
expect(datums[0], isA<QrEci>());
|
||||
expect((datums[0] as QrEci).value, 26);
|
||||
expect(datums[1], isA<QrByte>());
|
||||
});
|
||||
|
||||
test('Complex Emoji (UTF-8 with ECI)', () {
|
||||
// Woman + Medium Skin Tone + ZWJ + Heart + VS16 + ZWJ + Kiss Mark + ZWJ
|
||||
// + Man + Dark Brown Skin Tone
|
||||
const complexEmoji =
|
||||
'\u{1F469}\u{1F3FD}\u{200D}\u{2764}\u{FE0F}\u{200D}'
|
||||
'\u{1F48B}\u{200D}\u{1F468}\u{1F3FE}';
|
||||
final datums = QrDatum.toDatums(complexEmoji);
|
||||
expect(datums, hasLength(2));
|
||||
expect(datums[0], isA<QrEci>());
|
||||
expect((datums[0] as QrEci).value, 26);
|
||||
expect(datums[1], isA<QrByte>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import 'package:qr/qr.dart';
|
||||
import 'package:qr/src/byte.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('all digits 1 through 0', () {
|
||||
final qr = QrNumeric.fromString('1234567890');
|
||||
expect(qr.mode, 1);
|
||||
expect(qr.mode, QrMode.numeric);
|
||||
expect(qr.length, 10);
|
||||
final buffer = QrBitBuffer();
|
||||
qr.write(buffer);
|
||||
expect(buffer.length, 34);
|
||||
expect(buffer, hasLength(34));
|
||||
expect(
|
||||
buffer
|
||||
.getRange(0, 10)
|
||||
|
|
@ -54,11 +53,11 @@ void main() {
|
|||
|
||||
test('single numeric', () {
|
||||
final qr = QrNumeric.fromString('5');
|
||||
expect(qr.mode, 1);
|
||||
expect(qr.mode, QrMode.numeric);
|
||||
expect(qr.length, 1);
|
||||
final buffer = QrBitBuffer();
|
||||
qr.write(buffer);
|
||||
expect(buffer.length, 4);
|
||||
expect(buffer, hasLength(4));
|
||||
expect(
|
||||
buffer
|
||||
.getRange(0, 4)
|
||||
|
|
@ -73,11 +72,11 @@ void main() {
|
|||
|
||||
test('double numeric', () {
|
||||
final qr = QrNumeric.fromString('37');
|
||||
expect(qr.mode, 1);
|
||||
expect(qr.mode, QrMode.numeric);
|
||||
expect(qr.length, 2);
|
||||
final buffer = QrBitBuffer();
|
||||
qr.write(buffer);
|
||||
expect(buffer.length, 7, reason: 'n*3+1 = 7');
|
||||
expect(buffer, hasLength(7), reason: 'n*3+1 = 7');
|
||||
expect(
|
||||
buffer
|
||||
.getRange(0, 7)
|
||||
|
|
@ -92,11 +91,11 @@ void main() {
|
|||
|
||||
test('triple (even) numeric', () {
|
||||
final qr = QrNumeric.fromString('371');
|
||||
expect(qr.mode, 1);
|
||||
expect(qr.mode, QrMode.numeric);
|
||||
expect(qr.length, 3);
|
||||
final buffer = QrBitBuffer();
|
||||
qr.write(buffer);
|
||||
expect(buffer.length, 10, reason: 'n*3+1 = 10');
|
||||
expect(buffer, hasLength(10), reason: 'n*3+1 = 10');
|
||||
expect(
|
||||
buffer
|
||||
.getRange(0, 10)
|
||||
|
|
|
|||
41
qr/test/verify_emoji_test.dart
Normal file
41
qr/test/verify_emoji_test.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:qr/qr.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('Generate QR with Emoji', () {
|
||||
const emojiString = '👩🏽❤️💋👨🏾';
|
||||
final qr = QrCode.fromData(
|
||||
data: emojiString,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.low,
|
||||
);
|
||||
expect(qr.typeNumber, 2);
|
||||
expect(qr.typeNumber, greaterThan(0));
|
||||
// Verify we have multiple segments (ECI + Byte)
|
||||
// iterate over modules or check internal structure if possible
|
||||
// (but it's private)
|
||||
});
|
||||
|
||||
test('Generate QR with Complex Emoji (ZWJ support)', () {
|
||||
// Woman + Medium Skin Tone + ZWJ + Heart + VS16 + ZWJ + Kiss Mark + ZWJ
|
||||
// + Man + Dark Brown Skin Tone
|
||||
const complexEmoji =
|
||||
'\u{1F469}\u{1F3FD}\u{200D}\u{2764}\u{FE0F}\u{200D}'
|
||||
'\u{1F48B}\u{200D}\u{1F468}\u{1F3FE}';
|
||||
|
||||
final qr = QrCode.fromData(
|
||||
data: complexEmoji,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.low,
|
||||
);
|
||||
expect(qr.typeNumber, greaterThan(0));
|
||||
// Verify it didn't throw and created a valid QR structure
|
||||
// The exact type number depends on the overhead of ECI + Byte mode
|
||||
|
||||
// 4 segments:
|
||||
// 1. ECI (26 for UTF-8)
|
||||
// 2. Byte Data (the emoji bytes)
|
||||
|
||||
// We can't easily peek into _dataList, but we can verify the module count
|
||||
// implies it's not empty
|
||||
expect(qr.moduleCount, greaterThan(21));
|
||||
});
|
||||
}
|
||||
173
qr/test/verify_qr_tool_test.dart
Normal file
173
qr/test/verify_qr_tool_test.dart
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
@Tags(['require-zbar'])
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_process/test_process.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
|
||||
setUpAll(() {
|
||||
tempDir = Directory.systemTemp.createTempSync('qr_tool_test');
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
final configurations = [
|
||||
(version: null, correction: null),
|
||||
(version: 40, correction: 'H'),
|
||||
];
|
||||
|
||||
final inputs = [
|
||||
'123456',
|
||||
'HELLO WORLD',
|
||||
'Hello 👋 World 🌍',
|
||||
'👩🏽❤️💋👨🏾',
|
||||
'👩🏽❤️💋👨🏾',
|
||||
];
|
||||
|
||||
for (final config in configurations) {
|
||||
for (final input in inputs) {
|
||||
test(
|
||||
'Generate QR with config $config and input "$input"',
|
||||
() async {
|
||||
final bmpPath = p.join(
|
||||
tempDir.path,
|
||||
'test_${config.hashCode}_${input.hashCode}.bmp',
|
||||
);
|
||||
final args = [
|
||||
'tool/write_qr.dart',
|
||||
'-o',
|
||||
bmpPath,
|
||||
if (config.version != null) ...['-v', config.version.toString()],
|
||||
if (config.correction != null) ...['-c', config.correction!],
|
||||
'--scale',
|
||||
'10',
|
||||
input,
|
||||
];
|
||||
|
||||
final process = await TestProcess.start('dart', args);
|
||||
await process.shouldExit(0);
|
||||
|
||||
expect(
|
||||
File(bmpPath).existsSync(),
|
||||
isTrue,
|
||||
reason: 'BMP file should be created',
|
||||
);
|
||||
|
||||
// Validate with zbarimg
|
||||
// zbarimg output format: QR-Code:content
|
||||
final zbar = await TestProcess.start('zbarimg', ['--quiet', bmpPath]);
|
||||
await zbar.shouldExit(0);
|
||||
final output = (await zbar.stdout.rest.toList()).join('\n').trim();
|
||||
|
||||
if (output != 'QR-Code:$input') {
|
||||
print('zbarimg failed to match input.');
|
||||
print('Input: $input');
|
||||
print('Output: "$output"');
|
||||
}
|
||||
expect(output, 'QR-Code:$input');
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 20)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test('Generate QR with Version 1 (numeric input)', () async {
|
||||
const input = '123456';
|
||||
final bmpPath = p.join(tempDir.path, 'test_v1_numeric.bmp');
|
||||
final args = [
|
||||
'tool/write_qr.dart',
|
||||
'-o',
|
||||
bmpPath,
|
||||
'-v',
|
||||
'1',
|
||||
'-c',
|
||||
'L',
|
||||
'--scale',
|
||||
'10',
|
||||
input,
|
||||
];
|
||||
|
||||
final process = await TestProcess.start('dart', args);
|
||||
await process.shouldExit(0);
|
||||
expect(
|
||||
File(bmpPath).existsSync(),
|
||||
isTrue,
|
||||
reason: 'BMP file should be created',
|
||||
);
|
||||
|
||||
final zbar = await TestProcess.start('zbarimg', ['--quiet', bmpPath]);
|
||||
await zbar.shouldExit(0);
|
||||
final output = (await zbar.stdout.rest.toList()).join('\n').trim();
|
||||
|
||||
if (output != 'QR-Code:$input') {
|
||||
print('zbarimg failed to match input.');
|
||||
print('Input: $input');
|
||||
print('Output: "$output"');
|
||||
}
|
||||
expect(output, 'QR-Code:$input');
|
||||
});
|
||||
|
||||
test('Error case: Missing output argument', () async {
|
||||
final process = await TestProcess.start('dart', [
|
||||
'tool/write_qr.dart',
|
||||
'content',
|
||||
]);
|
||||
await process.shouldExit(1);
|
||||
final output = await process.stdout.next;
|
||||
expect(
|
||||
output,
|
||||
contains('Error: Invalid argument(s): Option output is mandatory.'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Error case: Invalid version', () async {
|
||||
final bmpPath = p.join(tempDir.path, 'invalid_version.bmp');
|
||||
final process = await TestProcess.start('dart', [
|
||||
'tool/write_qr.dart',
|
||||
'-o',
|
||||
bmpPath,
|
||||
'-v',
|
||||
'41',
|
||||
'content',
|
||||
]);
|
||||
await process.shouldExit(1);
|
||||
});
|
||||
|
||||
test('Error case: Invalid correction', () async {
|
||||
final bmpPath = p.join(tempDir.path, 'invalid_correction.bmp');
|
||||
final process = await TestProcess.start('dart', [
|
||||
'tool/write_qr.dart',
|
||||
'-o',
|
||||
bmpPath,
|
||||
'-c',
|
||||
'X',
|
||||
'content',
|
||||
]);
|
||||
await process.shouldExit(1); // ArgParser error
|
||||
});
|
||||
|
||||
test('Error case: Input too long for version (explicit version)', () async {
|
||||
const input =
|
||||
'This string is definitely too long for Version 1 with '
|
||||
'High error correction level.';
|
||||
final bmpPath = p.join(tempDir.path, 'too_long.bmp');
|
||||
final process = await TestProcess.start('dart', [
|
||||
'tool/write_qr.dart',
|
||||
'-o',
|
||||
bmpPath,
|
||||
'-v',
|
||||
'1',
|
||||
'-c',
|
||||
'H', // High error correction reduces capacity
|
||||
input,
|
||||
]);
|
||||
await process.shouldExit(1);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue