diff --git a/.github/architecture/11-element-system.md b/.github/architecture/11-element-system.md index 887f179b..188a9481 100644 --- a/.github/architecture/11-element-system.md +++ b/.github/architecture/11-element-system.md @@ -656,6 +656,15 @@ CanvasWorkspace 遍历 elements 每个物料包可提供 Viewer 渲染扩展: ```typescript +interface ViewerRenderContext { + readonly document: Document + data: Record + resolvedProps: Record + pageIndex: number + unit: string + zoom: number +} + interface MaterialViewerExtension { render(node: MaterialNode, context: ViewerRenderContext): ViewerRenderOutput measure?(node: MaterialNode, context: ViewerMeasureContext): ViewerMeasureResult @@ -666,6 +675,7 @@ interface MaterialViewerExtension { 其中职责边界如下: - `render()` 只负责生成物料内容 DOM/SVG。 +- `context.document` 是当前 Viewer host 的文档;物料返回 DOM/SVG 节点时必须使用它创建节点,不能依赖全局 `document`,以兼容 iframe 和 custom host。 - `measure()` 负责参与运行时测量和回流,典型场景是 table-data 这类动态高度物料。 - `getRenderSize()` 负责声明最终渲染容器尺寸,适用于“运行态视觉尺寸不等于 schema 几何尺寸”的物料;例如线条物料可把 legacy `lineWidth` 或最小可见厚度提升为实际渲染高度。 - `viewer` 的 `RenderSurface` 不允许根据 `node.type` 推断这些差异,必须统一通过 `MaterialRendererRegistry -> MaterialViewerExtension` 获取。 diff --git a/CLAUDE.md b/CLAUDE.md index 873fb942..d84132d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,4 +75,3 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - Each package room needs to import packages through a pnpm workspace - Ensure that the projects `pnpm build`, `pnpm lint`, and `pnpm typecheck` pass (in order) - Centralize dependencies workspace catalog -- The project is still evolving and should publish the current best design only. Do not add compatibility or migration wording, paths, shims, aliases, or documentation unless the user explicitly asks for them. diff --git a/eslint.config.js b/eslint.config.js index 02cfc693..2e3eccb3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ export default antfu( type: 'lib', vue: true, pnpm: false, - ignores: ['.github/**/*.md', 'docs/**/*.md', '**/bin/**', '**/obj/**', 'lib/EasyInk.Render/host/internal/easyink/runtime/easyink-viewer/assets/**'], + ignores: ['.github/**/*.md', 'docs/**/*.md', '**/bin/**', '**/obj/**', '**/.dart_tool/**', 'lib/**'], rules: { 'ts/explicit-function-return-type': 'off', }, diff --git a/internal-packages/viewer-runtime/package.json b/internal-packages/viewer-runtime/package.json index 1c0374d0..0e5d12f3 100644 --- a/internal-packages/viewer-runtime/package.json +++ b/internal-packages/viewer-runtime/package.json @@ -5,6 +5,7 @@ "private": true, "description": "Internal EasyInk.Render HTML runtime bundle built from @easyink/viewer", "sideEffects": [ + "./src/polyfills.ts", "./src/style.css" ], "scripts": { @@ -13,7 +14,8 @@ }, "dependencies": { "@easyink/builtin": "workspace:*", - "@easyink/viewer": "workspace:*" + "@easyink/viewer": "workspace:*", + "core-js": "catalog:framework" }, "devDependencies": { "@types/node": "catalog:types", diff --git a/internal-packages/viewer-runtime/src/main.ts b/internal-packages/viewer-runtime/src/main.ts index 8c0e8e74..a557297b 100644 --- a/internal-packages/viewer-runtime/src/main.ts +++ b/internal-packages/viewer-runtime/src/main.ts @@ -1,6 +1,7 @@ import type { DocumentSchema, DocumentSchemaInput, ViewerDiagnosticEvent } from '@easyink/viewer' import { registerBuiltinViewerMaterials } from '@easyink/builtin/all' import { createViewer, normalizeDocumentSchema } from '@easyink/viewer' +import './polyfills' import './style.css' interface RuntimePayload { diff --git a/internal-packages/viewer-runtime/src/polyfills.ts b/internal-packages/viewer-runtime/src/polyfills.ts new file mode 100644 index 00000000..263fda53 --- /dev/null +++ b/internal-packages/viewer-runtime/src/polyfills.ts @@ -0,0 +1,21 @@ +import 'core-js/stable' + +if (!Element.prototype.replaceChildren) { + Object.defineProperty(Element.prototype, 'replaceChildren', { + configurable: true, + writable: true, + value(this: Element, ...nodes: Array): void { + const fragment = this.ownerDocument.createDocumentFragment() + for (const node of nodes) { + fragment.appendChild( + typeof node === 'string' + ? this.ownerDocument.createTextNode(node) + : node, + ) + } + while (this.firstChild) + this.removeChild(this.firstChild) + this.appendChild(fragment) + }, + }) +} diff --git a/internal-packages/viewer-runtime/vite.config.ts b/internal-packages/viewer-runtime/vite.config.ts index f48b8949..e8fcc2c5 100644 --- a/internal-packages/viewer-runtime/vite.config.ts +++ b/internal-packages/viewer-runtime/vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ build: { outDir: 'dist/runtime/easyink-viewer/assets', emptyOutDir: true, - target: 'es2022', + target: 'chrome83', minify: false, cssCodeSplit: false, lib: { diff --git a/lib/EasyInk.Android/README.md b/lib/EasyInk.Android/README.md index 07aaa684..032181e8 100644 --- a/lib/EasyInk.Android/README.md +++ b/lib/EasyInk.Android/README.md @@ -12,6 +12,7 @@ EasyInk Android 是原生 AAR 渲染 SDK。它把 `html` 或 `easyink schema + d | 技术架构、边界、渲染流程、安全模型 | [ARCHITECTURE.md](ARCHITECTURE.md) | | 分阶段计划、完成度、剩余工作 | [IMPLEMENTATION_PLAN.md](IMPLEMENTATION_PLAN.md) | | 最小 Android 示例应用 | [samples/minimal](samples/minimal) | +| Flutter 插件化示例应用 | [samples/flutter](samples/flutter) | SDK 边界:本包只负责渲染 PDF 或图片文件,不负责物理打印、系统打印 UI、蓝牙/USB 打印机协议或厂商打印 SDK。 @@ -29,6 +30,7 @@ SDK 边界:本包只负责渲染 PDF 或图片文件,不负责物理打印 - PDF 输出使用 `renderedPages` 作为页面尺寸来源。 - 图片输出使用 `.ei-viewer-page` 页面 rect 和 `renderedPages` 控制单页截图尺寸。 - `samples/minimal` 提供最小 Android app,可在 Android Studio 中运行 PDF/图片输出验证。 +- `samples/flutter` 提供独立 Flutter plugin 与最小宿主,可分别验证 HTML 和 Schema 图片输出。 - `maven-publish` 和 release publication 已配置,发布仓库地址、凭据和签名流程仍需按目标仓库补齐。 待补齐: @@ -57,9 +59,22 @@ pnpm android:render-sdk:build ```text lib/EasyInk.Android/build/outputs/aar/easyink-android-render-release.aar +lib/EasyInk.Android/build/repository/com/easyink/easyink-android-render/0.1.0/ lib/EasyInk.Android/samples/minimal/build/outputs/apk/debug/sample-minimal-debug.apk ``` +Flutter 宿主示例: + +```bash +cd lib/EasyInk.Android/samples/flutter +flutter pub get +flutter run +``` + +Flutter sample 通过仓库内生成的 Maven publication 引用 Android SDK,并要求 +Flutter 3.38 或更高版本以匹配当前 AAR 的 Kotlin 2.2 metadata。运行时先点击 +`Render HTML images`,再点击 `Render Schema images`。 + 注意: - Gradle 下载和缓存放在 `lib/EasyInk.Android/.gradle/` 下,不在仓库根目录生成 `.gradle/`。 diff --git a/lib/EasyInk.Android/build.gradle.kts b/lib/EasyInk.Android/build.gradle.kts index 7e4606d6..4d7f6057 100644 --- a/lib/EasyInk.Android/build.gradle.kts +++ b/lib/EasyInk.Android/build.gradle.kts @@ -90,6 +90,12 @@ tasks.register("verifyAarRuntimeAssets") { afterEvaluate { publishing { + repositories { + maven { + name = "flutterSample" + url = layout.buildDirectory.dir("repository").get().asFile.toURI() + } + } publications { create("release") { from(components["release"]) diff --git a/lib/EasyInk.Android/samples/flutter/.gitignore b/lib/EasyInk.Android/samples/flutter/.gitignore new file mode 100644 index 00000000..24476c5d --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/lib/EasyInk.Android/samples/flutter/.metadata b/lib/EasyInk.Android/samples/flutter/.metadata new file mode 100644 index 00000000..73bb63c4 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: "8b872868494e429d94fa06dca855c306438b22c0" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 8b872868494e429d94fa06dca855c306438b22c0 + base_revision: 8b872868494e429d94fa06dca855c306438b22c0 + - platform: android + create_revision: 8b872868494e429d94fa06dca855c306438b22c0 + base_revision: 8b872868494e429d94fa06dca855c306438b22c0 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/lib/EasyInk.Android/samples/flutter/README.md b/lib/EasyInk.Android/samples/flutter/README.md new file mode 100644 index 00000000..237c5523 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/README.md @@ -0,0 +1,67 @@ +# EasyInk Flutter Sample + +This sample exercises the Android SDK through the local `easyink_android` +Flutter plugin. +It renders both a minimal HTML document and the bundled supermarket Schema/Data +fixture to PNG files. + +The project uses Flutter 3.38 or newer because the current AAR is compiled with +Kotlin 2.2 metadata. Older Flutter Android templates need to be upgraded before +they can compile this sample's Kotlin bridge. + +## Run + +Build the AAR and repository-local Maven publication first from the repository +root: + +```bash +pnpm android:render-sdk:build +``` + +Then run the Flutter app on an Android device or emulator: + +```bash +cd lib/EasyInk.Android/samples/flutter +flutter pub get +flutter run +``` + +The plugin resolves `com.easyink:easyink-android-render:0.1.0` from the local +Maven repository generated at `lib/EasyInk.Android/build/repository`. +The sample Android root declares that repository in `android/build.gradle.kts`; +production hosts should replace it with the configured remote Maven repository. +The Flutter app manifest enables Internet access and the SDK loopback network +security resource. The sample uses a 120-second readiness timeout for diagnosis. + +Use `Render HTML images` first. It tests the local HTML loading path without the +viewer Schema runtime. `Render Schema images` then tests Flutter asset loading, +MethodChannel string transport, and the embedded viewer runtime. + +Successful images are published to the public `Download/EasyInk` directory. On +Android 10 and newer the sample uses MediaStore; Android 9 and older request the +legacy storage permission before rendering. + +For focused logs: + +```bash +adb logcat -s EasyInkAndroidPlugin chromium cr_AwContents +``` + +The Android side logs only request sizes and the WebView package version; it does +not print the full Schema or Data payload. + +## Older System WebView + +The Schema path executes the viewer JavaScript embedded in the Android AAR. The +runtime avoids logical assignment syntax such as `||=` so it can be parsed by +older System WebView versions. If readiness still times out, inspect the WebView +console before increasing the timeout: a parse error prevents +`window.easyinkReady` from ever being set. + +## Structure + +- `lib/` contains only sample UI and calls the typed plugin API. +- `plugins/easyink_android/lib/` owns the public Dart API. +- `plugins/easyink_android/android/` owns MethodChannel, Activity permissions, + EasyInk SDK calls, task cleanup, and Downloads publishing. +- `MainActivity` is intentionally empty so plugin auto-registration is covered. diff --git a/lib/EasyInk.Android/samples/flutter/analysis_options.yaml b/lib/EasyInk.Android/samples/flutter/analysis_options.yaml new file mode 100644 index 00000000..66d6ef8f --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/analysis_options.yaml @@ -0,0 +1,27 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: {} + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/EasyInk.Android/samples/flutter/android/.gitignore b/lib/EasyInk.Android/samples/flutter/android/.gitignore new file mode 100644 index 00000000..6f568019 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/lib/EasyInk.Android/samples/flutter/android/app/build.gradle.kts b/lib/EasyInk.Android/samples/flutter/android/app/build.gradle.kts new file mode 100644 index 00000000..21037410 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.android.application") + id("kotlin-android") + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.easyink.sample.easyink_flutter_sample" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + applicationId = "com.easyink.sample.easyink_flutter_sample" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/debug/AndroidManifest.xml b/lib/EasyInk.Android/samples/flutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..58637f0e --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/AndroidManifest.xml b/lib/EasyInk.Android/samples/flutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ccdac804 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/kotlin/com/easyink/sample/easyink_flutter_sample/MainActivity.kt b/lib/EasyInk.Android/samples/flutter/android/app/src/main/kotlin/com/easyink/sample/easyink_flutter_sample/MainActivity.kt new file mode 100644 index 00000000..fa580d4c --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/main/kotlin/com/easyink/sample/easyink_flutter_sample/MainActivity.kt @@ -0,0 +1,5 @@ +package com.easyink.sample.easyink_flutter_sample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/drawable/launch_background.xml b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/values-night/styles.xml b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/values/styles.xml b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/lib/EasyInk.Android/samples/flutter/android/app/src/profile/AndroidManifest.xml b/lib/EasyInk.Android/samples/flutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..58637f0e --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/lib/EasyInk.Android/samples/flutter/android/build.gradle.kts b/lib/EasyInk.Android/samples/flutter/android/build.gradle.kts new file mode 100644 index 00000000..aa862f01 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + maven { + url = uri(rootProject.projectDir.resolve("../../../build/repository")) + } + } +} + +val newBuildDir = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.set(newBuildDir) + +subprojects { + layout.buildDirectory.set(newBuildDir.dir(name)) + evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/lib/EasyInk.Android/samples/flutter/android/gradle.properties b/lib/EasyInk.Android/samples/flutter/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/lib/EasyInk.Android/samples/flutter/android/gradle/wrapper/gradle-wrapper.properties b/lib/EasyInk.Android/samples/flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e4ef43fb --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/lib/EasyInk.Android/samples/flutter/android/settings.gradle.kts b/lib/EasyInk.Android/samples/flutter/android/settings.gradle.kts new file mode 100644 index 00000000..970fd7a2 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/android/settings.gradle.kts @@ -0,0 +1,24 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + requireNotNull(properties.getProperty("flutter.sdk")) { "flutter.sdk not set" } + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/lib/EasyInk.Android/samples/flutter/assets/supermarket-receipt.data.json b/lib/EasyInk.Android/samples/flutter/assets/supermarket-receipt.data.json new file mode 100644 index 00000000..6f6c426f --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/assets/supermarket-receipt.data.json @@ -0,0 +1,59 @@ +{ + "store": { + "name": "好又多超市", + "address": "北京市朝阳区望京街道示例路88号", + "phone": "010-12345678" + }, + "transaction": { + "no": "TXN-20260423-1234", + "date": "2026-04-23", + "time": "14:35:22", + "cashier": "小李", + "paymentMethod": "微信支付" + }, + "items": [ + { + "name": "有机牛奶250ml*4", + "qty": 1, + "unitPrice": 32.8, + "subtotal": 32.8 + }, + { + "name": "东北大米5kg", + "qty": 2, + "unitPrice": 45.9, + "subtotal": 91.8 + }, + { + "name": "进口苹果", + "qty": 1.5, + "unitPrice": 18, + "subtotal": 27 + }, + { + "name": "鸡蛋10枚装", + "qty": 3, + "unitPrice": 15.9, + "subtotal": 47.7 + }, + { + "name": "切片面包", + "qty": 1, + "unitPrice": 9.8, + "subtotal": 9.8 + } + ], + "subtotal": 209.1, + "itemDiscount": 10.2, + "couponAmount": 20, + "grandTotal": 178.9, + "amountPaid": 200, + "change": 21.1, + "member": { + "no": "M-20260001", + "earnedPoints": 178, + "totalPoints": 5412 + }, + "qrUrl": "https://example.com/receipt/TXN-20260423-1234", + "footer": "感谢您的惠顾,欢迎再次光临!" +} diff --git a/lib/EasyInk.Android/samples/flutter/assets/supermarket-receipt.schema.json b/lib/EasyInk.Android/samples/flutter/assets/supermarket-receipt.schema.json new file mode 100644 index 00000000..84ca10f6 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/assets/supermarket-receipt.schema.json @@ -0,0 +1,758 @@ +{ + "version": "1.0.0", + "unit": "mm", + "page": { + "mode": "continuous", + "width": 80, + "height": 200, + "layout": { + "strategy": "stack-flow", + "flowAxis": "y" + }, + "pagination": { + "strategy": "none" + }, + "reflow": { + "strategy": "flow-y", + "preserveTrailingGap": true, + "collisionPolicy": "diagnose" + } + }, + "guides": { + "x": [], + "y": [] + }, + "elements": [ + { + "id": "smkt_store_name", + "type": "text", + "x": 4, + "y": 4, + "width": 72, + "height": 8, + "props": { + "content": "{#店铺名称}", + "fontSize": 5.64, + "fontWeight": "bold", + "textAlign": "center", + "verticalAlign": "middle", + "color": "#1a1a1a" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "store/name", + "fieldLabel": "店铺名称" + } + }, + { + "id": "smkt_store_address", + "type": "text", + "x": 4, + "y": 14, + "width": 72, + "height": 5, + "props": { + "content": "{#地址}", + "fontSize": 2.82, + "textAlign": "center", + "verticalAlign": "middle", + "color": "#666666" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "store/address", + "fieldLabel": "地址" + } + }, + { + "id": "smkt_store_phone", + "type": "text", + "x": 4, + "y": 20, + "width": 72, + "height": 5, + "props": { + "content": "{#电话}", + "fontSize": 2.82, + "textAlign": "center", + "verticalAlign": "middle", + "color": "#666666" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "store/phone", + "fieldLabel": "电话" + } + }, + { + "id": "smkt_sep1", + "type": "line", + "x": 4, + "y": 27, + "width": 72, + "height": 0.5, + "props": { + "lineColor": "#333333", + "lineType": "dashed" + } + }, + { + "id": "smkt_txn_no", + "type": "text", + "x": 4, + "y": 30, + "width": 72, + "height": 5, + "props": { + "content": "单号: {#流水号}", + "fontSize": 2.82, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "transaction/no", + "fieldLabel": "流水号" + } + }, + { + "id": "smkt_txn_date", + "type": "text", + "x": 4, + "y": 36, + "width": 35, + "height": 5, + "props": { + "content": "{#日期}", + "fontSize": 2.82, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "transaction/date", + "fieldLabel": "日期" + } + }, + { + "id": "smkt_txn_time", + "type": "text", + "x": 39, + "y": 36, + "width": 37, + "height": 5, + "props": { + "content": "{#时间}", + "fontSize": 2.82, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "transaction/time", + "fieldLabel": "时间" + } + }, + { + "id": "smkt_txn_cashier", + "type": "text", + "x": 4, + "y": 42, + "width": 72, + "height": 5, + "props": { + "content": "收银员: {#收银员}", + "fontSize": 2.82, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "transaction/cashier", + "fieldLabel": "收银员" + } + }, + { + "id": "smkt_sep2", + "type": "line", + "x": 4, + "y": 49, + "width": 72, + "height": 0.5, + "props": { + "lineColor": "#333333", + "lineType": "dashed" + } + }, + { + "id": "smkt_items_table", + "type": "table-data", + "x": 4, + "y": 52, + "width": 72, + "height": 24, + "props": { + "headerBackground": "#f5f5f5", + "summaryBackground": "#fafafa", + "stripedRows": false, + "stripedColor": "#fafafa", + "borderWidth": 0, + "cellPadding": 0.35, + "typography": { + "fontSize": 2.82, + "color": "#1a1a1a", + "fontWeight": "normal", + "fontStyle": "normal", + "lineHeight": 1.2, + "letterSpacing": 0, + "textAlign": "left", + "verticalAlign": "middle" + } + }, + "table": { + "kind": "data", + "showHeader": true, + "showFooter": false, + "topology": { + "columns": [ + { + "ratio": 0.44 + }, + { + "ratio": 0.12 + }, + { + "ratio": 0.2 + }, + { + "ratio": 0.24 + } + ], + "rows": [ + { + "height": 4, + "role": "header", + "cells": [ + { + "content": { + "text": "品名" + } + }, + { + "content": { + "text": "数量" + } + }, + { + "content": { + "text": "单价" + } + }, + { + "content": { + "text": "小计" + } + } + ] + }, + { + "height": 4, + "role": "repeat-template", + "cells": [ + { + "binding": { + "sourceId": "supermarket", + "fieldPath": "items/name", + "fieldLabel": "品名" + } + }, + { + "binding": { + "sourceId": "supermarket", + "fieldPath": "items/qty", + "fieldLabel": "数量" + } + }, + { + "binding": { + "sourceId": "supermarket", + "fieldPath": "items/unitPrice", + "fieldLabel": "单价" + } + }, + { + "binding": { + "sourceId": "supermarket", + "fieldPath": "items/subtotal", + "fieldLabel": "小计" + } + } + ] + } + ] + }, + "layout": { + "borderAppearance": "none", + "borderWidth": 0, + "borderType": "solid", + "borderColor": "#cccccc" + } + } + }, + { + "id": "smkt_sep3", + "type": "line", + "x": 4, + "y": 79, + "width": 72, + "height": 0.5, + "props": { + "lineColor": "#333333", + "lineType": "dashed" + } + }, + { + "id": "smkt_subtotal_label", + "type": "text", + "x": 4, + "y": 82, + "width": 40, + "height": 5, + "props": { + "content": "商品合计", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_subtotal_value", + "type": "text", + "x": 44, + "y": 82, + "width": 32, + "height": 5, + "props": { + "content": "{#商品合计}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "subtotal", + "fieldLabel": "商品合计" + } + }, + { + "id": "smkt_discount_label", + "type": "text", + "x": 4, + "y": 88, + "width": 40, + "height": 5, + "props": { + "content": "商品折扣", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_discount_value", + "type": "text", + "x": 44, + "y": 88, + "width": 32, + "height": 5, + "props": { + "content": "-{#商品折扣}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#e04545" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "itemDiscount", + "fieldLabel": "商品折扣" + } + }, + { + "id": "smkt_coupon_label", + "type": "text", + "x": 4, + "y": 94, + "width": 40, + "height": 5, + "props": { + "content": "优惠券减免", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_coupon_value", + "type": "text", + "x": 44, + "y": 94, + "width": 32, + "height": 5, + "props": { + "content": "-{#优惠券减免}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#e04545" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "couponAmount", + "fieldLabel": "优惠券减免" + } + }, + { + "id": "smkt_sep4", + "type": "line", + "x": 4, + "y": 101, + "width": 72, + "height": 0.5, + "props": { + "lineColor": "#333333", + "lineType": "solid" + } + }, + { + "id": "smkt_total_label", + "type": "text", + "x": 4, + "y": 104, + "width": 40, + "height": 8, + "props": { + "content": "实付金额", + "fontSize": 4.23, + "fontWeight": "bold", + "textAlign": "left", + "verticalAlign": "middle", + "color": "#1a1a1a" + } + }, + { + "id": "smkt_total_value", + "type": "text", + "x": 44, + "y": 104, + "width": 32, + "height": 8, + "props": { + "content": "{#实付金额}", + "fontSize": 4.94, + "fontWeight": "bold", + "textAlign": "right", + "verticalAlign": "middle", + "color": "#1a1a1a" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "grandTotal", + "fieldLabel": "实付金额" + } + }, + { + "id": "smkt_sep5", + "type": "line", + "x": 4, + "y": 114, + "width": 72, + "height": 0.5, + "props": { + "lineColor": "#333333", + "lineType": "dashed" + } + }, + { + "id": "smkt_pay_method_label", + "type": "text", + "x": 4, + "y": 117, + "width": 40, + "height": 5, + "props": { + "content": "付款方式", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_pay_method_value", + "type": "text", + "x": 44, + "y": 117, + "width": 32, + "height": 5, + "props": { + "content": "{#付款方式}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "transaction/paymentMethod", + "fieldLabel": "付款方式" + } + }, + { + "id": "smkt_amount_paid_label", + "type": "text", + "x": 4, + "y": 123, + "width": 40, + "height": 5, + "props": { + "content": "付款金额", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_amount_paid_value", + "type": "text", + "x": 44, + "y": 123, + "width": 32, + "height": 5, + "props": { + "content": "{#付款金额}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "amountPaid", + "fieldLabel": "付款金额" + } + }, + { + "id": "smkt_change_label", + "type": "text", + "x": 4, + "y": 129, + "width": 40, + "height": 5, + "props": { + "content": "找零", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_change_value", + "type": "text", + "x": 44, + "y": 129, + "width": 32, + "height": 5, + "props": { + "content": "{#找零}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "change", + "fieldLabel": "找零" + } + }, + { + "id": "smkt_sep6", + "type": "line", + "x": 4, + "y": 136, + "width": 72, + "height": 0.5, + "props": { + "lineColor": "#333333", + "lineType": "dashed" + } + }, + { + "id": "smkt_member_no_label", + "type": "text", + "x": 4, + "y": 139, + "width": 40, + "height": 5, + "props": { + "content": "会员号", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_member_no_value", + "type": "text", + "x": 44, + "y": 139, + "width": 32, + "height": 5, + "props": { + "content": "{#会员号}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "member/no", + "fieldLabel": "会员号" + } + }, + { + "id": "smkt_member_pts_label", + "type": "text", + "x": 4, + "y": 145, + "width": 40, + "height": 5, + "props": { + "content": "本次积分", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_member_pts_value", + "type": "text", + "x": 44, + "y": 145, + "width": 32, + "height": 5, + "props": { + "content": "+{#本次积分}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#1677ff" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "member/earnedPoints", + "fieldLabel": "本次积分" + } + }, + { + "id": "smkt_member_total_label", + "type": "text", + "x": 4, + "y": 151, + "width": 40, + "height": 5, + "props": { + "content": "累计积分", + "fontSize": 3.18, + "textAlign": "left", + "verticalAlign": "middle", + "color": "#666666" + } + }, + { + "id": "smkt_member_total_value", + "type": "text", + "x": 44, + "y": 151, + "width": 32, + "height": 5, + "props": { + "content": "{#累计积分}", + "fontSize": 3.18, + "textAlign": "right", + "verticalAlign": "middle", + "color": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "member/totalPoints", + "fieldLabel": "累计积分" + } + }, + { + "id": "smkt_sep7", + "type": "line", + "x": 4, + "y": 158, + "width": 72, + "height": 0.5, + "props": { + "lineColor": "#333333", + "lineType": "dashed" + } + }, + { + "id": "smkt_qrcode", + "type": "qrcode", + "x": 30, + "y": 161, + "width": 20, + "height": 20, + "props": { + "value": "https://example.com/receipt", + "cellSize": 3, + "margin": 1, + "foreground": "#333333" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "qrUrl", + "fieldLabel": "二维码链接" + } + }, + { + "id": "smkt_footer", + "type": "text", + "x": 4, + "y": 184, + "width": 72, + "height": 5, + "props": { + "content": "{#底部文字}", + "fontSize": 2.82, + "textAlign": "center", + "verticalAlign": "middle", + "color": "#999999" + }, + "binding": { + "sourceId": "supermarket", + "fieldPath": "footer", + "fieldLabel": "底部文字" + } + } + ] +} diff --git a/lib/EasyInk.Android/samples/flutter/lib/main.dart b/lib/EasyInk.Android/samples/flutter/lib/main.dart new file mode 100644 index 00000000..14e1e18e --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/lib/main.dart @@ -0,0 +1,103 @@ +import 'package:easyink_android/easyink_android.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show PlatformException, rootBundle; + +void main() { + runApp(const EasyInkSampleApp()); +} + +class EasyInkSampleApp extends StatelessWidget { + const EasyInkSampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'EasyInk Flutter sample', + theme: ThemeData(primarySwatch: Colors.blue), + home: const EasyInkSamplePage(), + ); + } +} + +class EasyInkSamplePage extends StatefulWidget { + const EasyInkSamplePage({super.key}); + + @override + State createState() => _EasyInkSamplePageState(); +} + +class _EasyInkSamplePageState extends State { + final _renderer = EasyInkAndroid(); + + String _status = 'Ready'; + bool _busy = false; + + Future _render(String mode) async { + if (_busy) return; + + setState(() { + _busy = true; + _status = 'Rendering $mode...'; + }); + + try { + final files = mode == 'html' + ? await _renderer.renderHtmlImages( + html: + '
Flutter EasyInk test
', + ) + : await _renderer.renderSchemaImages( + schemaJson: await rootBundle.loadString( + 'assets/supermarket-receipt.schema.json', + ), + dataJson: await rootBundle.loadString( + 'assets/supermarket-receipt.data.json', + ), + ); + if (!mounted) return; + setState(() { + _status = 'Success\n${files.join('\n')}'; + }); + } on PlatformException catch (error) { + if (!mounted) return; + setState(() { + _status = 'Failed\n${error.code}: ${error.message}'; + }); + } finally { + if (mounted) { + setState(() { + _busy = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('EasyInk Flutter sample')), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: _busy ? null : () => _render('html'), + child: const Text('Render HTML images'), + ), + ElevatedButton( + onPressed: _busy ? null : () => _render('schema'), + child: const Text('Render Schema images'), + ), + const SizedBox(height: 16), + Expanded( + child: SingleChildScrollView( + child: SelectableText(_status), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/CHANGELOG.md b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/CHANGELOG.md new file mode 100644 index 00000000..408fecfa --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 0.1.0 + +- Add typed HTML and Schema image rendering APIs. +- Add Android FlutterPlugin and ActivityAware integration. +- Publish rendered PNG files to the public Download/EasyInk directory. diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/README.md b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/README.md new file mode 100644 index 00000000..9d5c3793 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/README.md @@ -0,0 +1,17 @@ +# easyink_android + +Local Flutter plugin used by the EasyInk Android Flutter sample. It provides a +typed Dart API over the native EasyInk AAR and publishes rendered PNG files to +the public `Download/EasyInk` directory. + +The plugin is intentionally Android-only and resolves +`com.easyink:easyink-android-render:0.1.0` from the repository-local Maven +repository generated under `lib/EasyInk.Android/build/repository`. + +```dart +final renderer = EasyInkAndroid(); +final files = await renderer.renderSchemaImages( + schemaJson: schemaJson, + dataJson: dataJson, +); +``` diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/analysis_options.yaml b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/analysis_options.yaml new file mode 100644 index 00000000..f9b30346 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/build.gradle b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/build.gradle new file mode 100644 index 00000000..5a4cee80 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/build.gradle @@ -0,0 +1,50 @@ +group = "com.easyink.flutter" +version = "0.1.0" + +buildscript { + ext.kotlin_version = "2.2.20" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.11.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven { + url = uri("${projectDir}/../../../../../build/repository") + } + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "com.easyink.flutter" + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + defaultConfig { + minSdk = 24 + } + + dependencies { + implementation("com.easyink:easyink-android-render:0.1.0") + } +} diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/settings.gradle b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/settings.gradle new file mode 100644 index 00000000..369656f3 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'easyink_android' diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/src/main/AndroidManifest.xml b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d0adfb22 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/src/main/kotlin/com/easyink/flutter/EasyInkAndroidPlugin.kt b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/src/main/kotlin/com/easyink/flutter/EasyInkAndroidPlugin.kt new file mode 100644 index 00000000..308a868b --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/android/src/main/kotlin/com/easyink/flutter/EasyInkAndroidPlugin.kt @@ -0,0 +1,426 @@ +package com.easyink.flutter + +import android.Manifest +import android.app.Activity +import android.content.ContentValues +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.media.MediaScannerConnection +import android.os.Build +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.util.Log +import android.webkit.WebView +import com.easyink.android.EasyInkImageFormat +import com.easyink.android.EasyInkImageOptions +import com.easyink.android.EasyInkImageRenderResult +import com.easyink.android.EasyInkPdfOptions +import com.easyink.android.EasyInkRenderCallback +import com.easyink.android.EasyInkRenderException +import com.easyink.android.EasyInkRenderRequest +import com.easyink.android.EasyInkRenderSource +import com.easyink.android.EasyInkRenderTask +import com.easyink.android.EasyInkRenderer +import com.easyink.android.EasyInkWaitOptions +import com.easyink.android.EasyInkWaitUntil +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry +import java.io.File +import java.util.Collections +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicReference + +class EasyInkAndroidPlugin : + FlutterPlugin, + MethodChannel.MethodCallHandler, + ActivityAware, + PluginRegistry.RequestPermissionsResultListener { + + private lateinit var applicationContext: Context + private lateinit var channel: MethodChannel + private var activityBinding: ActivityPluginBinding? = null + private var pendingPermission: PendingRender? = null + private var publishExecutor: ExecutorService? = null + private val mainHandler = Handler(Looper.getMainLooper()) + private val activeTasks = Collections.synchronizedSet(mutableSetOf()) + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + applicationContext = binding.applicationContext + publishExecutor = Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, "easyink-flutter-publisher") + } + channel = MethodChannel(binding.binaryMessenger, CHANNEL_NAME) + channel.setMethodCallHandler(this) + if (applicationContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) { + WebView.setWebContentsDebuggingEnabled(true) + } + Log.i(TAG, "attached WebView=${currentWebViewVersion() ?: "unknown"}") + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + cancelActiveTasks() + pendingPermission = null + publishExecutor?.shutdownNow() + publishExecutor = null + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activityBinding = binding + binding.addRequestPermissionsResultListener(this) + } + + override fun onDetachedFromActivityForConfigChanges() { + detachFromActivity(clearPending = false) + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } + + override fun onDetachedFromActivity() { + detachFromActivity(clearPending = true) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + if (call.method != METHOD_RENDER_IMAGES) { + result.notImplemented() + return + } + + val parsed = parseArguments(call.arguments, result) ?: return + if (needsLegacyStoragePermission()) { + requestStoragePermission(parsed, result) + return + } + renderImages(parsed, result) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ): Boolean { + if (requestCode != STORAGE_PERMISSION_REQUEST) { + return false + } + + val pending = pendingPermission ?: return true + pendingPermission = null + if (grantResults.firstOrNull() != PackageManager.PERMISSION_GRANTED) { + pending.result.error( + "STORAGE_PERMISSION_DENIED", + "Storage permission is required to publish files to Downloads.", + null, + ) + return true + } + renderImages(pending.arguments, pending.result) + return true + } + + private fun parseArguments(value: Any?, result: MethodChannel.Result): RenderArguments? { + val arguments = value as? Map<*, *> + if (arguments == null) { + result.error("INVALID_ARGUMENTS", "Arguments must be a map.", null) + return null + } + + val sourceType = arguments["sourceType"] as? String + val scale = (arguments["scale"] as? Number)?.toFloat() ?: 1f + val timeoutMs = (arguments["timeoutMs"] as? Number)?.toLong() ?: DEFAULT_TIMEOUT_MS + if (scale <= 0f || !scale.isFinite()) { + result.error("INVALID_ARGUMENTS", "scale must be greater than zero.", null) + return null + } + if (timeoutMs <= 0L) { + result.error("INVALID_ARGUMENTS", "timeoutMs must be greater than zero.", null) + return null + } + + val source = when (sourceType) { + "html" -> { + val html = arguments["html"] as? String + val width = (arguments["paperWidthMm"] as? Number)?.toDouble() ?: 80.0 + val height = (arguments["paperHeightMm"] as? Number)?.toDouble() ?: 120.0 + if (html.isNullOrBlank() || width <= 0.0 || height <= 0.0) { + result.error("INVALID_ARGUMENTS", "Valid HTML and paper dimensions are required.", null) + return null + } + RenderSource.Html(html, width, height) + } + "schema" -> { + val schemaJson = arguments["schemaJson"] as? String + val dataJson = arguments["dataJson"] as? String + if (schemaJson.isNullOrBlank() || dataJson.isNullOrBlank()) { + result.error("INVALID_ARGUMENTS", "Schema and data are required.", null) + return null + } + RenderSource.Schema(schemaJson, dataJson) + } + else -> { + result.error("INVALID_ARGUMENTS", "sourceType must be html or schema.", null) + return null + } + } + + Log.i( + TAG, + "render sourceType=$sourceType payloadLength=${source.payloadLength} " + + "scale=$scale timeoutMs=$timeoutMs", + ) + return RenderArguments(source, scale, timeoutMs) + } + + private fun requestStoragePermission( + arguments: RenderArguments, + result: MethodChannel.Result, + ) { + if (pendingPermission != null) { + result.error("PERMISSION_REQUEST_ACTIVE", "Another storage permission request is active.", null) + return + } + val activity = activityBinding?.activity + if (activity == null) { + result.error("ACTIVITY_UNAVAILABLE", "An Activity is required for storage permission.", null) + return + } + + pendingPermission = PendingRender(arguments, result) + activity.requestPermissions( + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + STORAGE_PERMISSION_REQUEST, + ) + } + + private fun renderImages(arguments: RenderArguments, result: MethodChannel.Result) { + val requestId = "flutter-${System.currentTimeMillis()}" + val outputDir = File(applicationContext.cacheDir, "easyink-images-$requestId") + if (!outputDir.mkdirs() && !outputDir.isDirectory) { + result.error("OUTPUT_FAILED", "Unable to create ${outputDir.absolutePath}.", null) + return + } + + val request = when (val source = arguments.source) { + is RenderSource.Html -> EasyInkRenderRequest( + requestId = requestId, + source = EasyInkRenderSource.Html(source.html), + pdf = EasyInkPdfOptions( + paperWidthMm = source.paperWidthMm, + paperHeightMm = source.paperHeightMm, + ), + wait = EasyInkWaitOptions( + until = EasyInkWaitUntil.AUTO, + timeoutMs = arguments.timeoutMs, + ), + ) + is RenderSource.Schema -> EasyInkRenderRequest( + requestId = requestId, + source = EasyInkRenderSource.Schema(source.schemaJson, source.dataJson), + wait = EasyInkWaitOptions( + until = EasyInkWaitUntil.AUTO, + timeoutMs = arguments.timeoutMs, + ), + ) + } + + val taskReference = AtomicReference() + try { + val task = EasyInkRenderer.renderImagesAsync( + context = applicationContext, + request = request, + outputDir = outputDir, + options = EasyInkImageOptions( + format = EasyInkImageFormat.PNG, + scale = arguments.scale, + ), + callback = object : EasyInkRenderCallback { + override fun onSuccess(data: EasyInkImageRenderResult) { + taskReference.get()?.let(activeTasks::remove) + publishResults(data, outputDir, result) + } + + override fun onError(error: EasyInkRenderException) { + taskReference.get()?.let(activeTasks::remove) + outputDir.deleteRecursively() + Log.e(TAG, "render failed code=${error.code} message=${error.message}", error) + result.error(error.code.name, error.message, null) + } + }, + ) + taskReference.set(task) + activeTasks += task + } + catch (error: Throwable) { + outputDir.deleteRecursively() + Log.e(TAG, "render invocation failed", error) + result.error("RUNTIME_ERROR", error.message, null) + } + } + + private fun publishResults( + data: EasyInkImageRenderResult, + cacheOutputDir: File, + result: MethodChannel.Result, + ) { + val executor = publishExecutor + if (executor == null || executor.isShutdown) { + result.error("PLUGIN_DETACHED", "Plugin detached before publishing completed.", null) + return + } + try { + executor.execute { + try { + val published = data.files.map(::publishToDownloads) + cacheOutputDir.deleteRecursively() + completeOnMain { result.success(published) } + } + catch (error: Throwable) { + Log.e(TAG, "publish failed", error) + completeOnMain { result.error("PUBLISH_FAILED", error.message, null) } + } + } + } + catch (error: Throwable) { + Log.e(TAG, "unable to schedule publishing", error) + result.error("PLUGIN_DETACHED", "Plugin detached before publishing started.", null) + } + } + + private fun publishToDownloads(source: File): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + publishWithMediaStore(source) + } + else { + publishToLegacyDownloads(source) + } + } + + private fun publishWithMediaStore(source: File): String { + val relativePath = "${Environment.DIRECTORY_DOWNLOADS}/$DOWNLOAD_FOLDER" + val resolver = applicationContext.contentResolver + val values = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, source.name) + put(MediaStore.Downloads.MIME_TYPE, "image/png") + put(MediaStore.Downloads.RELATIVE_PATH, relativePath) + put(MediaStore.Downloads.IS_PENDING, 1) + } + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + ?: error("Unable to create Downloads entry for ${source.name}.") + try { + resolver.openOutputStream(uri)?.use { output -> + source.inputStream().use { input -> input.copyTo(output) } + } ?: error("Unable to open Downloads output for ${source.name}.") + values.clear() + values.put(MediaStore.Downloads.IS_PENDING, 0) + resolver.update(uri, values, null, null) + return "$relativePath/${source.name}" + } + catch (error: Throwable) { + resolver.delete(uri, null, null) + throw error + } + } + + @Suppress("DEPRECATION") + private fun publishToLegacyDownloads(source: File): String { + val directory = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + DOWNLOAD_FOLDER, + ) + if (!directory.mkdirs() && !directory.isDirectory) { + error("Unable to create ${directory.absolutePath}.") + } + val target = File(directory, source.name) + source.copyTo(target, overwrite = true) + MediaScannerConnection.scanFile( + applicationContext, + arrayOf(target.absolutePath), + arrayOf("image/png"), + null, + ) + return target.absolutePath + } + + private fun needsLegacyStoragePermission(): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && + applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED + } + + private fun detachFromActivity(clearPending: Boolean) { + activityBinding?.removeRequestPermissionsResultListener(this) + activityBinding = null + if (clearPending) { + pendingPermission?.result?.error("ACTIVITY_DETACHED", "Activity detached.", null) + pendingPermission = null + } + } + + private fun cancelActiveTasks() { + synchronized(activeTasks) { + activeTasks.forEach(EasyInkRenderTask::cancel) + activeTasks.clear() + } + } + + private fun completeOnMain(block: () -> Unit) { + mainHandler.post(block) + } + + private fun currentWebViewVersion(): String? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + WebView.getCurrentWebViewPackage()?.versionName + } + else { + null + } + } + + private sealed interface RenderSource { + val payloadLength: Int + + data class Html( + val html: String, + val paperWidthMm: Double, + val paperHeightMm: Double, + ) : RenderSource { + override val payloadLength: Int = html.length + } + + data class Schema( + val schemaJson: String, + val dataJson: String, + ) : RenderSource { + override val payloadLength: Int = schemaJson.length + dataJson.length + } + } + + private data class RenderArguments( + val source: RenderSource, + val scale: Float, + val timeoutMs: Long, + ) + + private data class PendingRender( + val arguments: RenderArguments, + val result: MethodChannel.Result, + ) + + private companion object { + const val CHANNEL_NAME = "com.easyink.android/renderer" + const val DEFAULT_TIMEOUT_MS = 120_000L + const val DOWNLOAD_FOLDER = "EasyInk" + const val METHOD_RENDER_IMAGES = "renderImages" + const val STORAGE_PERMISSION_REQUEST = 1001 + const val TAG = "EasyInkAndroidPlugin" + } +} diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/lib/easyink_android.dart b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/lib/easyink_android.dart new file mode 100644 index 00000000..76b419de --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/lib/easyink_android.dart @@ -0,0 +1,80 @@ +import 'package:flutter/services.dart'; + +class EasyInkAndroid { + EasyInkAndroid({MethodChannel? channel}) + : _channel = channel ?? const MethodChannel(_channelName); + + static const _channelName = 'com.easyink.android/renderer'; + + final MethodChannel _channel; + + Future> renderHtmlImages({ + required String html, + double paperWidthMm = 80, + double paperHeightMm = 120, + double scale = 1, + Duration timeout = const Duration(seconds: 120), + }) { + if (html.trim().isEmpty) { + throw ArgumentError.value(html, 'html', 'must not be empty'); + } + if (paperWidthMm <= 0 || paperHeightMm <= 0) { + throw ArgumentError('Paper dimensions must be greater than zero.'); + } + return _renderImages({ + 'sourceType': 'html', + 'html': html, + 'paperWidthMm': paperWidthMm, + 'paperHeightMm': paperHeightMm, + 'scale': _validateScale(scale), + 'timeoutMs': _validateTimeout(timeout), + }); + } + + Future> renderSchemaImages({ + required String schemaJson, + required String dataJson, + double scale = 1, + Duration timeout = const Duration(seconds: 120), + }) { + if (schemaJson.trim().isEmpty) { + throw ArgumentError.value(schemaJson, 'schemaJson', 'must not be empty'); + } + if (dataJson.trim().isEmpty) { + throw ArgumentError.value(dataJson, 'dataJson', 'must not be empty'); + } + return _renderImages({ + 'sourceType': 'schema', + 'schemaJson': schemaJson, + 'dataJson': dataJson, + 'scale': _validateScale(scale), + 'timeoutMs': _validateTimeout(timeout), + }); + } + + Future> _renderImages(Map arguments) async { + final result = await _channel.invokeMethod>( + 'renderImages', + arguments, + ); + return List.unmodifiable((result ?? const []).cast()); + } + + double _validateScale(double scale) { + if (!scale.isFinite || scale <= 0) { + throw ArgumentError.value(scale, 'scale', 'must be greater than zero'); + } + return scale; + } + + int _validateTimeout(Duration timeout) { + if (timeout <= Duration.zero) { + throw ArgumentError.value( + timeout, + 'timeout', + 'must be greater than zero', + ); + } + return timeout.inMilliseconds; + } +} diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/pubspec.lock b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/pubspec.lock new file mode 100644 index 00000000..a64fad59 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/pubspec.lock @@ -0,0 +1,205 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.38.0" diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/pubspec.yaml b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/pubspec.yaml new file mode 100644 index 00000000..f5517532 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/pubspec.yaml @@ -0,0 +1,24 @@ +name: easyink_android +description: Flutter MethodChannel bridge for the EasyInk Android render SDK. +version: 0.1.0 +publish_to: none + +environment: + sdk: ^3.10.7 + flutter: '>=3.38.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + plugin: + platforms: + android: + package: com.easyink.flutter + pluginClass: EasyInkAndroidPlugin diff --git a/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/test/easyink_android_test.dart b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/test/easyink_android_test.dart new file mode 100644 index 00000000..f6d06068 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/plugins/easyink_android/test/easyink_android_test.dart @@ -0,0 +1,44 @@ +import 'package:easyink_android/easyink_android.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = MethodChannel('com.easyink.android/renderer'); + late MethodCall call; + late EasyInkAndroid renderer; + + setUp(() { + renderer = EasyInkAndroid(channel: channel); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + call = methodCall; + return ['Download/EasyInk/page-001.png']; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('sends schema render arguments and returns published paths', () async { + final files = await renderer.renderSchemaImages( + schemaJson: '{"page":{"width":80,"height":120}}', + dataJson: '{}', + scale: 2, + timeout: const Duration(seconds: 30), + ); + + expect(call.method, 'renderImages'); + expect(call.arguments, containsPair('sourceType', 'schema')); + expect(call.arguments, containsPair('scale', 2.0)); + expect(call.arguments, containsPair('timeoutMs', 30000)); + expect(files, ['Download/EasyInk/page-001.png']); + }); + + test('validates HTML before invoking the platform', () async { + expect(() => renderer.renderHtmlImages(html: ' '), throwsArgumentError); + }); +} diff --git a/lib/EasyInk.Android/samples/flutter/pubspec.lock b/lib/EasyInk.Android/samples/flutter/pubspec.lock new file mode 100644 index 00000000..f714c61b --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/pubspec.lock @@ -0,0 +1,212 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" + source: hosted + version: "2.10.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + easyink_android: + dependency: "direct main" + description: + path: "plugins/easyink_android" + relative: true + source: path + version: "0.1.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.38.0" diff --git a/lib/EasyInk.Android/samples/flutter/pubspec.yaml b/lib/EasyInk.Android/samples/flutter/pubspec.yaml new file mode 100644 index 00000000..8b41d60c --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/pubspec.yaml @@ -0,0 +1,85 @@ +name: easyink_flutter_sample +description: Flutter host sample for the EasyInk Android render SDK. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: none # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.10.7 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + easyink_android: + path: plugins/easyink_android + flutter: + sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + assets: + - assets/supermarket-receipt.schema.json + - assets/supermarket-receipt.data.json + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/lib/EasyInk.Android/samples/flutter/test/widget_test.dart b/lib/EasyInk.Android/samples/flutter/test/widget_test.dart new file mode 100644 index 00000000..a0fe1ab2 --- /dev/null +++ b/lib/EasyInk.Android/samples/flutter/test/widget_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:easyink_flutter_sample/main.dart'; + +void main() { + testWidgets('shows render actions', (WidgetTester tester) async { + await tester.pumpWidget(const EasyInkSampleApp()); + + expect(find.text('Render HTML images'), findsOneWidget); + expect(find.text('Render Schema images'), findsOneWidget); + }); +} diff --git a/package.json b/package.json index c87ae8da..a21bed9d 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ "sideEffects": false, "scripts": { "build": "turbo build", + "build:packages": "turbo build --filter='./packages/**'", "dev": "turbo dev --filter='./packages/**' --concurrency 50", - "play": "pnpm -F ./playground dev", + "play": "pnpm build:packages && pnpm -F ./playground dev", "lint": "eslint --cache .", "release": "node ./scripts/release.mjs", "sync:package-readmes": "node ./scripts/sync-package-readmes.mjs", diff --git a/packages/assistant/adapters/package.json b/packages/assistant/adapters/package.json index d170baee..f263d065 100644 --- a/packages/assistant/adapters/package.json +++ b/packages/assistant/adapters/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-adapters", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Lightweight external data adapters for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/capabilities/package.json b/packages/assistant/capabilities/package.json index 2bfb5599..e66ea815 100644 --- a/packages/assistant/capabilities/package.json +++ b/packages/assistant/capabilities/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-capabilities", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Pure EasyInk Assistant capabilities for schema, datasource alignment, validation, preview, diff, and patch", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/constraint-engine/package.json b/packages/assistant/constraint-engine/package.json index 4933be11..f8268546 100644 --- a/packages/assistant/constraint-engine/package.json +++ b/packages/assistant/constraint-engine/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-constraint-engine", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Executable constraint engine for real-time material validation and auto-fix in EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/designer-bridge/package.json b/packages/assistant/designer-bridge/package.json index 024144ae..a21d6267 100644 --- a/packages/assistant/designer-bridge/package.json +++ b/packages/assistant/designer-bridge/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-designer-bridge", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Designer contribution bridge for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/llm/package.json b/packages/assistant/llm/package.json index 42fdb954..4a3e9723 100644 --- a/packages/assistant/llm/package.json +++ b/packages/assistant/llm/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-llm", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Provider-neutral LLM gateway for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/material-knowledge/package.json b/packages/assistant/material-knowledge/package.json index 2950571d..f1b52ec6 100644 --- a/packages/assistant/material-knowledge/package.json +++ b/packages/assistant/material-knowledge/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-material-knowledge", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Structured material knowledge graph with executable constraints for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/orchestrator/package.json b/packages/assistant/orchestrator/package.json index 0010d18d..57238740 100644 --- a/packages/assistant/orchestrator/package.json +++ b/packages/assistant/orchestrator/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-orchestrator", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "LangGraph and Hono based orchestration service for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/plugin-placeholder-images/package.json b/packages/assistant/plugin-placeholder-images/package.json index a1a5ff60..c8fb29be 100644 --- a/packages/assistant/plugin-placeholder-images/package.json +++ b/packages/assistant/plugin-placeholder-images/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-plugin-placeholder-images", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Official placeholder image prompt plugin for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/plugin-prototype-designer/package.json b/packages/assistant/plugin-prototype-designer/package.json index f797a1f7..1d39109b 100644 --- a/packages/assistant/plugin-prototype-designer/package.json +++ b/packages/assistant/plugin-prototype-designer/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-plugin-prototype-designer", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Official prototype designer persona plugin for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/plugin-receipt-designer/package.json b/packages/assistant/plugin-receipt-designer/package.json index 96d11df9..8288edef 100644 --- a/packages/assistant/plugin-receipt-designer/package.json +++ b/packages/assistant/plugin-receipt-designer/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-plugin-receipt-designer", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Official receipt designer persona plugin for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/plugins/package.json b/packages/assistant/plugins/package.json index 4c28f3ff..9cafeecd 100644 --- a/packages/assistant/plugins/package.json +++ b/packages/assistant/plugins/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-plugins", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Plugin contribution protocol for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/scenario-templates/package.json b/packages/assistant/scenario-templates/package.json index 1abefc84..db0ad61c 100644 --- a/packages/assistant/scenario-templates/package.json +++ b/packages/assistant/scenario-templates/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-scenario-templates", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Scenario-based template library with classifier and progressive context loading for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/schema-builder/package.json b/packages/assistant/schema-builder/package.json index dc294972..53c7e45a 100644 --- a/packages/assistant/schema-builder/package.json +++ b/packages/assistant/schema-builder/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-schema-builder", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "High-level schema construction DSL with incremental building and instant validation for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/store/package.json b/packages/assistant/store/package.json index c9126235..8ded1a5f 100644 --- a/packages/assistant/store/package.json +++ b/packages/assistant/store/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-store", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Task, run, version, draft, and event stores for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/tool-registry/package.json b/packages/assistant/tool-registry/package.json index 3a3cc220..c38169a9 100644 --- a/packages/assistant/tool-registry/package.json +++ b/packages/assistant/tool-registry/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-tool-registry", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Agent tool registry with material, data, layout, schema and validation tools for EasyInk Composer Agent", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/type-aligner/package.json b/packages/assistant/type-aligner/package.json index d044d8ca..8866f492 100644 --- a/packages/assistant/type-aligner/package.json +++ b/packages/assistant/type-aligner/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-type-aligner", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Type-driven data alignment engine for automatic binding generation in EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/assistant/ui/package.json b/packages/assistant/ui/package.json index b9d2a428..7ae76dbf 100644 --- a/packages/assistant/ui/package.json +++ b/packages/assistant/ui/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/assistant-ui", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Vue task workbench UI for EasyInk Assistant", "author": "hackycy ", "license": "MIT", diff --git a/packages/builtin/package.json b/packages/builtin/package.json index 1d2e1bec..d8e6f527 100644 --- a/packages/builtin/package.json +++ b/packages/builtin/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/builtin", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Internal built-in material registry for EasyInk runtimes", "author": "hackycy ", "license": "MIT", diff --git a/packages/builtin/src/basic.ts b/packages/builtin/src/basic.ts index b07e2f0d..abf1f006 100644 --- a/packages/builtin/src/basic.ts +++ b/packages/builtin/src/basic.ts @@ -585,7 +585,7 @@ export const builtinViewerMaterialBundle: BuiltinViewerMaterialBundle = { }, }, { type: IMAGE_TYPE, binding: imageMaterialBinding, extension: { render: (node, ctx) => renderImage(node, ctx.unit) } }, - { type: BARCODE_TYPE, binding: barcodeMaterialBinding, extension: { render: node => renderBarcode(node) } }, + { type: BARCODE_TYPE, binding: barcodeMaterialBinding, extension: { render: (node, ctx) => renderBarcode(node, ctx) } }, { type: QRCODE_TYPE, binding: qrcodeMaterialBinding, extension: { render: node => renderQrcode(node) } }, { type: LINE_TYPE, binding: noMaterialBinding, extension: createLineViewerExtension() }, { type: RECT_TYPE, binding: noMaterialBinding, extension: { render: (node, ctx) => renderRect(node, ctx.unit) } }, diff --git a/packages/builtin/src/viewer.ts b/packages/builtin/src/viewer.ts index 0c4a3d4a..2fe25594 100644 --- a/packages/builtin/src/viewer.ts +++ b/packages/builtin/src/viewer.ts @@ -56,7 +56,7 @@ const ALL_BUILTIN_VIEWER_MATERIAL_BUNDLE: BuiltinViewerMaterialBundle = { }, }, { type: IMAGE_TYPE, binding: imageMaterialBinding, extension: { render: (node, ctx) => renderImage(node, ctx.unit) } }, - { type: BARCODE_TYPE, binding: barcodeMaterialBinding, extension: { render: node => renderBarcode(node) } }, + { type: BARCODE_TYPE, binding: barcodeMaterialBinding, extension: { render: (node, ctx) => renderBarcode(node, ctx) } }, { type: QRCODE_TYPE, binding: qrcodeMaterialBinding, extension: { render: node => renderQrcode(node) } }, { type: LINE_TYPE, binding: noMaterialBinding, extension: createLineViewerExtension() }, { type: RECT_TYPE, binding: noMaterialBinding, extension: { render: (node, ctx) => renderRect(node, ctx.unit) } }, diff --git a/packages/core/package.json b/packages/core/package.json index f3367a40..9e8899c1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/core", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Core logic for EasyInk: commands, selection, geometry, units, fonts", "author": "hackycy ", "license": "MIT", diff --git a/packages/core/src/material-viewer.ts b/packages/core/src/material-viewer.ts index 8baa4183..9e4b8004 100644 --- a/packages/core/src/material-viewer.ts +++ b/packages/core/src/material-viewer.ts @@ -31,6 +31,8 @@ export function readTrustedViewerHtml(html: TrustedViewerHtml): string { * Provides document-level unit and zoom for physical unit calculations. */ export interface ViewerRenderContext { + /** Host document used to create material-owned DOM nodes. */ + readonly document: Document data: Record resolvedProps: Record pageIndex: number diff --git a/packages/datasource/package.json b/packages/datasource/package.json index 41f87ff2..c464d038 100644 --- a/packages/datasource/package.json +++ b/packages/datasource/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/datasource", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Data source protocol and field tree for EasyInk designer", "author": "hackycy ", "license": "MIT", diff --git a/packages/designer/package.json b/packages/designer/package.json index e2363868..3a3f9753 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/designer", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk document/report designer workbench", "author": "hackycy ", "license": "MIT", diff --git a/packages/export/dom-capture/package.json b/packages/export/dom-capture/package.json index 2f86c287..49ed0120 100644 --- a/packages/export/dom-capture/package.json +++ b/packages/export/dom-capture/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/export-dom-capture", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Shared DOM capture utilities for EasyInk browser export plugins", "author": "hackycy ", "license": "MIT", diff --git a/packages/export/plugin/dom-image/package.json b/packages/export/plugin/dom-image/package.json index 71aa55bf..c6be0908 100644 --- a/packages/export/plugin/dom-image/package.json +++ b/packages/export/plugin/dom-image/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/export-plugin-dom-image", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "DOM-to-image export plugin for @easyink/export-runtime (html2canvas)", "author": "hackycy ", "license": "MIT", diff --git a/packages/export/plugin/dom-pdf/package.json b/packages/export/plugin/dom-pdf/package.json index 0b10ee46..a1c59b2c 100644 --- a/packages/export/plugin/dom-pdf/package.json +++ b/packages/export/plugin/dom-pdf/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/export-plugin-dom-pdf", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "DOM-to-PDF export plugin for @easyink/export-runtime (html2canvas + jsPDF)", "author": "hackycy ", "license": "MIT", diff --git a/packages/export/runtime/package.json b/packages/export/runtime/package.json index dd86e108..f4e7258a 100644 --- a/packages/export/runtime/package.json +++ b/packages/export/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/export-runtime", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Framework-agnostic export runtime for EasyInk", "author": "hackycy ", "license": "MIT", diff --git a/packages/icons/package.json b/packages/icons/package.json index 9640543b..7c707fcb 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/icons", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Icon assets for EasyInk", "author": "hackycy ", "license": "MIT", diff --git a/packages/locales/package.json b/packages/locales/package.json index f7be54ab..06e259b2 100644 --- a/packages/locales/package.json +++ b/packages/locales/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/locales", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Built-in locale messages for EasyInk Designer", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/barcode/package.json b/packages/materials/barcode/package.json index dd256882..8f0c3ffa 100644 --- a/packages/materials/barcode/package.json +++ b/packages/materials/barcode/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-barcode", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk barcode material", "author": "hackycy ", "license": "MIT", @@ -27,5 +27,8 @@ "@easyink/schema": "workspace:*", "@easyink/shared": "workspace:*", "jsbarcode": "catalog:framework" + }, + "devDependencies": { + "@types/jsbarcode": "catalog:types" } } diff --git a/packages/materials/barcode/src/designer.test.ts b/packages/materials/barcode/src/designer.test.ts new file mode 100644 index 00000000..77807d25 --- /dev/null +++ b/packages/materials/barcode/src/designer.test.ts @@ -0,0 +1,136 @@ +import type { MaterialExtensionContext, NodeSignal } from '@easyink/core' +import type { MaterialNode } from '@easyink/schema' +import { createDefaultSchema } from '@easyink/schema' +import { describe, expect, it } from 'vitest' +import { createBarcodeExtension } from './designer' +import { createBarcodeNode } from './schema' +import { installCanvasTextMeasurement } from './test-utils' + +installCanvasTextMeasurement() + +function createContext(): MaterialExtensionContext { + const schema = createDefaultSchema() + return { + getSchema: () => schema, + getNode: () => undefined, + getSelection: () => ({ ids: [], count: 0, isEmpty: true }), + getBindingLabel: binding => binding.fieldLabel || binding.fieldPath, + commitCommand: () => {}, + tx: { + run: () => {}, + batch: fn => fn(), + }, + requestPropertyPanel: () => {}, + emit: () => {}, + on: () => () => {}, + getZoom: () => 1, + getPageEl: () => null, + t: key => key, + } +} + +function createMutableNodeSignal(initialNode: MaterialNode): { + signal: NodeSignal + update: (node: MaterialNode) => void +} { + let currentNode = initialNode + let subscriber: ((node: MaterialNode) => void) | undefined + return { + signal: { + get: () => currentNode, + subscribe: (callback) => { + subscriber = callback + return () => { + subscriber = undefined + } + }, + }, + update: (node) => { + currentNode = node + subscriber?.(node) + }, + } +} + +describe('createBarcodeExtension', () => { + it('renders and repaints the official EAN-13 layout', () => { + const container = document.createElement('div') + const extension = createBarcodeExtension(createContext()) + const nodeSignal = createMutableNodeSignal(createBarcodeNode({ + props: { + value: '5901234123457', + format: 'EAN13', + showText: true, + }, + })) + const cleanup = extension.renderContent(nodeSignal.signal, container) + + expect(Array.from(container.querySelectorAll('text')).map(text => text.textContent).filter(Boolean)) + .toEqual(['5', '901234', '123457']) + + nodeSignal.update(createBarcodeNode({ + props: { + value: '5901234123457', + format: 'EAN13', + showText: false, + }, + })) + + expect(container.querySelectorAll('text')).toHaveLength(0) + cleanup() + }) + + it('shows the configured format sample for an empty value', () => { + const container = document.createElement('div') + const extension = createBarcodeExtension(createContext()) + const nodeSignal = createMutableNodeSignal(createBarcodeNode({ + props: { + value: '', + format: 'EAN13', + }, + })) + + extension.renderContent(nodeSignal.signal, container) + + expect(container.querySelector('svg')).not.toBeNull() + expect(container.querySelectorAll('text')).toHaveLength(0) + expect(container.textContent).toContain('EAN13') + }) + + it('writes binding labels through textContent', () => { + const container = document.createElement('div') + const extension = createBarcodeExtension(createContext()) + const nodeSignal = createMutableNodeSignal(createBarcodeNode({ + binding: { + sourceId: 'product', + fieldPath: 'ean', + fieldLabel: '', + }, + props: { + value: 'EasyInk', + format: 'CODE128', + }, + })) + + extension.renderContent(nodeSignal.signal, container) + + expect(container.textContent).toContain('{#}') + expect(container.querySelector('img')).toBeNull() + }) + + it('shows an error state for invalid values', () => { + const container = document.createElement('div') + const extension = createBarcodeExtension(createContext()) + const nodeSignal = createMutableNodeSignal(createBarcodeNode({ + props: { + value: 'invalid', + format: 'EAN13', + }, + })) + + extension.renderContent(nodeSignal.signal, container) + + expect(container.querySelector('svg')).toBeNull() + expect(container.textContent).toBe('Invalid: invalid') + }) +}) diff --git a/packages/materials/barcode/src/designer.ts b/packages/materials/barcode/src/designer.ts index bf489b7d..58a41b78 100644 --- a/packages/materials/barcode/src/designer.ts +++ b/packages/materials/barcode/src/designer.ts @@ -2,84 +2,126 @@ import type { MaterialDesignerExtension, MaterialExtensionContext } from '@easyi import type { MaterialNode } from '@easyink/schema' import type { BarcodeProps } from './schema' import { getBindingRefs } from '@easyink/schema' -import { escapeHtml } from '@easyink/shared' -import { generateBarcodeSvg } from './render' +import { createBarcodeSvgElement } from './render' import { BARCODE_FORMATS, resolveBarcodeProps } from './schema' -function buildPlaceholder(p: BarcodeProps, label: string): string { - const sampleValue = BARCODE_FORMATS.find(format => format.value === p.format)?.sampleValue || 'EasyInk' +function applyFrameStyles(frame: HTMLDivElement, props: BarcodeProps, unit: string): void { + frame.style.position = 'relative' + frame.style.width = '100%' + frame.style.height = '100%' + frame.style.boxSizing = 'border-box' + if (props.borderWidth) { + frame.style.border = `${props.borderWidth}${unit} ${props.borderType} ${props.borderColor}` + } +} + +function createErrorPlaceholder(document: Document, props: BarcodeProps, value: string): HTMLDivElement { + const error = document.createElement('div') + error.style.width = '100%' + error.style.height = '100%' + error.style.display = 'flex' + error.style.alignItems = 'center' + error.style.justifyContent = 'center' + error.style.boxSizing = 'border-box' + error.style.background = props.backgroundColor + error.style.color = '#e53e3e' + error.style.fontSize = '11px' + error.style.border = '1px dashed #e53e3e' + error.textContent = `Invalid: ${value}` + return error +} + +function appendLabelOverlay(document: Document, frame: HTMLElement, label: string, color: string): void { + const overlay = document.createElement('div') + overlay.style.position = 'absolute' + overlay.style.inset = '0' + overlay.style.display = 'flex' + overlay.style.alignItems = 'center' + overlay.style.justifyContent = 'center' + + const text = document.createElement('span') + text.style.maxWidth = '90%' + text.style.overflow = 'hidden' + text.style.padding = '1px 4px' + text.style.borderRadius = '2px' + text.style.background = 'rgba(255, 255, 255, 0.8)' + text.style.color = color + text.style.fontSize = '10px' + text.style.textOverflow = 'ellipsis' + text.style.whiteSpace = 'nowrap' + text.textContent = label + + overlay.appendChild(text) + frame.appendChild(overlay) +} + +function createPlaceholder(document: Document, props: BarcodeProps, label: string): HTMLElement { + const placeholder = document.createElement('div') + placeholder.style.position = 'relative' + placeholder.style.width = '100%' + placeholder.style.height = '100%' + placeholder.style.opacity = '0.4' - let svg: string + const sampleValue = BARCODE_FORMATS.find(format => format.value === props.format)?.sampleValue || 'EasyInk' try { - svg = generateBarcodeSvg(sampleValue, { - format: p.format, - lineWidth: p.lineWidth, - lineColor: p.lineColor, - backgroundColor: p.backgroundColor, + placeholder.appendChild(createBarcodeSvgElement(sampleValue, { + format: props.format, + lineWidth: props.lineWidth, + lineColor: props.lineColor, + backgroundColor: props.backgroundColor, showText: false, - }) + }, document)) } catch { - return buildErrorPlaceholder(p, label) + return createErrorPlaceholder(document, props, label) } - return `
${svg}
${label}
` + appendLabelOverlay(document, placeholder, label, props.lineColor) + return placeholder } -function buildErrorPlaceholder(p: BarcodeProps, value: string): string { - return `
Invalid: ${escapeHtml(value)}
` -} +function buildElement(node: MaterialNode, context: MaterialExtensionContext, document: Document): HTMLElement { + const props = resolveBarcodeProps(node) + const frame = document.createElement('div') + applyFrameStyles(frame, props, context.getSchema().unit) -function buildHtml(node: MaterialNode, context: MaterialExtensionContext): string { - const p = resolveBarcodeProps(node) - const unit = context.getSchema().unit - const DASH_MAP: Record = { dashed: 'dashed', dotted: 'dotted' } - const borderStyle = p.borderWidth ? `border:${p.borderWidth}${unit} ${DASH_MAP[p.borderType] || 'solid'} ${p.borderColor};box-sizing:border-box;` : '' - - let label: string | undefined - const b = getBindingRefs(node.binding)[0] - if (b) { - label = `{#${escapeHtml(context.getBindingLabel(b))}}` - } - - const value = p.value || '' + const binding = getBindingRefs(node.binding)[0] + const label = binding ? `{#${context.getBindingLabel(binding)}}` : undefined + const value = props.value == null ? '' : String(props.value) if (!value) { - const inner = buildPlaceholder(p, label || p.format) - return borderStyle ? `
${inner}
` : inner + frame.appendChild(createPlaceholder(document, props, label || props.format)) + return frame } - let svg: string try { - svg = generateBarcodeSvg(value, { - format: p.format, - lineWidth: p.lineWidth, - lineColor: p.lineColor, - backgroundColor: p.backgroundColor, - showText: p.showText, - }) + frame.appendChild(createBarcodeSvgElement(value, { + format: props.format, + lineWidth: props.lineWidth, + lineColor: props.lineColor, + backgroundColor: props.backgroundColor, + showText: props.showText, + }, document)) } catch { - const inner = buildErrorPlaceholder(p, value) - return borderStyle ? `
${inner}
` : inner + frame.appendChild(createErrorPlaceholder(document, props, value)) + return frame } - if (label) { - return `
${svg}
${label}
` - } + if (label) + appendLabelOverlay(document, frame, label, props.lineColor) - return borderStyle ? `
${svg}
` : svg + return frame } export function createBarcodeExtension(context: MaterialExtensionContext): MaterialDesignerExtension { return { renderContent(nodeSignal, container) { function render() { - container.innerHTML = buildHtml(nodeSignal.get(), context) + container.replaceChildren(buildElement(nodeSignal.get(), context, container.ownerDocument)) } render() - const unsub = nodeSignal.subscribe(render) - return unsub + return nodeSignal.subscribe(render) }, } } diff --git a/packages/materials/barcode/src/render.test.ts b/packages/materials/barcode/src/render.test.ts new file mode 100644 index 00000000..7e2a7ddb --- /dev/null +++ b/packages/materials/barcode/src/render.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest' +import { createBarcodeSvgElement, generateBarcodeSvg } from './render' +import { installCanvasTextMeasurement } from './test-utils' + +installCanvasTextMeasurement() + +function createEan13(value: string, showText = true): SVGSVGElement { + return createBarcodeSvgElement(value, { + format: 'EAN13', + lineWidth: 2, + lineColor: '#000000', + backgroundColor: '#ffffff', + showText, + }, document) +} + +describe('barcode SVG rendering', () => { + it('preserves the official EAN-13 text groups and guard bars', () => { + const svg = createEan13('5901234123457') + const textGroups = Array.from(svg.querySelectorAll('text')) + .map(text => text.textContent) + .filter(Boolean) + const barHeights = new Set(Array.from(svg.querySelectorAll('g rect')) + .map(rect => rect.getAttribute('height'))) + + expect(textGroups).toEqual(['5', '901234', '123457']) + expect(barHeights).toContain('100') + expect(barHeights).toContain('112') + expect(svg.getAttribute('viewBox')).toBe('0 0 234 142') + }) + + it('adds the EAN-13 checksum for a valid 12-digit value', () => { + const svg = createEan13('590123412345') + const textGroups = Array.from(svg.querySelectorAll('text')) + .map(text => text.textContent) + .filter(Boolean) + + expect(textGroups).toEqual(['5', '901234', '123457']) + }) + + it('keeps the 95-module EAN-13 symbol and guard bars when text is hidden', () => { + const svg = createEan13('5901234123457', false) + const barHeights = new Set(Array.from(svg.querySelectorAll('g rect')) + .map(rect => rect.getAttribute('height'))) + + expect(svg.querySelectorAll('text')).toHaveLength(0) + expect(svg.getAttribute('viewBox')).toBe('0 0 210 132') + expect((210 - 20) / 2).toBe(95) + expect(barHeights).toContain('100') + expect(barHeights).toContain('112') + }) + + it('keeps JsBarcode layout while applying responsive SVG attributes', () => { + const svg = createEan13('5901234123457') + const markup = generateBarcodeSvg('5901234123457', { + format: 'EAN13', + lineWidth: 2, + lineColor: '#000000', + backgroundColor: '#ffffff', + showText: true, + }, document) + + expect(svg.getAttribute('width')).toBe('100%') + expect(svg.getAttribute('height')).toBe('100%') + expect(svg.getAttribute('preserveAspectRatio')).toBe('xMidYMid meet') + expect(svg.getAttribute('shape-rendering')).toBe('crispEdges') + expect(markup).toContain('viewBox="0 0 234 142"') + expect(markup).toContain('>901234') + expect(markup).toContain('>123457') + }) +}) diff --git a/packages/materials/barcode/src/render.ts b/packages/materials/barcode/src/render.ts index da678e93..c92d0f04 100644 --- a/packages/materials/barcode/src/render.ts +++ b/packages/materials/barcode/src/render.ts @@ -1,6 +1,4 @@ -import { escapeHtml } from '@easyink/shared' -// @ts-expect-error -- JsBarcode internal encoders have no type declarations -import encoders from 'jsbarcode/bin/barcodes/index' +import JsBarcode from 'jsbarcode' export interface BarcodeSvgOptions { format: string @@ -10,83 +8,105 @@ export interface BarcodeSvgOptions { showText: boolean } -interface BarcodeEncoding { - data: string - text: string -} +const SVG_NAMESPACE = 'http://www.w3.org/2000/svg' +const EMPTY_BAR_RECTS = [ + [8, 2], + [13, 1], + [18, 4], + [27, 2], + [33, 1], + [38, 3], + [47, 2], + [53, 4], + [62, 1], + [67, 3], + [75, 2], + [82, 1], + [88, 4], + [97, 2], + [103, 3], + [112, 1], +] as const -function encode(value: string, format: string): BarcodeEncoding[] { - const Encoder = (encoders.default || encoders)[format] - if (!Encoder) { - throw new Error(`Unknown barcode format: ${format}`) - } - const instance = new Encoder(value, {}) - if (typeof instance.valid === 'function' && !instance.valid()) { - throw new Error(`Invalid value "${value}" for format ${format}`) - } - const encoded = instance.encode() - // encode() may return a single object or an array - return Array.isArray(encoded) ? encoded : [encoded] +function resolveDocument(xmlDocument?: Document): Document { + if (xmlDocument) + return xmlDocument + if (typeof document !== 'undefined') + return document + throw new Error('Barcode SVG rendering requires a DOM Document') } -function escapeSvgAttr(value: string): string { - return value - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(//g, '>') +function makeResponsive(svg: SVGSVGElement): SVGSVGElement { + svg.setAttribute('width', '100%') + svg.setAttribute('height', '100%') + svg.setAttribute('preserveAspectRatio', 'xMidYMid meet') + svg.setAttribute('shape-rendering', 'crispEdges') + svg.style.display = 'block' + return svg } -/** - * Generate a real barcode as an inline SVG string. - * Uses JsBarcode internal encoders to get the binary bar pattern, - * then renders it as SVG rects. - */ -export function generateBarcodeSvg(value: string, options: Partial & { format: string }): string { - const lineWidth = options.lineWidth || 2 - const lineColor = escapeSvgAttr(options.lineColor || '#000000') - const backgroundColor = escapeSvgAttr(options.backgroundColor || '#ffffff') - const showText = options.showText ?? true +export function createBarcodeSvgElement( + value: string, + options: Partial & { format: string }, + xmlDocument: Document, +): SVGSVGElement { + const svg = xmlDocument.createElementNS(SVG_NAMESPACE, 'svg') + const barcodeOptions: NonNullable[2]> & { xmlDocument: Document } = { + format: options.format, + width: options.lineWidth || 2, + lineColor: options.lineColor || '#000000', + background: options.backgroundColor || '#ffffff', + displayValue: options.showText ?? true, + xmlDocument, + } - const encodings = encode(value, options.format) + JsBarcode(svg, value, barcodeOptions) - // Merge all binary data segments - let binaryStr = '' - let text = '' - for (const enc of encodings) { - binaryStr += enc.data - if (enc.text) - text = enc.text - } + return makeResponsive(svg) +} - const barCount = binaryStr.length - const totalWidth = barCount * lineWidth - const textHeight = showText ? 16 : 0 - const barHeight = 60 - const svgHeight = barHeight + textHeight - const padding = 4 +export function generateBarcodeSvg( + value: string, + options: Partial & { format: string }, + xmlDocument?: Document, +): string { + return createBarcodeSvgElement(value, options, resolveDocument(xmlDocument)).outerHTML +} - const svgWidth = totalWidth + padding * 2 - const fullHeight = svgHeight + padding * 2 +export function createBarcodeEmptySvgElement( + options: Partial>, + xmlDocument: Document, +): SVGSVGElement { + const svg = xmlDocument.createElementNS(SVG_NAMESPACE, 'svg') + svg.setAttribute('xmlns', SVG_NAMESPACE) + svg.setAttribute('viewBox', '0 0 120 48') + svg.setAttribute('aria-hidden', 'true') - // Build bar rects - const rects: string[] = [] - for (let i = 0; i < barCount; i++) { - if (binaryStr[i] === '1') { - rects.push(``) - } - } + const background = xmlDocument.createElementNS(SVG_NAMESPACE, 'rect') + background.setAttribute('width', '120') + background.setAttribute('height', '48') + background.setAttribute('fill', options.backgroundColor || '#ffffff') + svg.appendChild(background) - const textEl = showText - ? `${escapeHtml(text || value)}` - : '' + const bars = xmlDocument.createElementNS(SVG_NAMESPACE, 'g') + bars.setAttribute('fill', options.lineColor || '#000000') + bars.setAttribute('opacity', '0.18') + for (const [x, width] of EMPTY_BAR_RECTS) { + const rect = xmlDocument.createElementNS(SVG_NAMESPACE, 'rect') + rect.setAttribute('x', String(x)) + rect.setAttribute('y', '8') + rect.setAttribute('width', String(width)) + rect.setAttribute('height', '32') + bars.appendChild(rect) + } + svg.appendChild(bars) - return `${rects.join('')}${textEl}` + return makeResponsive(svg) } -export function generateBarcodeEmptySvg(options: Partial>): string { - const lineColor = escapeSvgAttr(options.lineColor || '#000000') - const backgroundColor = escapeSvgAttr(options.backgroundColor || '#ffffff') - - return `` +export function generateBarcodeEmptySvg( + options: Partial>, + xmlDocument?: Document, +): string { + return createBarcodeEmptySvgElement(options, resolveDocument(xmlDocument)).outerHTML } diff --git a/packages/materials/barcode/src/test-utils.ts b/packages/materials/barcode/src/test-utils.ts new file mode 100644 index 00000000..c8445d47 --- /dev/null +++ b/packages/materials/barcode/src/test-utils.ts @@ -0,0 +1,16 @@ +import { afterAll, beforeAll } from 'vitest' + +export function installCanvasTextMeasurement(): void { + const getContext = HTMLCanvasElement.prototype.getContext + + beforeAll(() => { + HTMLCanvasElement.prototype.getContext = (() => ({ + font: '', + measureText: (value: string) => ({ width: value.length * 10 }), + } as unknown as CanvasRenderingContext2D)) as unknown as typeof HTMLCanvasElement.prototype.getContext + }) + + afterAll(() => { + HTMLCanvasElement.prototype.getContext = getContext + }) +} diff --git a/packages/materials/barcode/src/viewer.test.ts b/packages/materials/barcode/src/viewer.test.ts index 6feee7af..80df74b7 100644 --- a/packages/materials/barcode/src/viewer.test.ts +++ b/packages/materials/barcode/src/viewer.test.ts @@ -1,29 +1,34 @@ -import { readTrustedViewerHtml } from '@easyink/core' +import type { MaterialNode } from '@easyink/schema' import { describe, expect, it } from 'vitest' import { BARCODE_FORMATS, createBarcodeNode } from './schema' +import { installCanvasTextMeasurement } from './test-utils' import { renderBarcode } from './viewer' +installCanvasTextMeasurement() + +function renderElement(node: MaterialNode, ownerDocument: Document = document, unit = 'mm'): HTMLElement { + return renderBarcode(node, { document: ownerDocument, unit }).element! +} + describe('renderBarcode', () => { it('renders an empty-state svg without legacy placeholder text or border', () => { - const node = createBarcodeNode({ + const element = renderElement(createBarcodeNode({ props: { value: '', lineColor: '#123456', backgroundColor: '#ffffff', }, - }) - - const html = readTrustedViewerHtml(renderBarcode(node).html!) + })) + const svg = element.querySelector('svg')! - expect(html).toContain(' { - const node = createBarcodeNode({ + it('renders a real barcode with configured colors and responsive sizing', () => { + const element = renderElement(createBarcodeNode({ props: { value: 'EasyInk', format: 'CODE128', @@ -32,43 +37,67 @@ describe('renderBarcode', () => { backgroundColor: '#eeeeee', showText: true, }, - }) + })) + const svg = element.querySelector('svg')! - const html = readTrustedViewerHtml(renderBarcode(node).html!) - - expect(html).toContain(' { + it('renders every supported format through JsBarcode', () => { for (const format of BARCODE_FORMATS) { - const node = createBarcodeNode({ + const element = renderElement(createBarcodeNode({ props: { value: format.sampleValue, format: format.value, }, - }) - - const html = readTrustedViewerHtml(renderBarcode(node).html!) + })) - expect(html, format.value).toContain(' { - const node = createBarcodeNode({ + it('renders invalid values as DOM text', () => { + const element = renderElement(createBarcodeNode({ + props: { + value: '', + format: 'EAN13', + }, + })) + + expect(element.querySelector('svg')).toBeNull() + expect(element.textContent).toBe('Invalid: ') + expect(element.querySelector('invalid')).toBeNull() + }) + + it('applies viewer borders in the document unit', () => { + const element = renderElement(createBarcodeNode({ props: { value: 'EasyInk', - lineColor: '', + borderWidth: 1, + borderType: 'dashed', + borderColor: '#123456', }, - }) + }), document, 'pt') + + expect(element.style.borderWidth).toBe('1pt') + expect(element.style.borderStyle).toBe('dashed') + expect(element.style.borderColor).toBe('#123456') + }) - const html = readTrustedViewerHtml(renderBarcode(node).html!) + it('creates output in the Viewer host document', () => { + const ownerDocument = document.implementation.createHTMLDocument('barcode-host') + const element = renderElement(createBarcodeNode({ + props: { value: 'EasyInk' }, + }), ownerDocument) - expect(html).toContain('fill="#000000"') + expect(element.ownerDocument).toBe(ownerDocument) + expect(element.querySelector('svg')?.ownerDocument).toBe(ownerDocument) }) }) diff --git a/packages/materials/barcode/src/viewer.ts b/packages/materials/barcode/src/viewer.ts index 44e8b1e8..6a4b7fa4 100644 --- a/packages/materials/barcode/src/viewer.ts +++ b/packages/materials/barcode/src/viewer.ts @@ -1,36 +1,60 @@ +import type { ViewerRenderContext, ViewerRenderOutput } from '@easyink/core' import type { MaterialNode } from '@easyink/schema' -import { trustedViewerHtml } from '@easyink/core' -import { escapeHtml } from '@easyink/shared' -import { generateBarcodeEmptySvg, generateBarcodeSvg } from './render' +import { createBarcodeEmptySvgElement, createBarcodeSvgElement } from './render' import { resolveBarcodeProps } from './schema' -export function renderBarcode(node: MaterialNode) { +function createErrorPlaceholder(document: Document, backgroundColor: string, value: string): HTMLDivElement { + const error = document.createElement('div') + error.style.width = '100%' + error.style.height = '100%' + error.style.display = 'flex' + error.style.alignItems = 'center' + error.style.justifyContent = 'center' + error.style.boxSizing = 'border-box' + error.style.background = backgroundColor + error.style.color = '#e53e3e' + error.style.fontSize = '12px' + error.style.border = '1px dashed #e53e3e' + error.textContent = `Invalid: ${value}` + return error +} + +export function renderBarcode( + node: MaterialNode, + context: Pick, +): ViewerRenderOutput { const props = resolveBarcodeProps(node) const value = props.value == null ? '' : String(props.value) + const { document } = context + const frame = document.createElement('div') + frame.style.width = '100%' + frame.style.height = '100%' + frame.style.boxSizing = 'border-box' + + if (props.borderWidth) { + frame.style.border = `${props.borderWidth}${context.unit} ${props.borderType} ${props.borderColor}` + } if (!value) { - return { - html: trustedViewerHtml(generateBarcodeEmptySvg({ - lineColor: props.lineColor, - backgroundColor: props.backgroundColor, - })), - } + frame.appendChild(createBarcodeEmptySvgElement({ + lineColor: props.lineColor, + backgroundColor: props.backgroundColor, + }, document)) + return { element: frame } } try { - return { - html: trustedViewerHtml(generateBarcodeSvg(value, { - format: props.format, - lineWidth: props.lineWidth, - lineColor: props.lineColor, - backgroundColor: props.backgroundColor, - showText: props.showText, - })), - } + frame.appendChild(createBarcodeSvgElement(value, { + format: props.format, + lineWidth: props.lineWidth, + lineColor: props.lineColor, + backgroundColor: props.backgroundColor, + showText: props.showText, + }, document)) } catch { - return { - html: trustedViewerHtml(`
Invalid: ${escapeHtml(value)}
`), - } + frame.appendChild(createErrorPlaceholder(document, props.backgroundColor, value)) } + + return { element: frame } } diff --git a/packages/materials/chart/bar/package.json b/packages/materials/chart/bar/package.json index 2cb63a9f..b9a1ee2a 100644 --- a/packages/materials/chart/bar/package.json +++ b/packages/materials/chart/bar/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-bar", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk bar chart material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/chart/custom/package.json b/packages/materials/chart/custom/package.json index 863e29a6..e3dbf671 100644 --- a/packages/materials/chart/custom/package.json +++ b/packages/materials/chart/custom/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-custom", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk custom ECharts material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/chart/custom/src/viewer.test.ts b/packages/materials/chart/custom/src/viewer.test.ts index 46e391fe..f3d19321 100644 --- a/packages/materials/chart/custom/src/viewer.test.ts +++ b/packages/materials/chart/custom/src/viewer.test.ts @@ -12,6 +12,7 @@ describe('chart custom viewer', () => { }, }) const output = renderChartCustom(node, { + document, data: {}, resolvedProps: node.props, pageIndex: 0, diff --git a/packages/materials/chart/gauge/package.json b/packages/materials/chart/gauge/package.json index 23ecaada..8e8341e5 100644 --- a/packages/materials/chart/gauge/package.json +++ b/packages/materials/chart/gauge/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-gauge", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk gauge chart material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/chart/kernel/package.json b/packages/materials/chart/kernel/package.json index 7b17fb9c..8c3adc80 100644 --- a/packages/materials/chart/kernel/package.json +++ b/packages/materials/chart/kernel/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-kernel", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk shared chart kernel with ECharts integration helpers", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/chart/line/package.json b/packages/materials/chart/line/package.json index d14f1517..1348255c 100644 --- a/packages/materials/chart/line/package.json +++ b/packages/materials/chart/line/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-line", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk line chart material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/chart/pie/package.json b/packages/materials/chart/pie/package.json index ba4cd530..78ea9fab 100644 --- a/packages/materials/chart/pie/package.json +++ b/packages/materials/chart/pie/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-pie", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk pie chart material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/chart/radar/package.json b/packages/materials/chart/radar/package.json index 3e3ba036..63b9470c 100644 --- a/packages/materials/chart/radar/package.json +++ b/packages/materials/chart/radar/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-radar", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk radar chart material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/chart/scatter/package.json b/packages/materials/chart/scatter/package.json index da51f9fa..182307aa 100644 --- a/packages/materials/chart/scatter/package.json +++ b/packages/materials/chart/scatter/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-chart-scatter", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk scatter chart material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/ellipse/package.json b/packages/materials/ellipse/package.json index 6d753b7c..cf999698 100644 --- a/packages/materials/ellipse/package.json +++ b/packages/materials/ellipse/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-ellipse", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk ellipse material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/flow-row/package.json b/packages/materials/flow-row/package.json index 158c3b29..35c2ce3b 100644 --- a/packages/materials/flow-row/package.json +++ b/packages/materials/flow-row/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-flow-row", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk flow-row material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/flow-row/src/viewer.test.ts b/packages/materials/flow-row/src/viewer.test.ts index 78291dbb..d7ea3559 100644 --- a/packages/materials/flow-row/src/viewer.test.ts +++ b/packages/materials/flow-row/src/viewer.test.ts @@ -6,6 +6,7 @@ import { createFlowRowNode, FLOW_ROW_DEFAULTS } from './schema' import { measureFlowRow, renderFlowRow } from './viewer' const viewerContext = { + document, data: {}, resolvedProps: {}, pageIndex: 0, diff --git a/packages/materials/image/package.json b/packages/materials/image/package.json index 842ec65e..0bc5188f 100644 --- a/packages/materials/image/package.json +++ b/packages/materials/image/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-image", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk image material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/line/package.json b/packages/materials/line/package.json index 81b0b1d6..60058e14 100644 --- a/packages/materials/line/package.json +++ b/packages/materials/line/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-line", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk line material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/line/src/viewer.test.ts b/packages/materials/line/src/viewer.test.ts index 7d67ceb8..41ac38fc 100644 --- a/packages/materials/line/src/viewer.test.ts +++ b/packages/materials/line/src/viewer.test.ts @@ -14,6 +14,7 @@ describe('renderLine', () => { }) const output = renderLine(node, { + document, data: {}, resolvedProps: node.props, pageIndex: 0, @@ -47,6 +48,7 @@ describe('renderLine', () => { }) const dashedOutput = renderLine(dashed, { + document, data: {}, resolvedProps: dashed.props, pageIndex: 0, @@ -54,6 +56,7 @@ describe('renderLine', () => { zoom: 1, }) const dottedOutput = renderLine(dotted, { + document, data: {}, resolvedProps: dotted.props, pageIndex: 0, diff --git a/packages/materials/page-number/package.json b/packages/materials/page-number/package.json index 9e8b64bb..a14683cc 100644 --- a/packages/materials/page-number/package.json +++ b/packages/materials/page-number/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-page-number", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk page-number material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/progress/package.json b/packages/materials/progress/package.json index 715849d8..729c5b37 100644 --- a/packages/materials/progress/package.json +++ b/packages/materials/progress/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-progress", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk horizontal progress material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/qrcode/package.json b/packages/materials/qrcode/package.json index a33eb133..bc8fe7f9 100644 --- a/packages/materials/qrcode/package.json +++ b/packages/materials/qrcode/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-qrcode", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk qrcode material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/rating/package.json b/packages/materials/rating/package.json index cbbff7e7..e955b786 100644 --- a/packages/materials/rating/package.json +++ b/packages/materials/rating/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-rating", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk rating material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/rating/src/viewer.test.ts b/packages/materials/rating/src/viewer.test.ts index 223f588d..325e4177 100644 --- a/packages/materials/rating/src/viewer.test.ts +++ b/packages/materials/rating/src/viewer.test.ts @@ -20,6 +20,7 @@ describe('renderRating', () => { it('projects resolved bound values over preset props', () => { const node = createRatingNode({ props: { value: 20 } }) const html = readTrustedViewerHtml(renderRating(node, { + document, data: {}, resolvedProps: { value: 80 }, pageIndex: 0, diff --git a/packages/materials/rect/package.json b/packages/materials/rect/package.json index 4e2e4756..78ddc2ae 100644 --- a/packages/materials/rect/package.json +++ b/packages/materials/rect/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-rect", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk rect material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/ring-progress/package.json b/packages/materials/ring-progress/package.json index 73de9203..fc8ebdea 100644 --- a/packages/materials/ring-progress/package.json +++ b/packages/materials/ring-progress/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-ring-progress", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk ring progress material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/signature/package.json b/packages/materials/signature/package.json index b54077d5..636dd15f 100644 --- a/packages/materials/signature/package.json +++ b/packages/materials/signature/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-signature", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk signature material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/svg/custom/package.json b/packages/materials/svg/custom/package.json index 42665c1c..a1751a44 100644 --- a/packages/materials/svg/custom/package.json +++ b/packages/materials/svg/custom/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-svg-custom", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk custom SVG material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/svg/heart/package.json b/packages/materials/svg/heart/package.json index e720787b..3af478df 100644 --- a/packages/materials/svg/heart/package.json +++ b/packages/materials/svg/heart/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-svg-heart", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk heart SVG material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/svg/star/package.json b/packages/materials/svg/star/package.json index e353eb85..d6fd7019 100644 --- a/packages/materials/svg/star/package.json +++ b/packages/materials/svg/star/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-svg-star", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk star SVG material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/table/data/package.json b/packages/materials/table/data/package.json index 25f9583c..9761e246 100644 --- a/packages/materials/table/data/package.json +++ b/packages/materials/table/data/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-table-data", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk table-data material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/table/kernel/package.json b/packages/materials/table/kernel/package.json index 23d5bcf6..2007341f 100644 --- a/packages/materials/table/kernel/package.json +++ b/packages/materials/table/kernel/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-table-kernel", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk shared table kernel: geometry, rendering, topology helpers", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/table/static/package.json b/packages/materials/table/static/package.json index 035a1fcc..c5a6851a 100644 --- a/packages/materials/table/static/package.json +++ b/packages/materials/table/static/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-table-static", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk table-static material", "author": "hackycy ", "license": "MIT", diff --git a/packages/materials/text/package.json b/packages/materials/text/package.json index 9b129bbf..33a3a97b 100644 --- a/packages/materials/text/package.json +++ b/packages/materials/text/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/material-text", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "EasyInk text material", "author": "hackycy ", "license": "MIT", diff --git a/packages/print/core/package.json b/packages/print/core/package.json index 1354d1c9..86004bd9 100644 --- a/packages/print/core/package.json +++ b/packages/print/core/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/print-core", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Shared print utilities for EasyInk print drivers", "author": "hackycy ", "license": "MIT", diff --git a/packages/print/integration-easyink-printer/package.json b/packages/print/integration-easyink-printer/package.json index 36f244e6..e8124094 100644 --- a/packages/print/integration-easyink-printer/package.json +++ b/packages/print/integration-easyink-printer/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/print-integration-easyink-printer", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Official EasyInk Printer client and managed document printer", "author": "hackycy ", "license": "MIT", diff --git a/packages/print/integration-hiprint/package.json b/packages/print/integration-hiprint/package.json index 37aab522..87f1ba97 100644 --- a/packages/print/integration-hiprint/package.json +++ b/packages/print/integration-hiprint/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/print-integration-hiprint", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Official HiPrint client and managed document printer", "author": "hackycy ", "license": "MIT", diff --git a/packages/print/integration-lodop/package.json b/packages/print/integration-lodop/package.json index 1237fb60..279ad84f 100644 --- a/packages/print/integration-lodop/package.json +++ b/packages/print/integration-lodop/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/print-integration-lodop", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Official LODOP client and managed document printer", "author": "hackycy ", "license": "MIT", diff --git a/packages/prop-schemas/package.json b/packages/prop-schemas/package.json index 5e06c7b5..d7ad49df 100644 --- a/packages/prop-schemas/package.json +++ b/packages/prop-schemas/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/prop-schemas", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Built-in material property schemas for EasyInk Designer", "author": "hackycy ", "license": "MIT", diff --git a/packages/samples/package.json b/packages/samples/package.json index fc91f8d5..01864be2 100644 --- a/packages/samples/package.json +++ b/packages/samples/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/samples", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Sample templates and demo data for EasyInk", "author": "hackycy ", "license": "MIT", diff --git a/packages/schema-tools/package.json b/packages/schema-tools/package.json index 7003c772..6fd79a1b 100644 --- a/packages/schema-tools/package.json +++ b/packages/schema-tools/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/schema-tools", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Schema validation and data source alignment utilities for EasyInk", "author": "hackycy ", "license": "MIT", diff --git a/packages/schema/package.json b/packages/schema/package.json index bcfbbd46..549edf2b 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/schema", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Schema types, defaults, migration, and serialization for EasyInk", "author": "hackycy ", "license": "MIT", diff --git a/packages/shared/package.json b/packages/shared/package.json index 645198cb..ac1ade9f 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/shared", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Common types, utilities, and constants for EasyInk", "author": "hackycy ", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 147e5194..753db3dc 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/ui", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "UI components for EasyInk designer workbench", "author": "hackycy ", "license": "MIT", diff --git a/packages/viewer/package.json b/packages/viewer/package.json index 5c27b32a..6d0453d8 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -1,7 +1,7 @@ { "name": "@easyink/viewer", "type": "module", - "version": "0.0.30", + "version": "0.0.31", "description": "Viewer runtime for EasyInk: preview, print, export", "author": "hackycy ", "license": "MIT", diff --git a/packages/viewer/src/render-surface.ts b/packages/viewer/src/render-surface.ts index 124606a7..c62fd20f 100644 --- a/packages/viewer/src/render-surface.ts +++ b/packages/viewer/src/render-surface.ts @@ -48,6 +48,7 @@ export function renderPages( pageEl.appendChild(contentLayer) const context: ViewerRenderContext = { + document, data, resolvedProps: {}, pageIndex: page.index, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8c7c70c..3e0c030c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,6 +115,9 @@ catalogs: clsx: specifier: ^2.1.1 version: 2.1.1 + core-js: + specifier: ^3.49.0 + version: 3.49.0 curlconverter: specifier: ^4.12.0 version: 4.12.0 @@ -223,6 +226,9 @@ catalogs: specifier: ^2.8.1 version: 2.8.1 types: + '@types/jsbarcode': + specifier: ^3.11.4 + version: 3.11.4 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -347,6 +353,9 @@ importers: '@easyink/viewer': specifier: workspace:* version: link:../../packages/viewer + core-js: + specifier: catalog:framework + version: 3.49.0 devDependencies: '@types/node': specifier: catalog:types @@ -871,6 +880,10 @@ importers: jsbarcode: specifier: catalog:framework version: 3.12.3 + devDependencies: + '@types/jsbarcode': + specifier: catalog:types + version: 3.11.4 packages/materials/chart/bar: dependencies: @@ -2939,6 +2952,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/jsbarcode@3.11.4': + resolution: {integrity: sha512-VBcpTAnEMH0Gbh8JpV14CgOtJjCYjsvR2FoDRyoYPE0gUxtApf8N4c+HKEOyz/iiIZkMzqrzBA3XX7+KgKxxsA==} + '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} @@ -7109,6 +7125,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/jsbarcode@3.11.4': + dependencies: + '@types/node': 24.10.0 + '@types/jsesc@2.5.1': {} '@types/json-schema@7.0.15': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0ea5be61..8bc179d0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -58,6 +58,7 @@ catalogs: '@lezer/highlight': ^1.0.0 '@tanstack/vue-query': ^5.100.14 '@vueuse/core': ^14.3.0 + core-js: ^3.49.0 '@xstate/vue': ^5.0.0 ant-design-vue: ^4.2.6 class-variance-authority: ^0.7.1 @@ -98,6 +99,7 @@ catalogs: vue-plugin-hiprint: ^0.0.60 yaml: ^2.8.1 types: + '@types/jsbarcode': ^3.11.4 '@types/lodash-es': ^4.17.12 '@types/node': ^24.10.0 ignoreWorkspaceRootCheck: true diff --git a/scripts/android-render-sdk-build.mjs b/scripts/android-render-sdk-build.mjs index dbe4c718..8800c807 100644 --- a/scripts/android-render-sdk-build.mjs +++ b/scripts/android-render-sdk-build.mjs @@ -24,6 +24,7 @@ await run(gradleBin, [ 'clean', 'assembleRelease', 'verifyAarRuntimeAssets', + 'publishReleasePublicationToFlutterSampleRepository', ':sample-minimal:assembleDebug', ])