I think, the $zconvert() function will cover only the necessary entities. But you can use a simple method to convert characters to currently known(*) entities.

ClassMethod ToHTML(str)
{
   for i=$length(str):-1:1 set c=$ascii(str,i) set:$data(^entityChars(0,c),c) $extract(str,i)=c
   quit str
}

ClassMethod FromHTML(str)
{
   set i=0
   while $locate(str,"&[A-Za-z]+;",i,j,v) {
   set:$data(^entityChars(1,v),c) s=$length(v), $extract(str,j-s,j-1)=$c(c), j=j-s+1
   set i=j
   }
   quit str
}

I have a table (the ^entityChars() global) which contains more the 1400 entities. You can download the above class, together with the table from my FTP server (File: DC.Entity.xml):

Adr: ftp.kavay.at
Usr: dcmember
Psw: member-of-DC

A sample output:

USER>write ##class(DC.Entity).ToHTML("Flávio Lúcio Naves Júnior")
Flávio Lúcio Naves Júnior
USER>write ##class(DC.Entity).FromHTML("Flávio Lúcio Naves Júnior")
Flávio Lúcio Naves Júnior

(*) Currently known, because (1) I do not have all the currently known entities in my table and (2) with each new day, the W3C and the Unicode consortium can extend the current entity list.

With the global name lengths, there are two pitfalls,

- first, names could have arbitrary lengths, but only the first 31 characters are considered, but this was already mentioned  (Roger Merchberger)

- second, a global name can contain a period ("."), but the first (after the caret) or the last character must not be a period, where at "last" means, the period may not appear at the position 31.

The following short test shows this.

set glb="^abcdefghijklmnopqrstuvwxyz1234ABCDE.FG", @glb="myTest"
for i=37:-1:30 set $extract(glb,i-1,i)="."_$e(glb,i-1) try { write i,?4,glb," " write:$d(@glb)!1 $zr," ",$get(@glb),! } catch e { write e.Name,! }

the output is:

37  ^abcdefghijklmnopqrstuvwxyz1234ABCD.EFG ^abcdefghijklmnopqrstuvwxyz1234A myTest
36  ^abcdefghijklmnopqrstuvwxyz1234ABC.DEFG ^abcdefghijklmnopqrstuvwxyz1234A myTest
35  ^abcdefghijklmnopqrstuvwxyz1234AB.CDEFG ^abcdefghijklmnopqrstuvwxyz1234A myTest
34  ^abcdefghijklmnopqrstuvwxyz1234A.BCDEFG ^abcdefghijklmnopqrstuvwxyz1234A myTest
33  ^abcdefghijklmnopqrstuvwxyz1234.ABCDEFG <SYNTAX>
32  ^abcdefghijklmnopqrstuvwxyz123.4ABCDEFG ^abcdefghijklmnopqrstuvwxyz123.4
31  ^abcdefghijklmnopqrstuvwxyz12.34ABCDEFG ^abcdefghijklmnopqrstuvwxyz12.34
30  ^abcdefghijklmnopqrstuvwxyz1.234ABCDEFG ^abcdefghijklmnopqrstuvwxyz1.234

i = 34..37:  always the same global (^abcdefghijklmnopqrstuvwxyz1234A) hence, the same content

i = 33: the last character is a period, hence a syntax error

i = 30..32: different globals,

and the global name length is always 31 characters long.

By the way, if you start this example with

set glb = $name(^abcdefghijklmnopqrstuvwxyz1234ABCDE.FG)

which is the preferred method over 

set glb="^abcdefghijklmnopqrstuvwxyz1234ABCDE.FG"

the you end up with a cropped value in glb

^abcdefghijklmnopqrstuvwxyz1234A

This is not so simple because parts of a namespace could be mapped in more then one database.

But take a look at the %SYS.Namespace class:

do ##class(%SYS.Namespace).GetAllNSInfo("Namespace", .info)

write ##class(%SYS.Namespace).GetGlobalDest(,"aGlobalname")  // gives you the global database for a specific global

write ##class(%SYS.Namespace).GetRoutineDest(,"aRoutinename")  // gives you the global database specific routine

If you want to remove the N-th field, in the your example, "Field8" is the 6-th field, then:

set fields = "Field^Field1^Field3~Field4^Field5^Field6~Field7^Field8^Field8"
set N=6
set $piece(fields, "^", N, N+1) = $piece(fields, "^", N+1)

If you have to work with subfields, you have to get first those subfields in a temporary variable, for example, in the above example, you want to remove "Field3" then do this:

set temp=$piece(fields, "^", 3)  // get the subfields
set N=1
set $piece(temp, "~", N,N+1) = $piece(temp, "~", N+1)
set $piece(fields, "^", 3) = temp // put the subfields back into the outer string

If you want to remove the N-th field, in the your example, "Field8" is the 6-th field, then:

set fields = "Field^Field1^Field3~Field4^Field5^Field6~Field7^Field8^Field8"
set N=6
set $piece(fields, "^", N, N+1) = $piece(fields, "^", N+1)

If you have to work with subfields, you have to get first those subfields in a temporary variable, for example, in the above example, you want to remove "Field3" then do this:

set temp=$piece(fields, "^", 3)  // get the subfields
set N=1
set $piece(temp, "~", N,N+1) = $piece(temp, "~", N+1)
set $piece(fields, "^", 3) = temp // put the subfields back into the outer string

First of all, if you want to transform a hex string into base64 (or whatever), then first you have to say, WHAT IS that hex string?

According to your example

set hexString = "4C5803040101020179C3913EC3BA7C4C580708010101021824584D4C"

the string has 56 hex chars, so if you decode those 56 hex chars, the resulting string could be:
a) 28 eigth bit characters or
b) 14 sixteen bit chars or even
c)  7 characters, each 32 bits wide.

For cases b) and c) you also have to define the endianness (big- or little-endian).

Second, I assume, your hex-string represents 8-bit chars, so we get 28 characters after converting the hex-chars into a string.

Converting to base64 means, you get for every 3 (8bit) chars four printable (8bit) chars.

We add two padding chars to the 28 byte string, so we have 30 chars,  now which are divisible by 3. This gives you  40 base64 encoded characters. But your Base64 encoding has 44 characters, which must be wrong.

Here is a simple and working solution:

Class DC.Util Extends %RegisteredObject
{
/// Hex to Base64
ClassMethod HexToB64(hex)
{
   if $length(hex)#2 zt "ELEN" // trap, two hex chars should make up each byte
   set str=""
   for i=1:2:$length(hex) set str=str_$char($zhex($extract(hex,i,i+1)))
   quit $system.Encryption.Base64Encode(str,1)
}
/// Base64 to Hex
ClassMethod B64ToHex(b64)
{
   set str=$system.Encryption.Base64Decode(b64), hex=""
   for i=1:1:$length(str) set hex=hex_$extract($zhex($ascii(str,i)+256),2,3)
   quit hex
}
}

and a short test

set hexString = "4C5803040101020179C3913EC3BA7C4C580708010101021824584D4C"
set b64String=##class(DC.Util).HexToB64(hexString)
write b64String, !, ##class(DC.Util).B64ToHex(b64String), !,hexString

TFgDBAEBAgF5w5E+w7p8TFgHCAEBAQIYJFhNTA==
4C5803040101020179C3913EC3BA7C4C580708010101021824584D4C
4C5803040101020179C3913EC3BA7C4C580708010101021824584D4C

For such a task, the Horner's method was introduced. Fast and simple.

ClassMethod BinToDec(bin)
{
   if $translate(bin,10)="" { // formal check, bin should only contain '1' and '0'
      set res=0
      for i=1:1:$length(bin) set res=res*2+$extract(bin,i)
      quit res
   } else { ztrap "NBIN" }
}


Hardcore ObjectScript programer place those few commands into one line

bin2dec(bin) { s res=0 f i=1:1:$l(bin) { s res=res*2+$e(bin,i) } q res }


and doesn't care about errors ;-))

OK, this is the correct(ed) version:

ClassMethod RemoveNull(obj)
{
   set iter=obj.%GetIterator(),  rem=[]
   while iter.%GetNext(.key,.val) {
      if $isobject(val) { do ..RemoveNull(val) } elseif obj.%GetTypeOf(key)="null" { do rem.%Push({"o":(obj), "k":(key)}) }
   }
   for i=rem.%Size()-1:-1:0 set tmp=rem.%Get(i) do tmp.o.%Remove(tmp.k)
}

Just create a simple method like this:

ClassMethod RemoveNull(obj)
{
   set iter=obj.%GetIterator()
   while iter.%GetNext(.key,.val) {
   if $isobject(val) { do ..RemoveNull(val) } elseif obj.%GetTypeOf(key)="null" { do obj.%Remove(key) }
}

gives you

set json={"recipients": [{ "name":"Utsavi", "email":"utsavi@gmail.com"},{ "name":"Utsavi 1", "email":"utsavi1@gmail.com"},null, null],"content":[null, {"title":"Test.pdf", "data":"ygwehfbnwfbhew"} ]}

write json.%ToJSON() --> {"recipients":[{"name":"Utsavi","email":"utsavi@gmail.com"},{"name":"Utsavi 1","email":"utsavi1@gmail.com"},null,null],"content":[null,{"title":"Test.pdf","data":"ygwehfbnwfbhew"}]}

write ##class(some.class).RemoveNull(json) --> {"recipients":[{"name":"Utsavi","email":"utsavi@gmail.com"},{"name":"Utsavi 1","email":"utsavi1@gmail.com"},null],"content":[{"title":"Test.pdf","data":"ygwehfbnwfbhew"}]}

Just a hint, I would take $ZD($h,2). For today, my development system (and systems at customers site) shows:

Write $horolog - $zdate($horolog, 4) + 1 --> 65925.93
Write $zdate($horolog,4) --> 20.07.2021
Write $horolog - $zdate($horolog, 2) + 1  --> 65926 // expected value

Later, this value (65925.93), as a $zdate() argument,  gives you an <ILLEGAL VALUE> 

For $zdate($horolog,4), the link you provided says:

4 DD/MM/[YY]YY (01/07/97 or 27/03/2002) — European numeric format. You must specify the correct dateseparator character (/ or .) for the current locale.

Hello Matjaž,

I have a suspicion...

I tested your case with a 16201607 bytes large PNG file (and it worked).

First, to be able to do this, I had to made a change in the User.API class:

//Do model.%JSONExportToString(.json)
//Quit json

Do model.%JSONExportToStream(.str)
Quit str

so you get back a STREAM instead of a string.

As for IRIS and Cache, a string can't have more then 3641144 chars!

And take into account, a base64 encoded string is 33% longer then the orginal (exact: newSize = oldSize  + 2 \ 3 * 4), so you can use stringvariables up to an original picture size of (roughly, not counting the padding(s)): 

3641144 - 19 - $l(identname)  \ 4 * 3  //  19 bytes for {Ident:"","PNG":""}

By the way, can you output the encoded size (i.e. the length) of the JSONString you send and then the size of the same string in C#? Are they the same?

Zdravo Matjaž,

for the first glance, the above methods should work. Although I don't understand why you need the User.Model class? You can achieve the same thing by defining

Class User.Data Extends (%Persistend, %JSONAdapter) { }

and then 

ClassMethod Load(Ident As %String) As %String [ Language = objectscript ]
{
  Set data=##class(User.Data).%OpenId(Ident)
  Quit:data="" "null"

  Do data.%JSONExportToString(.json)
  Quit json
}

By the way, in the User.Data class, you can shorten the index definition to

Index Ident On Ident [ IdKey ];

because an IdKey is ALWAYS unique.

If you say, your C# gets a garbage PNG, then I would check two things:

1) does the PNG property (in User.Data) contain a valid PNG? Issue in a terminal session following commands: 

set data=##class(User.Data).%OpenId("...")
do data.PNG.Rewind() // not necessary straight after an open
zzdump data.PNG.Read(16)

the output should be:
0000: 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52         .PNG........IHDR

2) are you sure, C# gets the JSON-string returned by Load() method? Are you sure, no intermediate process (a middleware) changes this string by applying extra encoding or decoding?

 

Pozdrav z Beča (Dunaja)

Class Test.JD Extends (%Persistent, %JSON.Adaptor) [ Language = objectscript ]
{
Property Name As %String;
Property Type As %String;
Property Image As %Stream.GlobalBinary;
}

set obj=##class(Test.JD).%New()
set obj.Name="Joe"
do obj.Image.Write($system.Encryption.Base64Decode("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAAACXBIWXMA"))
do obj.Image.Write($system.Encryption.Base64Decode("AA7EAAAOxAGVKw4bAAAAGElEQVQokWNk+M9AEmAiTfmohlENQ0kDAD8vAR+xLJsiAAAAAElFTkSuQmCC"))

if obj.%JSONExportToString(.string) write string --> 

{"Name":"Joe","Image":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAA
AAAAABupgeRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAGElEQVQokWNk+M9AEmAiTfmohlENQ0kDAD8vA
R+xLJsiAAAAAElFTkSuQmCC"}

As you see in the above example, the export works and does Base64Encoding for the appropriate property (in my example this is 16x16 pixel green rectangle). Please show the relevant part (property) of your class (definition).