diff --git a/functions/v2/ocr/ocr-process-image/pom.xml b/functions/v2/ocr/ocr-process-image/pom.xml
new file mode 100644
index 00000000000..3a3254751ed
--- /dev/null
+++ b/functions/v2/ocr/ocr-process-image/pom.xml
@@ -0,0 +1,141 @@
+
+
+
+
+
+ 4.0.0
+
+ com.example.cloud.functions
+ functions-ocr-process-image
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+
+ com.google.cloud
+ libraries-bom
+ 25.4.0
+ pom
+ import
+
+
+
+
+
+
+ com.google.cloud.functions
+ functions-framework-api
+ 1.0.4
+ provided
+
+
+ io.cloudevents
+ cloudevents-core
+ 2.3.0
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+
+
+ com.google.cloud
+ google-cloud-vision
+
+
+ com.google.cloud
+ google-cloud-translate
+
+
+ com.google.cloud
+ google-cloud-pubsub
+
+
+ com.google.code.gson
+ gson
+ 2.9.1
+
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ com.google.truth
+ truth
+ 1.1.3
+ test
+
+
+ com.google.guava
+ guava-testlib
+ 31.1-jre
+ test
+
+
+
+
+
+
+
+ com.google.cloud.functions
+ function-maven-plugin
+ 0.10.0
+
+
+ functions.OcrProcessImage
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M7
+
+ ${skipTests}
+ sponge_log
+ false
+
+
+
+
+
diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrProcessImage.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrProcessImage.java
new file mode 100644
index 00000000000..e0fd90e7c99
--- /dev/null
+++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrProcessImage.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+// [START functions_ocr_process]
+
+import com.google.cloud.functions.CloudEventsFunction;
+import com.google.cloud.pubsub.v1.Publisher;
+import com.google.cloud.translate.v3.DetectLanguageRequest;
+import com.google.cloud.translate.v3.DetectLanguageResponse;
+import com.google.cloud.translate.v3.LocationName;
+import com.google.cloud.translate.v3.TranslationServiceClient;
+import com.google.cloud.vision.v1.AnnotateImageRequest;
+import com.google.cloud.vision.v1.AnnotateImageResponse;
+import com.google.cloud.vision.v1.Feature;
+import com.google.cloud.vision.v1.Image;
+import com.google.cloud.vision.v1.ImageAnnotatorClient;
+import com.google.cloud.vision.v1.ImageSource;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.protobuf.ByteString;
+import com.google.pubsub.v1.ProjectTopicName;
+import com.google.pubsub.v1.PubsubMessage;
+import functions.eventpojos.StorageObjectData;
+import io.cloudevents.CloudEvent;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+// [END functions_ocr_process]
+
+// [START functions_ocr_setup]
+public class OcrProcessImage implements CloudEventsFunction {
+ // TODO set these environment variables
+ private static final String PROJECT_ID = System.getenv("GCP_PROJECT");
+ private static final String TRANSLATE_TOPIC_NAME = System.getenv("TRANSLATE_TOPIC");
+ private static final String[] TO_LANGS = System.getenv("TO_LANG").split(",");
+
+ private static final Logger logger = Logger.getLogger(OcrProcessImage.class.getName());
+ private static final String LOCATION_NAME = LocationName.of(PROJECT_ID, "global").toString();
+ private Publisher publisher;
+
+ public OcrProcessImage() throws IOException {
+ publisher = Publisher.newBuilder(ProjectTopicName.of(PROJECT_ID, TRANSLATE_TOPIC_NAME)).build();
+ }
+
+ // Create custom deserializer to handle timestamps in event data
+ class DateDeserializer implements JsonDeserializer {
+ @Override
+ public OffsetDateTime deserialize(
+ JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ return OffsetDateTime.parse(json.getAsString());
+ }
+ }
+
+ Gson gson =
+ new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateDeserializer()).create();
+ // [END functions_ocr_setup]
+
+ // [START functions_ocr_process]
+ @Override
+ public void accept(CloudEvent event) {
+ // Unmarshal data from CloudEvent
+ StorageObjectData gcsEvent =
+ gson.fromJson(
+ new String(event.getData().toBytes(), StandardCharsets.UTF_8), StorageObjectData.class);
+ String bucket = gcsEvent.getBucket();
+ if (bucket == null) {
+ throw new IllegalArgumentException("Missing bucket parameter");
+ }
+ String filename = gcsEvent.getName();
+ if (filename == null) {
+ throw new IllegalArgumentException("Missing name parameter");
+ }
+
+ detectText(bucket, filename);
+ }
+ // [END functions_ocr_process]
+
+ // [START functions_ocr_detect]
+ private void detectText(String bucket, String filename) {
+ logger.info("Looking for text in image " + filename);
+
+ List visionRequests = new ArrayList<>();
+ String gcsPath = String.format("gs://%s/%s", bucket, filename);
+
+ ImageSource imgSource = ImageSource.newBuilder().setGcsImageUri(gcsPath).build();
+ Image img = Image.newBuilder().setSource(imgSource).build();
+
+ Feature textFeature = Feature.newBuilder().setType(Feature.Type.TEXT_DETECTION).build();
+ AnnotateImageRequest visionRequest =
+ AnnotateImageRequest.newBuilder().addFeatures(textFeature).setImage(img).build();
+ visionRequests.add(visionRequest);
+
+ // Detect text in an image using the Cloud Vision API
+ AnnotateImageResponse visionResponse;
+ try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
+ visionResponse = client.batchAnnotateImages(visionRequests).getResponses(0);
+ if (visionResponse == null || !visionResponse.hasFullTextAnnotation()) {
+ logger.info(String.format("Image %s contains no text", filename));
+ return;
+ }
+
+ if (visionResponse.hasError()) {
+ // Log error
+ logger.log(
+ Level.SEVERE, "Error in vision API call: " + visionResponse.getError().getMessage());
+ return;
+ }
+ } catch (IOException e) {
+ // Log error (since IOException cannot be thrown by a Cloud Function)
+ logger.log(Level.SEVERE, "Error detecting text: " + e.getMessage(), e);
+ return;
+ }
+
+ String text = visionResponse.getFullTextAnnotation().getText();
+ logger.info("Extracted text from image: " + text);
+
+ // Detect language using the Cloud Translation API
+ DetectLanguageRequest languageRequest =
+ DetectLanguageRequest.newBuilder()
+ .setParent(LOCATION_NAME)
+ .setMimeType("text/plain")
+ .setContent(text)
+ .build();
+ DetectLanguageResponse languageResponse;
+ try (TranslationServiceClient client = TranslationServiceClient.create()) {
+ languageResponse = client.detectLanguage(languageRequest);
+ } catch (IOException e) {
+ // Log error (since IOException cannot be thrown by a function)
+ logger.log(Level.SEVERE, "Error detecting language: " + e.getMessage(), e);
+ return;
+ }
+
+ if (languageResponse.getLanguagesCount() == 0) {
+ logger.info("No languages were detected for text: " + text);
+ return;
+ }
+
+ String languageCode = languageResponse.getLanguages(0).getLanguageCode();
+ logger.info(String.format("Detected language %s for file %s", languageCode, filename));
+
+ // Send a Pub/Sub translation request for every language we're going to translate to
+ for (String targetLanguage : TO_LANGS) {
+ logger.info("Sending translation request for language " + targetLanguage);
+ OcrTranslateApiMessage message = new OcrTranslateApiMessage(text, filename, targetLanguage);
+ ByteString byteStr = ByteString.copyFrom(message.toPubsubData());
+ PubsubMessage pubsubApiMessage = PubsubMessage.newBuilder().setData(byteStr).build();
+ try {
+ publisher.publish(pubsubApiMessage).get();
+ } catch (InterruptedException | ExecutionException e) {
+ // Log error
+ logger.log(Level.SEVERE, "Error publishing translation request: " + e.getMessage(), e);
+ return;
+ }
+ }
+ }
+ // [END functions_ocr_detect]
+
+ // [START functions_ocr_process]
+ // [START functions_ocr_setup]
+}
+// [END functions_ocr_setup]
+// [END functions_ocr_process]
diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrTranslateApiMessage.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrTranslateApiMessage.java
new file mode 100644
index 00000000000..cd880b9a3c7
--- /dev/null
+++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrTranslateApiMessage.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+// [START functions_ocr_translate_pojo]
+
+import com.google.gson.Gson;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+// Object for storing OCR translation requests
+public class OcrTranslateApiMessage {
+ private static final Gson gson = new Gson();
+
+ private String text;
+ private String filename;
+ private String lang;
+
+ public OcrTranslateApiMessage(String text, String filename, String lang) {
+ if (text == null) {
+ throw new IllegalArgumentException("Missing text parameter");
+ }
+ if (filename == null) {
+ throw new IllegalArgumentException("Missing filename parameter");
+ }
+ if (lang == null) {
+ throw new IllegalArgumentException("Missing lang parameter");
+ }
+
+ this.text = text;
+ this.filename = filename;
+ this.lang = lang;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public String getLang() {
+ return lang;
+ }
+
+ public static OcrTranslateApiMessage fromPubsubData(byte[] data) {
+ String jsonStr = new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
+ Map jsonMap = gson.fromJson(jsonStr, Map.class);
+
+ return new OcrTranslateApiMessage(
+ jsonMap.get("text"), jsonMap.get("filename"), jsonMap.get("lang"));
+ }
+
+ public byte[] toPubsubData() {
+ return gson.toJson(this).getBytes(StandardCharsets.UTF_8);
+ }
+}
+// [END functions_ocr_translate_pojo]
diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/CustomerEncryption.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/CustomerEncryption.java
new file mode 100644
index 00000000000..3d8bdf8ba23
--- /dev/null
+++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/CustomerEncryption.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions.eventpojos;
+
+// Metadata of customer-supplied encryption key for a Cloud Storage object
+// https://cloud.google.com/storage/docs/json_api/v1/objects
+@lombok.Data
+public class CustomerEncryption {
+ private String encryptionAlgorithm;
+ private String keySha256;
+}
diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/StorageObjectData.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/StorageObjectData.java
new file mode 100644
index 00000000000..24c3971a3a6
--- /dev/null
+++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/StorageObjectData.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions.eventpojos;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+// Represents an object within Cloud Storage
+// https://cloud.google.com/storage/docs/json_api/v1/objects
+@lombok.Data
+public class StorageObjectData {
+ private String bucket;
+ private String cacheControl;
+ private Long componentCount;
+ private String contentDisposition;
+ private String contentEncoding;
+ private String contentLanguage;
+ private String contentType;
+ private String crc32C;
+ private CustomerEncryption customerEncryption;
+ private String etag;
+ private Boolean eventBasedHold;
+ private Long generation;
+ private String id;
+ private String kind;
+ private String kmsKeyName;
+ private String md5Hash;
+ private String mediaLink;
+ private Map metadata;
+ private Long metageneration;
+ private String name;
+ private OffsetDateTime retentionExpirationTime;
+ private String selfLink;
+ private Long size;
+ private String storageClass;
+ private Boolean temporaryHold;
+ private OffsetDateTime timeCreated;
+ private OffsetDateTime timeDeleted;
+ private OffsetDateTime timeStorageClassUpdated;
+ private OffsetDateTime updated;
+}
diff --git a/functions/v2/ocr/ocr-process-image/src/test/java/functions/OcrProcessImageTest.java b/functions/v2/ocr/ocr-process-image/src/test/java/functions/OcrProcessImageTest.java
new file mode 100644
index 00000000000..1d88b96987b
--- /dev/null
+++ b/functions/v2/ocr/ocr-process-image/src/test/java/functions/OcrProcessImageTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+import com.google.common.testing.TestLogHandler;
+import com.google.common.truth.Truth;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import functions.eventpojos.StorageObjectData;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class OcrProcessImageTest {
+ private static String FUNCTIONS_BUCKET = "nodejs-docs-samples-tests";
+
+ private static final Logger logger = Logger.getLogger(OcrProcessImage.class.getName());
+
+ private static final TestLogHandler LOG_HANDLER = new TestLogHandler();
+
+ // Create custom serializer to handle timestamps in event data
+ class DateSerializer implements JsonSerializer {
+ @Override
+ public JsonElement serialize(
+ OffsetDateTime time, Type typeOfSrc, JsonSerializationContext context)
+ throws JsonParseException {
+ return new JsonPrimitive(time.toString());
+ }
+ }
+
+ private final Gson gson =
+ new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateSerializer()).create();
+
+ private static OcrProcessImage sampleUnderTest;
+
+ @BeforeClass
+ public static void setUpClass() throws IOException {
+ Truth.assertThat(System.getenv("GCP_PROJECT"));
+ Truth.assertThat(System.getenv("TO_LANG"));
+ Truth.assertThat(System.getenv("TRANSLATE_TOPIC"));
+ sampleUnderTest = new OcrProcessImage();
+ logger.addHandler(LOG_HANDLER);
+ }
+
+ @After
+ public void afterTest() {
+ LOG_HANDLER.clear();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void functionsOcrProcess_shouldValidateParams() throws IOException, URISyntaxException {
+ StorageObjectData data = new StorageObjectData();
+ CloudEvent event =
+ CloudEventBuilder.v1()
+ .withId("000")
+ .withType("google.cloud.storage.object.v1.finalized")
+ .withSource(new URI("curl-command"))
+ .withData("application/json", gson.toJson(data).getBytes())
+ .build();
+
+ sampleUnderTest.accept(event);
+ }
+
+ @Test
+ public void functionsOcrProcess_shouldDetectText() throws IOException, URISyntaxException {
+ StorageObjectData data = new StorageObjectData();
+ data.setBucket(FUNCTIONS_BUCKET);
+ data.setName("wakeupcat.jpg");
+ CloudEvent event =
+ CloudEventBuilder.v1()
+ .withId("000")
+ .withType("google.cloud.storage.object.v1.finalized")
+ .withSource(new URI("curl-command"))
+ .withData("application/json", gson.toJson(data).getBytes())
+ .build();
+
+ sampleUnderTest.accept(event);
+
+ List logs = LOG_HANDLER.getStoredLogRecords();
+ Truth.assertThat(logs.get(1).getMessage())
+ .contains("Extracted text from image: Wake up human!");
+ Truth.assertThat(logs.get(2).getMessage())
+ .contains("Detected language en for file wakeupcat.jpg");
+ }
+}
diff --git a/functions/v2/ocr/ocr-save-result/pom.xml b/functions/v2/ocr/ocr-save-result/pom.xml
new file mode 100644
index 00000000000..7c0b311bc86
--- /dev/null
+++ b/functions/v2/ocr/ocr-save-result/pom.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+ 4.0.0
+
+ com.example.cloud.functions
+ functions-ocr-save-result
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+
+ com.google.cloud
+ libraries-bom
+ 26.1.3
+ pom
+ import
+
+
+
+
+
+
+ com.google.cloud.functions
+ functions-framework-api
+ 1.0.4
+ provided
+
+
+ com.google.cloud
+ google-cloud-storage
+
+
+ io.cloudevents
+ cloudevents-core
+ 2.4.0
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+
+
+ com.google.code.gson
+ gson
+ 2.9.1
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ com.google.truth
+ truth
+ 1.1.3
+ test
+
+
+ com.google.guava
+ guava-testlib
+ 31.1-jre
+ test
+
+
+
+
+
+
+
+ com.google.cloud.functions
+ function-maven-plugin
+ 0.10.0
+
+
+ functions.OcrSaveResult
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M7
+
+ ${skipTests}
+ sponge_log
+ false
+
+
+
+
+
diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrSaveResult.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrSaveResult.java
new file mode 100644
index 00000000000..4f2fae681a6
--- /dev/null
+++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrSaveResult.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+// [START functions_ocr_save]
+
+import com.google.cloud.functions.CloudEventsFunction;
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import functions.eventpojos.MessagePublishedData;
+import io.cloudevents.CloudEvent;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.util.logging.Logger;
+
+public class OcrSaveResult implements CloudEventsFunction {
+ // TODO set this environment variable
+ private static final String RESULT_BUCKET = System.getenv("RESULT_BUCKET");
+
+ private static final Storage STORAGE = StorageOptions.getDefaultInstance().getService();
+ private static final Logger logger = Logger.getLogger(OcrSaveResult.class.getName());
+
+ // Configure Gson with custom deserializer to handle timestamps in event data
+ class DateDeserializer implements JsonDeserializer {
+ @Override
+ public OffsetDateTime deserialize(
+ JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ return OffsetDateTime.parse(json.getAsString());
+ }
+ }
+
+ Gson gson =
+ new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateDeserializer()).create();
+
+ @Override
+ public void accept(CloudEvent event) {
+ // Unmarshal data from CloudEvent
+ MessagePublishedData data =
+ gson.fromJson(
+ new String(event.getData().toBytes(), StandardCharsets.UTF_8),
+ MessagePublishedData.class);
+ OcrTranslateApiMessage ocrMessage =
+ OcrTranslateApiMessage.fromPubsubData(
+ data.getMessage().getData().getBytes(StandardCharsets.UTF_8));
+
+ logger.info("Received request to save file " + ocrMessage.getFilename());
+
+ String newFileName =
+ String.format("%s_to_%s.txt", ocrMessage.getFilename(), ocrMessage.getLang());
+
+ // Save file to RESULT_BUCKET with name newFileName
+ logger.info(String.format("Saving result to %s in bucket %s", newFileName, RESULT_BUCKET));
+ BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(RESULT_BUCKET, newFileName)).build();
+ STORAGE.create(blobInfo, ocrMessage.getText().getBytes(StandardCharsets.UTF_8));
+ logger.info("File saved");
+ }
+}
+// [END functions_ocr_save]
diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrTranslateApiMessage.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrTranslateApiMessage.java
new file mode 100644
index 00000000000..cd880b9a3c7
--- /dev/null
+++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrTranslateApiMessage.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+// [START functions_ocr_translate_pojo]
+
+import com.google.gson.Gson;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+// Object for storing OCR translation requests
+public class OcrTranslateApiMessage {
+ private static final Gson gson = new Gson();
+
+ private String text;
+ private String filename;
+ private String lang;
+
+ public OcrTranslateApiMessage(String text, String filename, String lang) {
+ if (text == null) {
+ throw new IllegalArgumentException("Missing text parameter");
+ }
+ if (filename == null) {
+ throw new IllegalArgumentException("Missing filename parameter");
+ }
+ if (lang == null) {
+ throw new IllegalArgumentException("Missing lang parameter");
+ }
+
+ this.text = text;
+ this.filename = filename;
+ this.lang = lang;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public String getLang() {
+ return lang;
+ }
+
+ public static OcrTranslateApiMessage fromPubsubData(byte[] data) {
+ String jsonStr = new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
+ Map jsonMap = gson.fromJson(jsonStr, Map.class);
+
+ return new OcrTranslateApiMessage(
+ jsonMap.get("text"), jsonMap.get("filename"), jsonMap.get("lang"));
+ }
+
+ public byte[] toPubsubData() {
+ return gson.toJson(this).getBytes(StandardCharsets.UTF_8);
+ }
+}
+// [END functions_ocr_translate_pojo]
diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/Message.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/Message.java
new file mode 100644
index 00000000000..1bc64926807
--- /dev/null
+++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/Message.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions.eventpojos;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+// Represents a PubSub message
+// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
+@lombok.Data
+public class Message {
+ private Map attributes;
+ private String data;
+ private String messageId;
+ private String orderingKey;
+ private OffsetDateTime publishTime;
+}
diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/MessagePublishedData.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/MessagePublishedData.java
new file mode 100644
index 00000000000..bc5030f29b7
--- /dev/null
+++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/MessagePublishedData.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions.eventpojos;
+
+// The event data when a message is published to a topic.
+@lombok.Data
+public class MessagePublishedData {
+ private Message message;
+ private String subscription;
+}
diff --git a/functions/v2/ocr/ocr-save-result/src/test/java/functions/OcrSaveResultTest.java b/functions/v2/ocr/ocr-save-result/src/test/java/functions/OcrSaveResultTest.java
new file mode 100644
index 00000000000..7a25275c2c0
--- /dev/null
+++ b/functions/v2/ocr/ocr-save-result/src/test/java/functions/OcrSaveResultTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import com.google.common.testing.TestLogHandler;
+import com.google.common.truth.Truth;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import functions.eventpojos.Message;
+import functions.eventpojos.MessagePublishedData;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.OffsetDateTime;
+import java.util.Base64;
+import java.util.List;
+import java.util.UUID;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class OcrSaveResultTest {
+ private static String RESULT_BUCKET = System.getenv("RESULT_BUCKET");
+
+ private static final Logger logger = Logger.getLogger(OcrSaveResult.class.getName());
+
+ private static final TestLogHandler LOG_HANDLER = new TestLogHandler();
+
+ // Create custom serializer to handle timestamps in event data
+ class DateSerializer implements JsonSerializer {
+ @Override
+ public JsonElement serialize(
+ OffsetDateTime time, Type typeOfSrc, JsonSerializationContext context)
+ throws JsonParseException {
+ return new JsonPrimitive(time.toString());
+ }
+ }
+
+ private final Gson gson =
+ new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateSerializer()).create();
+
+ private static final Storage STORAGE = StorageOptions.getDefaultInstance().getService();
+ private static final String RANDOM_STRING = UUID.randomUUID().toString();
+
+ @BeforeClass
+ public static void setUpClass() {
+ assertThat(RESULT_BUCKET).isNotNull();
+ logger.addHandler(LOG_HANDLER);
+ }
+
+ @After
+ public void afterTest() {
+ LOG_HANDLER.clear();
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ String deletedFilename = String.format("test-%s.jpg_to_es.txt", RANDOM_STRING);
+ STORAGE.delete(RESULT_BUCKET, deletedFilename);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void functionsOcrSave_shouldValidateParams() throws IOException, URISyntaxException {
+ MessagePublishedData data = new MessagePublishedData();
+ Message message = new Message();
+ message.setData(new String(Base64.getEncoder().encode("{}".getBytes())));
+ data.setMessage(message);
+
+ CloudEvent event =
+ CloudEventBuilder.v1()
+ .withId("000")
+ .withType("google.cloud.pubsub.topic.v1.messagePublished")
+ .withSource(new URI("curl-command"))
+ .withData("application/json", gson.toJson(data).getBytes())
+ .build();
+
+ new OcrSaveResult().accept(event);
+ }
+
+ @Test
+ public void functionsOcrSave_shouldPublishTranslatedText()
+ throws IOException, URISyntaxException {
+ String text = "Wake up human!";
+ String filename = String.format("test-%s.jpg", RANDOM_STRING);
+ String lang = "es";
+
+ JsonObject dataJson = new JsonObject();
+ dataJson.addProperty("text", text);
+ dataJson.addProperty("filename", filename);
+ dataJson.addProperty("lang", lang);
+
+ MessagePublishedData data = new MessagePublishedData();
+ Message message = new Message();
+ message.setData(new String(Base64.getEncoder().encode(gson.toJson(dataJson).getBytes())));
+ data.setMessage(message);
+ CloudEvent event =
+ CloudEventBuilder.v1()
+ .withId("000")
+ .withType("google.cloud.pubsub.topic.v1.messagePublished")
+ .withSource(new URI("curl-command"))
+ .withData("application/json", gson.toJson(data).getBytes())
+ .build();
+
+ new OcrSaveResult().accept(event);
+
+ String resultFilename = filename + "_to_es.txt";
+
+ // Check log messages
+ List logs = LOG_HANDLER.getStoredLogRecords();
+ String expectedMessage =
+ String.format("Saving result to %s in bucket %s", resultFilename, RESULT_BUCKET);
+ Truth.assertThat(LOG_HANDLER.getStoredLogRecords().get(1).getMessage())
+ .isEqualTo(expectedMessage);
+
+ // Check that file was written
+ BlobInfo resultBlob = STORAGE.get(RESULT_BUCKET, resultFilename);
+ assertThat(resultBlob).isNotNull();
+ }
+}
diff --git a/functions/v2/ocr/ocr-translate-text/pom.xml b/functions/v2/ocr/ocr-translate-text/pom.xml
new file mode 100644
index 00000000000..8cc2aeda734
--- /dev/null
+++ b/functions/v2/ocr/ocr-translate-text/pom.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+ 4.0.0
+
+ com.example.cloud.functions
+ functions-ocr-translate-text
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.2.0
+
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+
+ com.google.cloud
+ libraries-bom
+ 25.4.0
+ pom
+ import
+
+
+
+
+
+
+ com.google.cloud.functions
+ functions-framework-api
+ 1.0.4
+ provided
+
+
+ com.google.cloud
+ google-cloud-translate
+
+
+ com.google.cloud
+ google-cloud-pubsub
+
+
+ io.cloudevents
+ cloudevents-core
+ 2.3.0
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ com.google.truth
+ truth
+ 1.1.3
+ test
+
+
+ com.google.guava
+ guava-testlib
+ 31.1-jre
+ test
+
+
+
+
+
+
+
+ com.google.cloud.functions
+ function-maven-plugin
+ 0.10.0
+
+
+ functions.OcrTranslateText
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M7
+
+ ${skipTests}
+ sponge_log
+ false
+
+
+
+
+
diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateApiMessage.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateApiMessage.java
new file mode 100644
index 00000000000..cd880b9a3c7
--- /dev/null
+++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateApiMessage.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+// [START functions_ocr_translate_pojo]
+
+import com.google.gson.Gson;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+// Object for storing OCR translation requests
+public class OcrTranslateApiMessage {
+ private static final Gson gson = new Gson();
+
+ private String text;
+ private String filename;
+ private String lang;
+
+ public OcrTranslateApiMessage(String text, String filename, String lang) {
+ if (text == null) {
+ throw new IllegalArgumentException("Missing text parameter");
+ }
+ if (filename == null) {
+ throw new IllegalArgumentException("Missing filename parameter");
+ }
+ if (lang == null) {
+ throw new IllegalArgumentException("Missing lang parameter");
+ }
+
+ this.text = text;
+ this.filename = filename;
+ this.lang = lang;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public String getLang() {
+ return lang;
+ }
+
+ public static OcrTranslateApiMessage fromPubsubData(byte[] data) {
+ String jsonStr = new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
+ Map jsonMap = gson.fromJson(jsonStr, Map.class);
+
+ return new OcrTranslateApiMessage(
+ jsonMap.get("text"), jsonMap.get("filename"), jsonMap.get("lang"));
+ }
+
+ public byte[] toPubsubData() {
+ return gson.toJson(this).getBytes(StandardCharsets.UTF_8);
+ }
+}
+// [END functions_ocr_translate_pojo]
diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateText.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateText.java
new file mode 100644
index 00000000000..dafec796657
--- /dev/null
+++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateText.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+// [START functions_ocr_translate]
+
+import com.google.cloud.functions.CloudEventsFunction;
+import com.google.cloud.pubsub.v1.Publisher;
+import com.google.cloud.translate.v3.LocationName;
+import com.google.cloud.translate.v3.TranslateTextRequest;
+import com.google.cloud.translate.v3.TranslateTextResponse;
+import com.google.cloud.translate.v3.TranslationServiceClient;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.protobuf.ByteString;
+import com.google.pubsub.v1.ProjectTopicName;
+import com.google.pubsub.v1.PubsubMessage;
+import functions.eventpojos.MessagePublishedData;
+import io.cloudevents.CloudEvent;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class OcrTranslateText implements CloudEventsFunction {
+ private static final Logger logger = Logger.getLogger(OcrTranslateText.class.getName());
+
+ // TODO set these environment variables
+ private static final String PROJECT_ID = getenv("GCP_PROJECT");
+ private static final String RESULTS_TOPIC_NAME = getenv("RESULT_TOPIC");
+ private static final String LOCATION_NAME = LocationName.of(PROJECT_ID, "global").toString();
+
+ private Publisher publisher;
+
+ public OcrTranslateText() throws IOException {
+ publisher = Publisher.newBuilder(ProjectTopicName.of(PROJECT_ID, RESULTS_TOPIC_NAME)).build();
+ }
+
+ // Create custom deserializer to handle timestamps in event data
+ class DateDeserializer implements JsonDeserializer {
+ @Override
+ public OffsetDateTime deserialize(
+ JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ return OffsetDateTime.parse(json.getAsString());
+ }
+ }
+
+ Gson gson =
+ new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateDeserializer()).create();
+
+ @Override
+ public void accept(CloudEvent event) throws InterruptedException, IOException {
+ MessagePublishedData data =
+ gson.fromJson(
+ new String(event.getData().toBytes(), StandardCharsets.UTF_8),
+ MessagePublishedData.class);
+ OcrTranslateApiMessage ocrMessage =
+ OcrTranslateApiMessage.fromPubsubData(
+ data.getMessage().getData().getBytes(StandardCharsets.UTF_8));
+
+ String targetLang = ocrMessage.getLang();
+ logger.info("Translating text into " + targetLang);
+
+ // Translate text to target language
+ String text = ocrMessage.getText();
+ TranslateTextRequest request =
+ TranslateTextRequest.newBuilder()
+ .setParent(LOCATION_NAME)
+ .setMimeType("text/plain")
+ .setTargetLanguageCode(targetLang)
+ .addContents(text)
+ .build();
+
+ TranslateTextResponse response;
+ try (TranslationServiceClient client = TranslationServiceClient.create()) {
+ response = client.translateText(request);
+ } catch (IOException e) {
+ // Log error (since IOException cannot be thrown by a function)
+ logger.log(Level.SEVERE, "Error translating text: " + e.getMessage(), e);
+ return;
+ }
+ if (response.getTranslationsCount() == 0) {
+ return;
+ }
+
+ String translatedText = response.getTranslations(0).getTranslatedText();
+ logger.info("Translated text: " + translatedText);
+
+ // Send translated text to (subsequent) Pub/Sub topic
+ String filename = ocrMessage.getFilename();
+ OcrTranslateApiMessage translateMessage =
+ new OcrTranslateApiMessage(translatedText, filename, targetLang);
+ try {
+ ByteString byteStr = ByteString.copyFrom(translateMessage.toPubsubData());
+ PubsubMessage pubsubApiMessage = PubsubMessage.newBuilder().setData(byteStr).build();
+ publisher.publish(pubsubApiMessage).get();
+ logger.info("Text translated to " + targetLang);
+ } catch (InterruptedException | ExecutionException e) {
+ // Log error (since these exception types cannot be thrown by a function)
+ logger.log(Level.SEVERE, "Error publishing translation save request: " + e.getMessage(), e);
+ }
+ }
+
+ // Avoid ungraceful deployment failures due to unset environment variables.
+ // If you get this warning you should redeploy with the variable set.
+ private static String getenv(String name) {
+ String value = System.getenv(name);
+ if (value == null) {
+ logger.warning("Environment variable " + name + " was not set");
+ value = "MISSING";
+ }
+ return value;
+ }
+}
+// [END functions_ocr_translate]
diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/Message.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/Message.java
new file mode 100644
index 00000000000..1bc64926807
--- /dev/null
+++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/Message.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions.eventpojos;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+// Represents a PubSub message
+// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
+@lombok.Data
+public class Message {
+ private Map attributes;
+ private String data;
+ private String messageId;
+ private String orderingKey;
+ private OffsetDateTime publishTime;
+}
diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/MessagePublishedData.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/MessagePublishedData.java
new file mode 100644
index 00000000000..bc5030f29b7
--- /dev/null
+++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/MessagePublishedData.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions.eventpojos;
+
+// The event data when a message is published to a topic.
+@lombok.Data
+public class MessagePublishedData {
+ private Message message;
+ private String subscription;
+}
diff --git a/functions/v2/ocr/ocr-translate-text/src/test/java/functions/OcrTranslateTextTest.java b/functions/v2/ocr/ocr-translate-text/src/test/java/functions/OcrTranslateTextTest.java
new file mode 100644
index 00000000000..63c8eae334f
--- /dev/null
+++ b/functions/v2/ocr/ocr-translate-text/src/test/java/functions/OcrTranslateTextTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package functions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.TestLogHandler;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import functions.eventpojos.Message;
+import functions.eventpojos.MessagePublishedData;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.OffsetDateTime;
+import java.util.Base64;
+import java.util.List;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class OcrTranslateTextTest {
+ private static final Logger logger = Logger.getLogger(OcrTranslateText.class.getName());
+
+ private static final TestLogHandler LOG_HANDLER = new TestLogHandler();
+
+ // Create custom serializer to handle timestamps in event data
+ class DateSerializer implements JsonSerializer {
+ @Override
+ public JsonElement serialize(
+ OffsetDateTime time, Type typeOfSrc, JsonSerializationContext context)
+ throws JsonParseException {
+ return new JsonPrimitive(time.toString());
+ }
+ }
+
+ private final Gson gson =
+ new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateSerializer()).create();
+
+ private static OcrTranslateText sampleUnderTest;
+
+ @BeforeClass
+ public static void setUpClass() throws IOException {
+ assertThat(System.getenv("RESULT_TOPIC")).isNotNull();
+ sampleUnderTest = new OcrTranslateText();
+ logger.addHandler(LOG_HANDLER);
+ }
+
+ @After
+ public void afterTest() {
+ LOG_HANDLER.clear();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void functionsOcrTranslate_shouldValidateParams()
+ throws IOException, URISyntaxException, InterruptedException {
+ MessagePublishedData data = new MessagePublishedData();
+ Message message = new Message();
+ message.setData(new String(Base64.getEncoder().encode("{}".getBytes())));
+ data.setMessage(message);
+
+ CloudEvent event =
+ CloudEventBuilder.v1()
+ .withId("000")
+ .withType("google.cloud.pubsub.topic.v1.messagePublished")
+ .withSource(new URI("curl-command"))
+ .withData("application/json", gson.toJson(data).getBytes())
+ .build();
+ sampleUnderTest.accept(event);
+ }
+
+ @Test
+ public void functionsOcrTranslate_shouldTranslateText()
+ throws IOException, URISyntaxException, InterruptedException {
+ String text = "Wake up human!";
+ String filename = "wakeupcat.jpg";
+ String lang = "es";
+
+ JsonObject dataJson = new JsonObject();
+ dataJson.addProperty("text", text);
+ dataJson.addProperty("filename", filename);
+ dataJson.addProperty("lang", lang);
+
+ MessagePublishedData data = new MessagePublishedData();
+ Message message = new Message();
+ message.setData(new String(Base64.getEncoder().encode(gson.toJson(dataJson).getBytes())));
+ data.setMessage(message);
+ CloudEvent event =
+ CloudEventBuilder.v1()
+ .withId("000")
+ .withType("google.cloud.pubsub.topic.v1.messagePublished")
+ .withSource(new URI("curl-command"))
+ .withData("application/json", gson.toJson(data).getBytes())
+ .build();
+
+ sampleUnderTest.accept(event);
+
+ List logs = LOG_HANDLER.getStoredLogRecords();
+ assertThat(logs.get(1).getMessage()).contains("¡Despierta humano!");
+ assertThat(logs.get(2).getMessage()).isEqualTo("Text translated to es");
+ }
+}