Codeunit 2023 Image Analysis Wrapper V3.2, source in 29

Source29

src/Layers/W1/BaseApp/System/AI/ImageAnalysis/ImageAnalysisWrapperV32.Codeunit.al252 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.AI;

using System;
using System.Environment;
using System.Globalization;
using System.IO;
using System.Text;
using System.Utilities;

codeunit 2023 "Image Analysis Wrapper V3.2" implements "Image Analysis Provider"
{
    Access = Internal;

    // https://westus.dev.cognitive.microsoft.com/docs/services/computer-vision-v3-2/operations/56f91f2e778daf14a499f21b

    var
        RequestIdTelemetryMsg: Label 'Call to Image Analysis succeeded. ID present: %1; Request ID: %2.', Locked = true;
        CognitiveServicesErr: Label 'Could not contact the %1. %2 Status code: %3.', Comment = '%1: Error returned from called API. %2: the error message. %3: HTTP status code of error';
        MediaTooLargeErr: Label 'The media file is too large. Only images up to 4 MB are supported.';
        MediaTooSmallErr: Label 'The media file is too small. It must be at least 50x50 pixels.';
        MediaWrongFormatErr: Label 'The media file is not supported. Only images of the following types are supported: JPEG, PNG, GIF, BMP.';
        UnsupportedLanguageErr: Label 'You are trying to run image analysis in a language (%1) that is not supported.', Comment = '%1: a language code, for example 1033';
        CheckingImageSupportedTelemetryMsg: Label 'Checking image supported. Bytes: %1, Size: %2, Format: %3.', Locked = true;
        ComputerVisionApiTxt: Label 'Computer Vision API';
        AnalyzePathTxt: Label '/vision/v3.2/analyze', Locked = true;
        LanguageCodeQueryParameterTxt: Label 'language', Locked = true;
        VisualFeaturesQueryParameterTxt: Label 'visualFeatures', Locked = true;
        UseOAuth2: Boolean;
        LastError: Text;

    procedure InvokeAnalysis(var JSONManagement: Codeunit "JSON Management"; BaseUrl: Text; ImageAnalysisKey: SecretText; ImagePath: Text; ImageAnalysisTypes: List of [Enum "Image Analysis Type"]; LanguageId: Integer): Boolean
    begin
        exit(TryInvokeAnalysisInternal(JSONManagement, BaseUrl, ImageAnalysisKey, ImagePath, ImageAnalysisTypes, LanguageId));
    end;

    /// <summary>
    /// Sets whether OAuth2 authentication should be used for API calls.
    /// </summary>
    /// <param name="NewUseOAuth2">True to use OAuth2 bearer token authentication; false to use subscription key.</param>
    procedure SetUseOAuth2(NewUseOAuth2: Boolean)
    begin
        UseOAuth2 := NewUseOAuth2;
    end;

    [TryFunction]
    local procedure TryInvokeAnalysisInternal(var JSONManagement: Codeunit "JSON Management"; BaseUrl: Text; ImageAnalysisKey: SecretText; ImagePath: Text; ImageAnalysisTypes: List of [Enum "Image Analysis Type"]; LanguageId: Integer)
    var
        PostUrl: Text;
        Language: Text[10];
        HttpRequestMessage: HttpRequestMessage;
    begin
        if not TryGetImageAnalysisUserLanguage(LanguageId, Language, ImageAnalysisTypes) then
            Error(UnsupportedLanguageErr);

        PostUrl := BuildUri(BaseUrl, Language, ImageAnalysisTypes);
        PrepareRequest(HttpRequestMessage, PostUrl, ImageAnalysisKey);
        AddContent(HttpRequestMessage, ImagePath);
        SendRequest(HttpRequestMessage, JSONManagement);

        LogCorrelationToTelemetry(JSONManagement);
    end;

    local procedure LogCorrelationToTelemetry(var JSONManagement: Codeunit "JSON Management")
    var
        ImageAnalysisManagement: Codeunit "Image Analysis Management";
        JsonResult: DotNet JObject;
        RequestIdAsGuid: Guid;
        RequestIdPresent: Boolean;
    begin
        JSONManagement.GetJSONObject(JsonResult);

        RequestIdPresent := JSONManagement.GetGuidPropertyValueFromJObjectByName(JsonResult, 'requestId', RequestIdAsGuid);

        Session.LogMessage('0000K10', StrSubstNo(RequestIdTelemetryMsg, RequestIdPresent, RequestIdAsGuid),
            Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisManagement.GetTelemetryCategory());
    end;

    procedure GetLastError(): Text
    begin
        exit(LastError);
    end;

    procedure IsMediaSupported(MediaID: Guid): Boolean
    var
        TenantMedia: Record "Tenant Media";
        ImageAnalysisManagement: Codeunit "Image Analysis Management";
    begin
        TenantMedia.Get(MediaID);

        TenantMedia.CalcFields(Content);

        Session.LogMessage('0000JYU',
            StrSubstNo(CheckingImageSupportedTelemetryMsg, TenantMedia.Content.Length() > 4 * 1024 * 1024, (TenantMedia.Height < 50) or (TenantMedia.Width < 50), not LowerCase(TenantMedia."Mime Type").StartsWith('image')),
            Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ImageAnalysisManagement.GetTelemetryCategory());

        if TenantMedia.Content.Length() > 4 * 1024 * 1024 then begin
            LastError := MediaTooLargeErr;
            exit(false);
        end;

        if (TenantMedia.Height < 50) or (TenantMedia.Width < 50) then begin
            LastError := MediaTooSmallErr;
            exit(false);
        end;

        if not LowerCase(TenantMedia."Mime Type").StartsWith('image') then begin
            LastError := MediaWrongFormatErr;
            exit(false);
        end;

        exit(true);
    end;

    local procedure BuildUri(UriTxt: Text; LanguageCode: Text[10]; ImageAnalysisTypes: List of [Enum "Image Analysis Type"]): Text
    var
        Uri: Codeunit Uri;
        UriBuilder: Codeunit "Uri Builder";
        ImageAnalysisManagement: Codeunit "Image Analysis Management";
    begin
        UriBuilder.Init(UriTxt);
        UriBuilder.SetScheme('https');
        UriBuilder.SetPath(AnalyzePathTxt);
        UriBuilder.SetQuery('');
        if LanguageCode <> '' then
            UriBuilder.AddQueryParameter(LanguageCodeQueryParameterTxt, LanguageCode);

        UriBuilder.AddQueryParameter(VisualFeaturesQueryParameterTxt, ImageAnalysisManagement.ToCommaSeparatedList(ImageAnalysisTypes));
        UriBuilder.GetUri(Uri);
        exit(Uri.GetAbsoluteUri());
    end;

    local procedure PrepareRequest(var HttpRequestMsg: HttpRequestMessage; PostUrl: Text; ImageAnalysisKey: SecretText)
    var
        HttpHeaders: HttpHeaders;
    begin
        HttpRequestMsg.Method('POST');
        HttpRequestMsg.SetRequestUri(PostUrl);

        HttpRequestMsg.GetHeaders(HttpHeaders);
        HttpHeaders.Add('Accept', 'application/json');
        if UseOAuth2 then
            HttpHeaders.Add('Authorization', SecretStrSubstNo('Bearer %1', ImageAnalysisKey))
        else
            HttpHeaders.Add('Ocp-Apim-Subscription-Key', ImageAnalysisKey);
    end;

    [NonDebuggable]
    local procedure AddContent(var HttpRequestMsg: HttpRequestMessage; ImagePath: Text)
    var
        FileManagement: Codeunit "File Management";
        File: DotNet File;
        FileStream: DotNet FileStream;
        RequestHttpContent: HttpContent;
        ContentHttpHeaders: HttpHeaders;
    begin
        FileManagement.IsAllowedPath(ImagePath, false);
        FileStream := File.OpenRead(ImagePath);
        RequestHttpContent.WriteFrom(FileStream);
        HttpRequestMsg.Content := RequestHttpContent;
        HttpRequestMsg.Content.GetHeaders(ContentHttpHeaders);
        ContentHttpHeaders.Remove('Content-Type');
        ContentHttpHeaders.Add('Content-Type', 'application/octet-stream');

        FileStream.Dispose();
    end;

    [NonDebuggable]
    local procedure SendRequest(HttpRequestMsg: HttpRequestMessage; var JSONManagement: Codeunit "JSON Management")
    var
        ImageAnalysisManagement: Codeunit "Image Analysis Management";
        JsonResult: DotNet JObject;
        HttpResponseMessage: HttpResponseMessage;
        ResponseHttpContent: HttpContent;
        HttpClient: HttpClient;
        HttpStatusCode: Integer;
        Handled: Boolean;
        HttpContentText: Text;
        MessageText: Text;
        IsSuccessStatusCode: Boolean;
    begin
        ImageAnalysisManagement.OnBeforeSendImageAnalysisRequest(HttpRequestMsg.Content(), HttpRequestMsg.GetRequestUri(), HttpStatusCode, HttpContentText, Handled);
        if Handled then
            IsSuccessStatusCode := HttpStatusCode = 200
        else begin
            HttpClient.Send(HttpRequestMsg, HttpResponseMessage);
            HttpStatusCode := HttpResponseMessage.HttpStatusCode;
            IsSuccessStatusCode := HttpResponseMessage.IsSuccessStatusCode();
            ResponseHttpContent := HttpResponseMessage.Content;
            ResponseHttpContent.ReadAs(HttpContentText);
        end;

        JSONManagement.InitializeObject(HttpContentText);

        if not IsSuccessStatusCode then begin
            JSONManagement.GetJSONObject(JsonResult);
            JSONManagement.GetStringPropertyValueFromJObjectByName(JsonResult, 'message', MessageText);
            LastError := StrSubstNo(CognitiveServicesErr, ComputerVisionApiTxt, MessageText, HttpStatusCode);
            Error('');
        end;
    end;

    procedure IsLanguageSupported(AnalysisTypes: List of [Enum "Image Analysis Type"]; LanguageId: Integer): Boolean
    var
        LanguageCode: Text[10];
    begin
        exit(TryGetImageAnalysisUserLanguage(LanguageId, LanguageCode, AnalysisTypes));
    end;

    [TryFunction]
    local procedure TryGetImageAnalysisUserLanguage(InputLanguageId: Integer; var OutputLanguageCode: Text[10]; InputAnalysisTypes: List of [Enum "Image Analysis Type"])
    var
        DotNetCultureInfo: Codeunit DotNet_CultureInfo;
        CultureCode: Text[10];
        IsoLanguage: Text[10];
        SupportedLanguages: List of [Text[10]];
        LocalAnalysisTypes: List of [Enum "Image Analysis Type"];
    begin
        LocalAnalysisTypes.AddRange(InputAnalysisTypes);
        LocalAnalysisTypes.Remove(Enum::"Image Analysis Type"::Adult); // Adult is language independent

        // https://aka.ms/cv-languages
        case true of
            LocalAnalysisTypes.Count = 0:
                Error('');
            (LocalAnalysisTypes.Count = 1) and (LocalAnalysisTypes.Get(1) = Enum::"Image Analysis Type"::Tags):
                SupportedLanguages.AddRange(
                        'ar', 'az', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'el', 'en', 'es', 'et', 'eu',
                        'fi', 'fr', 'ga', 'gl', 'he', 'hi', 'hr', 'hu', 'id', 'it', 'ja',
                        'kk', 'ko', 'lt', 'lv', 'mk', 'ms', 'nb', 'nl',
                        'pl', 'prs', 'pt-BR', 'pt', 'pt-PT', 'ro', 'ru', 'sk', 'sl', 'sr-Cryl', 'sr-Latn', 'sv', 'th', 'tr',
                        'uk', 'vi', 'zh', 'zh-Hans', 'zh-Hant'
                    );
            else
                SupportedLanguages.Add('en');
        end;

        DotNetCultureInfo.GetCultureInfoById(InputLanguageId);

        CultureCode := CopyStr(DotNetCultureInfo.Name(), 1, MaxStrLen(CultureCode));
        IsoLanguage := CopyStr(DotNetCultureInfo.TwoLetterISOLanguageName(), 1, MaxStrLen(IsoLanguage));

        case true of
            SupportedLanguages.Contains(CultureCode):
                OutputLanguageCode := CultureCode;
            SupportedLanguages.Contains(IsoLanguage):
                OutputLanguageCode := IsoLanguage;
            else
                Error('');
        end;
    end;

}