How to obtain widevine payload (challenge) in google cast? - google-cast

I'm developing my own Custom Receiver Application and the stream I want to play is protected with widevine, I need to obtain my license from my own server and I need to pass content_id and payload. This my code:
playbackConfig.protectionSystem = cast.framework.ContentProtection.WIDEVINE;
playbackConfig.licenseRequestHandler = requestInfo => {
requestInfo.headers["Authorization"] = token;
requestInfo.headers["Content-Type"] = "application/json";
requestInfo.content = JSON.stringify({
type: "widevine",
type_request: "license",
content_id: content_id,
payload: <<missing_data>>
});
return requestInfo
};
I have it implemented on Android implmementing my own MediaDrmCallback and the class KeyRequest contains the needed information but the param content from object requestInfo doesn't provide that information

I make it work by doing
playbackConfig.licenseRequestHandler = requestInfo => {
requestInfo.headers["Authorization"] = token
requestInfo.headers["Content-Type"] = "application/json"
const wrapped = {}
wrapped.payload = arrayBufferToBase64(requestInfo.content)
wrapped.type = 'widevine'
wrapped.type_request = "license"
wrapped.content_id = content_id
const wrappedJson = JSON.stringify(wrapped)
requestInfo.content = shaka.util.StringUtils.toUTF8(wrappedJson)
return requestInfo
}
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa( binary );
}

Related

Swift encryption and NodeJS decryption producing inconsistent results

I'm not very familiar with Crypto, but i did ensure that the iv length buffer returns the correct length, encryptionKey used are the same.
The expected result from NodeJS, is a preset IV Length of 16 randombytes + encryptedText generated with aes-256-cbc combined into a hex String of length 64.
Tests encrypting in Node and decrypting it produces the expected result. But when iOS sends the payload it decrypts into an unknown string.
However, when iOS encrypts and sends the data. I'm unable to decrypt it to get the expected string.
For iOS i'm using the CommonCryto library
import CommonCrypto
struct AES {
private let key: String
init?(key: String) {
guard key.count == kCCKeySizeAES256 else {
debugPrint("Error: Failed to set a key.")
return nil
}
self.key = key
}
func encrypt(string: String) -> Data? {
return crypt(data: string.data(using: .utf8), operation: kCCEncrypt)
}
private func crypt(data: Data?, operation: Int) -> Data? {
guard let data = data else {
return nil
}
var ivBytes: [UInt8]
var inBytes: [UInt8]
var outLength: Int
if operation == kCCEncrypt {
ivBytes = [UInt8](repeating: 0, count: kCCBlockSizeAES128)
guard kCCSuccess == SecRandomCopyBytes(kSecRandomDefault, ivBytes.count, &ivBytes) else {
fatalError("IV creation failed!")
}
inBytes = Array(data)
outLength = data.count + kCCBlockSizeAES128
} else {
ivBytes = Array(Array(data).dropLast(data.count - kCCBlockSizeAES128))
inBytes = Array(Array(data).dropFirst(kCCBlockSizeAES128))
outLength = inBytes.count
}
var outBytes = [UInt8](repeating: 0, count: outLength)
var bytesMutated = 0
guard kCCSuccess == CCCrypt(CCOperation(operation), CCAlgorithm(kCCAlgorithmAES), CCOptions(kCCOptionPKCS7Padding), Array(key), key.count, &ivBytes, &inBytes, inBytes.count, &outBytes, outLength, &bytesMutated) else {
fatalError("Cryptography operation \(operation) failed")
}
var outData = Data(bytes: &outBytes, count: bytesMutated)
if operation == kCCEncrypt {
ivBytes.append(contentsOf: Array(outData))
outData = Data(ivBytes)
}
return outData
}
}
And how i decrypt in NodeJS:
const decrypt = functions.https.onCall(async (data, context) => {
const uid = context && context.auth && context.auth.uid;
if(!uid) {
return sendErrorResponse({
payload: 'Unauthorised',
statusCode: 401,
});
}
const { password } = data;
const MID = password.length / 2;
const textPart = [password.slice(0,MID),password.slice(MID)];
const iv = Buffer.from(textPart.shift(),'hex');
const encryptedText = Buffer.from(textPart.join(),'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', encryptionKey, iv).setAutoPadding(false);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
});
Look for cross plate-form AES encryption

Validate mining share in NodeJS before submit to pool (Stratum)

