The quoting of the slash character, "/", is optional in JSON.  When doing a $ZCVT output conversion ,"O", in JSON IRIS chooses to not quote the slash character.  It makes the output easier to read.  However, $ZCVT doing an input conversion, "I", will recognize a quoted slash character in JSON input.  E.g.:

USER>w $zcvt("abc\/def","I","JSON")
abc/def
USER>w $zcvt("abc/def","I","JSON") 
abc/def
 

Another translation is

 Read *R:20
 ;; Test error case

 If '$Test { Use Write !!!,"Expired time." Quit }
 ;; Test character "a" case

 If $c(R)="a" {
   Use Write !!!,"A letter a has been read."
   Quit
 }
 ;; I added more code here to demonstrate "fall through" in original
 ;; when neither timeout nor "a" occurs
 Use 0
 Write !,"A character other than ""a"" was read"
 Quit

 ;; Since all 3 cases execute Use 0, this statement can be placed after Read and the other 3 copies deleted
 

I am assuming your problem is that request.HttpResponse.Data.Read() is complaining because you are reading the entire pdf file into an ObjectScript variable with its maximum supported string length of 3,641,144 characters.  You will have to read it out in smaller chunks that individually fit into an ObjectScript string.  The chunksize will be important as you pass the chunked data to $system.Encryption.Base64Encode(content) and your chunks cannot end between the boundaries between two different BASE64 encoding blocks.  The results of each Base64Encode must then be sent to some form of %Stream (probably %Stream.GlobalBinary or %Stream.FileBinary) since only a %Stream can hold a block of data larger than 3,641,144 characters.  Using a small, appropriate chuncksize will limit the in-memory resources used by this conversion.

If you don't mind having the entire PDF file in memory at one time you can use %DynamicObject to hold and decode that base64 data.  The %Library.DynamicObject and %Library.DynamicArray class objects are usually used to represent data that was originally JSON encoded.  These Dynamic Objects exist only in memory but you can serialize them into JSON textual representation using the %ToJSON(output) method.  But if the JSON text representation contains more than 3,641,144 characters then you better direct 'output' into some form of %Stream.

You can convert a binary pdf file into BASE64 encoding doing something like:

SET DynObj={}  ;; Creates an empty %DynamicObject
DO Dynobj.%Set("pdffile",request.HtttpResponse.Data,"stream")
SET Base64pdf=Dynobj.%Get("pdffile",,"stream>base64")

Then Base64pdf will a readonly, in-memory %Stream.DynamicBinary object which is encoded in BASE64.  You can use Base64pdf.Read(chunksize) to read the BASE64 out of Base64pdf in ObjectScript supported chunks.  You do not have to worry about making sure the chunksize is a multiple of 3 or a multiple of 4 or a multiple of 72.  You can also copy the data in Base64pdf into a writeable %Stream.FileBinary or a %Stream.GlobalBinary using the OtherStream.CopyFrom(Base64pdf) method call.

If your HttpResponse contains a BASE64 encoded pdf file instead of a binary pdf file then you can do the reverse decoding by:

SET DynObj={}
DO Dynobj.%Set("pdffile",request.HtttpResponse.Data,"stream<base64")
SET BinaryPDF=Dynobj.%Get("pdffile",,"stream")

Then BinaryPDF is a readonly %Stream.DynamicBinary containing the decoded pdf data.  You can copy it to a %Stream.FileBinary object which can then be examined using a pdf reader application.

A canonical numeric string in ObjectScript can have a very large number of digits.  Such a string can be sorted with ObjectScript sorts-after operator, ]], and reasonably long canonical numeric strings can be used as subscripts and such numeric subscripts are arranged in numerical order before all the subscript strings that do not have the canonical numeric format.

However, when ObjectScript does other kinds arithmetic on a numeric string then that string is converted to an internal format, which has a restricted range and a restricted precision.  ObjectScript currently supports two internal formats.  The default format is a decimal floating-point representation with a precision of approximately 18.96 decimal digits and a maximum number about 9.2E145.  For customers doing scientific calculations or needing a larger range, ObjectScript also supports the IEEE double-precision binary floating-point representation with a precision around 16 decimal digits and a maximum number about 1.7E308.  You get the IEEE floating-point representation with its reduced precision but its greater range by using the $double(x) function or doing arithmetic on a string which would convert to a numeric value beyond the range of the ObjectScript decimal floating-point representation.  When doing arithmetic that combines ObjectScript decimal floating-point values with IEEE binary floating-point values then the decimal floating-point values will be converted to IEEE binary floating point before performing the arithmetic operation.

