Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[PDF.js] Set light and dark theme manually

The new PDF.js viewer design Photon supports a light and a dark mode. By default, the theme is set automatically. To overwrite this, you can set the viewerCssTheme property.

I’ve embedded my PDF viewer in an iFrame like this:

<iframe id="pdf-js-viewer" src="/pdf/web/viewer.html" title="webviewer" frameborder="0" width="100%" height="700" allowfullscreen="" webkitallowfullscreen=""/>

By setting the viewerCssTheme property, you are able to change the PDF viewer theme. It can be modified using the PDFViewerApplicationOptions.set() function and there are three possible values. The property has to be set before the Viewer is initialized. To archive this, you can listen to the event webviewerloaded (read more about it here). In my case, I also had to call _forceCssTheme() after that.

                document.addEventListener("webviewerloaded", async () => {
                    let pdfViewerIFrame = document.getElementById("pdf-js-viewer")
                    // https://github.com/mozilla/pdf.js/blob/master/web/app_options.js
                    pdfViewerIFrame.contentWindow.PDFViewerApplicationOptions.set("viewerCssTheme", 1)       // 0=automatic theme, 1=light theme, 2=dark theme 
                    pdfViewerIFrame.contentWindow.PDFViewerApplication._forceCssTheme()
                })

[PDF.js] Allow PDF documents to disable copying in the viewer

With pull request 11789, the PDF Viewer respects the PDF property Content Copying, if the viewer option enablePermissions is set to true.

I’ve embedded my viewer in an iFrame like this:

<iframe id="pdf-js-viewer" src="/pdf/web/viewer.html" title="webviewer" frameborder="0" width="100%" height="700" allowfullscreen="" webkitallowfullscreen=""/>

The enablePermissions property can be modified using the PDFViewerApplicationOptions.set() function. The property has to be set before the Viewer is initialized. To archive this, you can listen to the event webviewerloaded (read more about it here).

                document.addEventListener("webviewerloaded", async () => {
                    let pdfViewerIFrame = document.getElementById("pdf-js-viewer")
                    //https://github.com/mozilla/pdf.js/blob/master/web/app_options.js
                    pdfViewerIFrame.contentWindow.PDFViewerApplicationOptions.set("enablePermissions", true) //allow PDF documents to disable copying in the viewer
                    pdfViewerIFrame.contentWindow.PDFViewerApplicationOptions.set("defaultUrl", "") //prevent loading default pdf
                    }
                })

[JavaScript] Find specific object from an array of nested objects

I was again working on a task for my hierarchical tree structure (see previous post here). This time I needed to find a specific object with a specific property value.

So here again my nested array:

const aTreeData = [{
    "NodeId": 1,
    "HierarchyLevel": 1,
    "type": "folder",
    "nodes": [{
        "NodeId": 2,
        "HierarchyLevel": 2,
        "type": "folder",
        "nodes": [{
          "NodeId": 3,
          "HierarchyLevel": 3,
          "type": "category"
        }]
      },
      {
        "NodeId": 4,
        "HierarchyLevel": 2,
        "type": "category",
        "nodes": [{
          "NodeId": 5,
          "HierarchyLevel": 3,
          "type": "file"
        }]
      }
    ]
  },
  {
    "NodeId": 6,
    "HierarchyLevel": 1,
    "type": "folder",
    "nodes": [{
      "NodeId": 7,
      "HierarchyLevel": 2,
      "type": "category"
    }]
  }
]

I needed a function that was able to find any object by providing only the NodeId value. Fortunately I stumbled accross some excellent code snippet by Scott Sauyet which is exactly doing what I needed:

https://stackoverflow.com/questions/68559392/find-object-from-an-array-of-nested-objects-by-key-in-javascript

I just had to change the properties to match my objects and it was working!

const findNodeInTreeData = (aTreeData, nodeId) => {
  const deepFind = pred => ([x, ...xs] = []) =>
    x && (pred(x) ? x : deepFind(pred)(x.nodes) || deepFind(pred)(xs))

  const findByNodeId = id => obj =>
    deepFind(o => o.NodeId == id)([obj])

  return findByNodeId(nodeId)(aTreeData[0])
}

console.log(findNodeInTreeData(aTreeData, 5))

[SuccessFactors] Create JavaScript Date-Object from DateTimeOffset

The SuccessFactors oData v2 API is returning timestamps in Unix Epoch format (unix-style milliseconds since 1/1/1970).

Many timestamp fields are of type Edm.Int64. When receiving the milliseconds as Integer, you can directly create a date-object of it using Date(1658237847).

But some timestamps are of type Edm.DateTimeOffset, i.e.: "createdDate": "/Date(1652252620000+0000)/".
When binding a timestamp property with an ODataModel, the internal lib datajs will convert the /Date(...)/ value to a standard JS date-object.

But in my case I manually had to convert the timestamp and this is the shortest way I found to convert the epoch string into a JS date-object.

// SF epoch date string
const SFdateString = '/Date(1652252620000+0000)/' 

// remove the '/' on both sides and create the date object
const oDate = eval('new ' + SFdateString .slice(1, -1))

console.log(typeof oDate )
console.log(oDate )

const oDateTimeFormat = sap.ui.core.format.DateFormat.getDateTimeInstance({
          pattern: "YYYY-MM-dd HH:mm"
})
return oDateTimeFormat.format(oDate)


[JavaScript] Itereate through array of nested objects and delete specific objects

