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
|
libsignal_protocol_dart: 618f0c0b49534245a640a31d204265440cbac9ee
|
||||||
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
|
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
|
||||||
mutex: 84ca903a3ac863735e3228c75a212133621f680f
|
mutex: 84ca903a3ac863735e3228c75a212133621f680f
|
||||||
no_screenshot: 57b4a072e9193b4fa1257a6f1acb13ef307625e7
|
no_screenshot: 9ca2a492ff12e5179583a1fa015bf0843382b866
|
||||||
optional: 71c638891ce4f2aff35c7387727989f31f9d877d
|
optional: 71c638891ce4f2aff35c7387727989f31f9d877d
|
||||||
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
||||||
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
||||||
qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
|
qr: 5fa01fcccd6121b906dc7df4fffa9fa22ca94f75
|
||||||
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
||||||
restart_app: 12339f63bf8e9631e619c4f9f6b4e013fa324715
|
restart_app: 12339f63bf8e9631e619c4f9f6b4e013fa324715
|
||||||
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
package com.flutterplaza.no_screenshot
|
package com.flutterplaza.no_screenshot
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager.LayoutParams
|
import android.view.WindowManager.LayoutParams
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
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.EventChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
const val SCREENSHOT_ON_CONST = "screenshotOn"
|
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 SCREENSHOT_PATH = "screenshot_path"
|
||||||
const val PREF_KEY_SCREENSHOT = "is_screenshot_on"
|
const val PREF_KEY_SCREENSHOT = "is_screenshot_on"
|
||||||
const val SCREENSHOT_TAKEN = "was_screenshot_taken"
|
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_METHOD_CHANNEL = "com.flutterplaza.no_screenshot_methods"
|
||||||
const val SCREENSHOT_EVENT_CHANNEL = "com.flutterplaza.no_screenshot_streams"
|
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 methodChannel: MethodChannel
|
||||||
private lateinit var eventChannel: EventChannel
|
private lateinit var eventChannel: EventChannel
|
||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
private var activity: Activity? = null
|
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 var screenshotObserver: ContentObserver? = null
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private var eventSink: EventChannel.EventSink? = null
|
private var eventSink: EventChannel.EventSink? = null
|
||||||
private var lastSharedPreferencesState: String = ""
|
private var lastSharedPreferencesState: String = ""
|
||||||
private var hasSharedPreferencesChanged: Boolean = false
|
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) {
|
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
context = flutterPluginBinding.applicationContext
|
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)
|
methodChannel.setMethodCallHandler(this)
|
||||||
|
|
||||||
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_EVENT_CHANNEL)
|
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_EVENT_CHANNEL)
|
||||||
eventChannel.setStreamHandler(this)
|
eventChannel.setStreamHandler(this)
|
||||||
|
|
||||||
initScreenshotObserver()
|
initScreenshotObserver()
|
||||||
|
registerLifecycleCallbacks()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
methodChannel.setMethodCallHandler(null)
|
methodChannel.setMethodCallHandler(null)
|
||||||
screenshotObserver?.let { context.contentResolver.unregisterContentObserver(it) }
|
screenshotObserver?.let { context.contentResolver.unregisterContentObserver(it) }
|
||||||
|
unregisterLifecycleCallbacks()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity
|
activity = binding.activity
|
||||||
restoreScreenshotState()
|
restoreScreenshotState()
|
||||||
|
if (isRecordingListening) {
|
||||||
|
registerScreenCaptureCallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
|
unregisterScreenCaptureCallback()
|
||||||
|
removeImageOverlay()
|
||||||
|
activity = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromActivityForConfigChanges() {}
|
|
||||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity
|
activity = binding.activity
|
||||||
restoreScreenshotState()
|
restoreScreenshotState()
|
||||||
|
if (isRecordingListening) {
|
||||||
|
registerScreenCaptureCallback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromActivity() {}
|
override fun onDetachedFromActivity() {
|
||||||
|
unregisterScreenCaptureCallback()
|
||||||
|
removeImageOverlay()
|
||||||
|
activity = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
|
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
SCREENSHOT_ON_CONST -> {
|
SCREENSHOT_ON_CONST -> {
|
||||||
result.success(screenshotOn().also { updateSharedPreferencesState("") })
|
result.success(screenshotOn().also { updateSharedPreferencesState("") })
|
||||||
}
|
}
|
||||||
|
|
||||||
SCREENSHOT_OFF_CONST -> {
|
SCREENSHOT_OFF_CONST -> {
|
||||||
result.success(screenshotOff().also { updateSharedPreferencesState("") })
|
result.success(screenshotOff().also { updateSharedPreferencesState("") })
|
||||||
}
|
}
|
||||||
|
|
||||||
TOGGLE_SCREENSHOT_CONST -> {
|
TOGGLE_SCREENSHOT_CONST -> {
|
||||||
toggleScreenshot()
|
toggleScreenshot()
|
||||||
result.success(true.also { updateSharedPreferencesState("") })
|
result.success(true.also { updateSharedPreferencesState("") })
|
||||||
}
|
}
|
||||||
|
|
||||||
START_SCREENSHOT_LISTENING_CONST -> {
|
START_SCREENSHOT_LISTENING_CONST -> {
|
||||||
startListening()
|
startListening()
|
||||||
result.success("Listening started")
|
result.success("Listening started")
|
||||||
}
|
}
|
||||||
|
|
||||||
STOP_SCREENSHOT_LISTENING_CONST -> {
|
STOP_SCREENSHOT_LISTENING_CONST -> {
|
||||||
stopListening()
|
stopListening()
|
||||||
result.success("Listening stopped".also { updateSharedPreferencesState("") })
|
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()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,12 +164,137 @@ class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activ
|
||||||
eventSink = null
|
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() {
|
private fun initScreenshotObserver() {
|
||||||
screenshotObserver = object : ContentObserver(Handler()) {
|
screenshotObserver = object : ContentObserver(Handler()) {
|
||||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
super.onChange(selfChange, uri)
|
super.onChange(selfChange, uri)
|
||||||
uri?.let {
|
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")
|
Log.d("ScreenshotProtection", "Screenshot detected")
|
||||||
updateSharedPreferencesState(it.path ?: "")
|
updateSharedPreferencesState(it.path ?: "")
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +305,11 @@ class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activ
|
||||||
|
|
||||||
private fun startListening() {
|
private fun startListening() {
|
||||||
screenshotObserver?.let {
|
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) {
|
private fun saveScreenshotState(isSecure: Boolean) {
|
||||||
|
Executors.newSingleThreadExecutor().execute {
|
||||||
preferences.edit().putBoolean(PREF_KEY_SCREENSHOT, isSecure).apply()
|
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() {
|
private fun restoreScreenshotState() {
|
||||||
|
Executors.newSingleThreadExecutor().execute {
|
||||||
val isSecure = preferences.getBoolean(PREF_KEY_SCREENSHOT, false)
|
val isSecure = preferences.getBoolean(PREF_KEY_SCREENSHOT, false)
|
||||||
if (isSecure) {
|
val overlayEnabled = preferences.getBoolean(PREF_KEY_IMAGE_OVERLAY, false)
|
||||||
|
isImageOverlayModeEnabled = overlayEnabled
|
||||||
|
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
if (isImageOverlayModeEnabled || isSecure) {
|
||||||
screenshotOff()
|
screenshotOff()
|
||||||
} else {
|
} else {
|
||||||
screenshotOn()
|
screenshotOn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateSharedPreferencesState(screenshotData: String) {
|
private fun updateSharedPreferencesState(screenshotData: String) {
|
||||||
val jsonString = convertMapToJsonString(mapOf(
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
PREF_KEY_SCREENSHOT to preferences.getBoolean(PREF_KEY_SCREENSHOT, false),
|
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_PATH to screenshotData,
|
||||||
SCREENSHOT_TAKEN to screenshotData.isNotEmpty()
|
SCREENSHOT_TAKEN to screenshotData.isNotEmpty(),
|
||||||
))
|
IS_SCREEN_RECORDING to isScreenRecording
|
||||||
|
)
|
||||||
|
)
|
||||||
if (lastSharedPreferencesState != jsonString) {
|
if (lastSharedPreferencesState != jsonString) {
|
||||||
hasSharedPreferencesChanged = true
|
hasSharedPreferencesChanged = true
|
||||||
lastSharedPreferencesState = jsonString
|
lastSharedPreferencesState = jsonString
|
||||||
}
|
}
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertMapToJsonString(map: Map<String, Any>): String {
|
private fun convertMapToJsonString(map: Map<String, Any>): String {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,35 @@
|
||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
import ScreenProtectorKit
|
|
||||||
|
|
||||||
public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
|
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 methodChannel: FlutterMethodChannel? = nil
|
||||||
private static var eventChannel: FlutterEventChannel? = nil
|
private static var eventChannel: FlutterEventChannel? = nil
|
||||||
private static var preventScreenShot: Bool = false
|
private static var preventScreenShot: Bool = false
|
||||||
private var eventSink: FlutterEventSink? = nil
|
private var eventSink: FlutterEventSink? = nil
|
||||||
private var lastSharedPreferencesState: String = ""
|
private var lastSharedPreferencesState: String = ""
|
||||||
private var hasSharedPreferencesChanged: Bool = false
|
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 ENABLESCREENSHOT = false
|
||||||
private static let DISABLESCREENSHOT = true
|
private static let DISABLESCREENSHOT = true
|
||||||
|
|
||||||
private static let preventScreenShotKey = "preventScreenShot"
|
private static let preventScreenShotKey = "preventScreenShot"
|
||||||
|
private static let imageOverlayModeKey = "imageOverlayMode"
|
||||||
private static let methodChannelName = "com.flutterplaza.no_screenshot_methods"
|
private static let methodChannelName = "com.flutterplaza.no_screenshot_methods"
|
||||||
private static let eventChannelName = "com.flutterplaza.no_screenshot_streams"
|
private static let eventChannelName = "com.flutterplaza.no_screenshot_streams"
|
||||||
private static let screenshotPathPlaceholder = "screenshot_path_placeholder"
|
private static let screenshotPathPlaceholder = "screenshot_path_placeholder"
|
||||||
|
|
||||||
init(screenProtectorKit: ScreenProtectorKit) {
|
override init() {
|
||||||
self.screenProtectorKit = screenProtectorKit
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// Restore the saved state from UserDefaults
|
// 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)
|
updateScreenshotState(isScreenshotBlocked: fetchVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,30 +37,111 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
methodChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: registrar.messenger())
|
methodChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: registrar.messenger())
|
||||||
eventChannel = FlutterEventChannel(name: eventChannelName, binaryMessenger: registrar.messenger())
|
eventChannel = FlutterEventChannel(name: eventChannelName, binaryMessenger: registrar.messenger())
|
||||||
|
|
||||||
let window = UIApplication.shared.delegate?.window
|
let instance = IOSNoScreenshotPlugin()
|
||||||
let screenProtectorKit = ScreenProtectorKit(window: window as? UIWindow)
|
|
||||||
screenProtectorKit.configurePreventionScreenshot()
|
|
||||||
|
|
||||||
let instance = IOSNoScreenshotPlugin(screenProtectorKit: screenProtectorKit)
|
|
||||||
registrar.addMethodCallDelegate(instance, channel: methodChannel!)
|
registrar.addMethodCallDelegate(instance, channel: methodChannel!)
|
||||||
eventChannel?.setStreamHandler(instance)
|
eventChannel?.setStreamHandler(instance)
|
||||||
registrar.addApplicationDelegate(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) {
|
public func applicationWillResignActive(_ application: UIApplication) {
|
||||||
persistState()
|
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) {
|
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()
|
fetchPersistedState()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func applicationWillEnterForeground(_ application: UIApplication) {
|
public func applicationWillEnterForeground(_ application: UIApplication) {
|
||||||
fetchPersistedState()
|
// Image overlay removal is handled in applicationDidBecomeActive
|
||||||
|
// which always fires after this callback.
|
||||||
}
|
}
|
||||||
|
|
||||||
public func applicationDidEnterBackground(_ application: UIApplication) {
|
public func applicationDidEnterBackground(_ application: UIApplication) {
|
||||||
persistState()
|
persistState()
|
||||||
|
// Image overlay was already shown in applicationWillResignActive
|
||||||
|
// which always fires before this callback.
|
||||||
}
|
}
|
||||||
|
|
||||||
public func applicationWillTerminate(_ application: UIApplication) {
|
public func applicationWillTerminate(_ application: UIApplication) {
|
||||||
|
|
@ -65,15 +151,17 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
func persistState() {
|
func persistState() {
|
||||||
// Persist the state when changed
|
// Persist the state when changed
|
||||||
UserDefaults.standard.set(IOSNoScreenshotPlugin.preventScreenShot, forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
|
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("")
|
updateSharedPreferencesState("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPersistedState() {
|
func fetchPersistedState() {
|
||||||
// Restore the saved state from UserDefaults
|
// 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)
|
updateScreenshotState(isScreenshotBlocked: fetchVal)
|
||||||
print("Fetched state: \(IOSNoScreenshotPlugin.preventScreenShot)")
|
print("Fetched state: \(IOSNoScreenshotPlugin.preventScreenShot), imageOverlay: \(isImageOverlayModeEnabled)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
|
@ -84,6 +172,9 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
case "screenshotOn":
|
case "screenshotOn":
|
||||||
shotOn()
|
shotOn()
|
||||||
result(true)
|
result(true)
|
||||||
|
case "toggleScreenshotWithImage":
|
||||||
|
let isActive = toggleScreenshotWithImage()
|
||||||
|
result(isActive)
|
||||||
case "toggleScreenshot":
|
case "toggleScreenshot":
|
||||||
IOSNoScreenshotPlugin.preventScreenShot ? shotOn() : shotOff()
|
IOSNoScreenshotPlugin.preventScreenShot ? shotOn() : shotOff()
|
||||||
result(true)
|
result(true)
|
||||||
|
|
@ -93,6 +184,12 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
case "stopScreenshotListening":
|
case "stopScreenshotListening":
|
||||||
stopListening()
|
stopListening()
|
||||||
result("Listening stopped")
|
result("Listening stopped")
|
||||||
|
case "startScreenRecordingListening":
|
||||||
|
startRecordingListening()
|
||||||
|
result("Recording listening started")
|
||||||
|
case "stopScreenRecordingListening":
|
||||||
|
stopRecordingListening()
|
||||||
|
result("Recording listening stopped")
|
||||||
default:
|
default:
|
||||||
result(FlutterMethodNotImplemented)
|
result(FlutterMethodNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
@ -100,16 +197,35 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
|
|
||||||
private func shotOff() {
|
private func shotOff() {
|
||||||
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
|
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
|
||||||
screenProtectorKit?.enabledPreventScreenshot()
|
enablePreventScreenshot()
|
||||||
persistState()
|
persistState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shotOn() {
|
private func shotOn() {
|
||||||
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
|
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
|
||||||
screenProtectorKit?.disablePreventScreenshot()
|
disablePreventScreenshot()
|
||||||
persistState()
|
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() {
|
private func startListening() {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(screenshotDetected), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(screenshotDetected), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
||||||
persistState()
|
persistState()
|
||||||
|
|
@ -120,16 +236,60 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
persistState()
|
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() {
|
@objc private func screenshotDetected() {
|
||||||
print("Screenshot detected")
|
print("Screenshot detected")
|
||||||
updateSharedPreferencesState(IOSNoScreenshotPlugin.screenshotPathPlaceholder)
|
updateSharedPreferencesState(IOSNoScreenshotPlugin.screenshotPathPlaceholder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateScreenshotState(isScreenshotBlocked: Bool) {
|
private func updateScreenshotState(isScreenshotBlocked: Bool) {
|
||||||
|
attachWindowIfNeeded()
|
||||||
if isScreenshotBlocked {
|
if isScreenshotBlocked {
|
||||||
screenProtectorKit?.enabledPreventScreenshot()
|
enablePreventScreenshot()
|
||||||
} else {
|
} else {
|
||||||
screenProtectorKit?.disablePreventScreenshot()
|
disablePreventScreenshot()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +297,8 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
let map: [String: Any] = [
|
let map: [String: Any] = [
|
||||||
"is_screenshot_on": IOSNoScreenshotPlugin.preventScreenShot,
|
"is_screenshot_on": IOSNoScreenshotPlugin.preventScreenShot,
|
||||||
"screenshot_path": screenshotData,
|
"screenshot_path": screenshotData,
|
||||||
"was_screenshot_taken": !screenshotData.isEmpty
|
"was_screenshot_taken": !screenshotData.isEmpty,
|
||||||
|
"is_screen_recording": isScreenRecording
|
||||||
]
|
]
|
||||||
let jsonString = convertMapToJsonString(map)
|
let jsonString = convertMapToJsonString(map)
|
||||||
if lastSharedPreferencesState != jsonString {
|
if lastSharedPreferencesState != jsonString {
|
||||||
|
|
@ -176,7 +337,47 @@ public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
private func attachWindowIfNeeded() {
|
||||||
screenProtectorKit?.removeAllObserver()
|
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 = { :path => '.' }
|
||||||
s.source_files = 'Classes/**/*'
|
s.source_files = 'Classes/**/*'
|
||||||
s.dependency 'Flutter'
|
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'
|
s.platform = :ios, '10.0'
|
||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
const screenShotOnConst = "screenshotOn";
|
const screenShotOnConst = "screenshotOn";
|
||||||
const screenShotOffConst = "screenshotOff";
|
const screenShotOffConst = "screenshotOff";
|
||||||
|
const screenSetImage = "toggleScreenshotWithImage";
|
||||||
const toggleScreenShotConst = "toggleScreenshot";
|
const toggleScreenShotConst = "toggleScreenshot";
|
||||||
const startScreenshotListeningConst = 'startScreenshotListening';
|
const startScreenshotListeningConst = 'startScreenshotListening';
|
||||||
const stopScreenshotListeningConst = 'stopScreenshotListening';
|
const stopScreenshotListeningConst = 'stopScreenshotListening';
|
||||||
|
const startScreenRecordingListeningConst = 'startScreenRecordingListening';
|
||||||
|
const stopScreenRecordingListeningConst = 'stopScreenRecordingListening';
|
||||||
const screenshotMethodChannel = "com.flutterplaza.no_screenshot_methods";
|
const screenshotMethodChannel = "com.flutterplaza.no_screenshot_methods";
|
||||||
const screenshotEventChannel = "com.flutterplaza.no_screenshot_streams";
|
const screenshotEventChannel = "com.flutterplaza.no_screenshot_streams";
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ class NoScreenshot implements NoScreenshotPlatform {
|
||||||
return _instancePlatform.screenshotOn();
|
return _instancePlatform.screenshotOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> toggleScreenshotWithImage() {
|
||||||
|
return _instancePlatform.toggleScreenshotWithImage();
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if screenshot capabilities has been
|
/// Return `true` if screenshot capabilities has been
|
||||||
/// successfully toggle from it previous state and `false` if the attempt
|
/// successfully toggle from it previous state and `false` if the attempt
|
||||||
/// to toggle failed.
|
/// to toggle failed.
|
||||||
|
|
@ -61,6 +66,18 @@ class NoScreenshot implements NoScreenshotPlatform {
|
||||||
return _instancePlatform.stopScreenshotListening();
|
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
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ class MethodChannelNoScreenshot extends NoScreenshotPlatform {
|
||||||
return result ?? false;
|
return result ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> toggleScreenshotWithImage() async {
|
||||||
|
final result = await methodChannel.invokeMethod<bool>(screenSetImage);
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> startScreenshotListening() {
|
Future<void> startScreenshotListening() {
|
||||||
return methodChannel.invokeMethod<void>(startScreenshotListeningConst);
|
return methodChannel.invokeMethod<void>(startScreenshotListeningConst);
|
||||||
|
|
@ -49,4 +55,14 @@ class MethodChannelNoScreenshot extends NoScreenshotPlatform {
|
||||||
Future<void> stopScreenshotListening() {
|
Future<void> stopScreenshotListening() {
|
||||||
return methodChannel.invokeMethod<void>(stopScreenshotListeningConst);
|
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.');
|
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
|
/// Return `true` if screenshot capabilities has been
|
||||||
/// successfully toggle from it previous state and `false` if the attempt
|
/// successfully toggle from it previous state and `false` if the attempt
|
||||||
/// to toggle failed.
|
/// to toggle failed.
|
||||||
|
|
@ -65,4 +73,16 @@ abstract class NoScreenshotPlatform extends PlatformInterface {
|
||||||
throw UnimplementedError(
|
throw UnimplementedError(
|
||||||
'stopScreenshotListening has not been implemented.');
|
'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 {
|
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 String screenshotPath;
|
||||||
|
|
||||||
final bool isScreenshotProtectionOn;
|
final bool isScreenshotProtectionOn;
|
||||||
final bool wasScreenshotTaken;
|
final bool wasScreenshotTaken;
|
||||||
|
final bool isScreenRecording;
|
||||||
|
|
||||||
ScreenshotSnapshot({
|
ScreenshotSnapshot({
|
||||||
required this.screenshotPath,
|
required this.screenshotPath,
|
||||||
required this.isScreenshotProtectionOn,
|
required this.isScreenshotProtectionOn,
|
||||||
required this.wasScreenshotTaken,
|
required this.wasScreenshotTaken,
|
||||||
|
this.isScreenRecording = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ScreenshotSnapshot.fromMap(Map<String, dynamic> map) {
|
factory ScreenshotSnapshot.fromMap(Map<String, dynamic> map) {
|
||||||
|
|
@ -14,6 +24,7 @@ class ScreenshotSnapshot {
|
||||||
screenshotPath: map['screenshot_path'] as String? ?? '',
|
screenshotPath: map['screenshot_path'] as String? ?? '',
|
||||||
isScreenshotProtectionOn: map['is_screenshot_on'] as bool? ?? false,
|
isScreenshotProtectionOn: map['is_screenshot_on'] as bool? ?? false,
|
||||||
wasScreenshotTaken: map['was_screenshot_taken'] 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,
|
'screenshot_path': screenshotPath,
|
||||||
'is_screenshot_on': isScreenshotProtectionOn,
|
'is_screenshot_on': isScreenshotProtectionOn,
|
||||||
'was_screenshot_taken': wasScreenshotTaken,
|
'was_screenshot_taken': wasScreenshotTaken,
|
||||||
|
'is_screen_recording': isScreenRecording,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ScreenshotSnapshot(\nscreenshotPath: $screenshotPath, \nisScreenshotProtectionOn: $isScreenshotProtectionOn, \nwasScreenshotTaken: $wasScreenshotTaken\n)';
|
return 'ScreenshotSnapshot(\nscreenshotPath: $screenshotPath, \nisScreenshotProtectionOn: $isScreenshotProtectionOn, \nwasScreenshotTaken: $wasScreenshotTaken, \nisScreenRecording: $isScreenRecording\n)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -37,13 +49,15 @@ class ScreenshotSnapshot {
|
||||||
return other is ScreenshotSnapshot &&
|
return other is ScreenshotSnapshot &&
|
||||||
other.screenshotPath == screenshotPath &&
|
other.screenshotPath == screenshotPath &&
|
||||||
other.isScreenshotProtectionOn == isScreenshotProtectionOn &&
|
other.isScreenshotProtectionOn == isScreenshotProtectionOn &&
|
||||||
other.wasScreenshotTaken == wasScreenshotTaken;
|
other.wasScreenshotTaken == wasScreenshotTaken &&
|
||||||
|
other.isScreenRecording == isScreenRecording;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
return screenshotPath.hashCode ^
|
return screenshotPath.hashCode ^
|
||||||
isScreenshotProtectionOn.hashCode ^
|
isScreenshotProtectionOn.hashCode ^
|
||||||
wasScreenshotTaken.hashCode;
|
wasScreenshotTaken.hashCode ^
|
||||||
|
isScreenRecording.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
name: no_screenshot
|
name: no_screenshot
|
||||||
description: Flutter plugin to enable, disable, toggle or stream screenshot activities in your application.
|
description: Flutter plugin to enable, disable, toggle or stream screenshot and screen recording activities in your application.
|
||||||
version: 0.3.2
|
version: 0.4.0
|
||||||
homepage: https://flutterplaza.com
|
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:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
@ -30,3 +30,5 @@ flutter:
|
||||||
pluginClass: NoScreenshotPlugin
|
pluginClass: NoScreenshotPlugin
|
||||||
macos:
|
macos:
|
||||||
pluginClass: MacOSNoScreenshotPlugin
|
pluginClass: MacOSNoScreenshotPlugin
|
||||||
|
linux:
|
||||||
|
pluginClass: NoScreenshotPlugin
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,87 @@ void main() {
|
||||||
await platform.stopScreenshotListening();
|
await platform.stopScreenshotListening();
|
||||||
expect(true, true); // Add more specific expectations if needed
|
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', () {
|
group('ScreenshotSnapshot', () {
|
||||||
|
|
@ -96,6 +177,31 @@ void main() {
|
||||||
expect(snapshot.screenshotPath, '/example/path');
|
expect(snapshot.screenshotPath, '/example/path');
|
||||||
expect(snapshot.isScreenshotProtectionOn, true);
|
expect(snapshot.isScreenshotProtectionOn, true);
|
||||||
expect(snapshot.wasScreenshotTaken, 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', () {
|
test('toMap', () {
|
||||||
|
|
@ -103,11 +209,23 @@ void main() {
|
||||||
screenshotPath: '/example/path',
|
screenshotPath: '/example/path',
|
||||||
isScreenshotProtectionOn: true,
|
isScreenshotProtectionOn: true,
|
||||||
wasScreenshotTaken: true,
|
wasScreenshotTaken: true,
|
||||||
|
isScreenRecording: true,
|
||||||
);
|
);
|
||||||
final map = snapshot.toMap();
|
final map = snapshot.toMap();
|
||||||
expect(map['screenshot_path'], '/example/path');
|
expect(map['screenshot_path'], '/example/path');
|
||||||
expect(map['is_screenshot_on'], true);
|
expect(map['is_screenshot_on'], true);
|
||||||
expect(map['was_screenshot_taken'], 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', () {
|
test('equality operator', () {
|
||||||
|
|
@ -126,9 +244,16 @@ void main() {
|
||||||
isScreenshotProtectionOn: false,
|
isScreenshotProtectionOn: false,
|
||||||
wasScreenshotTaken: false,
|
wasScreenshotTaken: false,
|
||||||
);
|
);
|
||||||
|
final snapshot4 = ScreenshotSnapshot(
|
||||||
|
screenshotPath: '/example/path',
|
||||||
|
isScreenshotProtectionOn: true,
|
||||||
|
wasScreenshotTaken: true,
|
||||||
|
isScreenRecording: true,
|
||||||
|
);
|
||||||
|
|
||||||
expect(snapshot1 == snapshot2, true);
|
expect(snapshot1 == snapshot2, true);
|
||||||
expect(snapshot1 == snapshot3, false);
|
expect(snapshot1 == snapshot3, false);
|
||||||
|
expect(snapshot1 == snapshot4, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hashCode', () {
|
test('hashCode', () {
|
||||||
|
|
@ -152,6 +277,28 @@ void main() {
|
||||||
expect(snapshot1.hashCode, isNot(snapshot3.hashCode));
|
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', () {
|
test('toString', () {
|
||||||
final snapshot = ScreenshotSnapshot(
|
final snapshot = ScreenshotSnapshot(
|
||||||
screenshotPath: '/example/path',
|
screenshotPath: '/example/path',
|
||||||
|
|
@ -160,7 +307,19 @@ void main() {
|
||||||
);
|
);
|
||||||
final string = snapshot.toString();
|
final string = snapshot.toString();
|
||||||
expect(string,
|
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/no_screenshot_platform_interface.dart';
|
||||||
import 'package:no_screenshot/screenshot_snapshot.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 {
|
class MockNoScreenshotPlatform extends NoScreenshotPlatform {
|
||||||
@override
|
@override
|
||||||
Future<bool> screenshotOff() async {
|
Future<bool> screenshotOff() async {
|
||||||
|
|
@ -29,10 +33,25 @@ class MockNoScreenshotPlatform extends NoScreenshotPlatform {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> toggleScreenshotWithImage() async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stopScreenshotListening() async {
|
Future<void> stopScreenshotListening() async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> startScreenRecordingListening() async {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stopScreenRecordingListening() async {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -71,5 +90,86 @@ void main() {
|
||||||
() async {
|
() async {
|
||||||
expect(platform.stopScreenshotListening(), completes);
|
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);
|
return Future.value(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> toggleScreenshotWithImage() async {
|
||||||
|
return Future.value(true);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> toggleScreenshot() async {
|
Future<bool> toggleScreenshot() async {
|
||||||
// Mock implementation or return a fixed value
|
// Mock implementation or return a fixed value
|
||||||
|
|
@ -38,6 +43,16 @@ class MockNoScreenshotPlatform
|
||||||
Future<void> stopScreenshotListening() {
|
Future<void> stopScreenshotListening() {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> startScreenRecordingListening() {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stopScreenRecordingListening() {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -86,10 +101,27 @@ void main() {
|
||||||
expect(NoScreenshot.instance.stopScreenshotListening(), completes);
|
expect(NoScreenshot.instance.stopScreenshotListening(), completes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('toggleScreenshotWithImage', () async {
|
||||||
|
expect(await NoScreenshot.instance.toggleScreenshotWithImage(), true);
|
||||||
|
});
|
||||||
|
|
||||||
test('NoScreenshot equality operator', () {
|
test('NoScreenshot equality operator', () {
|
||||||
final instance1 = NoScreenshot.instance;
|
final instance1 = NoScreenshot.instance;
|
||||||
final instance2 = NoScreenshot.instance;
|
final instance2 = NoScreenshot.instance;
|
||||||
|
|
||||||
expect(instance1 == instance2, true, reason: 'Instances should be equal');
|
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/bit_buffer.dart';
|
||||||
|
export 'src/byte.dart';
|
||||||
|
export 'src/eci.dart';
|
||||||
|
export 'src/ecivalue.dart';
|
||||||
export 'src/error_correct_level.dart';
|
export 'src/error_correct_level.dart';
|
||||||
export 'src/input_too_long_exception.dart';
|
export 'src/input_too_long_exception.dart';
|
||||||
|
export 'src/mode.dart';
|
||||||
export 'src/qr_code.dart';
|
export 'src/qr_code.dart';
|
||||||
export 'src/qr_image.dart';
|
export 'src/qr_image.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,78 @@
|
||||||
import 'dart:collection';
|
/// A growable sequence of bits.
|
||||||
|
///
|
||||||
class QrBitBuffer extends Object with ListMixin<bool> {
|
/// Used internally to construct the data bit stream for a QR code.
|
||||||
final List<int> _buffer;
|
class QrBitBuffer extends Iterable<bool> {
|
||||||
|
final _buffer = <int>[];
|
||||||
int _length = 0;
|
int _length = 0;
|
||||||
|
|
||||||
QrBitBuffer() : _buffer = <int>[];
|
QrBitBuffer();
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get length => _length;
|
int get length => _length;
|
||||||
|
|
||||||
@override
|
@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];
|
int getByte(int index) => _buffer[index];
|
||||||
|
|
||||||
void put(int number, int length) {
|
void put(int number, int length) {
|
||||||
for (var i = 0; i < length; i++) {
|
if (length == 0) return;
|
||||||
final bit = ((number >> (length - i - 1)) & 1) == 1;
|
|
||||||
putBit(bit);
|
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) {
|
void putBit(bool bit) {
|
||||||
|
|
@ -43,4 +87,28 @@ class QrBitBuffer extends Object with ListMixin<bool> {
|
||||||
|
|
||||||
_length++;
|
_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 'dart:typed_data';
|
||||||
|
|
||||||
import 'bit_buffer.dart';
|
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 {
|
abstract class QrDatum {
|
||||||
int get mode;
|
QrMode get mode;
|
||||||
int get length;
|
int get length;
|
||||||
void write(QrBitBuffer buffer);
|
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 {
|
class QrByte implements QrDatum {
|
||||||
@override
|
@override
|
||||||
final int mode = qr_mode.mode8bitByte;
|
final QrMode mode = QrMode.byte;
|
||||||
final Uint8List _data;
|
final Uint8List _data;
|
||||||
|
|
||||||
factory QrByte(String input) =>
|
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 {
|
class QrNumeric implements QrDatum {
|
||||||
static final RegExp validationRegex = RegExp(r'^[0-9]+$');
|
static final RegExp validationRegex = RegExp(r'^[0-9]+$');
|
||||||
|
|
||||||
factory QrNumeric.fromString(String numberString) {
|
factory QrNumeric.fromString(String numberString) {
|
||||||
if (!validationRegex.hasMatch(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);
|
final newList = Uint8List(numberString.length);
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
@ -55,7 +92,7 @@ class QrNumeric implements QrDatum {
|
||||||
final Uint8List _data;
|
final Uint8List _data;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int mode = qr_mode.modeNumber;
|
final QrMode mode = QrMode.numeric;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(QrBitBuffer buffer) {
|
void write(QrBitBuffer buffer) {
|
||||||
|
|
@ -82,7 +119,10 @@ class QrNumeric implements QrDatum {
|
||||||
int get length => _data.length;
|
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 {
|
class QrAlphaNumeric implements QrDatum {
|
||||||
static const alphaNumTable = r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
static const alphaNumTable = r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
||||||
// Note: '-' anywhere in this string is a range character.
|
// Note: '-' anywhere in this string is a range character.
|
||||||
|
|
@ -102,9 +142,10 @@ class QrAlphaNumeric implements QrDatum {
|
||||||
|
|
||||||
factory QrAlphaNumeric.fromString(String alphaNumeric) {
|
factory QrAlphaNumeric.fromString(String alphaNumeric) {
|
||||||
if (!alphaNumeric.contains(validationRegex)) {
|
if (!alphaNumeric.contains(validationRegex)) {
|
||||||
throw ArgumentError(
|
throw ArgumentError.value(
|
||||||
'String does not contain valid ALPHA-NUM '
|
alphaNumeric,
|
||||||
'character set: $alphaNumeric',
|
'alphaNumeric',
|
||||||
|
'String does not contain valid ALPHA-NUM character set',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return QrAlphaNumeric._(alphaNumeric);
|
return QrAlphaNumeric._(alphaNumeric);
|
||||||
|
|
@ -113,7 +154,7 @@ class QrAlphaNumeric implements QrDatum {
|
||||||
QrAlphaNumeric._(this._string);
|
QrAlphaNumeric._(this._string);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int mode = qr_mode.modeAlphaNum;
|
final QrMode mode = QrMode.alphaNumeric;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(QrBitBuffer buffer) {
|
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
|
/// QR Code error correction level.
|
||||||
class QrErrorCorrectLevel {
|
///
|
||||||
static const int L = 1;
|
/// Recover capacity:
|
||||||
static const int M = 0;
|
/// * [low] : ~7%
|
||||||
static const int Q = 3;
|
/// * [medium] : ~15%
|
||||||
static const int H = 2;
|
/// * [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
|
/// Level M (Medium) ~15% error correction.
|
||||||
// all I know for sure: you can create longer messages w/ item N than N+1
|
medium(15),
|
||||||
// I assume this correcsponds to more error correction for N+1
|
|
||||||
static const List<int> levels = [L, M, Q, H];
|
|
||||||
|
|
||||||
static String getName(int level) => switch (level) {
|
/// Level L (Low) ~7% error correction.
|
||||||
L => 'Low',
|
low(7),
|
||||||
M => 'Medium',
|
|
||||||
Q => 'Quartile',
|
/// Level H (High) ~30% error correction.
|
||||||
H => 'High',
|
high(30),
|
||||||
_ => throw ArgumentError('level $level not supported'),
|
|
||||||
};
|
/// Level Q (Quartile) ~25% error correction.
|
||||||
|
quartile(25);
|
||||||
|
|
||||||
|
final int recoveryRate;
|
||||||
|
|
||||||
|
const QrErrorCorrectLevel(this.recoveryRate);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,25 @@
|
||||||
const int pattern000 = 0;
|
enum QrMaskPattern {
|
||||||
const int pattern001 = 1;
|
pattern000(_check000),
|
||||||
const int pattern010 = 2;
|
pattern001(_check001),
|
||||||
const int pattern011 = 3;
|
pattern010(_check010),
|
||||||
const int pattern100 = 4;
|
pattern011(_check011),
|
||||||
const int pattern101 = 5;
|
pattern100(_check100),
|
||||||
const int pattern110 = 6;
|
pattern101(_check101),
|
||||||
const int pattern111 = 7;
|
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 _logTable = _createLogTable();
|
||||||
final Uint8List _expTable = _createExpTable();
|
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];
|
int gexp(int n) => _expTable[n % 255];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,55 @@
|
||||||
const int modeNumber = 1 << 0;
|
/// The encoding mode of a QR code segment.
|
||||||
const int modeAlphaNum = 1 << 1;
|
enum QrMode {
|
||||||
const int mode8bitByte = 1 << 2;
|
/// Numeric mode (0-9). Most efficient.
|
||||||
const int modeKanji = 1 << 3;
|
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;
|
int get length => _values.length;
|
||||||
|
|
||||||
QrPolynomial multiply(QrPolynomial e) {
|
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++) {
|
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++) {
|
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) {
|
QrPolynomial mod(QrPolynomial e) {
|
||||||
if (length - e.length < 0) {
|
if (length - e.length < 0) {
|
||||||
// ignore: avoid_returning_this
|
|
||||||
return 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++) {
|
for (var i = 0; i < values.length - e.length + 1; i++) {
|
||||||
value[i] = this[i];
|
final v = values[i];
|
||||||
}
|
if (v == 0) continue;
|
||||||
|
|
||||||
for (var i = 0; i < e.length; i++) {
|
final ratio = qr_math.glog(v) - qr_math.glog(e[0]);
|
||||||
value[i] ^= qr_math.gexp(qr_math.glog(e[i]) + ratio);
|
|
||||||
}
|
|
||||||
|
|
||||||
// recursive call
|
for (var j = 0; j < e.length; j++) {
|
||||||
return QrPolynomial(value, 0).mod(e);
|
final eVal = e[j];
|
||||||
|
if (eVal == 0) continue;
|
||||||
|
values[i + j] ^= qr_math.gexp(qr_math.glog(eVal) + 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.
|
||||||
|
|
||||||
|
// 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 'bit_buffer.dart';
|
||||||
import 'byte.dart';
|
import 'byte.dart';
|
||||||
|
import 'eci.dart';
|
||||||
import 'error_correct_level.dart';
|
import 'error_correct_level.dart';
|
||||||
import 'input_too_long_exception.dart';
|
import 'input_too_long_exception.dart';
|
||||||
import 'math.dart' as qr_math;
|
import 'math.dart' as qr_math;
|
||||||
import 'mode.dart' as qr_mode;
|
|
||||||
import 'polynomial.dart';
|
import 'polynomial.dart';
|
||||||
import 'rs_block.dart';
|
import 'rs_block.dart';
|
||||||
|
|
||||||
class QrCode {
|
class QrCode {
|
||||||
final int typeNumber;
|
final int typeNumber;
|
||||||
final int errorCorrectLevel;
|
final QrErrorCorrectLevel errorCorrectLevel;
|
||||||
final int moduleCount;
|
final int moduleCount;
|
||||||
List<int>? _dataCache;
|
List<int>? _dataCache;
|
||||||
final _dataList = <QrDatum>[];
|
final _dataList = <QrDatum>[];
|
||||||
|
|
||||||
QrCode(this.typeNumber, this.errorCorrectLevel)
|
QrCode(this.typeNumber, this.errorCorrectLevel)
|
||||||
: moduleCount = typeNumber * 4 + 17 {
|
: 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.checkValueInInterval(typeNumber, 1, 40, 'typeNumber');
|
||||||
RangeError.checkValidIndex(
|
|
||||||
errorCorrectLevel,
|
|
||||||
QrErrorCorrectLevel.levels,
|
|
||||||
'errorCorrectLevel',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
factory QrCode.fromData({
|
factory QrCode.fromData({
|
||||||
required String data,
|
required String data,
|
||||||
required int errorCorrectLevel,
|
required QrErrorCorrectLevel errorCorrectLevel,
|
||||||
}) {
|
}) {
|
||||||
final QrDatum datum;
|
final datumList = QrDatum.toDatums(data);
|
||||||
// Automatically determine mode here
|
|
||||||
if (QrNumeric.validationRegex.hasMatch(data)) {
|
final typeNumber = _calculateTypeNumberFromData(
|
||||||
// Numeric mode for numbers only
|
errorCorrectLevel,
|
||||||
datum = QrNumeric.fromString(data);
|
datumList,
|
||||||
} else if (QrAlphaNumeric.validationRegex.hasMatch(data)) {
|
);
|
||||||
// Alphanumeric mode for alphanumeric characters only
|
|
||||||
datum = QrAlphaNumeric.fromString(data);
|
final qrCode = QrCode(typeNumber, errorCorrectLevel);
|
||||||
} else {
|
for (final datum in datumList) {
|
||||||
// Default to byte mode for other characters
|
qrCode._addToList(datum);
|
||||||
datum = QrByte(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, datum);
|
|
||||||
|
|
||||||
final qrCode = QrCode(typeNumber, errorCorrectLevel).._addToList(datum);
|
|
||||||
return qrCode;
|
return qrCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory QrCode.fromUint8List({
|
factory QrCode.fromUint8List({
|
||||||
required Uint8List data,
|
required Uint8List data,
|
||||||
required int errorCorrectLevel,
|
required QrErrorCorrectLevel errorCorrectLevel,
|
||||||
}) {
|
}) {
|
||||||
final typeNumber = _calculateTypeNumberFromData(
|
final datum = QrByte.fromUint8List(data);
|
||||||
errorCorrectLevel,
|
final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, [datum]);
|
||||||
QrByte.fromUint8List(data),
|
return QrCode(typeNumber, errorCorrectLevel).._addToList(datum);
|
||||||
);
|
|
||||||
return QrCode(typeNumber, errorCorrectLevel)
|
|
||||||
.._addToList(QrByte.fromUint8List(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int _calculateTotalDataBits(int typeNumber, int errorCorrectLevel) {
|
static int _calculateTotalDataBits(
|
||||||
|
int typeNumber,
|
||||||
|
QrErrorCorrectLevel errorCorrectLevel,
|
||||||
|
) {
|
||||||
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
|
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
|
||||||
var totalDataBits = 0;
|
var totalDataBits = 0;
|
||||||
for (var rsBlock in rsBlocks) {
|
for (var rsBlock in rsBlocks) {
|
||||||
|
|
@ -73,26 +68,35 @@ class QrCode {
|
||||||
return totalDataBits;
|
return totalDataBits;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int _calculateTypeNumberFromData(int errorCorrectLevel, QrDatum data) {
|
static int _calculateTypeNumberFromData(
|
||||||
|
QrErrorCorrectLevel errorCorrectLevel,
|
||||||
|
List<QrDatum> data,
|
||||||
|
) {
|
||||||
for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {
|
for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {
|
||||||
final totalDataBits = _calculateTotalDataBits(
|
final totalDataBits = _calculateTotalDataBits(
|
||||||
typeNumber,
|
typeNumber,
|
||||||
errorCorrectLevel,
|
errorCorrectLevel,
|
||||||
);
|
);
|
||||||
|
|
||||||
final buffer = QrBitBuffer()
|
final buffer = QrBitBuffer();
|
||||||
..put(data.mode, 4)
|
for (final datum in data) {
|
||||||
..put(data.length, _lengthInBits(data.mode, typeNumber));
|
buffer
|
||||||
data.write(buffer);
|
..put(datum.mode.value, 4)
|
||||||
|
..put(datum.length, datum.mode.getLengthBits(typeNumber));
|
||||||
|
datum.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
if (buffer.length <= totalDataBits) return typeNumber;
|
if (buffer.length <= totalDataBits) return typeNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we reach here, the data is too long for any QR Code version.
|
// If we reach here, the data is too long for any QR Code version.
|
||||||
final buffer = QrBitBuffer()
|
final buffer = QrBitBuffer();
|
||||||
..put(data.mode, 4)
|
for (final datum in data) {
|
||||||
..put(data.length, _lengthInBits(data.mode, 40));
|
buffer
|
||||||
data.write(buffer);
|
..put(datum.mode.value, 4)
|
||||||
|
..put(datum.length, datum.mode.getLengthBits(40));
|
||||||
|
datum.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
final maxBits = _calculateTotalDataBits(40, errorCorrectLevel);
|
final maxBits = _calculateTotalDataBits(40, errorCorrectLevel);
|
||||||
|
|
||||||
|
|
@ -100,20 +104,10 @@ class QrCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
void addData(String data) {
|
void addData(String data) {
|
||||||
final QrDatum datum;
|
for (final datum in QrDatum.toDatums(data)) {
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
_addToList(datum);
|
_addToList(datum);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void addByteData(ByteData data) => _addToList(QrByte.fromByteData(data));
|
void addByteData(ByteData data) => _addToList(QrByte.fromByteData(data));
|
||||||
|
|
||||||
|
|
@ -127,6 +121,8 @@ class QrCode {
|
||||||
void addAlphaNumeric(String alphaNumeric) =>
|
void addAlphaNumeric(String alphaNumeric) =>
|
||||||
_addToList(QrAlphaNumeric.fromString(alphaNumeric));
|
_addToList(QrAlphaNumeric.fromString(alphaNumeric));
|
||||||
|
|
||||||
|
void addECI(int eciValue) => _addToList(QrEci(eciValue));
|
||||||
|
|
||||||
void _addToList(QrDatum data) {
|
void _addToList(QrDatum data) {
|
||||||
_dataList.add(data);
|
_dataList.add(data);
|
||||||
_dataCache = null;
|
_dataCache = null;
|
||||||
|
|
@ -142,7 +138,7 @@ const int _pad1 = 0x11;
|
||||||
|
|
||||||
List<int> _createData(
|
List<int> _createData(
|
||||||
int typeNumber,
|
int typeNumber,
|
||||||
int errorCorrectLevel,
|
QrErrorCorrectLevel errorCorrectLevel,
|
||||||
List<QrDatum> dataList,
|
List<QrDatum> dataList,
|
||||||
) {
|
) {
|
||||||
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
|
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
|
||||||
|
|
@ -152,8 +148,8 @@ List<int> _createData(
|
||||||
for (var i = 0; i < dataList.length; i++) {
|
for (var i = 0; i < dataList.length; i++) {
|
||||||
final data = dataList[i];
|
final data = dataList[i];
|
||||||
buffer
|
buffer
|
||||||
..put(data.mode, 4)
|
..put(data.mode.value, 4)
|
||||||
..put(data.length, _lengthInBits(data.mode, typeNumber));
|
..put(data.length, data.mode.getLengthBits(typeNumber));
|
||||||
data.write(buffer);
|
data.write(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,6 +160,10 @@ List<int> _createData(
|
||||||
errorCorrectLevel,
|
errorCorrectLevel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (buffer.length > totalDataBits) {
|
||||||
|
throw InputTooLongException(buffer.length, totalDataBits);
|
||||||
|
}
|
||||||
|
|
||||||
// HUH?
|
// HUH?
|
||||||
// èIí[ÉRÅ[Éh
|
// èIí[ÉRÅ[Éh
|
||||||
if (buffer.length + 4 <= totalDataBits) {
|
if (buffer.length + 4 <= totalDataBits) {
|
||||||
|
|
@ -244,39 +244,6 @@ List<int> _createBytes(QrBitBuffer buffer, List<QrRsBlock> rsBlocks) {
|
||||||
return data;
|
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) {
|
QrPolynomial _errorCorrectPolynomial(int errorCorrectLength) {
|
||||||
var a = QrPolynomial([1], 0);
|
var a = QrPolynomial([1], 0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,72 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
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 'qr_code.dart';
|
||||||
import 'util.dart' as qr_util;
|
import 'util.dart' as qr_util;
|
||||||
|
|
||||||
/// Renders the encoded data from a [QrCode] in a portable format.
|
/// Renders the encoded data from a [QrCode] in a portable format.
|
||||||
class QrImage {
|
class QrImage {
|
||||||
|
static const _pixelUnassigned = 0;
|
||||||
|
static const _pixelLight = 1;
|
||||||
|
static const _pixelDark = 2;
|
||||||
|
|
||||||
final int moduleCount;
|
final int moduleCount;
|
||||||
final int typeNumber;
|
final int typeNumber;
|
||||||
final int errorCorrectLevel;
|
final QrErrorCorrectLevel errorCorrectLevel;
|
||||||
final int maskPattern;
|
final int maskPattern;
|
||||||
|
|
||||||
final _modules = <List<bool?>>[];
|
final Uint8List _data;
|
||||||
|
|
||||||
/// Generates a QrImage with the best mask pattern encoding [qrCode].
|
/// Generates a QrImage with the best mask pattern encoding [qrCode].
|
||||||
factory QrImage(QrCode qrCode) {
|
factory QrImage(QrCode qrCode) {
|
||||||
var minLostPoint = 0.0;
|
// Create a template with invariant patterns
|
||||||
QrImage? bestImage;
|
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++) {
|
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);
|
final lostPoint = _lostPoint(testImage);
|
||||||
|
|
||||||
if (i == 0 || minLostPoint > lostPoint) {
|
if (lostPoint < minLostPoint) {
|
||||||
minLostPoint = lostPoint;
|
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].
|
/// Generates a specific image for the [qrCode] and [maskPattern].
|
||||||
|
|
@ -36,35 +74,75 @@ class QrImage {
|
||||||
: assert(maskPattern >= 0 && maskPattern <= 7),
|
: assert(maskPattern >= 0 && maskPattern <= 7),
|
||||||
moduleCount = qrCode.moduleCount,
|
moduleCount = qrCode.moduleCount,
|
||||||
typeNumber = qrCode.typeNumber,
|
typeNumber = qrCode.typeNumber,
|
||||||
errorCorrectLevel = qrCode.errorCorrectLevel {
|
errorCorrectLevel = qrCode.errorCorrectLevel,
|
||||||
|
_data = Uint8List(qrCode.moduleCount * qrCode.moduleCount) {
|
||||||
_makeImpl(maskPattern, qrCode.dataCache, false);
|
_makeImpl(maskPattern, qrCode.dataCache, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
QrImage._test(QrCode qrCode, this.maskPattern)
|
/// Internal constructor for template creation
|
||||||
|
QrImage._template(QrCode qrCode)
|
||||||
: moduleCount = qrCode.moduleCount,
|
: moduleCount = qrCode.moduleCount,
|
||||||
typeNumber = qrCode.typeNumber,
|
typeNumber = qrCode.typeNumber,
|
||||||
errorCorrectLevel = qrCode.errorCorrectLevel {
|
errorCorrectLevel = qrCode.errorCorrectLevel,
|
||||||
_makeImpl(maskPattern, qrCode.dataCache, true);
|
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
|
@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() {
|
void _resetModules() {
|
||||||
_modules.clear();
|
_data.fillRange(0, _data.length, _pixelUnassigned);
|
||||||
for (var row = 0; row < moduleCount; row++) {
|
|
||||||
_modules.add(List<bool?>.filled(moduleCount, null));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDark(int row, int col) {
|
bool isDark(int row, int col) {
|
||||||
if (row < 0 || moduleCount <= row || col < 0 || moduleCount <= col) {
|
if (row < 0 || moduleCount <= row) {
|
||||||
throw ArgumentError('$row , $col');
|
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) {
|
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();
|
_resetModules();
|
||||||
_setupPositionProbePattern(0, 0);
|
_setupPositionProbePattern(0, 0);
|
||||||
_setupPositionProbePattern(moduleCount - 7, 0);
|
_setupPositionProbePattern(moduleCount - 7, 0);
|
||||||
|
|
@ -80,6 +158,13 @@ class QrImage {
|
||||||
_mapData(dataCache, maskPattern);
|
_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) {
|
void _setupPositionProbePattern(int row, int col) {
|
||||||
for (var r = -1; r <= 7; r++) {
|
for (var r = -1; r <= 7; r++) {
|
||||||
if (row + r <= -1 || moduleCount <= row + r) continue;
|
if (row + r <= -1 || moduleCount <= row + r) continue;
|
||||||
|
|
@ -90,9 +175,9 @@ class QrImage {
|
||||||
if ((0 <= r && r <= 6 && (c == 0 || c == 6)) ||
|
if ((0 <= r && r <= 6 && (c == 0 || c == 6)) ||
|
||||||
(0 <= c && c <= 6 && (r == 0 || r == 6)) ||
|
(0 <= c && c <= 6 && (r == 0 || r == 6)) ||
|
||||||
(2 <= r && r <= 4 && 2 <= c && c <= 4)) {
|
(2 <= r && r <= 4 && 2 <= c && c <= 4)) {
|
||||||
_modules[row + r][col + c] = true;
|
_set(row + r, col + c, true);
|
||||||
} else {
|
} else {
|
||||||
_modules[row + r][col + c] = false;
|
_set(row + r, col + c, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,16 +191,16 @@ class QrImage {
|
||||||
final row = pos[i];
|
final row = pos[i];
|
||||||
final col = pos[j];
|
final col = pos[j];
|
||||||
|
|
||||||
if (_modules[row][col] != null) {
|
if (_data[row * moduleCount + col] != _pixelUnassigned) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var r = -2; r <= 2; r++) {
|
for (var r = -2; r <= 2; r++) {
|
||||||
for (var c = -2; c <= 2; c++) {
|
for (var c = -2; c <= 2; c++) {
|
||||||
if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
|
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 {
|
} else {
|
||||||
_modules[row + r][col + c] = false;
|
_set(row + r, col + c, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,22 +210,22 @@ class QrImage {
|
||||||
|
|
||||||
void _setupTimingPattern() {
|
void _setupTimingPattern() {
|
||||||
for (var r = 8; r < moduleCount - 8; r++) {
|
for (var r = 8; r < moduleCount - 8; r++) {
|
||||||
if (_modules[r][6] != null) {
|
if (_data[r * moduleCount + 6] != _pixelUnassigned) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_modules[r][6] = r.isEven;
|
_set(r, 6, r.isEven);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var c = 8; c < moduleCount - 8; c++) {
|
for (var c = 8; c < moduleCount - 8; c++) {
|
||||||
if (_modules[6][c] != null) {
|
if (_data[6 * moduleCount + c] != _pixelUnassigned) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_modules[6][c] = c.isEven;
|
_set(6, c, c.isEven);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupTypeInfo(int maskPattern, bool test) {
|
void _setupTypeInfo(int maskPattern, bool test) {
|
||||||
final data = (errorCorrectLevel << 3) | maskPattern;
|
final data = (errorCorrectLevel.index << 3) | maskPattern;
|
||||||
final bits = qr_util.bchTypeInfo(data);
|
final bits = qr_util.bchTypeInfo(data);
|
||||||
|
|
||||||
int i;
|
int i;
|
||||||
|
|
@ -151,11 +236,11 @@ class QrImage {
|
||||||
mod = !test && ((bits >> i) & 1) == 1;
|
mod = !test && ((bits >> i) & 1) == 1;
|
||||||
|
|
||||||
if (i < 6) {
|
if (i < 6) {
|
||||||
_modules[i][8] = mod;
|
_set(i, 8, mod);
|
||||||
} else if (i < 8) {
|
} else if (i < 8) {
|
||||||
_modules[i + 1][8] = mod;
|
_set(i + 1, 8, mod);
|
||||||
} else {
|
} 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;
|
mod = !test && ((bits >> i) & 1) == 1;
|
||||||
|
|
||||||
if (i < 8) {
|
if (i < 8) {
|
||||||
_modules[8][moduleCount - i - 1] = mod;
|
_set(8, moduleCount - i - 1, mod);
|
||||||
} else if (i < 9) {
|
} else if (i < 9) {
|
||||||
_modules[8][15 - i - 1 + 1] = mod;
|
_set(8, 15 - i - 1 + 1, mod);
|
||||||
} else {
|
} else {
|
||||||
_modules[8][15 - i - 1] = mod;
|
_set(8, 15 - i - 1, mod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixed module
|
// fixed module
|
||||||
_modules[moduleCount - 8][8] = !test;
|
_set(moduleCount - 8, 8, !test);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupTypeNumber(bool test) {
|
void _setupTypeNumber(bool test) {
|
||||||
|
|
@ -181,12 +266,12 @@ class QrImage {
|
||||||
|
|
||||||
for (var i = 0; i < 18; i++) {
|
for (var i = 0; i < 18; i++) {
|
||||||
final mod = !test && ((bits >> i) & 1) == 1;
|
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++) {
|
for (var i = 0; i < 18; i++) {
|
||||||
final mod = !test && ((bits >> i) & 1) == 1;
|
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 (;;) {
|
||||||
for (var c = 0; c < 2; c++) {
|
for (var c = 0; c < 2; c++) {
|
||||||
if (_modules[row][col - c] == null) {
|
if (_data[row * moduleCount + (col - c)] == _pixelUnassigned) {
|
||||||
var dark = false;
|
var dark = false;
|
||||||
|
|
||||||
if (byteIndex < data.length) {
|
if (byteIndex < data.length) {
|
||||||
dark = ((data[byteIndex] >> bitIndex) & 1) == 1;
|
dark = ((data[byteIndex] >> bitIndex) & 1) == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
final mask = _mask(maskPattern, row, col - c);
|
final mask = QrMaskPattern.values[maskPattern].check(row, col - c);
|
||||||
|
|
||||||
if (mask) {
|
if (mask) {
|
||||||
dark = !dark;
|
dark = !dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
_modules[row][col - c] = dark;
|
_set(row, col - c, dark);
|
||||||
bitIndex--;
|
bitIndex--;
|
||||||
|
|
||||||
if (bitIndex == -1) {
|
if (bitIndex == -1) {
|
||||||
|
|
@ -234,49 +319,123 @@ class QrImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _mask(int maskPattern, int i, int j) => switch (maskPattern) {
|
_set(row, col - c, dark);
|
||||||
qr_mask_pattern.pattern000 => (i + j).isEven,
|
bitIndex--;
|
||||||
qr_mask_pattern.pattern001 => i.isEven,
|
|
||||||
qr_mask_pattern.pattern010 => j % 3 == 0,
|
if (bitIndex == -1) {
|
||||||
qr_mask_pattern.pattern011 => (i + j) % 3 == 0,
|
byteIndex++;
|
||||||
qr_mask_pattern.pattern100 => ((i ~/ 2) + (j ~/ 3)).isEven,
|
bitIndex = 7;
|
||||||
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'),
|
|
||||||
};
|
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) {
|
double _lostPoint(QrImage qrImage) {
|
||||||
final moduleCount = qrImage.moduleCount;
|
final moduleCount = qrImage.moduleCount;
|
||||||
|
final data = qrImage._data;
|
||||||
var lostPoint = 0.0;
|
var lostPoint = 0.0;
|
||||||
int row, col;
|
|
||||||
|
|
||||||
// LEVEL1
|
// Cache data length for faster access (though it's final)
|
||||||
for (row = 0; row < moduleCount; row++) {
|
// Accessing local vars is faster.
|
||||||
for (col = 0; col < moduleCount; col++) {
|
|
||||||
|
// Level 1
|
||||||
|
for (var row = 0; row < moduleCount; row++) {
|
||||||
|
for (var col = 0; col < moduleCount; col++) {
|
||||||
var sameCount = 0;
|
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++) {
|
// Check all 8 neighbors
|
||||||
if (row + r < 0 || moduleCount <= row + r) {
|
// Top row
|
||||||
continue;
|
if (row > 0) {
|
||||||
}
|
final upIdx = currentIdx - moduleCount;
|
||||||
|
if (col > 0 && (data[upIdx - 1] == QrImage._pixelDark) == isDark) {
|
||||||
for (var c = -1; c <= 1; c++) {
|
|
||||||
if (col + c < 0 || moduleCount <= col + c) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == 0 && c == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dark == qrImage.isDark(row + r, col + c)) {
|
|
||||||
sameCount++;
|
sameCount++;
|
||||||
}
|
}
|
||||||
|
if ((data[upIdx] == QrImage._pixelDark) == isDark) sameCount++;
|
||||||
|
if (col < moduleCount - 1 &&
|
||||||
|
(data[upIdx + 1] == QrImage._pixelDark) == isDark) {
|
||||||
|
sameCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Level 2: 2x2 blocks of same color
|
||||||
for (row = 0; row < moduleCount - 1; row++) {
|
for (var row = 0; row < moduleCount - 1; row++) {
|
||||||
for (col = 0; col < moduleCount - 1; col++) {
|
for (var col = 0; col < moduleCount - 1; col++) {
|
||||||
var count = 0;
|
final idx = row * moduleCount + col;
|
||||||
if (qrImage.isDark(row, col)) count++;
|
final p00 = data[idx];
|
||||||
if (qrImage.isDark(row + 1, col)) count++;
|
final p01 = data[idx + 1];
|
||||||
if (qrImage.isDark(row, col + 1)) count++;
|
final p10 = data[idx + moduleCount];
|
||||||
if (qrImage.isDark(row + 1, col + 1)) count++;
|
final p11 = data[idx + moduleCount + 1];
|
||||||
if (count == 0 || count == 4) {
|
|
||||||
|
if (p00 == p01 && p00 == p10 && p00 == p11) {
|
||||||
lostPoint += 3;
|
lostPoint += 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LEVEL3
|
// Level 3: 1:1:3:1:1 pattern
|
||||||
for (row = 0; row < moduleCount; row++) {
|
// Dark, Light, Dark, Dark, Dark, Light, Dark
|
||||||
for (col = 0; col < moduleCount - 6; col++) {
|
for (var row = 0; row < moduleCount; row++) {
|
||||||
if (qrImage.isDark(row, col) &&
|
for (var col = 0; col < moduleCount - 6; col++) {
|
||||||
!qrImage.isDark(row, col + 1) &&
|
final idx = row * moduleCount + col;
|
||||||
qrImage.isDark(row, col + 2) &&
|
if (data[idx] == QrImage._pixelDark &&
|
||||||
qrImage.isDark(row, col + 3) &&
|
data[idx + 1] == QrImage._pixelLight &&
|
||||||
qrImage.isDark(row, col + 4) &&
|
data[idx + 2] == QrImage._pixelDark &&
|
||||||
!qrImage.isDark(row, col + 5) &&
|
data[idx + 3] == QrImage._pixelDark &&
|
||||||
qrImage.isDark(row, col + 6)) {
|
data[idx + 4] == QrImage._pixelDark &&
|
||||||
|
data[idx + 5] == QrImage._pixelLight &&
|
||||||
|
data[idx + 6] == QrImage._pixelDark) {
|
||||||
lostPoint += 40;
|
lostPoint += 40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (col = 0; col < moduleCount; col++) {
|
// Check cols
|
||||||
for (row = 0; row < moduleCount - 6; row++) {
|
for (var col = 0; col < moduleCount; col++) {
|
||||||
if (qrImage.isDark(row, col) &&
|
for (var row = 0; row < moduleCount - 6; row++) {
|
||||||
!qrImage.isDark(row + 1, col) &&
|
final idx = row * moduleCount + col;
|
||||||
qrImage.isDark(row + 2, col) &&
|
if (data[idx] == QrImage._pixelDark &&
|
||||||
qrImage.isDark(row + 3, col) &&
|
data[idx + moduleCount] == QrImage._pixelLight &&
|
||||||
qrImage.isDark(row + 4, col) &&
|
data[idx + 2 * moduleCount] == QrImage._pixelDark &&
|
||||||
!qrImage.isDark(row + 5, col) &&
|
data[idx + 3 * moduleCount] == QrImage._pixelDark &&
|
||||||
qrImage.isDark(row + 6, col)) {
|
data[idx + 4 * moduleCount] == QrImage._pixelDark &&
|
||||||
|
data[idx + 5 * moduleCount] == QrImage._pixelLight &&
|
||||||
|
data[idx + 6 * moduleCount] == QrImage._pixelDark) {
|
||||||
lostPoint += 40;
|
lostPoint += 40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LEVEL4
|
// Level 4: Dark ratio
|
||||||
var darkCount = 0;
|
var darkCount = 0;
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
for (col = 0; col < moduleCount; col++) {
|
if (data[i] == QrImage._pixelDark) darkCount++;
|
||||||
for (row = 0; row < moduleCount; row++) {
|
|
||||||
if (qrImage.isDark(row, col)) {
|
|
||||||
darkCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final ratio = (100 * darkCount / moduleCount / moduleCount - 50).abs() / 5;
|
final ratio = (100 * darkCount / moduleCount / moduleCount - 50).abs() / 5;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ class QrRsBlock {
|
||||||
|
|
||||||
QrRsBlock._(this.totalCount, this.dataCount);
|
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 rsBlock = _getRsBlockTable(typeNumber, errorCorrectLevel);
|
||||||
|
|
||||||
final length = rsBlock.length ~/ 3;
|
final length = rsBlock.length ~/ 3;
|
||||||
|
|
@ -29,15 +32,12 @@ class QrRsBlock {
|
||||||
|
|
||||||
List<int> _getRsBlockTable(
|
List<int> _getRsBlockTable(
|
||||||
int typeNumber,
|
int typeNumber,
|
||||||
int errorCorrectLevel,
|
QrErrorCorrectLevel errorCorrectLevel,
|
||||||
) => switch (errorCorrectLevel) {
|
) => switch (errorCorrectLevel) {
|
||||||
QrErrorCorrectLevel.L => _rsBlockTable[(typeNumber - 1) * 4 + 0],
|
QrErrorCorrectLevel.low => _rsBlockTable[(typeNumber - 1) * 4 + 0],
|
||||||
QrErrorCorrectLevel.M => _rsBlockTable[(typeNumber - 1) * 4 + 1],
|
QrErrorCorrectLevel.medium => _rsBlockTable[(typeNumber - 1) * 4 + 1],
|
||||||
QrErrorCorrectLevel.Q => _rsBlockTable[(typeNumber - 1) * 4 + 2],
|
QrErrorCorrectLevel.quartile => _rsBlockTable[(typeNumber - 1) * 4 + 2],
|
||||||
QrErrorCorrectLevel.H => _rsBlockTable[(typeNumber - 1) * 4 + 3],
|
QrErrorCorrectLevel.high => _rsBlockTable[(typeNumber - 1) * 4 + 3],
|
||||||
_ => throw ArgumentError(
|
|
||||||
'bad rs block @ typeNumber: $typeNumber/errorCorrectLevel:$errorCorrectLevel',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const List<List<int>> _rsBlockTable = [
|
const List<List<int>> _rsBlockTable = [
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,13 @@ dependencies:
|
||||||
meta: ^1.7.0
|
meta: ^1.7.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
args: ^2.1.0
|
||||||
|
benchmark_harness: ^2.0.0
|
||||||
build_runner: ^2.2.1
|
build_runner: ^2.2.1
|
||||||
build_web_compilers: ^4.1.4
|
build_web_compilers: ^4.1.4
|
||||||
dart_flutter_team_lints: ^3.0.0
|
dart_flutter_team_lints: ^3.0.0
|
||||||
|
path: ^1.9.1
|
||||||
stream_transform: ^2.0.0
|
stream_transform: ^2.0.0
|
||||||
test: ^1.21.6
|
test: ^1.21.6
|
||||||
|
test_process: ^2.1.1
|
||||||
web: ^1.1.0
|
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/qr.dart';
|
||||||
import 'package:qr/src/byte.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -7,11 +6,11 @@ void main() {
|
||||||
final qr = QrAlphaNumeric.fromString(
|
final qr = QrAlphaNumeric.fromString(
|
||||||
r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:',
|
r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:',
|
||||||
);
|
);
|
||||||
expect(qr.mode, 2);
|
expect(qr.mode, QrMode.alphaNumeric);
|
||||||
expect(qr.length, 45);
|
expect(qr.length, 45);
|
||||||
final buffer = QrBitBuffer();
|
final buffer = QrBitBuffer();
|
||||||
qr.write(buffer);
|
qr.write(buffer);
|
||||||
expect(buffer.length, 248);
|
expect(buffer, hasLength(248));
|
||||||
expect(
|
expect(
|
||||||
buffer.map<String>((e) => e ? '1' : '0').join(),
|
buffer.map<String>((e) => e ? '1' : '0').join(),
|
||||||
'00000000001'
|
'00000000001'
|
||||||
|
|
@ -42,21 +41,21 @@ void main() {
|
||||||
|
|
||||||
test('single alphanumeric', () {
|
test('single alphanumeric', () {
|
||||||
final qr = QrAlphaNumeric.fromString(r'$');
|
final qr = QrAlphaNumeric.fromString(r'$');
|
||||||
expect(qr.mode, 2);
|
expect(qr.mode, QrMode.alphaNumeric);
|
||||||
expect(qr.length, 1);
|
expect(qr.length, 1);
|
||||||
final buffer = QrBitBuffer();
|
final buffer = QrBitBuffer();
|
||||||
qr.write(buffer);
|
qr.write(buffer);
|
||||||
expect(buffer.length, 6);
|
expect(buffer, hasLength(6));
|
||||||
expect(buffer.map<String>((e) => e ? '1' : '0').join(), '100101');
|
expect(buffer.map<String>((e) => e ? '1' : '0').join(), '100101');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('double (even) alphanumeric', () {
|
test('double (even) alphanumeric', () {
|
||||||
final qr = QrAlphaNumeric.fromString('3Z');
|
final qr = QrAlphaNumeric.fromString('3Z');
|
||||||
expect(qr.mode, 2);
|
expect(qr.mode, QrMode.alphaNumeric);
|
||||||
expect(qr.length, 2);
|
expect(qr.length, 2);
|
||||||
final buffer = QrBitBuffer();
|
final buffer = QrBitBuffer();
|
||||||
qr.write(buffer);
|
qr.write(buffer);
|
||||||
expect(buffer.length, 11, reason: 'n*5+1 = 11');
|
expect(buffer, hasLength(11), reason: 'n*5+1 = 11');
|
||||||
expect(
|
expect(
|
||||||
buffer
|
buffer
|
||||||
.getRange(0, 11)
|
.getRange(0, 11)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:qr/qr.dart';
|
import 'package:qr/qr.dart';
|
||||||
import 'package:qr/src/byte.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
||||||
|
|
@ -11,36 +11,32 @@ import 'qr_code_test_data_with_mask.dart';
|
||||||
void main() {
|
void main() {
|
||||||
test('simple', () {
|
test('simple', () {
|
||||||
for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {
|
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 qr = QrImage(QrCode(typeNumber, quality)..addData('shanna!'));
|
||||||
final modules = qr.qrModules;
|
final modules = qr.qrModules;
|
||||||
for (var i = 0; i < modules.length; i++) {
|
|
||||||
expect(
|
expect(
|
||||||
_encodeBoolListToString(modules[i]),
|
modules.map(_encodeBoolListToString),
|
||||||
qrCodeTestData[typeNumber.toString()][quality.toString()][i],
|
qrCodeTestData[typeNumber.toString()][quality.index.toString()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fromData', () {
|
test('fromData', () {
|
||||||
for (var quality in QrErrorCorrectLevel.levels) {
|
for (var quality in QrErrorCorrectLevel.values) {
|
||||||
final qr = QrImage(
|
final qr = QrImage(
|
||||||
QrCode.fromData(data: 'shanna!', errorCorrectLevel: quality),
|
QrCode.fromData(data: 'shanna!', errorCorrectLevel: quality),
|
||||||
);
|
);
|
||||||
final modules = qr.qrModules;
|
final modules = qr.qrModules;
|
||||||
for (var i = 0; i < modules.length; i++) {
|
|
||||||
expect(
|
expect(
|
||||||
_encodeBoolListToString(modules[i]),
|
modules.map(_encodeBoolListToString),
|
||||||
qrCodeTestData['1'][quality.toString()][i],
|
qrCodeTestData['1'][quality.index.toString()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fromUint8List', () {
|
test('fromUint8List', () {
|
||||||
for (var quality in QrErrorCorrectLevel.levels) {
|
for (var quality in QrErrorCorrectLevel.values) {
|
||||||
final qr = QrImage(
|
final qr = QrImage(
|
||||||
QrCode.fromUint8List(
|
QrCode.fromUint8List(
|
||||||
data: Uint8List.fromList([115, 104, 97, 110, 110, 97, 33]),
|
data: Uint8List.fromList([115, 104, 97, 110, 110, 97, 33]),
|
||||||
|
|
@ -48,67 +44,53 @@ void main() {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final modules = qr.qrModules;
|
final modules = qr.qrModules;
|
||||||
for (var i = 0; i < modules.length; i++) {
|
|
||||||
expect(
|
expect(
|
||||||
_encodeBoolListToString(modules[i]),
|
modules.map(_encodeBoolListToString),
|
||||||
qrCodeTestData['1'][quality.toString()][i],
|
qrCodeTestData['1'][quality.index.toString()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('WHEN mask pattern is provided, SHOULD make a masked QR Code', () {
|
test('WHEN mask pattern is provided, SHOULD make a masked QR Code', () {
|
||||||
for (var mask = 0; mask <= 7; mask++) {
|
for (var mask = 0; mask <= 7; mask++) {
|
||||||
final qr = QrImage.withMaskPattern(
|
final qr = QrImage.withMaskPattern(
|
||||||
QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
|
QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'),
|
||||||
mask,
|
mask,
|
||||||
);
|
);
|
||||||
final modules = qr.qrModules;
|
final modules = qr.qrModules;
|
||||||
for (var i = 0; i < modules.length; i++) {
|
|
||||||
expect(
|
expect(
|
||||||
_encodeBoolListToString(modules[i]),
|
modules.map(_encodeBoolListToString),
|
||||||
qrCodeTestDataWithMask[mask.toString()][i],
|
qrCodeTestDataWithMask[mask.toString()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('WHEN provided mask pattern is smaller than 0, '
|
||||||
'''
|
'SHOULD throw an AssertionError', () {
|
||||||
WHEN provided mask pattern is smaller than 0,
|
|
||||||
SHOULD throw an AssertionError
|
|
||||||
''',
|
|
||||||
() {
|
|
||||||
expect(() {
|
expect(() {
|
||||||
QrImage.withMaskPattern(
|
QrImage.withMaskPattern(
|
||||||
QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
|
QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'),
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}, throwsA(isA<AssertionError>()));
|
}, throwsA(isA<AssertionError>()));
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test('WHEN provided mask pattern is bigger than 7, '
|
||||||
'''
|
'SHOULD throw an AssertionError', () {
|
||||||
WHEN provided mask pattern is bigger than 7,
|
|
||||||
SHOULD throw an AssertionError
|
|
||||||
''',
|
|
||||||
() {
|
|
||||||
expect(() {
|
expect(() {
|
||||||
QrImage.withMaskPattern(
|
QrImage.withMaskPattern(
|
||||||
QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
|
QrCode(1, QrErrorCorrectLevel.high)..addData('shanna!'),
|
||||||
8,
|
8,
|
||||||
);
|
);
|
||||||
}, throwsA(isA<AssertionError>()));
|
}, throwsA(isA<AssertionError>()));
|
||||||
},
|
});
|
||||||
);
|
|
||||||
group('QrCode.fromData Automatic Mode Detection', () {
|
group('QrCode.fromData Automatic Mode Detection', () {
|
||||||
// Numeric Mode
|
// Numeric Mode
|
||||||
test('should use Numeric Mode for numbers', () {
|
test('should use Numeric Mode for numbers', () {
|
||||||
// 9 numeric chars fit version 1 (H level).
|
// 9 numeric chars fit version 1 (H level).
|
||||||
final qr = QrCode.fromData(
|
final qr = QrCode.fromData(
|
||||||
data: '123456789',
|
data: '123456789',
|
||||||
errorCorrectLevel: QrErrorCorrectLevel.H,
|
errorCorrectLevel: QrErrorCorrectLevel.high,
|
||||||
);
|
);
|
||||||
expect(qr.typeNumber, 1);
|
expect(qr.typeNumber, 1);
|
||||||
});
|
});
|
||||||
|
|
@ -119,7 +101,7 @@ void main() {
|
||||||
// version 2 (H level, 16 chars).
|
// version 2 (H level, 16 chars).
|
||||||
final qr = QrCode.fromData(
|
final qr = QrCode.fromData(
|
||||||
data: 'HELLO WORLD A',
|
data: 'HELLO WORLD A',
|
||||||
errorCorrectLevel: QrErrorCorrectLevel.H,
|
errorCorrectLevel: QrErrorCorrectLevel.high,
|
||||||
);
|
);
|
||||||
expect(qr.typeNumber, 2);
|
expect(qr.typeNumber, 2);
|
||||||
});
|
});
|
||||||
|
|
@ -130,7 +112,7 @@ void main() {
|
||||||
// '機械学習' (12 bytes) fits version 2 (H level, 16 bytes).
|
// '機械学習' (12 bytes) fits version 2 (H level, 16 bytes).
|
||||||
final qr = QrCode.fromData(
|
final qr = QrCode.fromData(
|
||||||
data: '機械学習',
|
data: '機械学習',
|
||||||
errorCorrectLevel: QrErrorCorrectLevel.H,
|
errorCorrectLevel: QrErrorCorrectLevel.high,
|
||||||
);
|
);
|
||||||
expect(qr.typeNumber, 2);
|
expect(qr.typeNumber, 2);
|
||||||
});
|
});
|
||||||
|
|
@ -140,7 +122,7 @@ void main() {
|
||||||
// Numeric Mode
|
// Numeric Mode
|
||||||
test('should use Numeric Mode for numbers', () {
|
test('should use Numeric Mode for numbers', () {
|
||||||
// 9 numeric characters fit version 1 (H level).
|
// 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);
|
expect(qr.typeNumber, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -148,7 +130,7 @@ void main() {
|
||||||
test('should use Alphanumeric Mode', () {
|
test('should use Alphanumeric Mode', () {
|
||||||
// 13 alphanumeric characters exceed version 1 (7 chars) but fit
|
// 13 alphanumeric characters exceed version 1 (7 chars) but fit
|
||||||
// version 2 (H level, 16 chars).
|
// 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);
|
expect(qr.typeNumber, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -156,7 +138,7 @@ void main() {
|
||||||
test('should use Byte Mode for non-alphanumeric characters', () {
|
test('should use Byte Mode for non-alphanumeric characters', () {
|
||||||
// Kanji characters are UTF-8 encoded.
|
// Kanji characters are UTF-8 encoded.
|
||||||
// '機械学習' (12 bytes) fits version 2 (H level, 16 bytes).
|
// '機械学習' (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);
|
expect(qr.typeNumber, 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -168,7 +150,7 @@ void main() {
|
||||||
|
|
||||||
final qrCode = QrCode.fromData(
|
final qrCode = QrCode.fromData(
|
||||||
data: largeData,
|
data: largeData,
|
||||||
errorCorrectLevel: QrErrorCorrectLevel.L,
|
errorCorrectLevel: QrErrorCorrectLevel.low,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(qrCode.typeNumber, 40);
|
expect(qrCode.typeNumber, 40);
|
||||||
|
|
@ -182,12 +164,20 @@ void main() {
|
||||||
expect(
|
expect(
|
||||||
() => QrCode.fromData(
|
() => QrCode.fromData(
|
||||||
data: excessivelyLargeData,
|
data: excessivelyLargeData,
|
||||||
errorCorrectLevel: QrErrorCorrectLevel.L,
|
errorCorrectLevel: QrErrorCorrectLevel.low,
|
||||||
),
|
),
|
||||||
throwsA(isA<InputTooLongException>()),
|
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) =>
|
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/qr.dart';
|
||||||
import 'package:qr/src/byte.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('all digits 1 through 0', () {
|
test('all digits 1 through 0', () {
|
||||||
final qr = QrNumeric.fromString('1234567890');
|
final qr = QrNumeric.fromString('1234567890');
|
||||||
expect(qr.mode, 1);
|
expect(qr.mode, QrMode.numeric);
|
||||||
expect(qr.length, 10);
|
expect(qr.length, 10);
|
||||||
final buffer = QrBitBuffer();
|
final buffer = QrBitBuffer();
|
||||||
qr.write(buffer);
|
qr.write(buffer);
|
||||||
expect(buffer.length, 34);
|
expect(buffer, hasLength(34));
|
||||||
expect(
|
expect(
|
||||||
buffer
|
buffer
|
||||||
.getRange(0, 10)
|
.getRange(0, 10)
|
||||||
|
|
@ -54,11 +53,11 @@ void main() {
|
||||||
|
|
||||||
test('single numeric', () {
|
test('single numeric', () {
|
||||||
final qr = QrNumeric.fromString('5');
|
final qr = QrNumeric.fromString('5');
|
||||||
expect(qr.mode, 1);
|
expect(qr.mode, QrMode.numeric);
|
||||||
expect(qr.length, 1);
|
expect(qr.length, 1);
|
||||||
final buffer = QrBitBuffer();
|
final buffer = QrBitBuffer();
|
||||||
qr.write(buffer);
|
qr.write(buffer);
|
||||||
expect(buffer.length, 4);
|
expect(buffer, hasLength(4));
|
||||||
expect(
|
expect(
|
||||||
buffer
|
buffer
|
||||||
.getRange(0, 4)
|
.getRange(0, 4)
|
||||||
|
|
@ -73,11 +72,11 @@ void main() {
|
||||||
|
|
||||||
test('double numeric', () {
|
test('double numeric', () {
|
||||||
final qr = QrNumeric.fromString('37');
|
final qr = QrNumeric.fromString('37');
|
||||||
expect(qr.mode, 1);
|
expect(qr.mode, QrMode.numeric);
|
||||||
expect(qr.length, 2);
|
expect(qr.length, 2);
|
||||||
final buffer = QrBitBuffer();
|
final buffer = QrBitBuffer();
|
||||||
qr.write(buffer);
|
qr.write(buffer);
|
||||||
expect(buffer.length, 7, reason: 'n*3+1 = 7');
|
expect(buffer, hasLength(7), reason: 'n*3+1 = 7');
|
||||||
expect(
|
expect(
|
||||||
buffer
|
buffer
|
||||||
.getRange(0, 7)
|
.getRange(0, 7)
|
||||||
|
|
@ -92,11 +91,11 @@ void main() {
|
||||||
|
|
||||||
test('triple (even) numeric', () {
|
test('triple (even) numeric', () {
|
||||||
final qr = QrNumeric.fromString('371');
|
final qr = QrNumeric.fromString('371');
|
||||||
expect(qr.mode, 1);
|
expect(qr.mode, QrMode.numeric);
|
||||||
expect(qr.length, 3);
|
expect(qr.length, 3);
|
||||||
final buffer = QrBitBuffer();
|
final buffer = QrBitBuffer();
|
||||||
qr.write(buffer);
|
qr.write(buffer);
|
||||||
expect(buffer.length, 10, reason: 'n*3+1 = 10');
|
expect(buffer, hasLength(10), reason: 'n*3+1 = 10');
|
||||||
expect(
|
expect(
|
||||||
buffer
|
buffer
|
||||||
.getRange(0, 10)
|
.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