Here are more picky details.

The ObjectScript decimal floating-point representation has a 64-bit signed significand with a value between -9223372036854775808 and +9223372036854775807 combined with a decimal exponent multiplier between 10**-128 and 10**127.  I.e., a 64-bit twos-complement integer significand and a signed byte as the decimal exponent.  This decimal floating-point representation can exactly represent decimal fractions like 0.1 or 0.005.

The IEEE binary floating-point representation has a sign-bit, an 11-bit exponent exponent encoding, and a 52 bit significand encoding. The significand usually encodes a 53-bit range values between 1.0 and 2.0 - 2**-52 and the exponent usually encodes a power-of-two multiplier between 2**1023 and 2**-1022.  However, certain other encodings will handle +0, -0, +infinity, -infinity and a large number of NaNs (Not-a-Number symbols.)  There are also some encodings with less than 53 bits of precision for very small values in the underflow range of values.  IEEE 64-bit binary floating-point cannot exactly represent most decimal fractions.  The numbers $double(0.1) and $double(0.005) are approximated by values near 0.10000000000000000556 and 0.0050000000000000001041.

I have written some ObjectScript code that can do add, subtract and modulo on long canonical numeric strings for use in a banking application.  However, if you are doing some serious computations on large precision values then you should use the call-in/call-out capabilities of IRIS to access external code in a language other than ObjectScript. Python might be a good choice.  You could use canonical numeric strings as your call-in/call-out representation or you could invent a new encoding using binary string values that could be stored/fetched from an IRIS data base.

ObjectScript was designed to do efficient movements and rearrangements of data stored in data bases.  If you are doing some serious computations between your data base operations then using a programming language other than ObjectScript will probably provide better capabilities for solving your problem.

The ObjectScript $ZDATETIME function (also-know-as $ZDT) contains lots of options, some of which are close to what your want.  [[ Note $HOROLOG is also-known-as $H; $ZTIMESTAMP is aka $ZTS. ]]

$ZDT($H,3,1) gives the current local time, where character positions 1-10 contain the date you want and positions 12-19 contain the time you want.  However, character position 11 contains a blank, " ", instead of a "T".

$ZDT($ZTS,3,1) gives the current UTC time with character position 11 containing a blank.

Assigning
    SET $EXTRACT(datetime,11)="T"
to your datetime result will fix the character position 11 issue.

Instead of using time format 1, you can use time formats 5  and 7 with $H.  $ZDT($H,3,5) gives local time in the format you want except character positions 20-27 contain the local offset from UTC.  $ZDT($H,3,7) converts the local $H date-time to UTC date-time and makes character position 20 contain "Z" to indicate the "Zulu" UTC time zone.  However, if your local time-zone includes a Daylight Saving Time (DST) offset change when DST "falls back" causing a local hour to be repeated then the time format 5 UTC offset or the time format 7 conversion from local to UTC will probably be incorrect during one of those repeated hours.

Although the above description says $ORDER scans "down" a multidimensional global, other programers might say it scans "sideways".  There are many different structures for databases.  E.g., there are network databases (sometimes called CODASYL databases); there are hierarchical databases (like ObjectScript multidimensional arrays/globals); there are relational databases (often accessed by the SQL language); ...

ObjectScript is based on the ANSI M language standard.  I believe that the name of the ANSI M hierarchical function $QUERY has always been $QUERY but the original name of the ANSI M hierarchical function $ORDER was formerly $NEXT.  $NEXT is very similar to $ORDER but $NEXT had problems with its starting/ending subscript values.  IRIS ObjectScript no longer documents the obsolete $NEXT function but the ObjectScript compiler still accepts programs using $NEXT for backwards compatibility.

Consider the following ObjectScript global array:

USER>ZWRITE ^g
^g("a")="a"
^g("a",1)="a1"
^g("b",1)="b1"
^g("b",1,"c")="b1c"
^g("c")="c"
^g("c","b10")="cb10"
^g("c","b2")="cb2"
^g("d",2)="d2"
^g("d",10)="d10"

Consider the following walk sideways by $ORDER along the first subscript of ^g"

USER>set s=""
USER>for { SET s=$ORDER(^g(s))  QUIT:s=""  WRITE $NAME(^g(s)),! }    
^g("a")
^g("b")
^g("c")
^g("d")

