diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml
index 661e537..070914c 100644
--- a/.github/workflows/apk.yml
+++ b/.github/workflows/apk.yml
@@ -44,13 +44,13 @@ jobs:
- name: Rename APK with version
run: |
cd example/build/app/outputs/flutter-apk/
- mv app-release.apk document_scanner_example_v${{ steps.ver.outputs.VERSION }}.apk
+ mv app-release.apk DocumentScanner_v${{ steps.ver.outputs.VERSION }}.apk
ls -la *.apk
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
- name: document_scanner_example_v${{ steps.ver.outputs.VERSION }}-apk
- path: example/build/app/outputs/flutter-apk/document_scanner_example_v${{ steps.ver.outputs.VERSION }}.apk
+ name: DocumentScanner_v${{ steps.ver.outputs.VERSION }}-apk
+ path: example/build/app/outputs/flutter-apk/DocumentScanner_v${{ steps.ver.outputs.VERSION }}.apk
if-no-files-found: error
retention-days: 30
diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml
index 0e9a019..c49240a 100644
--- a/.github/workflows/build-and-release.yml
+++ b/.github/workflows/build-and-release.yml
@@ -83,7 +83,7 @@ jobs:
- name: Rename APK with version
run: |
cd example/build/app/outputs/flutter-apk/
- mv app-release.apk document_scanner_example_v${{ steps.get_version.outputs.VERSION }}.apk
+ mv app-release.apk DocumentScanner_v${{ steps.get_version.outputs.VERSION }}.apk
ls -la *.apk
- name: Generate release notes
@@ -139,7 +139,7 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
- files: example/build/app/outputs/flutter-apk/document_scanner_example_v${{ steps.get_version.outputs.VERSION }}.apk
+ files: example/build/app/outputs/flutter-apk/DocumentScanner_v${{ steps.get_version.outputs.VERSION }}.apk
body_path: release_notes.md
name: Document Scanner v${{ steps.get_version.outputs.VERSION }}
tag_name: ${{ github.ref_name }}
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 7873cef..4ba5d06 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -11,7 +11,7 @@
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..23f4f69 100644
Binary files a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..3c909ee 100644
Binary files a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..596a326 100644
Binary files a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..5f1f891 100644
Binary files a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..b7bf5e2 100644
Binary files a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/assets/icon/icon.png b/example/assets/icon/icon.png
new file mode 100644
index 0000000..fcde7ce
Binary files /dev/null and b/example/assets/icon/icon.png differ
diff --git a/example/assets/icon/icon_background.png b/example/assets/icon/icon_background.png
new file mode 100644
index 0000000..992e9e9
Binary files /dev/null and b/example/assets/icon/icon_background.png differ
diff --git a/example/assets/icon/icon_foreground.png b/example/assets/icon/icon_foreground.png
new file mode 100644
index 0000000..96fd67d
Binary files /dev/null and b/example/assets/icon/icon_foreground.png differ
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 9771dd6..cad2015 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.7"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
async:
dependency: transitive
description:
@@ -89,6 +97,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.4"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.2"
clock:
dependency: transitive
description:
@@ -197,6 +221,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ name: flutter_launcher_icons
+ sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -311,6 +343,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -692,6 +732,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.5.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.3"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.32.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 6d8d768..ca5bc7f 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -19,6 +19,17 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
+ flutter_launcher_icons: ^0.14.1
flutter:
uses-material-design: true
+
+# Launcher icon: a sheet of paper with a smartphone scanning it.
+# Regenerate with: dart run flutter_launcher_icons
+flutter_launcher_icons:
+ android: true
+ ios: false
+ image_path: "assets/icon/icon.png"
+ adaptive_icon_foreground: "assets/icon/icon_foreground.png"
+ adaptive_icon_background: "assets/icon/icon_background.png"
+ min_sdk_android: 21
diff --git a/example/tool/make_icon.py b/example/tool/make_icon.py
new file mode 100644
index 0000000..28877e2
--- /dev/null
+++ b/example/tool/make_icon.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""Generate the Document Scanner launcher icon: a sheet of paper with a
+smartphone scanning it (green scan brackets + beam)."""
+from PIL import Image, ImageDraw
+import sys
+
+SS = 4 # supersample factor for anti-aliasing
+S = 1024 # final size
+W = S * SS # working size
+
+BLUE_TOP = (30, 136, 229)
+BLUE_BOT = (13, 71, 161)
+WHITE = (255, 255, 255, 255)
+LINE = (207, 216, 220, 255)
+PHONE = (38, 50, 56, 255)
+SCREEN = (236, 239, 241, 255)
+LENS = (96, 125, 139, 255)
+GREEN = (0, 230, 118, 255)
+
+
+def px(v): # scale a 1024-space value into working space
+ return int(v * SS)
+
+
+def vgradient_rounded(size, top, bottom, radius):
+ base = Image.new("RGBA", (size, size), (0, 0, 0, 0))
+ grad = Image.new("RGBA", (size, size))
+ gd = ImageDraw.Draw(grad)
+ for y in range(size):
+ t = y / (size - 1)
+ r = int(top[0] + (bottom[0] - top[0]) * t)
+ g = int(top[1] + (bottom[1] - top[1]) * t)
+ b = int(top[2] + (bottom[2] - top[2]) * t)
+ gd.line([(0, y), (size, y)], fill=(r, g, b, 255))
+ mask = Image.new("L", (size, size), 0)
+ ImageDraw.Draw(mask).rounded_rectangle(
+ [0, 0, size - 1, size - 1], radius=radius, fill=255
+ )
+ base.paste(grad, (0, 0), mask)
+ return base
+
+
+def make_subject():
+ """Subject (sheet + scanning phone) on a transparent WxW canvas, centered."""
+ canvas = Image.new("RGBA", (W, W), (0, 0, 0, 0))
+ cx = W // 2
+
+ # --- document (back), tilted left ---
+ dw, dh = px(430), px(560)
+ doc = Image.new("RGBA", (dw, dh), (0, 0, 0, 0))
+ dd = ImageDraw.Draw(doc)
+ dd.rounded_rectangle([0, 0, dw - 1, dh - 1], radius=px(26), fill=WHITE)
+ lx0, lx1 = px(58), dw - px(58)
+ ly, gap, lh = px(86), px(74), px(26)
+ for i in range(5):
+ x1 = lx1 if i != 4 else int(dw * 0.6)
+ dd.rounded_rectangle([lx0, ly, x1, ly + lh], radius=lh // 2, fill=LINE)
+ ly += gap
+ doc = doc.rotate(11, expand=True, resample=Image.BICUBIC)
+ canvas.alpha_composite(
+ doc, (cx - px(150) - doc.width // 2, W // 2 - px(70) - doc.height // 2)
+ )
+
+ # --- smartphone (front, tilted right) actively scanning ---
+ pw, ph = px(320), px(600)
+ phone = Image.new("RGBA", (pw, ph), (0, 0, 0, 0))
+ pd = ImageDraw.Draw(phone)
+ pd.rounded_rectangle([0, 0, pw - 1, ph - 1], radius=px(58), fill=PHONE)
+ m = px(20)
+ pd.rounded_rectangle(
+ [m, px(64), pw - m, ph - px(64)], radius=px(40), fill=SCREEN
+ )
+ pd.ellipse([pw // 2 - px(9), px(30), pw // 2 + px(9), px(48)], fill=LENS)
+
+ # green scan corner brackets + beam on the phone screen
+ sx0, sy0 = m + px(30), px(140)
+ sx1, sy1 = pw - m - px(30), ph - px(140)
+ t, blen = px(15), px(66)
+ pd.rectangle([sx0, sy0, sx0 + blen, sy0 + t], fill=GREEN)
+ pd.rectangle([sx0, sy0, sx0 + t, sy0 + blen], fill=GREEN)
+ pd.rectangle([sx1 - blen, sy0, sx1, sy0 + t], fill=GREEN)
+ pd.rectangle([sx1 - t, sy0, sx1, sy0 + blen], fill=GREEN)
+ pd.rectangle([sx0, sy1 - t, sx0 + blen, sy1], fill=GREEN)
+ pd.rectangle([sx0, sy1 - blen, sx0 + t, sy1], fill=GREEN)
+ pd.rectangle([sx1 - blen, sy1 - t, sx1, sy1], fill=GREEN)
+ pd.rectangle([sx1 - t, sy1 - blen, sx1, sy1], fill=GREEN)
+ by = (sy0 + sy1) // 2
+ pd.rectangle([sx0, by - px(5), sx1, by + px(5)], fill=(0, 230, 118, 200))
+
+ phone = phone.rotate(-11, expand=True, resample=Image.BICUBIC)
+ canvas.alpha_composite(
+ phone, (cx + px(150) - phone.width // 2, W // 2 + px(80) - phone.height // 2)
+ )
+ return canvas
+
+
+def fit_subject(subject, frac):
+ """Return a WxW canvas with the subject scaled to occupy `frac` of width."""
+ bbox = subject.getbbox()
+ cropped = subject.crop(bbox)
+ target_w = int(W * frac)
+ scale = target_w / cropped.width
+ target_h = int(cropped.height * scale)
+ if target_h > int(W * frac):
+ scale = int(W * frac) / cropped.height
+ target_w = int(cropped.width * scale)
+ target_h = int(W * frac)
+ resized = cropped.resize((target_w, target_h), Image.LANCZOS)
+ out = Image.new("RGBA", (W, W), (0, 0, 0, 0))
+ out.alpha_composite(resized, ((W - target_w) // 2, (W - target_h) // 2))
+ return out
+
+
+def main():
+ legacy_path, fg_path, bg_path = sys.argv[1], sys.argv[2], sys.argv[3]
+ subject = make_subject()
+
+ # Legacy full icon: gradient rounded-square bg + subject
+ bg = vgradient_rounded(W, BLUE_TOP, BLUE_BOT, radius=px(180))
+ legacy = bg.copy()
+ legacy.alpha_composite(fit_subject(subject, 0.74))
+ legacy.resize((S, S), Image.LANCZOS).save(legacy_path)
+
+ # Adaptive foreground: subject fills most of the drawable; the adaptive
+ # XML adds a 16% inset for the safe zone (0.90 * 0.68 ≈ 0.61 of the icon).
+ fit_subject(subject, 0.90).resize((S, S), Image.LANCZOS).save(fg_path)
+
+ # Adaptive background: full-bleed gradient (launcher applies the mask)
+ fullbg = Image.new("RGBA", (W, W))
+ gd = ImageDraw.Draw(fullbg)
+ for y in range(W):
+ t = y / (W - 1)
+ gd.line(
+ [(0, y), (W, y)],
+ fill=(
+ int(BLUE_TOP[0] + (BLUE_BOT[0] - BLUE_TOP[0]) * t),
+ int(BLUE_TOP[1] + (BLUE_BOT[1] - BLUE_TOP[1]) * t),
+ int(BLUE_TOP[2] + (BLUE_BOT[2] - BLUE_TOP[2]) * t),
+ 255,
+ ),
+ )
+ fullbg.resize((S, S), Image.LANCZOS).save(bg_path)
+
+ print("wrote", legacy_path, fg_path, bg_path)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/lib/src/ui/document_camera_screen.dart b/lib/src/ui/document_camera_screen.dart
index 566ebae..166ccde 100644
--- a/lib/src/ui/document_camera_screen.dart
+++ b/lib/src/ui/document_camera_screen.dart
@@ -200,6 +200,15 @@ class _DocumentCameraScreenState extends State
}
Widget _buildCameraView() {
+ // `controller.value.aspectRatio` is the sensor ratio in *landscape*
+ // (>= 1). On a portrait screen we must invert it, otherwise the preview is
+ // forced into a wide/short box: small and horizontally squashed.
+ final sensorRatio = _controller!.value.aspectRatio;
+ final previewRatio =
+ MediaQuery.of(context).orientation == Orientation.portrait
+ ? 1 / sensorRatio
+ : sensorRatio;
+
return Stack(
fit: StackFit.expand,
children: [
@@ -207,7 +216,7 @@ class _DocumentCameraScreenState extends State
// so that fractional corner positions map directly to the captured image.
Center(
child: AspectRatio(
- aspectRatio: _controller!.value.aspectRatio,
+ aspectRatio: previewRatio,
child: Stack(
fit: StackFit.expand,
children: [