[
    {
        "NodeId": 1,
        "HierarchyLevel": 1,
        "type": "folder",
        "nodes": [
            {
                "NodeId": 2,
                "HierarchyLevel": 2,
                "type": "folder",
                "nodes": [
                    {
                        "NodeId": 3,
                        "HierarchyLevel": 3,
                        "type": "category"
                    }
                ]
            },
            {
                "NodeId": 4,
                "HierarchyLevel": 2,
                "type": "category",
                "nodes": [
                    {
                        "NodeId": 5,
                        "HierarchyLevel": 3,
                        "type": "file"
                    }
                ]
            }
        ]
    },
    {
        "NodeId": 6,
        "HierarchyLevel": 1,
        "type": "folder",
        "nodes": [
            {
                "NodeId": 7,
                "HierarchyLevel": 2,
                "type": "category"
            }
        ]
    }
]

My task was to get rid of every Node which has no subnodes of type file at the last level of the hierachy. So for this example the result I needed was an array containing only the nodes 1,2,4,5.
Of course in reality the nested structure was way more complex. My approach was a recursive function which checks every element’s type and nodes length property and calls itself if there are any subnodes. Also it is recommended to loop backwards through the array while deleting from it.

            
            const removeEmptyNodes = nodes => {
                for (let i = nodes.length - 1; i > -1; i--) {
                    const n = nodes[i]
                    //call function recursive to go deeper through the nested structure
                    if (n.nodes) removeEmptyNodes(n.nodes)                        
                    //remove element if it's not a file and has no subnodes
                    if (n.type !== 'file' && (!n.nodes || n.nodes.length === 0)) nodes.splice(i, 1)
                }
            }

            // nodes contains the array data from above
            removeEmptyNodes(nodes)

[JavaScript] Copy an array of objects to a new array without any object references

The user tim-montague posted a great overview on the different deep copy techniques on stackoverflow:

In my case I had an array of objects which I wanted to copy without reference, therefore I used the option in the middle.

const copy = JSON.parse(JSON.stringify(myArray))

# Another way using ES6 Syntax
const copy = myArray.map(object => ({ ...object }))

[nodejs] Parsing multipart/mixed response (containing a file stream)

Recently I had to consume an API which returned multipart/mixed data. A response looked like this:

--Boundary_0000000000001
Content-Type: application/octet-stream
Content-Disposition: attachment; filename"test.pdf"

%PDF-1.7
%�������
1 0 obj
...
%%EOF

--Boundary_0000000000001
Content-Type: application/json

{"data":[]}
--Boundary_0000000000001--

There are some node packages for parsing multipart responses, but most can only handle multipart/formData and not multipart/mixed. The most recommended package for multipart/mixed is Dicer, but to be honest, I wasn’t sure how to use it properly. Therefore, I built my own parser. Luckily the user idbehold provided a function to parse a response string into a json object here. To get it working, I just had to change the regex expressions in the split function. The most important step is to convert the data from the arrayBuffer to a String in binary encoding before parsing.

Also, I wrote two helper functions. The first one to parse the boundary string from the Content-Type and the second one to parse the filename from the Content-Dispositon Header of your response.

module.exports = new class multipartMixedParser {

    parse(boundary, buffer) {
        const body = buffer.toString('binary') //toString encodes to utf-8 as default, this would lead to corrupted pdf's     
        return body.split(boundary).reduce((parts, part) => {
            if (part && part !== '--\r\n') {
                const [head, body] = part.trim().split(/\r\n\r\n/g)
                console.log({ body })
                parts.push({
                    body: body,
                    headers: head.split(/\r\n/g).reduce((headers, header) => {
                        const [key, value] = header.split(/:\s+/)
                        headers[key.toLowerCase()] = value
                        return headers
                    }, {})
                })
            }
            return parts
        }, [])
    }

    getBoundaryFromResponseHeaders(headers) {
        //example: multipart/mixed;boundary=Boundary_0000000000001 -> --Boundary_0000000000001
        const contentType = headers.get('content-type')
        return '--' + contentType.split("=")[1].split(";")[0]
    }

    getFileNameFromContentDisposition(cd) {
        //example: 'attachment; filename="example.pdf"' -> example.pdf
        return cd.slice(
            cd.indexOf('"') + 1,
            cd.lastIndexOf('"')
        )
    }

}

And that’s how I’m calling the API and using the multipartMixedParser Class to parse the response. The API I was using is expecting a file as formData and is also returning a file (as part of the multipart/mixed response).
It’s important to get the buffer from the response. If you would use response.getText() it would convert the data to an utf-8 encoded string which will lead to corrupted files.

Please note, I’m using node-fetch. When using Axios, the response object will look different.

const btoa = require('btoa')
const FormData = require('form-data')
const fetch = require('node-fetch')
const multipartMixedParser = require('./multipartMixedParser') 

function callAPI(file) {

        const form = new FormData()
        form.append('file', file.content, {
            contentType: file.mediaType,
            filename: file.fileName
        })

        const headers = {
            'Authorization': 'Basic ' + btoa(username + ':' + password),
            ...form.getHeaders()
        }

        const url = /my/api/path

        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: headers,
                body: form
            })
            if (!response.ok) throw new Error(response.statusText)

            //parse the response
            const buffer = await response.buffer() 
            const boundary = multipartMixedParser.getBoundaryFromResponseHeaders(response.headers)

            const result = multipartMixedParser.parse(boundary, buffer)

            // in my case I only returned the file content as buffer and filename 
            return {
                fileContent: Buffer.from(result[0].body, 'binary'),
                fileName: multipartMixedParser.getFileNameFromContentDisposition(result[0].headers["content-disposition"])
            }
        } catch (err) {
            console.log("Error message: " + err.message)
        }

}