Although these 4 globals contain values below them, the $ORDER walks did not walk down to deeper subscripts.  As it walked sideways, it returned the subscripts "b" and "d" even though ^g("b") and ^g("d") did not have values of their own but only values underneath them.

Now consider the walk down deeper by $QUERY through all the subscripts of ^g(...) at all subscript levels:

USER>set s="^g"
USER>for { WRITE s,!  SET s=$QUERY(@s)  QUIT:s="" }               
^g
^g("a")
^g("a",1)
^g("b",1)
^g("b",1,"c")
^g("c")
^g("c","b10")
^g("c","b2")
^g("d",2)
^g("d",10)

This walk by $QUERY was told to start at ^g and every call on $QUERY went through every subscripted node of ^g(...) that contained a value regardless of the number of subscripts needed.  However, elements ^g("b") and ^g("d") that did not contain values of their own were skipped by the $QUERY walk as it continued on to nodes with deeper subscripts that did contain values.

Also note that each $ORDER evaluation returned only a single subscript value as it walked sideways while each $QUERY evaluation returned a string containing the variable name along with all the subscript values of that particular multidimensional array node.

You might consider using the %Library.DynamicObject and %Library.DynamicArray classes which are built into ObjectScript.  ObjectScript supports JSON constructors for %DynamicObject, {...}, and for %DynamicArray, [...].  a %DynamicObject/Array can contain both JSON values and ObjectScript values.   There is also a %FromJSON(x) which reads JSON objects/arrays when x is a %Stream or a FileNme or an ObjectScript %String containing JSON syntax.  Here is an example from a recent IRIS release:


USER>zload foo

USER>zprint
foo()    {
            set DynObj = {
            "notanumber":"28001",
            "aboolean":true,
            "anumber":12345,
            "adecimal":1.2,
            "adate":"01/01/2023",
            "adate2":"01-01-2023",
            "anull":null,
            "anull2":null,
            "anull3":null
            }
            write DynObj.%ToJSON(),!,!

            set DynObj.Bool1 = 1    ; Set without type
            do DynObj.%Set("Bool2",1,"boolean") ; Set with type
            set DynObj.old1 = "abc"   ; Set without type
            do DynObj.%Set("new1","abc","null") ; Set with type
            set DynObj.old2 = ""   ; Set without type
            do DynObj.%Set("new2","","null") ; Set with type
            write DynObj.%ToJSON()
}        

USER>do ^foo()
{"notanumber":"28001","aboolean":true,"anumber":12345,"adecimal":1.2,"adate":"01/01/2023","adate2":"01-01-2023","anull":null,"anull2":null,"anull3":null}

{"notanumber":"28001","aboolean":true,"anumber":12345,"adecimal":1.2,"adate":"01/01/2023","adate2":"01-01-2023","anull":null,"anull2":null,"anull3":null,"Bool1":1,"Bool2":true,"old1":"abc","new1":"abc","old2":"","new2":null}

Your can use ordinary ObjectScript assignment to assign ObjectScript values to entries in a %DynamicObject/Array.  You can use the %Set(key,value,type) method to assign ObjectScript values into an object with an optional type parameter controlling which JSON value to which to convert the ObjectScript value.  I.e., type "boolean" converts ObjectScript numeric expressions into JSON false/true values and type "null" converts an ObjectScript %String into a JSON string *except* that the empty string is converted to the JSON null value.  (Note:  some older implementations of %Set with type "null" only accepted "" as the value while new implementations accept any string expression as the value.)

The JSON constructor operators in ObjectScript, besides accepting JSON literals as values, can also accept an ObjectScript expression enclosed in round parentheses. 

E.g.,  SET DynObj2 = { "Pi":3.14159, "2Pi":(2*$ZPI) }

When asking about a 'ROUTINE' you may be asking about the difference between a 'Routine', 'Procedure', 'Subroutine', 'Function', 'Label', 'Class' and 'Method'

A 'Routine' is a source code file with name like 'myroutine.mac'.  Source code can also be a 'Method' which is found in a 'Class' file with a name like 'myclass.cls"

A Routine file can contain Procedures, Subroutines, Functions and Labels.  See https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCOS_usercode#GCOS_usercode_overview for some InterSystems documentation.