I am trying to validate share if it meets minimum difficulty.
I have all required data to create block hash and compare with difficulty.
My code is not generating valid block hash, but don't understand why.
My code with example data:
const crypto = require('crypto');
sha256 = function(buffer){
var hash1 = crypto.createHash('sha256');
hash1.update(buffer);
return hash1.digest();
};
sha256d = function(buffer){
return sha256(sha256(buffer));
};
reverseBuffer = function(buff){
var reversed = new Buffer.alloc(buff.length);
for (var i = buff.length - 1; i >= 0; i--)
reversed[buff.length - i - 1] = buff[i];
return reversed;
};
reverseHex = function(hex){
return reverseBuffer(Buffer.from(hex, 'hex')).toString('hex');
};
serializeCoinbase = function(coinbase1, coinbase2, extraNonce1, extraNonce2){
var coinbase = coinbase1+
extraNonce1+
extraNonce2+
coinbase2;
return Buffer.from(coinbase, 'hex');
};
MerkleRootWithCoinbase = function(merkleTree,coinbaseHash){
var hash = coinbaseHash;
merkleTree.forEach(a => {
hash = sha256d(Buffer.from(hash + a,'hex')).toString('hex')
});
return hash
}
convertPreviousblockhash = function(previousblockhash){
return previousblockhash.match(/.{1,8}/g).reverse().join('')
}
blockHeader = function(blockVersion, previousblockhash, merkleRoot, nTime, nBits, nonce){
previousblockhash = convertPreviousblockhash(previousblockhash)
var hash = reverseHex(blockVersion)+
reverseHex(previousblockhash)+
reverseHex(merkleRoot)+
reverseHex(nTime)+
reverseHex(nBits)+
reverseHex(nonce);
return Buffer.from(hash, 'hex');
};
// blockTemplate is received from pool (mining.notify)
let blockTemplate = {
ExtraNonce1: '929e4bb4',
ExtraNonce2_size: 4,
previousblockhash: '83ed60ce078736fe15528d3f5ea5cfdd0ed72b04000a31440000000000000000',
coinbase: [
'01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff5c0332410a192f5669614254432f4d696e6564206279206c6576616d75732f2cfabe6d6def4d71fab9c4856256f87621806274036feb16b79c411157a920716275e722951000000000000000108ad5ef13de0027ea',
'ffffffff04ce4c6229000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3acac6189d090598816a3e1092b966aad3c6aef15e3c826a334cea7f21002fd6540000000000000000266a24b9e11b6d65ba1a6db71ac060e04c3cb5bf049af5478970ded266f93b2f5b89f86348c9fc0000000000000000266a24aa21a9edf2716abb1dd5ac06ad1fed7f6bde943fc1657a0fd53a1c11823a4ffb5cc823e400000000'
],
merkleTree: [
'628a4d82f4950e0d55407f231057e8524a27e724143667cfd6c4bb3bc323d75f',
'e1ec3b078e79bd6da16899697e47a1eff05f27304baf7bf4fb3784145b452343',
'dfac6e968c29d40b35f34474b081b3fb9a054497c8ce8cebfca36471e9b5f912',
'35ea339bda0105cd0401e5f4dd89c511a41ea34117d839d6d3e32b575f4e4dc0',
'97bd6bfb590b9cb2a6f388f3a4039a926ed0487f82f6215860443a4bef28fe10',
'3126b182f4115fe6705c36a43737cd38eaf3e600add0de49bd6823c7f0fa6a11',
'793fa91f40722794f2234fd5ab7904a94432f8376b5cef86e7440a457b3482c4',
'd1370e331d8752b53e6b07d560a3d0b9f03569e2686e786739e3a96cc2ae7eb8',
'e99e29d9953dea3642621c08e31bd78ab9a3648b630af7dcff2dd90bdbca08b8',
'5655477db1423bef022382ccfe88d2c597f64bc69df6f4f520adca95a29f94aa',
'3c719be7fbeed1101542c6420fde3f4e5fd773284effacf8f9f21e4a05e5d416'
],
blockVersion: '20000000',
nBits: '170cf4e3',
nTime: '6036f006'
}
// extraNonce2,nTime and nonce is received from miner (mining.submit)
let extraNonce2 = "301a0000"
let nTime = "6036f00a"
let nonce = "e4abf319"
let coinbaseBuffer = serializeCoinbase(blockTemplate.coinbase[0], blockTemplate.coinbase[1], blockTemplate.ExtraNonce1, extraNonce2);
let coinbaseHash = sha256d(coinbaseBuffer);
console.log(coinbaseHash.toString('hex'))
// result: a97941791004f1ad8fe01d9e1a0116b932e65c37b7de2bd29ebd238c0705aa72
let merkleRoot = MerkleRootWithCoinbase(blockTemplate.merkleTree,coinbaseHash.toString('hex'));
console.log(merkleRoot.toString('hex'))
// result: 7c1a57f3f75d94e3f1014afca791101e80eebed18b3bca014f798e7399f3ceef
let headerBuffer = blockHeader(blockTemplate.blockVersion, blockTemplate.previousblockhash, merkleRoot, nTime, blockTemplate.nBits, nonce);
console.log(headerBuffer.toString('hex'))
// result: 00000020ce60ed83fe3687073f8d5215ddcfa55e042bd70e44310a000000000000000000efcef399738e794f01ca3b8bd1beee801e1091a7fc4a01f1e3945df7f3571a7c0af03660e3f40c1719f3abe4
let headerHash = sha256d(headerBuffer);
console.log(headerHash.toString('hex'))
// result: 87d83009bcca8068760bbdf5130a19b88e0f120c17cb97590397aed4b62ef4e4
let headerHashReversed = reverseBuffer(headerHash);
console.log(headerHashReversed.toString('hex'))
// result: e4f42eb6d4ae97035997cb170c120f8eb8190a13f5bd0b766880cabc0930d887
result hash is not valid, don't understand where I have invalid calculation.
I fixed problem and you can find working example below:
const crypto = require('crypto');
sha256 = function(buffer){
var hash1 = crypto.createHash('sha256');
hash1.update(buffer);
return hash1.digest();
};
sha256d = function(buffer){
return sha256(sha256(buffer));
};
reverseBuffer = function(buff){
var reversed = new Buffer.alloc(buff.length);
for (var i = buff.length - 1; i >= 0; i--)
reversed[buff.length - i - 1] = buff[i];
return reversed;
};
reverseHex = function(hex){
return reverseBuffer(Buffer.from(hex, 'hex')).toString('hex');
};
serializeCoinbase = function(coinbase1, coinbase2, extraNonce1, extraNonce2){
var coinbase = coinbase1+
extraNonce1+
extraNonce2+
coinbase2;
return Buffer.from(coinbase, 'hex');
};
MerkleRootWithCoinbase = function(merkleTree,coinbaseHash){
var hash = coinbaseHash;
merkleTree.forEach(a => {
hash = sha256d(Buffer.from(hash + a,'hex')).toString('hex')
});
return hash
}
convertPreviousblockhash = function(previousblockhash){
return previousblockhash.match(/.{1,8}/g).reverse().join('')
}
blockHeader = function(blockVersion, previousblockhash, merkleRoot, nTime, nBits, nonce){
previousblockhash = convertPreviousblockhash(previousblockhash)
var hash = reverseHex(blockVersion)+
reverseHex(previousblockhash)+
merkleRoot+
reverseHex(nTime)+
reverseHex(nBits)+
reverseHex(nonce);
return Buffer.from(hash, 'hex');
};
// blockTemplate is received from pool (mining.notify)
let blockTemplate = {
ExtraNonce1: '929e4bb4',
ExtraNonce2_size: 4,
previousblockhash: '83ed60ce078736fe15528d3f5ea5cfdd0ed72b04000a31440000000000000000',
coinbase: [
'01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff5c0332410a192f5669614254432f4d696e6564206279206c6576616d75732f2cfabe6d6def4d71fab9c4856256f87621806274036feb16b79c411157a920716275e722951000000000000000108ad5ef13de0027ea',
'ffffffff04ce4c6229000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3acac6189d090598816a3e1092b966aad3c6aef15e3c826a334cea7f21002fd6540000000000000000266a24b9e11b6d65ba1a6db71ac060e04c3cb5bf049af5478970ded266f93b2f5b89f86348c9fc0000000000000000266a24aa21a9edf2716abb1dd5ac06ad1fed7f6bde943fc1657a0fd53a1c11823a4ffb5cc823e400000000'
],
merkleTree: [
'628a4d82f4950e0d55407f231057e8524a27e724143667cfd6c4bb3bc323d75f',
'e1ec3b078e79bd6da16899697e47a1eff05f27304baf7bf4fb3784145b452343',
'dfac6e968c29d40b35f34474b081b3fb9a054497c8ce8cebfca36471e9b5f912',
'35ea339bda0105cd0401e5f4dd89c511a41ea34117d839d6d3e32b575f4e4dc0',
'97bd6bfb590b9cb2a6f388f3a4039a926ed0487f82f6215860443a4bef28fe10',
'3126b182f4115fe6705c36a43737cd38eaf3e600add0de49bd6823c7f0fa6a11',
'793fa91f40722794f2234fd5ab7904a94432f8376b5cef86e7440a457b3482c4',
'd1370e331d8752b53e6b07d560a3d0b9f03569e2686e786739e3a96cc2ae7eb8',
'e99e29d9953dea3642621c08e31bd78ab9a3648b630af7dcff2dd90bdbca08b8',
'5655477db1423bef022382ccfe88d2c597f64bc69df6f4f520adca95a29f94aa',
'3c719be7fbeed1101542c6420fde3f4e5fd773284effacf8f9f21e4a05e5d416'
],
blockVersion: '20000000',
nBits: '170cf4e3',
nTime: '6036f006'
}
// extraNonce2,nTime and nonce is received from miner (mining.submit)
let extraNonce2 = "301a0000"
let nTime = "6036f00a"
let nonce = "e4abf319"
let coinbaseBuffer = serializeCoinbase(blockTemplate.coinbase[0], blockTemplate.coinbase[1], blockTemplate.ExtraNonce1, extraNonce2);
let coinbaseHash = sha256d(coinbaseBuffer);
console.log(coinbaseHash.toString('hex'))
// result: a97941791004f1ad8fe01d9e1a0116b932e65c37b7de2bd29ebd238c0705aa72
let merkleRoot = MerkleRootWithCoinbase(blockTemplate.merkleTree,coinbaseHash.toString('hex'));
console.log(merkleRoot.toString('hex'))
// result: 7c1a57f3f75d94e3f1014afca791101e80eebed18b3bca014f798e7399f3ceef
let headerBuffer = blockHeader(blockTemplate.blockVersion, blockTemplate.previousblockhash, merkleRoot, nTime, blockTemplate.nBits, nonce);
console.log(headerBuffer.toString('hex'))
// result: 00000020ce60ed83fe3687073f8d5215ddcfa55e042bd70e44310a0000000000000000007c1a57f3f75d94e3f1014afca791101e80eebed18b3bca014f798e7399f3ceef0af03660e3f40c1719f3abe4
let headerHash = sha256d(headerBuffer);
console.log(headerHash.toString('hex'))
// result: 5edd9d1f993bb74a4f1b5fcd3ab48140df048af0c3bfa43c16c1000000000000
let headerHashReversed = reverseBuffer(headerHash);
console.log(headerHashReversed.toString('hex'))
// result: 000000000000c1163ca4bfc3f08a04df4081b43acd5f1b4f4ab73b991f9ddd5e
If the the miner is using version-rolling it means that miner is able to manipulate bits as specified in BIP320 - within mask 0x1fffe000.
Try including the version from the submit into block hash calculation.

