Files
BadNote/windows/runner/ocr_channel.cpp
Akiba So 3615f2bd88
All checks were successful
CI / Windows build (push) Successful in 8m7s
fix(windows): don't crash on startup from OCR apartment
The app showed no window because RegisterOcrChannel ran
winrt::init_apartment() (defaults to MTA) on the Flutter platform
thread, which main.cpp already put in an STA via CoInitializeEx. That
throws RPC_E_CHANGED_MODE and kills the app before the window appears.
Remove that call; run the WinRT OCR on a dedicated MTA worker thread
per recognize call instead, joining before returning the result.
2026-06-21 16:19:51 +08:00

127 lines
3.9 KiB
C++

#include "ocr_channel.h"
#include <flutter/encodable_value.h>
#include <flutter/flutter_engine.h>
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Graphics.Imaging.h>
#include <winrt/Windows.Media.Ocr.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/base.h>
namespace {
std::vector<uint8_t> ExtractBytes(const flutter::EncodableValue& value) {
if (const auto* bytes = std::get_if<std::vector<uint8_t>>(&value)) {
return *bytes;
}
if (const auto* list = std::get_if<flutter::EncodableList>(&value)) {
std::vector<uint8_t> out;
out.reserve(list->size());
for (const auto& item : *list) {
if (const auto* i = std::get_if<int32_t>(&item)) {
out.push_back(static_cast<uint8_t>(*i));
}
}
return out;
}
return {};
}
std::string RecognizePng(const std::vector<uint8_t>& bytes) {
if (bytes.empty()) {
return {};
}
using namespace winrt;
using namespace Windows::Storage::Streams;
using namespace Windows::Graphics::Imaging;
using namespace Windows::Media::Ocr;
InMemoryRandomAccessStream stream;
{
DataWriter writer{stream};
writer.WriteBytes(bytes);
writer.StoreAsync().get();
writer.DetachStream();
}
stream.Seek(0);
BitmapDecoder decoder = BitmapDecoder::CreateAsync(stream).get();
SoftwareBitmap bitmap = decoder.GetSoftwareBitmapAsync().get();
OcrEngine engine = OcrEngine::TryCreateFromUserProfileLanguages();
if (!engine) {
return {};
}
OcrResult result = engine.RecognizeAsync(bitmap).get();
return winrt::to_string(result.Text());
}
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> g_ocr_channel;
} // namespace
void RegisterOcrChannel(flutter::FlutterEngine* engine) {
// NOTE: do NOT call winrt::init_apartment() here. This runs on the Flutter
// platform thread, which main.cpp has already initialized as an STA via
// CoInitializeEx(COINIT_APARTMENTTHREADED). Calling init_apartment() (which
// defaults to multi-threaded) would throw RPC_E_CHANGED_MODE and crash the
// app before the window appears. The WinRT OCR work runs on its own
// dedicated MTA worker thread per call instead (see below).
g_ocr_channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
engine->messenger(), "badnote/ocr",
&flutter::StandardMethodCodec::GetInstance());
g_ocr_channel->SetMethodCallHandler(
[](const flutter::MethodCall<flutter::EncodableValue>& call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>
result) {
if (call.method_name() != "recognize") {
result->NotImplemented();
return;
}
if (!call.arguments()) {
result->Error("invalid_args", "Expected PNG byte data");
return;
}
const auto png_bytes = ExtractBytes(*call.arguments());
// Run the WinRT OCR on a dedicated multi-threaded-apartment worker so
// its blocking .get() calls behave correctly and the platform thread's
// STA apartment is left untouched. We join before returning so the
// MethodResult is completed on the platform thread.
std::string text;
std::string error;
std::thread worker([&]() {
try {
winrt::init_apartment(winrt::apartment_type::multi_threaded);
text = RecognizePng(png_bytes);
winrt::uninit_apartment();
} catch (const winrt::hresult_error& e) {
error = winrt::to_string(e.message());
} catch (...) {
error = "Unknown OCR error";
}
});
worker.join();
if (!error.empty()) {
result->Error("ocr_failed", error);
} else {
result->Success(flutter::EncodableValue(text));
}
});
}