| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/scanner/scanner_session.h" |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/public/cpp/scanner/scanner_profile_scoped_delegate.h" |
| #include "ash/scanner/scanner_action_view_model.h" |
| #include "ash/scanner/scanner_controller.h" |
| #include "ash/scanner/scanner_metrics.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "components/manta/manta_status.h" |
| #include "components/manta/proto/scanner.pb.h" |
| #include "skia/ext/image_operations.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/codec/jpeg_codec.h" |
| |
| namespace ash { |
| |
| namespace { |
| // The maximum number of pixels in either width or height of an image to be |
| // processed by scanner. |
| // We chose a conservative number based on typical laptop screen sizes in case |
| // that is required by the backend. |
| constexpr int64_t kMaxEdgePixels = 2300; |
| |
| // The quality attribute (out of 100) for the JPEG compression algorithm used. |
| constexpr int kJpegQuality = 90; |
| |
| // Downscales so that the width and height are both less than kMaxEdgePixels and |
| // retains the ratio. |
| scoped_refptr<base::RefCountedMemory> DownscaleImageIfNeeded( |
| scoped_refptr<base::RefCountedMemory> original_bytes) { |
| if (original_bytes == nullptr) { |
| return nullptr; |
| } |
| |
| SkBitmap img = gfx::JPEGCodec::Decode(*original_bytes); |
| int width = img.width(); |
| int height = img.height(); |
| |
| if (width <= 0 || height <= 0 || |
| (width <= kMaxEdgePixels && height <= kMaxEdgePixels) || |
| (width * height <= kMaxEdgePixels * kMaxEdgePixels)) { |
| return original_bytes; |
| } |
| |
| if (width > height) { |
| height = kMaxEdgePixels * height / width; |
| width = kMaxEdgePixels; |
| } else { |
| width = kMaxEdgePixels * width / height; |
| height = kMaxEdgePixels; |
| } |
| |
| // If the height and width is 0 given that we only try to resize when the |
| // total pixels are less than 2300x2300, it will only be possible if it was |
| // 5 million pixels in one edge and then 1 pixel in the other. |
| // |
| // This is clearly an edge case so we will return nullptr to stop processing |
| // this image. |
| if (height == 0 || width == 0) { |
| return nullptr; |
| } |
| |
| std::optional<std::vector<uint8_t>> shrunk_jpeg_bytes = |
| gfx::JPEGCodec::Encode( |
| skia::ImageOperations::Resize(img, skia::ImageOperations::RESIZE_BEST, |
| width, height), |
| kJpegQuality); |
| |
| if (shrunk_jpeg_bytes.has_value()) { |
| return base::MakeRefCounted<base::RefCountedBytes>( |
| std::move(*shrunk_jpeg_bytes)); |
| } |
| |
| // Fallback. |
| return original_bytes; |
| } |
| |
| // Runs the callback with the populated proto from the response of a call to |
| // `FetchActionDetailsForImage`. |
| void OnActionPopulated(ScannerSession::PopulateActionCallback callback, |
| std::unique_ptr<manta::proto::ScannerOutput> output, |
| manta::MantaStatus status) { |
| if (output == nullptr) { |
| // TODO(b/363100868): Handle error case |
| std::move(callback).Run(manta::proto::ScannerAction()); |
| return; |
| } |
| |
| if (output->objects_size() != 1) { |
| std::move(callback).Run(manta::proto::ScannerAction()); |
| return; |
| } |
| manta::proto::ScannerObject& object = *output->mutable_objects(0); |
| |
| if (object.actions_size() != 1) { |
| std::move(callback).Run(manta::proto::ScannerAction()); |
| return; |
| } |
| manta::proto::ScannerAction& action = *object.mutable_actions(0); |
| |
| std::move(callback).Run(std::move(action)); |
| } |
| |
| void RecordDetectedAction(manta::proto::ScannerAction::ActionCase action_case) { |
| switch (action_case) { |
| case manta::proto::ScannerAction::kNewEvent: |
| RecordScannerFeatureUserState( |
| ScannerFeatureUserState::kNewCalendarEventActionDetected); |
| return; |
| case manta::proto::ScannerAction::kNewContact: |
| RecordScannerFeatureUserState( |
| ScannerFeatureUserState::kNewContactActionDetected); |
| return; |
| case manta::proto::ScannerAction::kNewGoogleDoc: |
| RecordScannerFeatureUserState( |
| ScannerFeatureUserState::kNewGoogleDocActionDetected); |
| return; |
| case manta::proto::ScannerAction::kNewGoogleSheet: |
| RecordScannerFeatureUserState( |
| ScannerFeatureUserState::kNewGoogleSheetActionDetected); |
| return; |
| case manta::proto::ScannerAction::kCopyToClipboard: |
| RecordScannerFeatureUserState( |
| ScannerFeatureUserState::kCopyToClipboardActionDetected); |
| return; |
| case manta::proto::ScannerAction::ACTION_NOT_SET: |
| return; |
| } |
| // This should never be reached, as `action_case()` should always return a |
| // valid enum value. If the oneof field is set to something which is not |
| // recognised by this client, that is indistinguishable from an unknown field, |
| // and the above case should be `ACTION_NOT_SET`. |
| NOTREACHED(); |
| } |
| |
| void RecordAllDetectedActions(const manta::proto::ScannerOutput& output) { |
| bool action_found = false; |
| for (const manta::proto::ScannerObject& object : output.objects()) { |
| for (const manta::proto::ScannerAction& action : object.actions()) { |
| if (action.action_case() != manta::proto::ScannerAction::ACTION_NOT_SET) { |
| action_found = true; |
| RecordDetectedAction(action.action_case()); |
| } |
| } |
| } |
| |
| if (!action_found) { |
| RecordScannerFeatureUserState(ScannerFeatureUserState::kNoActionsDetected); |
| } |
| } |
| |
| } // namespace |
| |
| ScannerSession::ScannerSession(ScannerProfileScopedDelegate* delegate) |
| : delegate_(delegate) {} |
| |
| ScannerSession::~ScannerSession() = default; |
| |
| void ScannerSession::FetchActionsForImage( |
| scoped_refptr<base::RefCountedMemory> jpeg_bytes, |
| FetchActionsCallback callback) { |
| scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes = |
| DownscaleImageIfNeeded(std::move(jpeg_bytes)); |
| |
| if (mock_scanner_output_) { |
| OnActionsReturned(downscaled_jpeg_bytes, base::TimeTicks::Now(), |
| std::move(callback), std::move(mock_scanner_output_), |
| manta::MantaStatus(manta::MantaStatusCode::kOk)); |
| return; |
| } |
| |
| delegate_->FetchActionsForImage( |
| downscaled_jpeg_bytes, |
| base::BindOnce(&ScannerSession::OnActionsReturned, |
| weak_ptr_factory_.GetWeakPtr(), downscaled_jpeg_bytes, |
| base::TimeTicks::Now(), std::move(callback))); |
| } |
| |
| void ScannerSession::SetMockScannerOutput( |
| std::unique_ptr<manta::proto::ScannerOutput> mock_output) { |
| mock_scanner_output_ = std::move(mock_output); |
| } |
| |
| void ScannerSession::OnActionsReturned( |
| scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes, |
| base::TimeTicks request_start_time, |
| FetchActionsCallback callback, |
| std::unique_ptr<manta::proto::ScannerOutput> output, |
| manta::MantaStatus status) { |
| base::UmaHistogramMediumTimes(kScannerFeatureTimerFetchActionsForImage, |
| base::TimeTicks::Now() - request_start_time); |
| |
| if (status.status_code == manta::MantaStatusCode::kUnsupportedLanguage) { |
| std::move(callback).Run(base::unexpected(FetchError{ |
| .error_message = l10n_util::GetStringUTF16( |
| IDS_ASH_SCANNER_ERROR_UNSUPPORTED_LANGUAGE), |
| .can_try_again = false, |
| })); |
| return; |
| } |
| |
| if (output == nullptr) { |
| std::move(callback).Run(base::unexpected(FetchError{ |
| .error_message = |
| l10n_util::GetStringUTF16(IDS_ASH_SCANNER_ERROR_GENERIC), |
| .can_try_again = true, |
| })); |
| return; |
| } |
| |
| RecordAllDetectedActions(*output); |
| if (output->objects_size() == 0) { |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| std::vector<ScannerActionViewModel> action_view_models; |
| |
| for (manta::proto::ScannerAction& proto_action : |
| *output->mutable_objects(0)->mutable_actions()) { |
| if (proto_action.action_case() != |
| manta::proto::ScannerAction::ACTION_NOT_SET) { |
| action_view_models.emplace_back(std::move(proto_action), |
| downscaled_jpeg_bytes); |
| } |
| } |
| |
| std::move(callback).Run(std::move(action_view_models)); |
| } |
| |
| void ScannerSession::PopulateAction( |
| scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes, |
| manta::proto::ScannerAction unpopulated_action, |
| PopulateActionCallback callback) { |
| if (mock_scanner_output_) { |
| OnActionPopulated(std::move(callback), std::move(mock_scanner_output_), |
| {manta::MantaStatusCode::kOk}); |
| return; |
| } |
| |
| delegate_->FetchActionDetailsForImage( |
| std::move(downscaled_jpeg_bytes), std::move(unpopulated_action), |
| base::BindOnce(&OnActionPopulated, std::move(callback))); |
| } |
| |
| } // namespace ash |