NetSuite SuiteTalk API - Get Inventory Details

I'm using the SuiteTalk (API) service for NetSuite to retrieve a list of Assemblies. I need to load the InventoryDetails fields on the results to view the serial/lot numbers assigned to the items. This is the current code that I'm using, but the results still show those fields to come back as NULL, although I can see the other fields for the AssemblyBuild object. How do I get the inventory details (serials/lot#'s) to return on a transaction search?
public static List<AssemblyBuildResult> Get()
{
var listAssemblyBuilds = new List<AssemblyBuildResult>();
var service = Service.Context();
var ts = new TransactionSearch();
var tsb = new TransactionSearchBasic();
var sfType = new SearchEnumMultiSelectField
{
#operator = SearchEnumMultiSelectFieldOperator.anyOf,
operatorSpecified = true,
searchValue = new string[] { "_assemblyBuild" }
};
tsb.type = sfType;
ts.basic = tsb;
ts.inventoryDetailJoin = new InventoryDetailSearchBasic();
// perform the search
var response = service.search(ts);
response.pageSizeSpecified = true;
// Process response
if (response.status.isSuccess)
{
// Process the records returned in the response
// Get more records with pagination
if (response.totalRecords > 0)
{
for (var x = 1; x <= response.totalPages; x++)
{
var records = response.recordList;
foreach (var t in records)
{
var ab = (AssemblyBuild) t;
listAssemblyBuilds.Add(GetAssemblyBuildsResult(ab));
}
if (response.pageIndex < response.totalPages)
{
response = service.searchMoreWithId(response.searchId, x + 1);
}
}
}
}
// Parse and return NetSuite WorkOrder into assembly WorkOrderResult list
return listAssemblyBuilds;
}
After much pain and suffering, I was able to solve this problem with the following code:
/// <summary>
/// Returns List of AssemblyBuilds from NetSuite
/// </summary>
/// <returns></returns>
public static List<AssemblyBuildResult> Get(string id = "", bool getDetails = false)
{
// Object to populate and return results
var listAssemblyBuilds = new List<AssemblyBuildResult>();
// Initiate Service and SavedSearch (TransactionSearchAdvanced)
var service = Service.Context();
var tsa = new TransactionSearchAdvanced
{
savedSearchScriptId = "customsearch_web_assemblysearchmainlist"
};
// Filter by ID if specified
if (id != "")
{
tsa.criteria = new TransactionSearch()
{
basic = new TransactionSearchBasic()
{
internalId = new SearchMultiSelectField
{
#operator = SearchMultiSelectFieldOperator.anyOf,
operatorSpecified = true,
searchValue = new[] {
new RecordRef() {
type = RecordType.assemblyBuild,
typeSpecified = true,
internalId = id
}
}
}
}
};
}
// Construct custom columns to return
var tsr = new TransactionSearchRow();
var tsrb = new TransactionSearchRowBasic();
var orderIdCols = new SearchColumnSelectField[1];
var orderIdCol = new SearchColumnSelectField();
orderIdCols[0] = orderIdCol;
tsrb.internalId = orderIdCols;
var tranDateCols = new SearchColumnDateField[1];
var tranDateCol = new SearchColumnDateField();
tranDateCols[0] = tranDateCol;
tsrb.tranDate = tranDateCols;
var serialNumberCols = new SearchColumnStringField[1];
var serialNumberCol = new SearchColumnStringField();
serialNumberCols[0] = serialNumberCol;
tsrb.serialNumbers = serialNumberCols;
// Perform the Search
tsr.basic = tsrb;
tsa.columns = tsr;
var response = service.search(tsa);
// Process response
if (response.status.isSuccess)
{
var searchRows = response.searchRowList;
if (searchRows != null && searchRows.Length >= 1)
{
foreach (SearchRow t in searchRows)
{
var transactionRow = (TransactionSearchRow)t;
listAssemblyBuilds.Add(GetAssemblyBuildsResult(transactionRow, getDetails));
}
}
}
// Parse and return NetSuite WorkOrder into assembly WorkOrderResult list
return listAssemblyBuilds;
}
private static string GetAssemblyBuildLotNumbers(string id)
{
var service = Service.Context();
var serialNumbers = "";
var tsa = new TransactionSearchAdvanced
{
savedSearchScriptId = "customsearch_web_assemblysearchlineitems"
};
service.searchPreferences = new SearchPreferences { bodyFieldsOnly = false };
tsa.criteria = new TransactionSearch()
{
basic = new TransactionSearchBasic()
{
internalId = new SearchMultiSelectField
{
#operator = SearchMultiSelectFieldOperator.anyOf,
operatorSpecified = true,
searchValue = new[] {
new RecordRef() {
type = RecordType.assemblyBuild,
typeSpecified = true,
internalId = id
}
}
}
}
};
// Construct custom columns to return
var tsr = new TransactionSearchRow();
var tsrb = new TransactionSearchRowBasic();
var orderIdCols = new SearchColumnSelectField[1];
var orderIdCol = new SearchColumnSelectField();
orderIdCols[0] = orderIdCol;
tsrb.internalId = orderIdCols;
var serialNumberCols = new SearchColumnStringField[1];
var serialNumberCol = new SearchColumnStringField();
serialNumberCols[0] = serialNumberCol;
tsrb.serialNumbers = serialNumberCols;
tsr.basic = tsrb;
tsa.columns = tsr;
var response = service.search(tsa);
if (response.status.isSuccess)
{
var searchRows = response.searchRowList;
if (searchRows != null && searchRows.Length >= 1)
{
foreach (SearchRow t in searchRows)
{
var transactionRow = (TransactionSearchRow)t;
if (transactionRow.basic.serialNumbers != null)
{
return transactionRow.basic.serialNumbers[0].searchValue;
}
}
}
}
return serialNumbers;
}
private static AssemblyBuildResult GetAssemblyBuildsResult(TransactionSearchRow tsr, bool getDetails)
{
if (tsr != null)
{
var assemblyInfo = new AssemblyBuildResult
{
NetSuiteId = tsr.basic.internalId[0].searchValue.internalId,
ManufacturedDate = tsr.basic.tranDate[0].searchValue,
SerialNumbers = tsr.basic.serialNumbers[0].searchValue
};
// If selected, this will do additional NetSuite queries to get detailed data (slower)
if (getDetails)
{
// Look up Lot Number
assemblyInfo.LotNumber = GetAssemblyBuildLotNumbers(tsr.basic.internalId[0].searchValue.internalId);
}
return assemblyInfo;
}
return null;
}
What I learned about pulling data from NetSuite:
Using SavedSearches is the best method to pull data that doesn't automatically come through in the API objects
It is barely supported
Don't specify an ID on the SavedSearch, specify a criteria in the TransactionSearch to get one record
You will need to specify which columns to actually pull down. NetSuite doesn't just send you the data from a SavedSearch automatically
You cannot view data in a SavedSearch that contains a Grouping
In the Saved Search, use the Criteria Main Line = true/false to read data from the main record (top of UI screen), and line items (bottom of screen)

How do I replace a string in a PDF file using NodeJS?

I have a template PDF file, and I want to replace some marker strings to generate new PDF files and save them. What's the best/simplest way to do this? I don't need to add graphics or anything fancy, just a simple text replacement, so I don't want anything too complicated.
Thanks!
Edit: Just found HummusJS, I'll see if I can make progress and post it here.
I found this question by searching, so I think it deserves the answer. I found the answer by BrighTide here: https://github.com/galkahana/HummusJS/issues/71#issuecomment-275956347
Basically, there is this very powerful Hummus package which uses library written in C++ (crossplatform of course). I think the answer given in that github comment can be functionalized like this:
var hummus = require('hummus');
/**
* Returns a byteArray string
*
* #param {string} str - input string
*/
function strToByteArray(str) {
var myBuffer = [];
var buffer = new Buffer(str);
for (var i = 0; i < buffer.length; i++) {
myBuffer.push(buffer[i]);
}
return myBuffer;
}
function replaceText(sourceFile, targetFile, pageNumber, findText, replaceText) {
var writer = hummus.createWriterToModify(sourceFile, {
modifiedFilePath: targetFile
});
var sourceParser = writer.createPDFCopyingContextForModifiedFile().getSourceDocumentParser();
var pageObject = sourceParser.parsePage(pageNumber);
var textObjectId = pageObject.getDictionary().toJSObject().Contents.getObjectID();
var textStream = sourceParser.queryDictionaryObject(pageObject.getDictionary(), 'Contents');
//read the original block of text data
var data = [];
var readStream = sourceParser.startReadingFromStream(textStream);
while(readStream.notEnded()){
Array.prototype.push.apply(data, readStream.read(10000));
}
var string = new Buffer(data).toString().replace(findText, replaceText);
//Create and write our new text object
var objectsContext = writer.getObjectsContext();
objectsContext.startModifiedIndirectObject(textObjectId);
var stream = objectsContext.startUnfilteredPDFStream();
stream.getWriteStream().write(strToByteArray(string));
objectsContext.endPDFStream(stream);
objectsContext.endIndirectObject();
writer.end();
}
// replaceText('source.pdf', 'output.pdf', 0, /REPLACEME/g, 'My New Custom Text');
UPDATE:
The version used at the time of writing an example was 1.0.83, things might change recently.
UPDATE 2:
Recently I got an issue with another PDF file which had a different font. For some reason the text got split into small chunks, i.e. string QWERTYUIOPASDFGHJKLZXCVBNM1234567890- got represented as -286(Q)9(WER)24(T)-8(YUIOP)116(ASDF)19(GHJKLZX)15(CVBNM1234567890-)
I had no idea what else to do rather than make up a regex.. So instead of this one line:
var string = new Buffer(data).toString().replace(findText, replaceText);
I have something like this now:
var string = Buffer.from(data).toString();
var characters = REPLACE_ME;
var match = [];
for (var a = 0; a < characters.length; a++) {
match.push('(-?[0-9]+)?(\\()?' + characters[a] + '(\\))?');
}
string = string.replace(new RegExp(match.join('')), function(m, m1) {
// m1 holds the first item which is a space
return m1 + '( ' + REPLACE_WITH_THIS + ')';
});
Building on Alex's (and other's) solution, I noticed an issue where some non-text data were becoming corrupted. I tracked this down to encoding/decoding the PDF text as utf-8 instead of as a binary string. Anyways here's a modified solution that:
Avoids corrupting non-text data
Uses streams instead of files
Allows multiple patterns/replacements
Uses the MuhammaraJS package which is a maintained fork of HummusJS (should be able to swap in HummusJS just fine as well)
Is written in TypeScript (feel free to remove the types for JS)
import muhammara from "muhammara";
interface Pattern {
searchValue: RegExp | string;
replaceValue: string;
}
/**
* Modify a PDF by replacing text in it
*/
const modifyPdf = ({
sourceStream,
targetStream,
patterns,
}: {
sourceStream: muhammara.ReadStream;
targetStream: muhammara.WriteStream;
patterns: Pattern[];
}): void => {
const modPdfWriter = muhammara.createWriterToModify(sourceStream, targetStream, { compress: false });
const numPages = modPdfWriter
.createPDFCopyingContextForModifiedFile()
.getSourceDocumentParser()
.getPagesCount();
for (let page = 0; page < numPages; page++) {
const copyingContext = modPdfWriter.createPDFCopyingContextForModifiedFile();
const objectsContext = modPdfWriter.getObjectsContext();
const pageObject = copyingContext.getSourceDocumentParser().parsePage(page);
const textStream = copyingContext
.getSourceDocumentParser()
.queryDictionaryObject(pageObject.getDictionary(), "Contents");
const textObjectID = pageObject.getDictionary().toJSObject().Contents.getObjectID();
let data: number[] = [];
const readStream = copyingContext.getSourceDocumentParser().startReadingFromStream(textStream);
while (readStream.notEnded()) {
const readData = readStream.read(10000);
data = data.concat(readData);
}
const pdfPageAsString = Buffer.from(data).toString("binary"); // key change 1
let modifiedPdfPageAsString = pdfPageAsString;
for (const pattern of patterns) {
modifiedPdfPageAsString = modifiedPdfPageAsString.replaceAll(pattern.searchValue, pattern.replaceValue);
}
// Create what will become our new text object
objectsContext.startModifiedIndirectObject(textObjectID);
const stream = objectsContext.startUnfilteredPDFStream();
stream.getWriteStream().write(strToByteArray(modifiedPdfPageAsString));
objectsContext.endPDFStream(stream);
objectsContext.endIndirectObject();
}
modPdfWriter.end();
};
/**
* Create a byte array from a string, as muhammara expects
*/
const strToByteArray = (str: string): number[] => {
const myBuffer = [];
const buffer = Buffer.from(str, "binary"); // key change 2
for (let i = 0; i < buffer.length; i++) {
myBuffer.push(buffer[i]);
}
return myBuffer;
};
And then to use it:
/**
* Fill a PDF with template data
*/
export const fillPdf = async (sourceBuffer: Buffer): Promise<Buffer> => {
const sourceStream = new muhammara.PDFRStreamForBuffer(sourceBuffer);
const targetStream = new muhammara.PDFWStreamForBuffer();
modifyPdf({
sourceStream,
targetStream,
patterns: [{ searchValue: "home", replaceValue: "emoh" }], // TODO use actual patterns
});
return targetStream.buffer;
};
There is another Node.js Package asposepdfcloud, Aspose.PDF Cloud SDK for Node.js. You can use it to replace text in your PDF document conveniently. Its free plan offers 150 credits monthly. Here is sample code to replace text in PDF document, don't forget to install asposepdfcloud first.
const { PdfApi } = require("asposepdfcloud");
const { TextReplaceListRequest }= require("asposepdfcloud/src/models/textReplaceListRequest");
const { TextReplace }= require("asposepdfcloud/src/models/textReplace");
// Get App key and App SID from https://aspose.cloud
pdfApi = new PdfApi("xxxxx-xxxxx-xxxx-xxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxb");
var fs = require('fs');
const name = "02_pages.pdf";
const remoteTempFolder = "Temp";
//const localTestDataFolder = "C:\\Temp";
//const path = remoteTempFolder + "\\" + name;
//var data = fs.readFileSync(localTestDataFolder + "\\" + name);
const textReplace= new TextReplace();
textReplace.oldValue= "origami";
textReplace.newValue= "aspose";
textReplace.regex= false;
const textReplace1= new TextReplace();
textReplace1.oldValue= "candy";
textReplace1.newValue= "biscuit";
textReplace1.regex= false;
const trr = new TextReplaceListRequest();
trr.textReplaces = [textReplace,textReplace1];
// Upload File
//pdfApi.uploadFile(path, data).then((result) => {
// console.log("Uploaded File");
// }).catch(function(err) {
// Deal with an error
// console.log(err);
//});
// Replace text
pdfApi.postDocumentTextReplace(name, trr, null, remoteTempFolder).then((result) => {
console.log(result.body.code);
}).catch(function(err) {
// Deal with an error
console.log(err);
});
P.S: I'm developer evangelist at aspose.