You can call a subroutine with the statement 'DO MySubroutine^myroutine(arg)'; You can call a function with the expression '$$MyFunction^myroutine(arg)';  You can call the procedure 'MyProcedure^myroutine(arg)' using either the syntax for a subroutine call or function call depending on whether you need the value returned by the procedure; and, You can Goto the source code following a label with the statement 'GO MyLabel^myroutine'.  If you reference a subroutine/function/procedure/label inside a Routine file (e.g. myroutine.mac) and that subroutine/function/procedure/label access (e.g.$$MyFunction^myroutine(arg)) is referencing a name defined in the same Routine file then you do not have to specify the ^Routine name in the call syntax (e.g. $$MyFunction(arg)).

The local variables used inside a subroutine or function are 'public' variables and those named variables are shared with all other subroutines and functions.  You can use the NEW command inside a subroutine or function to temporarily create new public variables without modifying previous instances of public variables with the same names.

The local variables used inside a procedure are private variables.  The private variables are only available inside the procedure.  Those names do not conflict with variables used by any caller of the procedure.  Those names are not available in any code called by the procedure although it is possible to pass either the private variable or the value of the private variable as an argument when calling out of a procedure.  The declaration of a procedure can optionally include a list of public variables.  Names in public list reference the public variable when they are accessed by code in the procedure.  You can use the NEW command with an argument that is a public variable within a procedure.  A label defined inside a procedure is also private and it cannot be referenced by any GO command outside that procedure.

Methods defined in a class file default to being a procedure with private variables and private labels.  However, it is possible to specify that a method is a subroutine or function.   Also, a method procedure declaration can optionally include a list of global variables.

As mentioned in another Developer Community Question, a recent version of IRIS would allow you to  evaluate object.%Get("pdfKeyName",,"stream") which would return to you an %Stream object containing the JSON string in question as raw characters.  Also, %Get in IRIS can support object.%Get("pdfKeyName",,"stream<base64") which would do Base64 decoding as it creates the %Stream. However, you said you need to stick with an older version of Caché/Ensemble which predates these %Get features.

It is possible to convert the long JSON string component to a %Stream but it will take some parsing passes.

(1) First use SET json1Obj=[ ].%FromJSON(FileOrStream) to create json1OBJ containing all the elements of the original JSON coming from a File or Stream.

(2) If your pdf JSON string is nested in json1OBJ then select down to the closest containing %DynamicObject containing your pdf JSON string,  I.e. Set json2Obj=json1Obj.level1.level2 if your original JSON looks like {"level1":{"level2":{"pdfKeyName":"Very long JSON string containing pdf", ...}, ...}, ...}

(3) Create a new %Stream containing the JSON representation of that closest containing %DynamicObject.  I.e.,

   SET TempStream=##class(%Stream.TmpBinary).%New()
   DO json2Obj.%ToJSON(TempStream)

(4) Read out buffers from TempStream looking for "pdfKeyName":" .  Note that this 14-character string could span a buffer boundary.

(5) Continue reading additional buffers until you find a "-characer not preceded by a \-character; Or until you find a "-character preceded by an even number of \-characters.

(6) Take characters read in step (5) and pass them through $ZCVT(chars,"I","JSON",handle) to convert JSON string escape characters to binary characters and then write them to a %Stream.FileBinary (or some other Binary %Stream of your choosing.)

(7) If your JSON "pdfKeyName" element contains Base64 encoding  then you will also need to use $SYSTEM.Encryption.Base64Decode(string). Unfortunately $SYSTEM.Encryption.Base64Decode(string), unlike $ZCVT(chars,"I",trantable,handle), does not include a 'handle' argument to support the buffer boundary cases in those situations where you have broken a very large string into smaller buffers.  Therefore you should remove the whitespace characters from 'string' before calling $SYSTEM.Encryption.Base64Decode(string) and you must make sure the argument 'string' has a length which is a multiple of 4 so that a group of 4 data characters cannot cross a buffer boundary.

Again the more complete support in IRIS can do steps (2) through (7) without worrying about buffer boundaries by executing

   Set BinaryStreamTmp = json1Obj.level1.level2.%Get("pdfKeyName",,"stream<base64")

and you can then copy the BinaryStreamTmp contents to wherever you want to resulting .pdf file.

The latest version of IRIS will have improved %DynamicObject (and the %DynamicArray) class objects.  They will support a method call like obj.%Get(key,,”stream”) which will return a %Stream.DynamicCharacter oref and this %Stream can contain a very large number of characters.  This can be copied into a %Stream.GlobalCharacter or a %Stream.FileCharacter if you want to save those characters in a persistent object.

This new form of %Get will also be able to include encoding/decoding using Base64 representation.  Similar extensions have been added to the %Set method.