├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ ├── publish.yml │ ├── publishable.yml │ └── runnable.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── lib │ └── main.dart ├── lib ├── extended_image_library.dart └── src │ ├── _extended_network_image_utils_io.dart │ ├── _extended_network_image_utils_web.dart │ ├── _platform_io.dart │ ├── _platform_web.dart │ ├── extended_asset_bundle_image_provider.dart │ ├── extended_file_image_provider.dart │ ├── extended_image_provider.dart │ ├── extended_memory_image_provider.dart │ ├── extended_resize_image_provider.dart │ ├── network │ ├── extended_network_image_provider.dart │ ├── network_image_io.dart │ └── network_image_web.dart │ └── platform.dart └── pubspec.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: zmtzawqlp 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | custom: http://zmtzawqlp.gitee.io/my_images/images/qrcode.png 13 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: true -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v3 14 | - name: Publish 15 | uses: k-paxian/dart-package-publisher@master 16 | with: 17 | credentialJson: ${{ secrets.CREDENTIAL_JSON }} 18 | flutter: true 19 | skipTests: true -------------------------------------------------------------------------------- /.github/workflows/publishable.yml: -------------------------------------------------------------------------------- 1 | name: Publishable 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - "**.md" 12 | - "**.yaml" 13 | - "**.yml" 14 | 15 | jobs: 16 | publish-dry-run: 17 | name: Publish dry-run with packages 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: k-paxian/dart-package-publisher@master 22 | with: 23 | credentialJson: 'MockCredentialJson' 24 | flutter: true 25 | dryRunOnly: true 26 | skipTests: true -------------------------------------------------------------------------------- /.github/workflows/runnable.yml: -------------------------------------------------------------------------------- 1 | name: Runnable (stable) 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - "**.md" 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze on ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ ubuntu-latest ] 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: actions/setup-java@v3 23 | with: 24 | distribution: 'adopt' 25 | java-version: '11.x' 26 | - uses: subosito/flutter-action@v2 27 | with: 28 | channel: 'stable' 29 | - name: Log Dart/Flutter versions 30 | run: | 31 | dart --version 32 | flutter --version 33 | - name: Prepare dependencies 34 | run: flutter pub get 35 | - name: Analyse the repo 36 | run: flutter analyze lib example/lib 37 | - name: Run tests 38 | run: flutter test 39 | - name: Generate docs 40 | run: | 41 | dart pub global activate dartdoc 42 | dart pub global run dartdoc . 43 | 44 | test_iOS: 45 | needs: analyze 46 | name: Test iOS 47 | runs-on: macos-latest 48 | steps: 49 | - uses: actions/checkout@v3 50 | - uses: actions/setup-java@v3 51 | with: 52 | distribution: 'adopt' 53 | java-version: '11.x' 54 | - uses: subosito/flutter-action@v2.8.0 55 | with: 56 | channel: stable 57 | - run: dart --version 58 | - run: flutter --version 59 | - run: flutter pub get 60 | - run: cd example; flutter build ios --no-codesign 61 | 62 | test_android: 63 | needs: analyze 64 | name: Test Android 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v3 68 | - uses: actions/setup-java@v3 69 | with: 70 | distribution: 'adopt' 71 | java-version: '11.x' 72 | - uses: subosito/flutter-action@v2.8.0 73 | with: 74 | channel: stable 75 | - run: dart --version 76 | - run: flutter --version 77 | - run: flutter pub get 78 | - run: sudo echo "y" | sudo $ANDROID_HOME/tools/bin/sdkmanager "ndk;21.4.7075529" 79 | - run: cd example; flutter build apk --debug -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | 65 | # Exceptions to above rules. 66 | !**/ios/**/default.mode1v3 67 | !**/ios/**/default.mode2v3 68 | !**/ios/**/default.pbxuser 69 | !**/ios/**/default.perspectivev3 70 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 71 | pubspec.lock 72 | .flutter-plugins-dependencies -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 96f15c74adebb221eb044d3fc71b2d62da0046c0 8 | channel: unknown 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 5.0.1 2 | 3 | * Make the package WASM compatible 4 | 5 | ## 5.0.0 6 | 7 | * Migrate to 3.29.0 8 | * Add WebHtmlElementStrategy for ExtendedNetworkImageProvider on Web 9 | 10 | ## 4.0.6 11 | 12 | * Make the package WASM compatible 13 | 14 | ## 4.0.5 15 | 16 | * Loosen `web` version to `0.3.0~9.x.x`, and fit flutter sdk: >= 3.16 17 | 18 | ## 4.0.4 19 | 20 | * Fix AssetImage flicker(#655) 21 | 22 | ## 4.0.3 23 | 24 | * Loosen `web` version to `0.4.0~0.5.x`. 25 | * Fix lints. 26 | 27 | ## 4.0.2 28 | 29 | * Upgrade `web` form 0.3.0 to 0.4.0 30 | 31 | ## 4.0.1 32 | 33 | * Fix error that it can't find File.length() method at web. 34 | 35 | ## 4.0.0 36 | 37 | * Migrate to 3.16.0 38 | * [ExtendedFileImageProvider] use ImmutableBuffer.fromFilePath to prevent crash for big image when we don't need to cache raw data. 39 | 40 | ## 3.6.0 41 | 42 | * Migrate to 3.13.0 43 | 44 | ## 3.5.3 45 | 46 | * upgrade http to 1.0.0 47 | 48 | ## 3.5.2 49 | 50 | * Fix issue that can't load image if cacheWidth or cacheHeight set on web platform #56 51 | * Mark [ExtendedResizeImage.compressionRatio] and [ExtendedResizeImage.maxBytes] are not supported on web. (Error: Unsupported operation: ImageDescriptor is not supported on web.) 52 | 53 | ## 3.5.1 54 | 55 | * Fix miss _network_image_web.dart #582 56 | 57 | ## 3.5.0 58 | 59 | * Breaking Change: remove loadBuffer method, and add loadImage method [https://github.com/flutter/flutter/pull/118966] 60 | * Migrate to 3.7.0 61 | 62 | ## 3.4.2 63 | 64 | * Fix issue that cannot compile ExtendedImage.file on the web (566#) 65 | 66 | ## 3.4.1 67 | 68 | * clearMemoryCacheWhenDispose is not working with imageCacheName property, obtainCacheStatus method should be overrided.(#44) 69 | 70 | ## 3.4.0 71 | 72 | * Migrate to 3.3.0 (load=>loadBuffer) 73 | 74 | ## 3.3.0 75 | 76 | * Migrate to 3.0.0 77 | 78 | ## 3.2.0 79 | 80 | * override == and hashCode for ExtendedResizeImage 81 | * fix issue that ExtendedResizeImage can't get rawImageData 82 | * ExtendedResizeImage.maxBytes is actual bytes of Image, not decode bytes. 83 | 84 | ## 3.1.4 85 | 86 | * Use list instead of listSync to look for cached files 87 | 88 | ## 3.1.3 89 | 90 | * Make abstract ExtendedNetworkImageProvider with ExtendedImageProvider 91 | 92 | ## 3.1.2 93 | 94 | * Fix issue that using headers might cause a lot of rebuilds (#39) 95 | 96 | ## 3.1.1 97 | 98 | * Fix socket leak (#38) 99 | 100 | ## 3.1.0 101 | 102 | * Improve: 103 | 104 | 1. add [ExtendedNetworkImageProvider.cacheMaxAge] to set max age to be cached. 105 | 106 | ## 3.0.0 107 | 108 | * Breaking change: 109 | 110 | 1. we cache raw image pixels as default behavior at previous versions, it's not good for heap memory usage. so add [ExtendedImageProvider.cacheRawData] to support whether should cache the raw image pixels. It's [false] now. 111 | 112 | * Improve: 113 | 114 | 1. add [ExtendedResizeImage] to support resize image more convenient. 115 | 2. add [ExtendedImageProvider.imageCacheName] to support custom ImageCache to store ExtendedImageProvider. 116 | ## 2.0.2 117 | 118 | * fix null-safety cast error 119 | ## 2.0.1 120 | 121 | * add [ExtendedNetworkImageProvider.printError] 122 | 123 | ## 2.0.0 124 | 125 | * support-null-safety 126 | ## 1.0.1 127 | 128 | * add cache key for utils 129 | 130 | ## 1.0.0 131 | 132 | * fix web capability at pub.dev 133 | * add cache key #288 134 | 135 | ## 0.3.1 136 | 137 | * support chunkEvents for network web 138 | ## 0.3.0 139 | 140 | * export http_client_helper 141 | 142 | ## 0.2.3 143 | 144 | * fix analysis_options.yaml base on flutter sdk 145 | 146 | ## 0.2.2 147 | 148 | * add analysis_options.yaml 149 | * fix null exception of chunkEvents 150 | 151 | ## 0.2.1 152 | 153 | * support loading progress for network 154 | * public HttpClient of ExtendedNetworkImageProvider 155 | * add getCachedSizeBytes method 156 | 157 | ## 0.2.0 158 | 159 | * web support 160 | 161 | ## 0.1.9 162 | 163 | * fix breaking change for flutter 1.10.15 about miss load parameter 164 | 165 | ## 0.1.8 166 | 167 | * add ExtendedAssetBundleImageKey to support to cache rawImageData 168 | 169 | ## 0.1.7 170 | 171 | * override == method to set rawImageData 172 | 173 | ## 0.1.6 174 | 175 | * add ExtendedImageProvider 176 | ExtendedExactAssetImageProvider 177 | ExtendedAssetImageProvider 178 | ExtendedFileImageProvider 179 | ExtendedMemoryImageProvider 180 | now we can get raw image data from ExtendedImageProvider 181 | 182 | ## 0.1.5 183 | 184 | * add getCachedImageFile(url) method 185 | 186 | ## 0.1.4 187 | 188 | * improve codes base on v1.7.8 189 | 190 | ## 0.1.3 191 | 192 | * update path_provider 1.1.0 193 | 194 | ## 0.1.1 195 | 196 | * disabled informationCollector to keep backwards compatibility for now 197 | 198 | ## 0.1.0 199 | 200 | * add extended_network_image_provider.dart and extended_network_image_utils.dart 201 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zmtzawqlp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zmtzawqlp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extended_image_library 2 | 3 | [![pub package](https://img.shields.io/pub/v/extended_image_library.svg)](https://pub.dartlang.org/packages/extended_image_library) 4 | 5 | package library for extended_image 6 | 7 | # extended_image 8 | 9 | [![pub package](https://img.shields.io/pub/v/extended_image.svg)](https://pub.dartlang.org/packages/extended_image) 10 | 11 | A powerful official extension library of image, which support placeholder(loading)/ failed state, cache network, zoom pan image, photo view, slide out page, editor(crop,rotate,flip), paint custom etc. 12 | 13 | ![img](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_image/editor.gif) 14 | 15 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are other similar analysis options files in the flutter repos, 11 | # which should be kept in sync with this file: 12 | # 13 | # - analysis_options.yaml (this file) 14 | # - packages/flutter/lib/analysis_options_user.yaml 15 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 16 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 17 | # 18 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 19 | # Android Studio, and the `flutter analyze` command. 20 | 21 | analyzer: 22 | errors: 23 | # treat missing required parameters as a warning (not a hint) 24 | missing_required_param: warning 25 | # treat missing returns as a warning (not a hint) 26 | missing_return: warning 27 | # allow having TODOs in the code 28 | todo: ignore 29 | # Ignore analyzer hints for updating pubspecs when using Future or 30 | # Stream and not importing dart:async 31 | # Please see https://github.com/flutter/flutter/pull/24528 for details. 32 | # sdk_version_async_exported_from_core: ignore 33 | # exclude: 34 | # - "src/_network_image_web.dart" 35 | 36 | linter: 37 | rules: 38 | # these rules are documented on and in the same order as 39 | # the Dart Lint rules page to make maintenance easier 40 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 41 | - always_declare_return_types 42 | - always_put_control_body_on_new_line 43 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 44 | # - always_require_non_null_named_parameters 45 | - always_specify_types 46 | - annotate_overrides 47 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 48 | # - avoid_as # required for implicit-casts: true 49 | - avoid_bool_literals_in_conditional_expressions 50 | # - avoid_catches_without_on_clauses # we do this commonly 51 | # - avoid_catching_errors # we do this commonly 52 | - avoid_classes_with_only_static_members 53 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 54 | - avoid_empty_else 55 | # - avoid_equals_and_hash_code_on_mutable_classes # not yet tested 56 | - avoid_field_initializers_in_const_classes 57 | - avoid_function_literals_in_foreach_calls 58 | # - avoid_implementing_value_types # not yet tested 59 | - avoid_init_to_null 60 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 61 | - avoid_null_checks_in_equality_operators 62 | # - avoid_positional_boolean_parameters # not yet tested 63 | # - avoid_print # not yet tested 64 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 65 | # - avoid_redundant_argument_values # not yet tested 66 | - avoid_relative_lib_imports 67 | - avoid_renaming_method_parameters 68 | - avoid_return_types_on_setters 69 | # - avoid_returning_null # there are plenty of valid reasons to return null 70 | # - avoid_returning_null_for_future # not yet tested 71 | - avoid_returning_null_for_void 72 | # - avoid_returning_this # there are plenty of valid reasons to return this 73 | # - avoid_setters_without_getters # not yet tested 74 | # - avoid_shadowing_type_parameters # not yet tested 75 | - avoid_single_cascade_in_expression_statements 76 | - avoid_slow_async_io 77 | - avoid_types_as_parameter_names 78 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 79 | # - avoid_unnecessary_containers # not yet tested 80 | - avoid_unused_constructor_parameters 81 | - avoid_void_async 82 | # - avoid_web_libraries_in_flutter # not yet tested 83 | - await_only_futures 84 | - camel_case_extensions 85 | - camel_case_types 86 | - cancel_subscriptions 87 | # - cascade_invocations # not yet tested 88 | # - close_sinks # not reliable enough 89 | # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 90 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 91 | - control_flow_in_finally 92 | # - curly_braces_in_flow_control_structures # not yet tested 93 | # - diagnostic_describe_all_properties # not yet tested 94 | - directives_ordering 95 | - empty_catches 96 | - empty_constructor_bodies 97 | - empty_statements 98 | # - file_names # not yet tested 99 | - flutter_style_todos 100 | - hash_and_equals 101 | - implementation_imports 102 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 103 | # - iterable_contains_unrelated_type 104 | # - join_return_with_assignment # not yet tested 105 | - library_names 106 | - library_prefixes 107 | # - lines_longer_than_80_chars # not yet tested 108 | # - list_remove_unrelated_type 109 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 110 | # - missing_whitespace_between_adjacent_strings # not yet tested 111 | - no_adjacent_strings_in_list 112 | - no_duplicate_case_values 113 | # - no_logic_in_create_state # not yet tested 114 | # - no_runtimeType_toString # not yet tested 115 | - non_constant_identifier_names 116 | # - null_closures # not yet tested 117 | # - omit_local_variable_types # opposite of always_specify_types 118 | # - one_member_abstracts # too many false positives 119 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 120 | - overridden_fields 121 | - package_api_docs 122 | - package_names 123 | - package_prefixed_library_names 124 | # - parameter_assignments # we do this commonly 125 | - prefer_adjacent_string_concatenation 126 | - prefer_asserts_in_initializer_lists 127 | # - prefer_asserts_with_message # not yet tested 128 | - prefer_collection_literals 129 | - prefer_conditional_assignment 130 | - prefer_const_constructors 131 | - prefer_const_constructors_in_immutables 132 | - prefer_const_declarations 133 | - prefer_const_literals_to_create_immutables 134 | # - prefer_constructors_over_static_methods # not yet tested 135 | - prefer_contains 136 | # - prefer_double_quotes # opposite of prefer_single_quotes 137 | # - prefer_equal_for_default_values 138 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 139 | - prefer_final_fields 140 | - prefer_final_in_for_each 141 | - prefer_final_locals 142 | - prefer_for_elements_to_map_fromIterable 143 | - prefer_foreach 144 | # - prefer_function_declarations_over_variables # not yet tested 145 | - prefer_generic_function_type_aliases 146 | - prefer_if_elements_to_conditional_expressions 147 | - prefer_if_null_operators 148 | - prefer_initializing_formals 149 | - prefer_inlined_adds 150 | # - prefer_int_literals # not yet tested 151 | # - prefer_interpolation_to_compose_strings # not yet tested 152 | - prefer_is_empty 153 | - prefer_is_not_empty 154 | - prefer_is_not_operator 155 | - prefer_iterable_whereType 156 | # - prefer_mixin # https://github.com/dart-lang/language/issues/32 157 | # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932 158 | # - prefer_relative_imports # not yet tested 159 | - prefer_single_quotes 160 | - prefer_spread_collections 161 | - prefer_typing_uninitialized_variables 162 | - prefer_void_to_null 163 | # - provide_deprecation_message # not yet tested 164 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 165 | - recursive_getters 166 | - slash_for_doc_comments 167 | # - sort_child_properties_last # not yet tested 168 | - sort_constructors_first 169 | - sort_pub_dependencies 170 | - sort_unnamed_constructors_first 171 | - test_types_in_equals 172 | - throw_in_finally 173 | # - type_annotate_public_apis # subset of always_specify_types 174 | - type_init_formals 175 | # - unawaited_futures # too many false positives 176 | # - unnecessary_await_in_return # not yet tested 177 | - unnecessary_brace_in_string_interps 178 | - unnecessary_const 179 | # - unnecessary_final # conflicts with prefer_final_locals 180 | - unnecessary_getters_setters 181 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 182 | - unnecessary_new 183 | - unnecessary_null_aware_assignments 184 | - unnecessary_null_in_if_null_operators 185 | - unnecessary_overrides 186 | - unnecessary_parenthesis 187 | - unnecessary_statements 188 | - unnecessary_string_interpolations 189 | - unnecessary_this 190 | - unrelated_type_equality_checks 191 | # - unsafe_html # not yet tested 192 | - use_full_hex_values_for_flutter_colors 193 | # - use_function_type_syntax_for_parameters # not yet tested 194 | # - use_key_in_widget_constructors # not yet tested 195 | - use_rethrow_when_possible 196 | # - use_setters_to_change_properties # not yet tested 197 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 198 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 199 | - valid_regexps 200 | - void_checks 201 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/extended_image_library.dart: -------------------------------------------------------------------------------- 1 | library extended_image_library; 2 | 3 | export 'package:http_client_helper/http_client_helper.dart' hide Response; 4 | 5 | export 'src/extended_asset_bundle_image_provider.dart'; 6 | export 'src/extended_file_image_provider.dart'; 7 | export 'src/extended_image_provider.dart'; 8 | export 'src/extended_memory_image_provider.dart'; 9 | export 'src/extended_resize_image_provider.dart'; 10 | export 'src/network/extended_network_image_provider.dart'; 11 | export 'src/platform.dart'; 12 | -------------------------------------------------------------------------------- /lib/src/_extended_network_image_utils_io.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:path/path.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'platform.dart'; 6 | 7 | /// Clear the disk cache directory then return if it succeed. 8 | Future clearDiskCachedImages({Duration? duration}) async { 9 | try { 10 | final Directory cacheImagesDirectory = Directory( 11 | join((await getTemporaryDirectory()).path, cacheImageFolderName), 12 | ); 13 | if (cacheImagesDirectory.existsSync()) { 14 | if (duration == null) { 15 | cacheImagesDirectory.deleteSync(recursive: true); 16 | } else { 17 | final DateTime now = DateTime.now(); 18 | await for (final FileSystemEntity file in cacheImagesDirectory.list()) { 19 | final FileStat fs = file.statSync(); 20 | if (now.subtract(duration).isAfter(fs.changed)) { 21 | file.deleteSync(recursive: true); 22 | } 23 | } 24 | } 25 | } 26 | } catch (_) { 27 | return false; 28 | } 29 | return true; 30 | } 31 | 32 | /// Clear the disk cache image then return if it succeed. 33 | Future clearDiskCachedImage(String url, {String? cacheKey}) async { 34 | try { 35 | final File? file = await getCachedImageFile(url, cacheKey: cacheKey); 36 | if (file != null) { 37 | await file.delete(recursive: true); 38 | } 39 | } catch (_) { 40 | return false; 41 | } 42 | return true; 43 | } 44 | 45 | /// Get the local file of the cached image 46 | 47 | Future getCachedImageFile(String url, {String? cacheKey}) async { 48 | try { 49 | final String key = cacheKey ?? keyToMd5(url); 50 | final Directory cacheImagesDirectory = Directory( 51 | join((await getTemporaryDirectory()).path, cacheImageFolderName), 52 | ); 53 | if (cacheImagesDirectory.existsSync()) { 54 | await for (final FileSystemEntity file in cacheImagesDirectory.list()) { 55 | if (file.path.endsWith(key)) { 56 | return File(file.path); 57 | } 58 | } 59 | } 60 | } catch (_) { 61 | return null; 62 | } 63 | return null; 64 | } 65 | 66 | /// Check if the image exists in cache 67 | Future cachedImageExists(String url, {String? cacheKey}) async { 68 | try { 69 | final String key = cacheKey ?? keyToMd5(url); 70 | final Directory cacheImagesDirectory = Directory( 71 | join((await getTemporaryDirectory()).path, cacheImageFolderName), 72 | ); 73 | if (cacheImagesDirectory.existsSync()) { 74 | await for (final FileSystemEntity file in cacheImagesDirectory.list()) { 75 | if (file.path.endsWith(key)) { 76 | return true; 77 | } 78 | } 79 | } 80 | return false; 81 | } catch (e) { 82 | return false; 83 | } 84 | } 85 | 86 | /// Get total size of cached image 87 | Future getCachedSizeBytes() async { 88 | int size = 0; 89 | final Directory cacheImagesDirectory = Directory( 90 | join((await getTemporaryDirectory()).path, cacheImageFolderName), 91 | ); 92 | if (cacheImagesDirectory.existsSync()) { 93 | await for (final FileSystemEntity file in cacheImagesDirectory.list()) { 94 | size += file.statSync().size; 95 | } 96 | } 97 | return size; 98 | } 99 | 100 | /// Get the local file path of the cached image 101 | Future getCachedImageFilePath(String url, {String? cacheKey}) async { 102 | final File? file = await getCachedImageFile(url, cacheKey: cacheKey); 103 | return file?.path; 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/_extended_network_image_utils_web.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import '_platform_web.dart'; 3 | 4 | /// clear the disk cache directory then return if it succeed. 5 | /// timespan to compute whether file has expired or not 6 | Future clearDiskCachedImages({Duration? duration}) async { 7 | assert(false, 'not support on web'); 8 | return false; 9 | } 10 | 11 | /// clear the disk cache image then return if it succeed. 12 | /// clear specific one 13 | Future clearDiskCachedImage(String url, {String? cacheKey}) async { 14 | assert(false, 'not support on web'); 15 | return false; 16 | } 17 | 18 | /// Get the local file of the cached image 19 | 20 | Future getCachedImageFile(String url, {String? cacheKey}) async { 21 | assert(false, 'not support on web'); 22 | return null; 23 | } 24 | 25 | /// Check if the image exists in cache 26 | Future cachedImageExists(String url, {String? cacheKey}) async { 27 | assert(false, 'not support on web'); 28 | return false; 29 | } 30 | 31 | /// Get total size of cached image 32 | Future getCachedSizeBytes() async { 33 | assert(false, 'not support on web'); 34 | return 0; 35 | } 36 | 37 | /// Get the local file path of the cached image 38 | Future getCachedImageFilePath(String url, {String? cacheKey}) async { 39 | assert(false, 'not support on web'); 40 | return null; 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/_platform_io.dart: -------------------------------------------------------------------------------- 1 | export 'dart:io' show File; 2 | export 'package:flutter/widgets.dart' show FileImage; 3 | -------------------------------------------------------------------------------- /lib/src/_platform_web.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | /// mock web File 5 | /// no implement 6 | // ignore_for_file: always_specify_types,avoid_unused_constructor_parameters,unused_field 7 | class File { 8 | /// Creates a [File] object. 9 | /// 10 | /// If [path] is a relative path, it will be interpreted relative to the 11 | /// current working directory (see [Directory.current]), when used. 12 | /// 13 | /// If [path] is an absolute path, it will be immune to changes to the 14 | /// current working directory. 15 | /// 16 | 17 | File(this.path) : assert(false, 'not support on web'); 18 | 19 | /// Reads the entire file contents as a list of bytes. 20 | /// 21 | /// Returns a `Future` that completes with the list of bytes that 22 | /// is the contents of the file. 23 | Future readAsBytes() async => Uint8List.fromList([]); 24 | 25 | /// The length of the file. 26 | /// 27 | /// Returns a `Future` that completes with the length in bytes. 28 | Future length() async { 29 | return 0; 30 | } 31 | 32 | /// The path of the file underlying this random access file. 33 | final String path; 34 | } 35 | 36 | /// mock web File 37 | /// no implement 38 | class FileImage extends ImageProvider { 39 | /// Creates an object that decodes a [File] as an image. 40 | /// 41 | /// The arguments must not be null. 42 | const FileImage(this.file, {this.scale = 1.0}); 43 | 44 | /// The file to decode into an image. 45 | final File file; 46 | 47 | /// The scale to place in the [ImageInfo] object of the image. 48 | final double scale; 49 | 50 | @override 51 | Future obtainKey(ImageConfiguration configuration) { 52 | return SynchronousFuture(this); 53 | } 54 | 55 | @override 56 | bool operator ==(Object other) { 57 | if (other.runtimeType != runtimeType) { 58 | return false; 59 | } 60 | return other is FileImage && 61 | other.file.path == file.path && 62 | other.scale == scale; 63 | } 64 | 65 | @override 66 | int get hashCode => Object.hash(file.path, scale); 67 | 68 | @override 69 | String toString() => 70 | '${objectRuntimeType(this, 'FileImage')}("${file.path}", scale: $scale)'; 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/extended_asset_bundle_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui' as ui show Codec; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'extended_image_provider.dart'; 8 | 9 | class ExtendedExactAssetImageProvider extends ExactAssetImage 10 | with ExtendedImageProvider { 11 | const ExtendedExactAssetImageProvider( 12 | String assetName, { 13 | AssetBundle? bundle, 14 | String? package, 15 | double scale = 1.0, 16 | this.cacheRawData = false, 17 | this.imageCacheName, 18 | }) : super(assetName, bundle: bundle, package: package, scale: scale); 19 | 20 | /// Whether cache raw data if you need to get raw data directly. 21 | /// For example, we need raw image data to edit, 22 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 23 | /// data here. 24 | @override 25 | final bool cacheRawData; 26 | 27 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 28 | @override 29 | final String? imageCacheName; 30 | @override 31 | Future obtainKey( 32 | ImageConfiguration configuration, 33 | ) { 34 | return SynchronousFuture( 35 | ExtendedAssetBundleImageKey( 36 | bundle: bundle ?? configuration.bundle ?? rootBundle, 37 | name: keyName, 38 | scale: scale, 39 | cacheRawData: cacheRawData, 40 | imageCacheName: imageCacheName, 41 | ), 42 | ); 43 | } 44 | 45 | @override 46 | ImageStreamCompleter loadImage( 47 | AssetBundleImageKey key, 48 | ImageDecoderCallback decode, 49 | ) { 50 | return MultiFrameImageStreamCompleter( 51 | codec: _loadAsync(key, decode), 52 | scale: key.scale, 53 | informationCollector: () sync* { 54 | yield DiagnosticsProperty('Image provider', this); 55 | yield DiagnosticsProperty('Image key', key); 56 | }, 57 | ); 58 | } 59 | 60 | /// Fetches the image from the asset bundle, decodes it, and returns a 61 | /// corresponding [ImageInfo] object. 62 | /// 63 | /// This function is used by [load]. 64 | @protected 65 | Future _loadAsync( 66 | AssetBundleImageKey key, 67 | ImageDecoderCallback decode, 68 | ) async { 69 | ByteData data; 70 | // Hot reload/restart could change whether an asset bundle or key in a 71 | // bundle are available, or if it is a network backed bundle. 72 | try { 73 | data = await key.bundle.load(key.name); 74 | } on FlutterError { 75 | this.imageCache.evict(key); 76 | rethrow; 77 | } 78 | final Uint8List result = data.buffer.asUint8List(); 79 | return await instantiateImageCodec(result, decode); 80 | } 81 | } 82 | 83 | class ExtendedAssetImageProvider extends AssetImage 84 | with ExtendedImageProvider { 85 | const ExtendedAssetImageProvider( 86 | String assetName, { 87 | AssetBundle? bundle, 88 | String? package, 89 | this.cacheRawData = false, 90 | this.imageCacheName, 91 | }) : super(assetName, bundle: bundle, package: package); 92 | 93 | /// Whether cache raw data if you need to get raw data directly. 94 | /// For example, we need raw image data to edit, 95 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 96 | /// data here. 97 | @override 98 | final bool cacheRawData; 99 | 100 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 101 | @override 102 | final String? imageCacheName; 103 | 104 | @override 105 | Future obtainKey( 106 | ImageConfiguration configuration, 107 | ) { 108 | return obtainNewKey( 109 | (AssetBundleImageKey value) => ExtendedAssetBundleImageKey( 110 | bundle: value.bundle, 111 | scale: value.scale, 112 | name: value.name, 113 | cacheRawData: cacheRawData, 114 | imageCacheName: imageCacheName, 115 | ), 116 | () => super.obtainKey(configuration), 117 | ); 118 | } 119 | 120 | @override 121 | ImageStreamCompleter loadImage( 122 | AssetBundleImageKey key, 123 | ImageDecoderCallback decode, 124 | ) { 125 | return MultiFrameImageStreamCompleter( 126 | codec: _loadAsync(key, decode), 127 | scale: key.scale, 128 | informationCollector: () sync* { 129 | yield DiagnosticsProperty('Image provider', this); 130 | yield DiagnosticsProperty('Image key', key); 131 | }, 132 | ); 133 | } 134 | 135 | /// Fetches the image from the asset bundle, decodes it, and returns a 136 | /// corresponding [ImageInfo] object. 137 | /// 138 | /// This function is used by [load]. 139 | @protected 140 | Future _loadAsync( 141 | AssetBundleImageKey key, 142 | ImageDecoderCallback decode, 143 | ) async { 144 | ByteData data; 145 | // Hot reload/restart could change whether an asset bundle or key in a 146 | // bundle are available, or if it is a network backed bundle. 147 | try { 148 | data = await key.bundle.load(key.name); 149 | } on FlutterError { 150 | this.imageCache.evict(key); 151 | rethrow; 152 | } 153 | 154 | final Uint8List result = data.buffer.asUint8List(); 155 | return await instantiateImageCodec(result, decode); 156 | } 157 | } 158 | 159 | class ExtendedAssetBundleImageKey extends AssetBundleImageKey { 160 | const ExtendedAssetBundleImageKey({ 161 | required AssetBundle bundle, 162 | required String name, 163 | required double scale, 164 | required this.cacheRawData, 165 | required this.imageCacheName, 166 | }) : super(bundle: bundle, name: name, scale: scale); 167 | 168 | /// Whether cache raw data if you need to get raw data directly. 169 | /// For example, we need raw image data to edit, 170 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 171 | /// data here. 172 | final bool cacheRawData; 173 | 174 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 175 | 176 | final String? imageCacheName; 177 | 178 | @override 179 | bool operator ==(Object other) { 180 | if (other.runtimeType != runtimeType) { 181 | return false; 182 | } 183 | return other is ExtendedAssetBundleImageKey && 184 | bundle == other.bundle && 185 | name == other.name && 186 | scale == other.scale && 187 | cacheRawData == other.cacheRawData && 188 | imageCacheName == other.imageCacheName; 189 | } 190 | 191 | @override 192 | int get hashCode => 193 | Object.hash(bundle, name, scale, cacheRawData, imageCacheName); 194 | } 195 | -------------------------------------------------------------------------------- /lib/src/extended_file_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui show Codec, ImmutableBuffer; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/widgets.dart' hide FileImage; 4 | import 'extended_image_provider.dart'; 5 | import 'platform.dart'; 6 | 7 | class ExtendedFileImageProvider extends FileImage 8 | with ExtendedImageProvider { 9 | const ExtendedFileImageProvider( 10 | File file, { 11 | double scale = 1.0, 12 | this.cacheRawData = false, 13 | this.imageCacheName, 14 | }) : assert(!kIsWeb, 'not support on web'), 15 | super(file, scale: scale); 16 | 17 | /// Whether cache raw data if you need to get raw data directly. 18 | /// For example, we need raw image data to edit, 19 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 20 | /// data here. 21 | @override 22 | final bool cacheRawData; 23 | 24 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 25 | @override 26 | final String? imageCacheName; 27 | 28 | @override 29 | ImageStreamCompleter loadImage(FileImage key, ImageDecoderCallback decode) { 30 | return MultiFrameImageStreamCompleter( 31 | codec: _loadAsync(key, decode), 32 | scale: key.scale, 33 | debugLabel: key.file.path, 34 | informationCollector: 35 | () => [ErrorDescription('Path: ${file.path}')], 36 | ); 37 | } 38 | 39 | Future _loadAsync( 40 | FileImage key, 41 | ImageDecoderCallback decode, 42 | ) async { 43 | assert(key == this); 44 | 45 | // TODO(jonahwilliams): making this sync caused test failures that seem to 46 | // indicate that we can fail to call evict unless at least one await has 47 | // occurred in the test. 48 | // https://github.com/flutter/flutter/issues/113044 49 | final int lengthInBytes = await file.length(); 50 | if (lengthInBytes == 0) { 51 | // The file may become available later. 52 | PaintingBinding.instance.imageCache.evict(key); 53 | throw StateError('$file is empty and cannot be loaded as an image.'); 54 | } 55 | // TODO(zmtzawqlp): https://github.com/flutter/flutter/pull/112892 56 | // if we use ImmutableBuffer.fromFilePath, we can't cache bytes to edit 57 | // 58 | if (cacheRawData) { 59 | final Uint8List bytes = await file.readAsBytes(); 60 | return await instantiateImageCodec(bytes, decode); 61 | } else { 62 | return (file.runtimeType == File) 63 | ? decode(await ui.ImmutableBuffer.fromFilePath(file.path)) 64 | : decode( 65 | await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes()), 66 | ); 67 | } 68 | } 69 | 70 | @override 71 | bool operator ==(Object other) { 72 | if (other.runtimeType != runtimeType) { 73 | return false; 74 | } 75 | return other is ExtendedFileImageProvider && 76 | file.path == other.file.path && 77 | scale == other.scale && 78 | cacheRawData == other.cacheRawData && 79 | imageCacheName == other.imageCacheName; 80 | } 81 | 82 | @override 83 | int get hashCode => 84 | Object.hash(file.path, scale, cacheRawData, imageCacheName); 85 | 86 | @override 87 | String toString() => 88 | '${objectRuntimeType(this, 'FileImage')}("${file.path}", scale: $scale)'; 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/extended_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | // ignore: unnecessary_import 3 | import 'dart:typed_data'; 4 | import 'dart:ui' as ui show Codec, ImmutableBuffer; 5 | import 'package:extended_image_library/src/extended_resize_image_provider.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/painting.dart' hide imageCache; 8 | 9 | /// The cached raw image data 10 | Map, Uint8List> rawImageDataMap = 11 | , Uint8List>{}; 12 | 13 | /// The imageCaches to store custom ImageCache 14 | Map imageCaches = {}; 15 | 16 | mixin ExtendedImageProvider on ImageProvider { 17 | /// Whether cache raw data if you need to get raw data directly. 18 | /// For example, we need raw image data to edit, 19 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 20 | /// data here. 21 | /// 22 | bool get cacheRawData; 23 | 24 | /// The name of [ImageCache], you can define custom [ImageCache] to store this image. 25 | String? get imageCacheName; 26 | 27 | /// The [ImageCache] which this is stored in it. 28 | ImageCache get imageCache { 29 | if (imageCacheName != null) { 30 | return imageCaches.putIfAbsent(imageCacheName!, () => ImageCache()); 31 | } else { 32 | return PaintingBinding.instance.imageCache; 33 | } 34 | } 35 | 36 | /// The raw data of image 37 | Uint8List get rawImageData { 38 | assert( 39 | cacheRawData, 40 | 'you should set [ExtendedImageProvider.cacheRawData] to true, if you want to get rawImageData from provider.', 41 | ); 42 | 43 | ImageProvider provider = this; 44 | if (this is ExtendedResizeImage) { 45 | provider = (this as ExtendedResizeImage).imageProvider; 46 | } 47 | 48 | assert( 49 | rawImageDataMap.containsKey(provider), 50 | 'raw image data is not already now!', 51 | ); 52 | final Uint8List raw = rawImageDataMap[provider]!; 53 | 54 | return raw; 55 | } 56 | 57 | /// Override this method, so that you can handle raw image data, 58 | /// for example, compress 59 | Future instantiateImageCodec( 60 | Uint8List data, 61 | ImageDecoderCallback decode, 62 | ) async { 63 | if (cacheRawData) { 64 | rawImageDataMap[this] = data; 65 | } 66 | final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List( 67 | data, 68 | ); 69 | return await decode(buffer); 70 | } 71 | 72 | /// Called by [resolve] with the key returned by [obtainKey]. 73 | /// 74 | /// Subclasses should override this method rather than calling [obtainKey] if 75 | /// they need to use a key directly. The [resolve] method installs appropriate 76 | /// error handling guards so that errors will bubble up to the right places in 77 | /// the framework, and passes those guards along to this method via the 78 | /// [handleError] parameter. 79 | /// 80 | /// It is safe for the implementation of this method to call [handleError] 81 | /// multiple times if multiple errors occur, or if an error is thrown both 82 | /// synchronously into the current part of the stack and thrown into the 83 | /// enclosing [Zone]. 84 | /// 85 | /// The default implementation uses the key to interact with the [ImageCache], 86 | /// calling [ImageCache.putIfAbsent] and notifying listeners of the [stream]. 87 | /// Implementers that do not call super are expected to correctly use the 88 | /// [ImageCache]. 89 | @override 90 | void resolveStreamForKey( 91 | ImageConfiguration configuration, 92 | ImageStream stream, 93 | T key, 94 | ImageErrorListener handleError, 95 | ) { 96 | // This is an unusual edge case where someone has told us that they found 97 | // the image we want before getting to this method. We should avoid calling 98 | // load again, but still update the image cache with LRU information. 99 | if (stream.completer != null) { 100 | final ImageStreamCompleter? completer = imageCache.putIfAbsent( 101 | key, 102 | () => stream.completer!, 103 | onError: handleError, 104 | ); 105 | assert(identical(completer, stream.completer)); 106 | return; 107 | } 108 | final ImageStreamCompleter? completer = imageCache.putIfAbsent( 109 | key, 110 | () => loadImage( 111 | key, 112 | PaintingBinding.instance.instantiateImageCodecWithSize, 113 | ), 114 | onError: handleError, 115 | ); 116 | if (completer != null) { 117 | stream.setCompleter(completer); 118 | } 119 | } 120 | 121 | /// Evicts an entry from the image cache. 122 | @override 123 | Future evict({ 124 | ImageCache? cache, 125 | ImageConfiguration configuration = ImageConfiguration.empty, 126 | bool includeLive = true, 127 | }) async { 128 | rawImageDataMap.remove(this); 129 | 130 | cache ??= imageCache; 131 | final T key = await obtainKey(configuration); 132 | return cache.evict(key, includeLive: includeLive); 133 | } 134 | 135 | @override 136 | Future obtainCacheStatus({ 137 | required ImageConfiguration configuration, 138 | ImageErrorListener? handleError, 139 | }) { 140 | return _obtainCacheStatus( 141 | configuration: configuration, 142 | handleError: handleError, 143 | ); 144 | } 145 | 146 | // copy from offical 147 | Future _obtainCacheStatus({ 148 | required ImageConfiguration configuration, 149 | ImageErrorListener? handleError, 150 | }) { 151 | // ignore: unnecessary_null_comparison 152 | assert(configuration != null); 153 | final Completer completer = 154 | Completer(); 155 | _createErrorHandlerAndKey( 156 | configuration, 157 | (T key, ImageErrorListener innerHandleError) { 158 | completer.complete(imageCache.statusForKey(key)); 159 | }, 160 | (T? key, Object exception, StackTrace? stack) async { 161 | if (handleError != null) { 162 | handleError(exception, stack); 163 | } else { 164 | InformationCollector? collector; 165 | assert(() { 166 | collector = 167 | () => [ 168 | DiagnosticsProperty('Image provider', this), 169 | DiagnosticsProperty( 170 | 'Image configuration', 171 | configuration, 172 | ), 173 | DiagnosticsProperty('Image key', key, defaultValue: null), 174 | ]; 175 | return true; 176 | }()); 177 | FlutterError.reportError( 178 | FlutterErrorDetails( 179 | context: ErrorDescription( 180 | 'while checking the cache location of an image', 181 | ), 182 | informationCollector: collector, 183 | exception: exception, 184 | stack: stack, 185 | ), 186 | ); 187 | completer.complete(null); 188 | } 189 | }, 190 | ); 191 | return completer.future; 192 | } 193 | 194 | /// This method is used by both [resolve] and [obtainCacheStatus] to ensure 195 | /// that errors thrown during key creation are handled whether synchronous or 196 | /// asynchronous. 197 | void _createErrorHandlerAndKey( 198 | ImageConfiguration configuration, 199 | _KeyAndErrorHandlerCallback successCallback, 200 | _AsyncKeyErrorHandler errorCallback, 201 | ) { 202 | T? obtainedKey; 203 | bool didError = false; 204 | Future handleError(Object exception, StackTrace? stack) async { 205 | if (didError) { 206 | return; 207 | } 208 | if (!didError) { 209 | errorCallback(obtainedKey, exception, stack); 210 | } 211 | didError = true; 212 | } 213 | 214 | Future key; 215 | try { 216 | key = obtainKey(configuration); 217 | } catch (error, stackTrace) { 218 | handleError(error, stackTrace); 219 | return; 220 | } 221 | key 222 | .then((T key) { 223 | obtainedKey = key; 224 | try { 225 | successCallback(key, handleError); 226 | } catch (error, stackTrace) { 227 | handleError(error, stackTrace); 228 | } 229 | }) 230 | .catchError(handleError); 231 | } 232 | 233 | /// obtain new key base on old key 234 | Future obtainNewKey( 235 | S Function(T value) createNewKey, 236 | Future Function() obtainKey, 237 | ) { 238 | Completer? completer; 239 | Future? result; 240 | 241 | obtainKey().then((T value) { 242 | final S key = createNewKey(value); 243 | if (completer != null) { 244 | // We already returned from this function, which means we are in the 245 | // asynchronous mode. Pass the value to the completer. The completer's 246 | // future is what we returned. 247 | completer.complete(key); 248 | } else { 249 | // We haven't yet returned, so we must have been called synchronously 250 | // just after loadStructuredData returned (which means it provided us 251 | // with a SynchronousFuture). Let's return a SynchronousFuture 252 | // ourselves. 253 | result = SynchronousFuture(key); 254 | } 255 | }); 256 | if (result != null) { 257 | return result!; 258 | } 259 | 260 | completer = Completer(); 261 | return completer.future; 262 | } 263 | } 264 | 265 | /// Signature for the callback taken by [_createErrorHandlerAndKey]. 266 | typedef _KeyAndErrorHandlerCallback = 267 | void Function(T key, ImageErrorListener handleError); 268 | 269 | /// Signature used for error handling by [_createErrorHandlerAndKey]. 270 | typedef _AsyncKeyErrorHandler = 271 | Future Function(T key, Object exception, StackTrace? stack); 272 | -------------------------------------------------------------------------------- /lib/src/extended_memory_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:ui' as ui show Codec; 3 | import 'package:extended_image_library/src/extended_image_provider.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | class ExtendedMemoryImageProvider extends MemoryImage 7 | with ExtendedImageProvider { 8 | const ExtendedMemoryImageProvider( 9 | Uint8List bytes, { 10 | double scale = 1.0, 11 | this.cacheRawData = false, 12 | this.imageCacheName, 13 | }) : super(bytes, scale: scale); 14 | 15 | /// Whether cache raw data if you need to get raw data directly. 16 | /// For example, we need raw image data to edit, 17 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 18 | /// data here. 19 | @override 20 | final bool cacheRawData; 21 | 22 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 23 | @override 24 | final String? imageCacheName; 25 | 26 | @override 27 | Uint8List get rawImageData => bytes; 28 | 29 | @override 30 | ImageStreamCompleter loadImage(MemoryImage key, ImageDecoderCallback decode) { 31 | return MultiFrameImageStreamCompleter( 32 | codec: _loadAsync(key, decode), 33 | scale: key.scale, 34 | ); 35 | } 36 | 37 | Future _loadAsync(MemoryImage key, ImageDecoderCallback decode) { 38 | assert(key == this); 39 | return instantiateImageCodec(bytes, decode); 40 | } 41 | 42 | @override 43 | bool operator ==(Object other) { 44 | if (other.runtimeType != runtimeType) { 45 | return false; 46 | } 47 | return other is ExtendedMemoryImageProvider && 48 | other.bytes == bytes && 49 | other.scale == scale && 50 | cacheRawData == other.cacheRawData && 51 | imageCacheName == other.imageCacheName; 52 | } 53 | 54 | @override 55 | int get hashCode => 56 | Object.hash(bytes.hashCode, scale, cacheRawData, imageCacheName); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/extended_resize_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | import 'dart:ui'; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'extended_image_provider.dart'; 8 | 9 | /// Instructs Flutter to decode the image at the specified dimensions 10 | /// instead of at its native size. 11 | /// 12 | /// This allows finer control of the size of the image in [ImageCache] and is 13 | /// generally used to reduce the memory footprint of [ImageCache]. 14 | /// 15 | /// The decoded image may still be displayed at sizes other than the 16 | /// cached size provided here. 17 | class ExtendedResizeImage extends ImageProvider<_SizeAwareCacheKey> 18 | with ExtendedImageProvider<_SizeAwareCacheKey> { 19 | const ExtendedResizeImage( 20 | this.imageProvider, { 21 | this.compressionRatio, 22 | this.maxBytes = 50 << 10, 23 | this.width, 24 | this.height, 25 | this.allowUpscaling = false, 26 | this.cacheRawData = false, 27 | this.imageCacheName, 28 | this.policy = ResizeImagePolicy.exact, 29 | }) : assert( 30 | (compressionRatio != null && 31 | compressionRatio > 0 && 32 | compressionRatio < 1 && 33 | !kIsWeb) || 34 | (maxBytes != null && maxBytes > 0 && !kIsWeb) || 35 | width != null || 36 | height != null, 37 | ); 38 | 39 | /// The [ImageProvider] that this class wraps. 40 | final ImageProvider imageProvider; 41 | 42 | /// [ExtendedResizeImage] will compress the image to a size 43 | /// that is smaller than [maxBytes]. The default size is 50KB. 44 | /// It's actual bytes of Image, not decode bytes 45 | /// it's not supported on web 46 | final int? maxBytes; 47 | 48 | /// The image`s size will resize to original * [compressionRatio]. 49 | /// It's ExtendedResizeImage`s first pick. 50 | /// The compressionRatio`s range is from 0.0 (exclusive), to 51 | /// 1.0 (exclusive). 52 | /// it's not supported on web 53 | final double? compressionRatio; 54 | 55 | /// The width the image should decode to and cache. 56 | final int? width; 57 | 58 | /// The height the image should decode to and cache. 59 | final int? height; 60 | 61 | /// The policy that determines how [width] and [height] are interpreted. 62 | /// 63 | /// Defaults to [ResizeImagePolicy.exact]. 64 | final ResizeImagePolicy policy; 65 | 66 | /// Whether the [width] and [height] parameters should be clamped to the 67 | /// intrinsic width and height of the image. 68 | /// 69 | /// In general, it is better for memory usage to avoid scaling the image 70 | /// beyond its intrinsic dimensions when decoding it. If there is a need to 71 | /// scale an image larger, it is better to apply a scale to the canvas, or 72 | /// to use an appropriate [Image.fit]. 73 | final bool allowUpscaling; 74 | 75 | /// Whether cache raw data if you need to get raw data directly. 76 | /// For example, we need raw image data to edit, 77 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 78 | /// data here. 79 | @override 80 | final bool cacheRawData; 81 | 82 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 83 | @override 84 | final String? imageCacheName; 85 | 86 | /// Composes the `provider` in a [ResizeImage] only when `cacheWidth` and 87 | /// `cacheHeight` are not both null. 88 | /// 89 | /// When `cacheWidth` and `cacheHeight` are both null, this will return the 90 | /// `provider` directly. 91 | /// 92 | /// Extended with `scaling` and `maxBytes`. 93 | static ImageProvider resizeIfNeeded({ 94 | required ImageProvider provider, 95 | int? cacheWidth, 96 | int? cacheHeight, 97 | double? compressionRatio, 98 | int? maxBytes, 99 | bool cacheRawData = false, 100 | String? imageCacheName, 101 | }) { 102 | if ((compressionRatio != null && 103 | compressionRatio > 0 && 104 | compressionRatio < 1) || 105 | (maxBytes != null && maxBytes > 0) || 106 | cacheWidth != null || 107 | cacheHeight != null) { 108 | return ExtendedResizeImage( 109 | provider, 110 | width: cacheWidth, 111 | height: cacheHeight, 112 | maxBytes: maxBytes, 113 | compressionRatio: compressionRatio, 114 | cacheRawData: cacheRawData, 115 | imageCacheName: imageCacheName, 116 | ); 117 | } 118 | return provider; 119 | } 120 | 121 | @override 122 | ImageStreamCompleter loadImage( 123 | _SizeAwareCacheKey key, 124 | ImageDecoderCallback decode, 125 | ) { 126 | Future decodeResize( 127 | ImmutableBuffer buffer, { 128 | TargetImageSizeCallback? getTargetSize, 129 | }) { 130 | assert( 131 | getTargetSize == null, 132 | 'ResizeImage cannot be composed with another ImageProvider that applies ' 133 | 'cacheWidth, cacheHeight, or allowUpscaling.', 134 | ); 135 | 136 | return _instantiateImageCodec( 137 | buffer, 138 | decode, 139 | compressionRatio: compressionRatio, 140 | maxBytes: maxBytes, 141 | targetWidth: width, 142 | targetHeight: height, 143 | ); 144 | } 145 | 146 | final ImageStreamCompleter completer = imageProvider.loadImage( 147 | key._providerCacheKey, 148 | decodeResize, 149 | ); 150 | if (!kReleaseMode) { 151 | completer.debugLabel = 152 | '${completer.debugLabel} - Resized(${key._width}×${key._height})'; 153 | } 154 | 155 | return completer; 156 | } 157 | 158 | @override 159 | Future<_SizeAwareCacheKey> obtainKey(ImageConfiguration configuration) { 160 | Completer<_SizeAwareCacheKey>? completer; 161 | // If the imageProvider.obtainKey future is synchronous, then we will be able to fill in result with 162 | // a value before completer is initialized below. 163 | SynchronousFuture<_SizeAwareCacheKey>? result; 164 | imageProvider.obtainKey(configuration).then((Object key) { 165 | if (completer == null) { 166 | // This future has completed synchronously (completer was never assigned), 167 | // so we can directly create the synchronous result to return. 168 | result = SynchronousFuture<_SizeAwareCacheKey>( 169 | _SizeAwareCacheKey( 170 | key, 171 | compressionRatio, 172 | maxBytes, 173 | width, 174 | height, 175 | cacheRawData, 176 | imageCacheName, 177 | ), 178 | ); 179 | } else { 180 | // This future did not synchronously complete. 181 | completer.complete( 182 | _SizeAwareCacheKey( 183 | key, 184 | compressionRatio, 185 | maxBytes, 186 | width, 187 | height, 188 | cacheRawData, 189 | imageCacheName, 190 | ), 191 | ); 192 | } 193 | }); 194 | if (result != null) { 195 | return result!; 196 | } 197 | // If the code reaches here, it means the imageProvider.obtainKey was not 198 | // completed sync, so we initialize the completer for completion later. 199 | completer = Completer<_SizeAwareCacheKey>(); 200 | return completer.future; 201 | } 202 | 203 | Future _instantiateImageCodec( 204 | ImmutableBuffer buffer, 205 | ImageDecoderCallback decode, { 206 | double? compressionRatio, 207 | int? maxBytes, 208 | int? targetWidth, 209 | int? targetHeight, 210 | }) async { 211 | if (!kIsWeb && 212 | (compressionRatio != null || 213 | (maxBytes != null && maxBytes < buffer.length))) { 214 | final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer); 215 | final int totalBytes = 216 | descriptor.width * descriptor.height * descriptor.bytesPerPixel; 217 | if (compressionRatio != null) { 218 | final _IntSize size = _resize( 219 | descriptor.width, 220 | descriptor.height, 221 | (totalBytes * compressionRatio).toInt(), 222 | descriptor.bytesPerPixel, 223 | ); 224 | targetWidth = size.width; 225 | targetHeight = size.height; 226 | } else if (maxBytes != null && maxBytes < buffer.length) { 227 | final _IntSize size = _resize( 228 | descriptor.width, 229 | descriptor.height, 230 | totalBytes * maxBytes ~/ buffer.length, 231 | descriptor.bytesPerPixel, 232 | ); 233 | targetWidth = size.width; 234 | targetHeight = size.height; 235 | } 236 | 237 | return descriptor.instantiateCodec( 238 | targetWidth: targetWidth, 239 | targetHeight: targetHeight, 240 | ); 241 | } else { 242 | return decode( 243 | buffer, 244 | getTargetSize: (int intrinsicWidth, int intrinsicHeight) { 245 | switch (policy) { 246 | case ResizeImagePolicy.exact: 247 | int? targetWidth = width; 248 | int? targetHeight = height; 249 | 250 | if (!allowUpscaling) { 251 | if (targetWidth != null && targetWidth > intrinsicWidth) { 252 | targetWidth = intrinsicWidth; 253 | } 254 | if (targetHeight != null && targetHeight > intrinsicHeight) { 255 | targetHeight = intrinsicHeight; 256 | } 257 | } 258 | 259 | return TargetImageSize(width: targetWidth, height: targetHeight); 260 | case ResizeImagePolicy.fit: 261 | final double aspectRatio = intrinsicWidth / intrinsicHeight; 262 | final int maxWidth = width ?? intrinsicWidth; 263 | final int maxHeight = height ?? intrinsicHeight; 264 | int targetWidth = intrinsicWidth; 265 | int targetHeight = intrinsicHeight; 266 | 267 | if (targetWidth > maxWidth) { 268 | targetWidth = maxWidth; 269 | targetHeight = targetWidth ~/ aspectRatio; 270 | } 271 | 272 | if (targetHeight > maxHeight) { 273 | targetHeight = maxHeight; 274 | targetWidth = (targetHeight * aspectRatio).floor(); 275 | } 276 | 277 | if (allowUpscaling) { 278 | if (width == null) { 279 | assert(height != null); 280 | targetHeight = height!; 281 | targetWidth = (targetHeight * aspectRatio).floor(); 282 | } else if (height == null) { 283 | targetWidth = width!; 284 | targetHeight = targetWidth ~/ aspectRatio; 285 | } else { 286 | final int derivedMaxWidth = (maxHeight * aspectRatio).floor(); 287 | final int derivedMaxHeight = maxWidth ~/ aspectRatio; 288 | targetWidth = min(maxWidth, derivedMaxWidth); 289 | targetHeight = min(maxHeight, derivedMaxHeight); 290 | } 291 | } 292 | 293 | return TargetImageSize(width: targetWidth, height: targetHeight); 294 | } 295 | }, 296 | ); 297 | } 298 | } 299 | 300 | /// Calculate fittest size. 301 | /// [width] The image's original width. 302 | /// [height] The image's original height. 303 | /// [maxBytes] The size that image will resize to. 304 | /// 305 | _IntSize _resize(int width, int height, int maxBytes, int bytesPerPixel) { 306 | final double ratio = width / height; 307 | final int maxSize_1_4 = maxBytes ~/ bytesPerPixel; 308 | final int targetHeight = sqrt(maxSize_1_4 / ratio).floor(); 309 | final int targetWidth = (ratio * targetHeight).floor(); 310 | return _IntSize(targetWidth, targetHeight); 311 | } 312 | 313 | @override 314 | bool operator ==(Object other) { 315 | if (other.runtimeType != runtimeType) { 316 | return false; 317 | } 318 | return other is ExtendedResizeImage && 319 | imageProvider == other.imageProvider && 320 | compressionRatio == other.compressionRatio && 321 | maxBytes == other.maxBytes && 322 | width == other.width && 323 | height == other.height && 324 | allowUpscaling == other.allowUpscaling && 325 | cacheRawData == other.cacheRawData && 326 | imageCacheName == other.imageCacheName; 327 | } 328 | 329 | @override 330 | int get hashCode => Object.hash( 331 | imageProvider, 332 | compressionRatio, 333 | maxBytes, 334 | width, 335 | height, 336 | allowUpscaling, 337 | cacheRawData, 338 | imageCacheName, 339 | ); 340 | } 341 | 342 | @immutable 343 | class _IntSize { 344 | const _IntSize(this.width, this.height); 345 | 346 | final int width; 347 | final int height; 348 | } 349 | 350 | @immutable 351 | class _SizeAwareCacheKey { 352 | const _SizeAwareCacheKey( 353 | this._providerCacheKey, 354 | this.compressionRatio, 355 | this.maxBytes, 356 | this._width, 357 | this._height, 358 | this.cacheRawData, 359 | this.imageCacheName, 360 | ); 361 | 362 | final Object _providerCacheKey; 363 | 364 | final int? maxBytes; 365 | 366 | final double? compressionRatio; 367 | 368 | final int? _width; 369 | 370 | final int? _height; 371 | 372 | /// Whether cache raw data if you need to get raw data directly. 373 | /// For example, we need raw image data to edit, 374 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 375 | /// data here. 376 | final bool cacheRawData; 377 | 378 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 379 | 380 | final String? imageCacheName; 381 | 382 | @override 383 | bool operator ==(Object other) { 384 | if (other.runtimeType != runtimeType) { 385 | return false; 386 | } 387 | return other is _SizeAwareCacheKey && 388 | other._providerCacheKey == _providerCacheKey && 389 | other.maxBytes == maxBytes && 390 | other.compressionRatio == compressionRatio && 391 | other._width == _width && 392 | other._height == _height && 393 | cacheRawData == other.cacheRawData && 394 | imageCacheName == other.imageCacheName; 395 | } 396 | 397 | @override 398 | int get hashCode => Object.hash( 399 | _providerCacheKey, 400 | maxBytes, 401 | compressionRatio, 402 | _width, 403 | _height, 404 | cacheRawData, 405 | imageCacheName, 406 | ); 407 | } 408 | -------------------------------------------------------------------------------- /lib/src/network/extended_network_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | import 'package:extended_image_library/src/extended_image_provider.dart'; 4 | import 'package:flutter/painting.dart'; 5 | import 'package:http_client_helper/http_client_helper.dart'; 6 | import 'network_image_io.dart' 7 | if (dart.library.js_interop) 'network_image_web.dart' 8 | as network_image; 9 | 10 | /// [NetworkImage] 11 | abstract class ExtendedNetworkImageProvider 12 | extends ImageProvider 13 | with ExtendedImageProvider { 14 | /// Creates an object that fetches the image at the given URL. 15 | /// 16 | /// The arguments [url] and [scale] must not be null. 17 | factory ExtendedNetworkImageProvider( 18 | String url, { 19 | double scale, 20 | Map? headers, 21 | bool cache, 22 | int retries, 23 | Duration? timeLimit, 24 | Duration timeRetry, 25 | CancellationToken? cancelToken, 26 | String? cacheKey, 27 | bool printError, 28 | bool cacheRawData, 29 | String? imageCacheName, 30 | Duration? cacheMaxAge, 31 | WebHtmlElementStrategy webHtmlElementStrategy, 32 | }) = network_image.ExtendedNetworkImageProvider; 33 | 34 | /// Time Limit to request image 35 | Duration? get timeLimit; 36 | 37 | /// The time to retry to request 38 | int get retries; 39 | 40 | /// The time duration to retry to request 41 | Duration get timeRetry; 42 | 43 | /// Whether cache image to local 44 | bool get cache; 45 | 46 | /// The URL from which the image will be fetched. 47 | String get url; 48 | 49 | /// The scale to place in the [ImageInfo] object of the image. 50 | double get scale; 51 | 52 | /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network. 53 | Map? get headers; 54 | 55 | /// Token to cancel network request 56 | CancellationToken? get cancelToken; 57 | 58 | /// Custom cache key 59 | String? get cacheKey; 60 | 61 | /// print error 62 | bool get printError; 63 | 64 | /// The max duration to cahce image. 65 | /// After this time the cache is expired and the image is reloaded. 66 | Duration? get cacheMaxAge; 67 | 68 | /// On the Web platform, specifies when the image is loaded as a 69 | /// [WebImageInfo], which causes [Image.network] to display the image in an 70 | /// HTML element in a platform view. 71 | /// 72 | /// See [Image.network] for more explanation. 73 | /// 74 | /// Defaults to [WebHtmlElementStrategy.never]. 75 | /// 76 | /// Has no effect on other platforms, which always fetch bytes. 77 | WebHtmlElementStrategy get webHtmlElementStrategy; 78 | 79 | ///get network image data from cached 80 | Future getNetworkImageData({ 81 | StreamController? chunkEvents, 82 | }); 83 | 84 | ///HttpClient for network, it's null on web 85 | static dynamic get httpClient => 86 | network_image.ExtendedNetworkImageProvider.httpClient; 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/network/network_image_io.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:ui' as ui show Codec; 4 | import 'package:extended_image_library/src/extended_image_provider.dart'; 5 | import 'package:extended_image_library/src/platform.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | import 'package:http_client_helper/http_client_helper.dart'; 9 | import 'package:path/path.dart'; 10 | import 'package:path_provider/path_provider.dart'; 11 | 12 | import 'extended_network_image_provider.dart' as image_provider; 13 | 14 | class ExtendedNetworkImageProvider 15 | extends ImageProvider 16 | with ExtendedImageProvider 17 | implements image_provider.ExtendedNetworkImageProvider { 18 | /// Creates an object that fetches the image at the given URL. 19 | /// 20 | /// The arguments must not be null. 21 | ExtendedNetworkImageProvider( 22 | this.url, { 23 | this.scale = 1.0, 24 | this.headers, 25 | this.cache = false, 26 | this.retries = 3, 27 | this.timeLimit, 28 | this.timeRetry = const Duration(milliseconds: 100), 29 | this.cacheKey, 30 | this.printError = true, 31 | this.cacheRawData = false, 32 | this.cancelToken, 33 | this.imageCacheName, 34 | this.cacheMaxAge, 35 | this.webHtmlElementStrategy = WebHtmlElementStrategy.never, 36 | }); 37 | 38 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 39 | @override 40 | final String? imageCacheName; 41 | 42 | /// Whether cache raw data if you need to get raw data directly. 43 | /// For example, we need raw image data to edit, 44 | /// but [ui.Image.toByteData()] is very slow. So we cache the image 45 | /// data here. 46 | @override 47 | final bool cacheRawData; 48 | 49 | /// The time limit to request image 50 | @override 51 | final Duration? timeLimit; 52 | 53 | /// The time to retry to request 54 | @override 55 | final int retries; 56 | 57 | /// The time duration to retry to request 58 | @override 59 | final Duration timeRetry; 60 | 61 | /// Whether cache image to local 62 | @override 63 | final bool cache; 64 | 65 | /// The URL from which the image will be fetched. 66 | @override 67 | final String url; 68 | 69 | /// The scale to place in the [ImageInfo] object of the image. 70 | @override 71 | final double scale; 72 | 73 | /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network. 74 | @override 75 | final Map? headers; 76 | 77 | /// The token to cancel network request 78 | @override 79 | final CancellationToken? cancelToken; 80 | 81 | /// Custom cache key 82 | @override 83 | final String? cacheKey; 84 | 85 | /// print error 86 | @override 87 | final bool printError; 88 | 89 | /// The max duration to cahce image. 90 | /// After this time the cache is expired and the image is reloaded. 91 | @override 92 | final Duration? cacheMaxAge; 93 | 94 | @override 95 | final WebHtmlElementStrategy webHtmlElementStrategy; 96 | 97 | @override 98 | ImageStreamCompleter loadImage( 99 | image_provider.ExtendedNetworkImageProvider key, 100 | ImageDecoderCallback decode, 101 | ) { 102 | // Ownership of this controller is handed off to [_loadAsync]; it is that 103 | // method's responsibility to close the controller's stream when the image 104 | // has been loaded or an error is thrown. 105 | final StreamController chunkEvents = 106 | StreamController(); 107 | 108 | return MultiFrameImageStreamCompleter( 109 | codec: _loadAsync( 110 | key as ExtendedNetworkImageProvider, 111 | chunkEvents, 112 | decode, 113 | ), 114 | scale: key.scale, 115 | chunkEvents: chunkEvents.stream, 116 | debugLabel: key.url, 117 | informationCollector: () { 118 | return [ 119 | DiagnosticsProperty('Image provider', this), 120 | DiagnosticsProperty( 121 | 'Image key', 122 | key, 123 | ), 124 | ]; 125 | }, 126 | ); 127 | } 128 | 129 | @override 130 | Future obtainKey( 131 | ImageConfiguration configuration, 132 | ) { 133 | return SynchronousFuture(this); 134 | } 135 | 136 | Future _loadAsync( 137 | ExtendedNetworkImageProvider key, 138 | StreamController chunkEvents, 139 | ImageDecoderCallback decode, 140 | ) async { 141 | assert(key == this); 142 | final String md5Key = cacheKey ?? keyToMd5(key.url); 143 | ui.Codec? result; 144 | if (cache) { 145 | try { 146 | final Uint8List? data = await _loadCache(key, chunkEvents, md5Key); 147 | if (data != null) { 148 | result = await instantiateImageCodec(data, decode); 149 | } 150 | } catch (e) { 151 | if (printError) { 152 | print(e); 153 | } 154 | } 155 | } 156 | 157 | if (result == null) { 158 | try { 159 | final Uint8List? data = await _loadNetwork(key, chunkEvents); 160 | if (data != null) { 161 | result = await instantiateImageCodec(data, decode); 162 | } 163 | } catch (e) { 164 | if (printError) { 165 | print(e); 166 | } 167 | } 168 | } 169 | 170 | //Failed to load 171 | if (result == null) { 172 | //result = await ui.instantiateImageCodec(kTransparentImage); 173 | return Future.error(StateError('Failed to load $url.')); 174 | } 175 | 176 | return result; 177 | } 178 | 179 | /// Get the image from cache folder. 180 | Future _loadCache( 181 | ExtendedNetworkImageProvider key, 182 | StreamController? chunkEvents, 183 | String md5Key, 184 | ) async { 185 | final Directory _cacheImagesDirectory = Directory( 186 | join((await getTemporaryDirectory()).path, cacheImageFolderName), 187 | ); 188 | Uint8List? data; 189 | // exist, try to find cache image file 190 | if (_cacheImagesDirectory.existsSync()) { 191 | final File cacheFlie = File(join(_cacheImagesDirectory.path, md5Key)); 192 | if (cacheFlie.existsSync()) { 193 | if (key.cacheMaxAge != null) { 194 | final DateTime now = DateTime.now(); 195 | final FileStat fs = cacheFlie.statSync(); 196 | if (now.subtract(key.cacheMaxAge!).isAfter(fs.changed)) { 197 | cacheFlie.deleteSync(recursive: true); 198 | } else { 199 | data = await cacheFlie.readAsBytes(); 200 | } 201 | } else { 202 | data = await cacheFlie.readAsBytes(); 203 | } 204 | } 205 | } 206 | // create folder 207 | else { 208 | await _cacheImagesDirectory.create(); 209 | } 210 | // load from network 211 | if (data == null) { 212 | data = await _loadNetwork(key, chunkEvents); 213 | if (data != null) { 214 | // cache image file 215 | await File(join(_cacheImagesDirectory.path, md5Key)).writeAsBytes(data); 216 | } 217 | } 218 | 219 | return data; 220 | } 221 | 222 | /// Get the image from network. 223 | Future _loadNetwork( 224 | ExtendedNetworkImageProvider key, 225 | StreamController? chunkEvents, 226 | ) async { 227 | try { 228 | final Uri resolved = Uri.base.resolve(key.url); 229 | final HttpClientResponse? response = await _tryGetResponse(resolved); 230 | if (response == null || response.statusCode != HttpStatus.ok) { 231 | if (response != null) { 232 | // The network may be only temporarily unavailable, or the file will be 233 | // added on the server later. Avoid having future calls to resolve 234 | // fail to check the network again. 235 | await response.drain>([]); 236 | } 237 | return null; 238 | } 239 | 240 | final Uint8List bytes = await consolidateHttpClientResponseBytes( 241 | response, 242 | onBytesReceived: 243 | chunkEvents != null 244 | ? (int cumulative, int? total) { 245 | chunkEvents.add( 246 | ImageChunkEvent( 247 | cumulativeBytesLoaded: cumulative, 248 | expectedTotalBytes: total, 249 | ), 250 | ); 251 | } 252 | : null, 253 | ); 254 | if (bytes.lengthInBytes == 0) { 255 | return Future.error( 256 | StateError('NetworkImage is an empty file: $resolved'), 257 | ); 258 | } 259 | 260 | return bytes; 261 | } on OperationCanceledError catch (_) { 262 | if (printError) { 263 | print('User cancel request $url.'); 264 | } 265 | return Future.error(StateError('User cancel request $url.')); 266 | } catch (e) { 267 | if (printError) { 268 | print(e); 269 | } 270 | // [ExtendedImage.clearMemoryCacheIfFailed] can clear cache 271 | // Depending on where the exception was thrown, the image cache may not 272 | // have had a chance to track the key in the cache at all. 273 | // Schedule a microtask to give the cache a chance to add the key. 274 | // scheduleMicrotask(() { 275 | // PaintingBinding.instance.imageCache.evict(key); 276 | // }); 277 | // rethrow; 278 | } finally { 279 | await chunkEvents?.close(); 280 | } 281 | return null; 282 | } 283 | 284 | Future _getResponse(Uri resolved) async { 285 | final HttpClientRequest request = await httpClient.getUrl(resolved); 286 | headers?.forEach((String name, String value) { 287 | request.headers.add(name, value); 288 | }); 289 | final HttpClientResponse response = await request.close(); 290 | if (timeLimit != null) { 291 | response.timeout(timeLimit!); 292 | } 293 | return response; 294 | } 295 | 296 | // Http get with cancel, delay try again 297 | Future _tryGetResponse(Uri resolved) async { 298 | cancelToken?.throwIfCancellationRequested(); 299 | return await RetryHelper.tryRun( 300 | () { 301 | return CancellationTokenSource.register( 302 | cancelToken, 303 | _getResponse(resolved), 304 | ); 305 | }, 306 | cancelToken: cancelToken, 307 | timeRetry: timeRetry, 308 | retries: retries, 309 | ); 310 | } 311 | 312 | @override 313 | bool operator ==(Object other) { 314 | if (other.runtimeType != runtimeType) { 315 | return false; 316 | } 317 | return other is ExtendedNetworkImageProvider && 318 | url == other.url && 319 | scale == other.scale && 320 | cacheRawData == other.cacheRawData && 321 | timeLimit == other.timeLimit && 322 | cancelToken == other.cancelToken && 323 | timeRetry == other.timeRetry && 324 | cache == other.cache && 325 | cacheKey == other.cacheKey && 326 | //headers == other.headers && 327 | retries == other.retries && 328 | imageCacheName == other.imageCacheName && 329 | cacheMaxAge == other.cacheMaxAge; 330 | } 331 | 332 | @override 333 | int get hashCode => Object.hash( 334 | url, 335 | scale, 336 | cacheRawData, 337 | timeLimit, 338 | cancelToken, 339 | timeRetry, 340 | cache, 341 | cacheKey, 342 | //headers, 343 | retries, 344 | imageCacheName, 345 | cacheMaxAge, 346 | ); 347 | 348 | @override 349 | String toString() => '$runtimeType("$url", scale: $scale)'; 350 | 351 | @override 352 | /// Get network image data from cached 353 | Future getNetworkImageData({ 354 | StreamController? chunkEvents, 355 | }) async { 356 | final String uId = cacheKey ?? keyToMd5(url); 357 | 358 | if (cache) { 359 | return await _loadCache(this, chunkEvents, uId); 360 | } 361 | 362 | return await _loadNetwork(this, chunkEvents); 363 | } 364 | 365 | // Do not access this field directly; use [_httpClient] instead. 366 | // We set `autoUncompress` to false to ensure that we can trust the value of 367 | // the `Content-Length` HTTP header. We automatically uncompress the content 368 | // in our call to [consolidateHttpClientResponseBytes]. 369 | static final HttpClient _sharedHttpClient = 370 | HttpClient()..autoUncompress = false; 371 | 372 | static HttpClient get httpClient { 373 | HttpClient client = _sharedHttpClient; 374 | assert(() { 375 | if (debugNetworkImageHttpClientProvider != null) { 376 | client = debugNetworkImageHttpClientProvider!(); 377 | } 378 | return true; 379 | }()); 380 | return client; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /lib/src/network/network_image_web.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // ignore_for_file: implementation_imports 6 | 7 | import 'dart:async'; 8 | import 'dart:js_interop'; 9 | import 'dart:ui' as ui; 10 | import 'dart:ui'; 11 | import 'dart:ui_web' as ui_web; 12 | 13 | import 'package:extended_image_library/src/extended_image_provider.dart'; 14 | import 'package:flutter/foundation.dart'; 15 | import 'package:flutter/rendering.dart'; 16 | import 'package:http_client_helper/http_client_helper.dart'; 17 | import 'package:web/web.dart' as web; 18 | import 'extended_network_image_provider.dart' as extended_image_provider; 19 | // ignore: directives_ordering 20 | import 'package:flutter/src/painting/image_provider.dart' as image_provider; 21 | 22 | /// Creates a type for an overridable factory function for testing purposes. 23 | typedef HttpRequestFactory = web.XMLHttpRequest Function(); 24 | 25 | /// The type for an overridable factory function for creating HTML elements to 26 | /// display images, used for testing purposes. 27 | typedef HtmlElementFactory = web.HTMLImageElement Function(); 28 | 29 | // Method signature for _loadAsync decode callbacks. 30 | typedef _SimpleDecoderCallback = 31 | Future Function(ui.ImmutableBuffer buffer); 32 | 33 | /// The default HTTP client. 34 | web.XMLHttpRequest _httpClient() { 35 | return web.XMLHttpRequest(); 36 | } 37 | 38 | /// Creates an overridable factory function. 39 | @visibleForTesting 40 | HttpRequestFactory httpRequestFactory = _httpClient; 41 | 42 | /// Restores the default HTTP request factory. 43 | @visibleForTesting 44 | void debugRestoreHttpRequestFactory() { 45 | httpRequestFactory = _httpClient; 46 | } 47 | 48 | /// The default HTML element factory. 49 | web.HTMLImageElement _imgElementFactory() { 50 | return web.document.createElement('img') as web.HTMLImageElement; 51 | } 52 | 53 | /// The factory function that creates HTML elements, can be overridden for 54 | /// tests. 55 | @visibleForTesting 56 | HtmlElementFactory imgElementFactory = _imgElementFactory; 57 | 58 | /// Restores the default HTML element factory. 59 | @visibleForTesting 60 | void debugRestoreImgElementFactory() { 61 | imgElementFactory = _imgElementFactory; 62 | } 63 | 64 | /// The web implementation of [image_provider.NetworkImage]. 65 | /// 66 | /// NetworkImage on the web does not support decoding to a specified size. 67 | @immutable 68 | class ExtendedNetworkImageProvider 69 | extends ImageProvider 70 | with 71 | ExtendedImageProvider< 72 | extended_image_provider.ExtendedNetworkImageProvider 73 | > 74 | implements extended_image_provider.ExtendedNetworkImageProvider { 75 | /// Creates an object that fetches the image at the given URL. 76 | /// 77 | /// The arguments [url] and [scale] must not be null. 78 | ExtendedNetworkImageProvider( 79 | this.url, { 80 | this.scale = 1.0, 81 | this.headers, 82 | this.cache = false, 83 | this.retries = 3, 84 | this.timeLimit, 85 | this.timeRetry = const Duration(milliseconds: 100), 86 | this.cancelToken, 87 | this.cacheKey, 88 | this.printError = true, 89 | this.cacheRawData = false, 90 | this.imageCacheName, 91 | this.cacheMaxAge, 92 | this.webHtmlElementStrategy = WebHtmlElementStrategy.never, 93 | }); 94 | 95 | @override 96 | final String url; 97 | 98 | @override 99 | final double scale; 100 | 101 | @override 102 | final Map? headers; 103 | 104 | @override 105 | final bool cache; 106 | 107 | @override 108 | final CancellationToken? cancelToken; 109 | 110 | @override 111 | final int retries; 112 | 113 | @override 114 | final Duration? timeLimit; 115 | 116 | @override 117 | final Duration timeRetry; 118 | 119 | @override 120 | final String? cacheKey; 121 | 122 | /// print error 123 | @override 124 | final bool printError; 125 | 126 | @override 127 | final bool cacheRawData; 128 | 129 | /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. 130 | @override 131 | final String? imageCacheName; 132 | 133 | /// The duration before local cache is expired. 134 | /// After this time the cache is expired and the image is reloaded. 135 | @override 136 | final Duration? cacheMaxAge; 137 | 138 | @override 139 | final WebHtmlElementStrategy webHtmlElementStrategy; 140 | 141 | @override 142 | Future obtainKey( 143 | ImageConfiguration configuration, 144 | ) { 145 | return SynchronousFuture(this); 146 | } 147 | 148 | @override 149 | ImageStreamCompleter loadImage( 150 | extended_image_provider.ExtendedNetworkImageProvider key, 151 | image_provider.ImageDecoderCallback decode, 152 | ) { 153 | // Ownership of this controller is handed off to [_loadAsync]; it is that 154 | // method's responsibility to close the controller's stream when the image 155 | // has been loaded or an error is thrown. 156 | final StreamController chunkEvents = 157 | StreamController(); 158 | 159 | return _ForwardingImageStreamCompleter( 160 | _loadAsync(key, decode, chunkEvents), 161 | informationCollector: _imageStreamInformationCollector(key), 162 | debugLabel: key.url, 163 | ); 164 | } 165 | 166 | InformationCollector? _imageStreamInformationCollector( 167 | extended_image_provider.ExtendedNetworkImageProvider key, 168 | ) { 169 | InformationCollector? collector; 170 | assert(() { 171 | collector = 172 | () => [ 173 | DiagnosticsProperty( 174 | 'Image provider', 175 | this, 176 | ), 177 | DiagnosticsProperty< 178 | extended_image_provider.ExtendedNetworkImageProvider 179 | >('Image key', key), 180 | ]; 181 | return true; 182 | }()); 183 | return collector; 184 | } 185 | 186 | // Html renderer does not support decoding network images to a specified size. The decode parameter 187 | // here is ignored and `ui_web.createImageCodecFromUrl` will be used directly 188 | // in place of the typical `instantiateImageCodec` method. 189 | Future _loadAsync( 190 | extended_image_provider.ExtendedNetworkImageProvider key, 191 | _SimpleDecoderCallback decode, 192 | StreamController chunkEvents, 193 | ) async { 194 | assert(key == this); 195 | 196 | Future loadViaDecode() async { 197 | // Resolve the Codec before passing it to 198 | // [MultiFrameImageStreamCompleter] so any errors aren't reported 199 | // twice (once from the MultiFrameImageStreamCompleter and again 200 | // from the wrapping [ForwardingImageStreamCompleter]). 201 | final ui.Codec codec = await _fetchImageBytes(decode); 202 | return MultiFrameImageStreamCompleter( 203 | chunkEvents: chunkEvents.stream, 204 | codec: Future.value(codec), 205 | scale: key.scale, 206 | debugLabel: key.url, 207 | informationCollector: _imageStreamInformationCollector(key), 208 | ); 209 | } 210 | 211 | Future loadViaImgElement() async { 212 | // If we failed to fetch the bytes, try to load the image in an 213 | // element instead. 214 | final web.HTMLImageElement imageElement = imgElementFactory(); 215 | imageElement.src = key.url; 216 | // Decode the element before creating the ImageStreamCompleter 217 | // to avoid double reporting the error. 218 | await imageElement.decode().toDart; 219 | return OneFrameImageStreamCompleter( 220 | Future.value( 221 | WebImageInfo(imageElement, debugLabel: key.url), 222 | ), 223 | informationCollector: _imageStreamInformationCollector(key), 224 | )..debugLabel = key.url; 225 | } 226 | 227 | final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false; 228 | // When headers are set, the image can only be loaded by decoding. 229 | // 230 | // For the HTML renderer, `ui_web.createImageCodecFromUrl` method is not 231 | // capable of handling headers. 232 | // 233 | // For CanvasKit and Skwasm, it is not possible to load an element and 234 | // pass the headers with the request to fetch the image. Since the user has 235 | // provided headers, this function should assume the headers are required to 236 | // resolve to the correct resource and should not attempt to load the image 237 | // in an tag without the headers. 238 | if (containsNetworkImageHeaders) { 239 | return loadViaDecode(); 240 | } 241 | 242 | if (!isSkiaWeb) { 243 | // This branch is only hit by the HTML renderer, which is deprecated. The 244 | // HTML renderer supports loading images with CORS restrictions, so we 245 | // don't need to catch errors and try loading the image in an tag 246 | // in this case. 247 | 248 | // Resolve the Codec before passing it to 249 | // [MultiFrameImageStreamCompleter] so any errors aren't reported 250 | // twice (once from the MultiFrameImageStreamCompleter) and again 251 | // from the wrapping [ForwardingImageStreamCompleter]. 252 | final Uri resolved = Uri.base.resolve(key.url); 253 | final ui.Codec codec = await ui_web.createImageCodecFromUrl( 254 | resolved, 255 | chunkCallback: (int bytes, int total) { 256 | chunkEvents.add( 257 | ImageChunkEvent( 258 | cumulativeBytesLoaded: bytes, 259 | expectedTotalBytes: total, 260 | ), 261 | ); 262 | }, 263 | ); 264 | return MultiFrameImageStreamCompleter( 265 | chunkEvents: chunkEvents.stream, 266 | codec: Future.value(codec), 267 | scale: key.scale, 268 | debugLabel: key.url, 269 | informationCollector: _imageStreamInformationCollector(key), 270 | ); 271 | } 272 | 273 | switch (webHtmlElementStrategy) { 274 | case image_provider.WebHtmlElementStrategy.never: 275 | return loadViaDecode(); 276 | case image_provider.WebHtmlElementStrategy.prefer: 277 | return loadViaImgElement(); 278 | case image_provider.WebHtmlElementStrategy.fallback: 279 | try { 280 | // Await here so that errors occurred during the asynchronous process 281 | // of `loadViaDecode` are caught and triggers `loadViaImgElement`. 282 | return await loadViaDecode(); 283 | } catch (e) { 284 | return loadViaImgElement(); 285 | } 286 | } 287 | } 288 | 289 | Future _fetchImageBytes(_SimpleDecoderCallback decode) async { 290 | final Uri resolved = Uri.base.resolve(url); 291 | 292 | final bool containsNetworkImageHeaders = headers?.isNotEmpty ?? false; 293 | 294 | final Completer completer = 295 | Completer(); 296 | final web.XMLHttpRequest request = httpRequestFactory(); 297 | 298 | request.open('GET', url, true); 299 | request.responseType = 'arraybuffer'; 300 | if (containsNetworkImageHeaders) { 301 | headers!.forEach((String header, String value) { 302 | request.setRequestHeader(header, value); 303 | }); 304 | } 305 | 306 | request.addEventListener( 307 | 'load', 308 | (web.Event e) { 309 | final int status = request.status; 310 | final bool accepted = status >= 200 && status < 300; 311 | final bool fileUri = status == 0; // file:// URIs have status of 0. 312 | final bool notModified = status == 304; 313 | final bool unknownRedirect = status > 307 && status < 400; 314 | final bool success = 315 | accepted || fileUri || notModified || unknownRedirect; 316 | 317 | if (success) { 318 | completer.complete(request); 319 | } else { 320 | completer.completeError( 321 | image_provider.NetworkImageLoadException( 322 | statusCode: status, 323 | uri: resolved, 324 | ), 325 | ); 326 | } 327 | }.toJS, 328 | ); 329 | 330 | request.addEventListener( 331 | 'error', 332 | ((JSObject e) => completer.completeError( 333 | image_provider.NetworkImageLoadException( 334 | statusCode: request.status, 335 | uri: resolved, 336 | ), 337 | )) 338 | .toJS, 339 | ); 340 | 341 | request.send(); 342 | 343 | await completer.future; 344 | 345 | final Uint8List bytes = 346 | (request.response! as JSArrayBuffer).toDart.asUint8List(); 347 | 348 | if (bytes.lengthInBytes == 0) { 349 | throw image_provider.NetworkImageLoadException( 350 | statusCode: request.status, 351 | uri: resolved, 352 | ); 353 | } 354 | 355 | return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); 356 | } 357 | 358 | @override 359 | bool operator ==(Object other) { 360 | if (other.runtimeType != runtimeType) { 361 | return false; 362 | } 363 | return other is ExtendedNetworkImageProvider && 364 | url == other.url && 365 | scale == other.scale && 366 | //cacheRawData == other.cacheRawData && 367 | imageCacheName == other.imageCacheName; 368 | } 369 | 370 | @override 371 | int get hashCode => Object.hash( 372 | url, 373 | scale, 374 | //cacheRawData, 375 | imageCacheName, 376 | ); 377 | 378 | @override 379 | String toString() => '$runtimeType("$url", scale: $scale)'; 380 | 381 | // not support on web 382 | @override 383 | Future getNetworkImageData({ 384 | StreamController? chunkEvents, 385 | }) { 386 | return Future.error('not support on web'); 387 | } 388 | 389 | static dynamic get httpClient => null; 390 | } 391 | 392 | /// An [ImageStreamCompleter] that delegates to another [ImageStreamCompleter] 393 | /// that is loaded asynchronously. 394 | /// 395 | /// This completer keeps its child completer alive until this completer is disposed. 396 | class _ForwardingImageStreamCompleter extends ImageStreamCompleter { 397 | _ForwardingImageStreamCompleter( 398 | this.task, { 399 | InformationCollector? informationCollector, 400 | String? debugLabel, 401 | }) { 402 | this.debugLabel = debugLabel; 403 | task.then( 404 | (ImageStreamCompleter value) { 405 | resolved = true; 406 | if (_disposed) { 407 | // Add a listener since the delegate completer won't dispose if it never 408 | // had a listener. 409 | value.addListener(ImageStreamListener((_, _) {})); 410 | value.maybeDispose(); 411 | return; 412 | } 413 | completer = value; 414 | handle = completer.keepAlive(); 415 | completer.addListener( 416 | ImageStreamListener( 417 | (ImageInfo image, bool synchronousCall) { 418 | setImage(image); 419 | }, 420 | onChunk: (ImageChunkEvent event) { 421 | reportImageChunkEvent(event); 422 | }, 423 | onError: (Object exception, StackTrace? stackTrace) { 424 | reportError(exception: exception, stack: stackTrace); 425 | }, 426 | ), 427 | ); 428 | }, 429 | onError: (Object error, StackTrace stack) { 430 | reportError( 431 | context: ErrorDescription('resolving an image stream completer'), 432 | exception: error, 433 | stack: stack, 434 | informationCollector: informationCollector, 435 | silent: true, 436 | ); 437 | }, 438 | ); 439 | } 440 | 441 | final Future task; 442 | bool resolved = false; 443 | late final ImageStreamCompleter completer; 444 | late final ImageStreamCompleterHandle handle; 445 | 446 | bool _disposed = false; 447 | 448 | @override 449 | void onDisposed() { 450 | if (resolved) { 451 | handle.dispose(); 452 | } 453 | _disposed = true; 454 | super.onDisposed(); 455 | } 456 | } 457 | 458 | /// An [ImageInfo] object indicating that the image can only be displayed in 459 | /// an HTML element, and no [dart:ui.Image] can be created for it. 460 | /// 461 | /// This occurs on the web when the image resource is from a different origin 462 | /// and is not configured for CORS. Since the image bytes cannot be directly 463 | /// fetched, [Image]s cannot be created from it. However, the image can 464 | /// still be displayed if an HTML element is used. 465 | class WebImageInfo implements ImageInfo { 466 | /// Creates a new [WebImageInfo] from a given HTML element. 467 | WebImageInfo(this.htmlImage, {this.debugLabel}); 468 | 469 | /// The HTML element used to display this image. This HTML element has already 470 | /// decoded the image, so size information can be retrieved from it. 471 | final web.HTMLImageElement htmlImage; 472 | 473 | @override 474 | final String? debugLabel; 475 | 476 | @override 477 | WebImageInfo clone() { 478 | // There is no need to actually clone the element here. We create 479 | // another reference to the element and let the browser garbage 480 | // collect it when there are no more live references. 481 | return WebImageInfo(htmlImage, debugLabel: debugLabel); 482 | } 483 | 484 | @override 485 | void dispose() { 486 | // There is nothing to do here. There is no way to delete an element 487 | // directly, the most we can do is remove it from the DOM. But the 488 | // element here is never even added to the DOM. The browser will 489 | // automatically garbage collect the element when there are no longer any 490 | // live references to it. 491 | } 492 | 493 | @override 494 | Image get image => 495 | throw UnsupportedError( 496 | 'Could not create image data for this image because access to it is ' 497 | 'restricted by the Same-Origin Policy.\n' 498 | 'See https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy', 499 | ); 500 | 501 | @override 502 | bool isCloneOf(ImageInfo other) { 503 | if (other is! WebImageInfo) { 504 | return false; 505 | } 506 | 507 | // It is a clone if it points to the same element. 508 | return other.htmlImage == htmlImage && other.debugLabel == debugLabel; 509 | } 510 | 511 | @override 512 | double get scale => 1.0; 513 | 514 | @override 515 | int get sizeBytes => 516 | (4 * htmlImage.naturalWidth * htmlImage.naturalHeight).toInt(); 517 | } 518 | -------------------------------------------------------------------------------- /lib/src/platform.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:crypto/crypto.dart'; 6 | import 'package:extended_image_library/extended_image_library.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | 9 | export '_extended_network_image_utils_io.dart' 10 | if (dart.library.js_interop) '_extended_network_image_utils_web.dart'; 11 | export '_platform_io.dart' if (dart.library.js_interop) '_platform_web.dart'; 12 | 13 | const String cacheImageFolderName = 'cacheimage'; 14 | 15 | ///clear all of image in memory 16 | void clearMemoryImageCache([String? name]) { 17 | if (name != null) { 18 | if (imageCaches.containsKey(name)) { 19 | imageCaches[name]!.clear(); 20 | imageCaches[name]!.clearLiveImages(); 21 | imageCaches.remove(name); 22 | } 23 | } else { 24 | PaintingBinding.instance.imageCache.clear(); 25 | 26 | PaintingBinding.instance.imageCache.clearLiveImages(); 27 | } 28 | } 29 | 30 | /// get ImageCache 31 | ImageCache? getMemoryImageCache([String? name]) { 32 | if (name != null) { 33 | if (imageCaches.containsKey(name)) { 34 | return imageCaches[name]; 35 | } else { 36 | return null; 37 | } 38 | } else { 39 | return PaintingBinding.instance.imageCache; 40 | } 41 | } 42 | 43 | /// get network image data from cached 44 | Future getNetworkImageData( 45 | String url, { 46 | bool useCache = true, 47 | StreamController? chunkEvents, 48 | }) async { 49 | return ExtendedNetworkImageProvider( 50 | url, 51 | cache: useCache, 52 | ).getNetworkImageData(chunkEvents: chunkEvents); 53 | } 54 | 55 | /// get md5 from key 56 | String keyToMd5(String key) => md5.convert(utf8.encode(key)).toString(); 57 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: extended_image_library 2 | description: Library that contains common base class for `extended_image`, `extended_text`, and `extended_text_field`. 3 | repository: https://github.com/fluttercandies/extended_image_library 4 | version: 5.0.1 5 | 6 | environment: 7 | sdk: ^3.7.0 8 | flutter: ">=3.29.0" 9 | 10 | dependencies: 11 | crypto: ^3.0.0 12 | flutter: 13 | sdk: flutter 14 | http_client_helper: ^3.0.0 15 | js: ">=0.6.0 <0.8.0" 16 | path: ^1.9.0 17 | path_provider: ^2.1.0 18 | web: ">=0.3.0 <10.0.0" 19 | --------------------------------------------------------------------------------