Serialization-deserialization with Apache Thrift in nodejs

I am working on a Node.js application and I need to serialize and deserialize instances of the structs defined in an .thrift file, like the following:
struct Notification {
1: string subject,
2: string message
}
Now this is easy doable in Java, according to the tutorial at http://www.gettingcirrius.com/2011/03/rabbitmq-with-thrift-serialization.html :
Notification notification = new Notification();
TDeserializer deserializer = new TDeserializer();
deserializer.deserialize(notification, serializedNotification);
System.out.println("Received "+ notification.toString());
But I can't find how this is done using the nodejs library of Thrift. Can anyone help, please?
Ok, after wasting a lot of time on research and trying different solutions, I finally came to the answer to my own question:
//SERIALIZATION:
var buffer = new Buffer(notification);
var transport = new thrift.TFramedTransport(buffer);
var binaryProt = new thrift.TBinaryProtocol(transport);
notification.write(binaryProt);
where notification is the object I wish to serialize. At this point, the byte array can be found in the transport.outBuffers field:
var byteArray = transport.outBuffers;
For deserialization:
var tTransport = new thrift.TFramedTransport(byteArray);
var tProtocol = new thrift.TBinaryProtocol(tTransport);
var receivedNotif = new notification_type.Notification();
receivedNotif.read(tProtocol);
Assuming that the following lines have been added to the index.js file from the nodejs library for thrift:
exports.TFramedTransport = require('./transport').TFramedTransport;
exports.TBufferedTransport = require('./transport').TBufferedTransport;
exports.TBinaryProtocol = require('./protocol').TBinaryProtocol;
Here is my TypeScript version which runs in a browser. npm install buffer before use.
It should work on node if you remove import { Buffer }.
/*
Thrift serializer for browser and node.js
Author: Hirano Satoshi
Usage:
let byteArray = thriftSerialize(thriftObj);
let thriftObj2 = thriftDeserialize(byteArray, new ThriftClass())
let mayBeTrue = byteArrayCompare(byteArray, thriftSerialize(thriftObj2))
*/
import { TBufferedTransport, TFramedTransport, TJSONProtocol, TBinaryProtocol } from 'thrift';
import { Buffer } from 'buffer';
export function thriftSerialize(thriftObj: any): Buffer {
let transport = new TBufferedTransport(null);
let protocol = new TBinaryProtocol(transport);
thriftObj.write(protocol);
// copy array of array into byteArray
let source = transport.outBuffers;
var byteArrayLen = 0;
for (var i = 0, len = source.length; i < len; i++)
byteArrayLen += source[i].length;
let byteArray = new Buffer(byteArrayLen);
for (var i = 0, len = source.length, pos = 0; i < len; i++) {
let chunk = source[i];
chunk.copy(byteArray, pos);
pos += chunk.length;
}
return byteArray;
}
export function thriftDeserialize(byteArray: Buffer, thriftObj: any): any {
let transport = new TBufferedTransport(byteArray);
let callback = (transport_with_data) => {
var proto = new TBinaryProtocol(transport_with_data);
// var proto = new TJSONProtocol(transport);
thriftObj.read(proto);
}
// var buf = new Buffer(byteArray);
TBufferedTransport.receiver(callback)(byteArray);
return thriftObj;
}
export function byteArrayCompare(array1, array2): boolean {
if (!array1 || !array2)
return false;
let val = array1.length === array2.length && array1.every((value, index) => value === array2[index])
return val;
}
Somehow i did not find the the byte array at:
transport.outBuffers
i needed to do the following:
var transport = new Thrift.TFramedTransport(null, function(bytes){
dataWrapper.out = bytes;
cb(dataWrapper)
})
var binaryProt = new Thrift.TCompactProtocol(transport);
notification.write(binaryProt) ;
transport.flush() ; //important without the flush the transport callback will not be invoked

Resources