Thursday, January 5, 2023

How to extract Chinese hardcoded subtitles from video files

Here is a simple way to extract Chinese hardcoded subtitles from video files for free.

It will help to have basic knowledge about ffmpeg. You will need to be patient if your video file has a lot of hardcoded subtitles. You can try this tutorial on a short video clip that has hardcoded subtitles to see how it works. A lot of movie trailers have hardcoded subtitles. 1 - Cropping the video file: Here is is the ffmpeg script to crop the video.mp4 file used in this tutorial. Please adjust parameters according to your video file and note the file extension ffmpeg -i video.mp4 -filter:v "crop=1850:250:0:830" -c:a copy video-cropped.mp4
Note the crop parameter is "crop=Croped Length:Croped Width:Start X:StartY"
2 - Inserting Timestamps into the video file: This is the ffmpeg script to embed timestamps in video-cropped.mp4 ffmpeg -i video-cropped.mp4 -vf "drawtext=text='timestamp\: %{pts \: hms}': x=0: y=2: fontsize=28:fontcolor=yellow: box=1: boxcolor=black" -c:a copy video-cropped-timestamps.mp4
Note the ':' is an argument separator in ffmpeg so if the parameters contains ':' , it should be escaped using '\'
3 - Creating Image Files every second: This is the ffmpeg script to create image files every second from video file video-cropped-timestamps.mp4 ffmpeg -i video-cropped-timestamps.mp4 -start_number 1 -vf fps=1 video-%04d.jpg Note: For a long movie, creating image files every 1 second will generate a huge number of files. You could try creating image files every 2 or 3 seconds or even more.
Here is the script every 2 seconds ffmpeg -i video-cropped-timestamps.mp4 -start_number 1 -vf fps=1/2 video-%04d.jpg Here is the script every 3 seconds ffmpeg -i video-cropped-timestamps.mp4 -start_number 1 -vf fps=1/3 video-%04d.jpg

Note it's better to set to create images every seconds so that those short subtitles will not be missed.

4 - Using Adobe Bridge/ACD See to select images with first appeared text:
Using ctrl-I to flag those images using Adobe Bridge (ACDSee had similar functionality and much smaller than Adobe Bridge) and copy them to different folder
Select 60 images each time and print them into one pdf using Microsoft print to pdf with Unchecked fit pictures to frame option.

Upload the pdf file to Google Drive and open it using Google Docs to do OCR and copy result into text file.
Edit the text file to fix some errors.

5. Using special text editor to block select time frames and text separately and save to different text files and Import the 2 files into Subtitle Edit and combine those repeating and save to srt file.

6. Load the video with hardcoded subtitle into Subtitle Edit with the new edited srt file. Synchronize the new subtitle file with video.

The final text file can be used as subtitle file.

The above tip is from following youtube link but I added my own note above when I applied them:
https://www.youtube.com/watch?v=2o08WUNDUfY

Friday, August 30, 2019

AngularJS Binding Explain

AngularJS has 3 bindings inside a directive. @, =, and &.

@  attribute string binding
 =   two way binding
 &  callback method binding

For @, it is the binding for passing strings. The variables inside the directive's parent scope can pass into the current directive. Using @ looked like the current directive has parameters who was passed by their parent html.

For =, it is the two way model binding, Usually it will be a model which exists both in the current directive's isolated scope and the parent directive's scope. The 2 models are linked together so that any change in one model will be reflected in another model.

For &, it is the methods from the parent scope, so that the current directive can use the method in the parent directive's scope.


https://stackoverflow.com/questions/14050195/what-is-the-difference-between-and-in-directive-scope-in-angularjs

Tuesday, April 23, 2019

Visual Studio Compile error The item "obj\Debug\SampleProject.Forms.MDIMain.resources" was specified more than once in the "Resources" parameter.

When merged develop branch to master branch it could happen that the project file contains repeating items. As result, compiling your visual studio project will have such compile error like:
The item "xxx" was specified more than once in the "Resources" parameter. Duplicate items are not supported by the "Resources" parameter.

The solution is easy:
1. Right click on your project and select "Unload the project"
2. Right click on your project and select "Edit YourProject.csproj"
3. Locate the repeating items and remove them.
4. Reload the project

Then these compile error will be resolved.



Resolve issues while openning old project in Visual Studio 2015/2017

When you opened your old project in Visual studio 2015/2017, you will find a lot of missing packages. When you tried to install any one of those missing packages, the visual studio will warn you that the soltuion is not saved.

How to resolve this?

My easy solution is:

1. Under File, choose save your solution.
2. Open Tools, Nuget Package Manager, Package Manger Console, and enter following command:

Update-Package –reinstall Microsoft.AspNet.Web.Optimization

Note Microsoft.AspNet.Web.Optimization can be any one of your missing package, then all missing package will be reinstalled automatically.

Reference:
https://developercommunity.visualstudio.com/content/problem/40958/update-package-reinstall-forces-all-packages-to-re.html

Thursday, March 28, 2019

How to upload local project from windows to github

First you need to install Git for Windows, once it's done, using Command Prompt and enter following commands:

cd <the folder>
git init
git add .   //for all files under the folder
commit -m "adding files" git remote add origin https://github.com/<your-user-name>/<your-repository-name>.git
git push -u origin master

how-to-upload-a-project-to-github

Tuesday, May 22, 2018

Process with an ID #### is not running on Visual Studio 2015

When I installed both Visual Studio 2013 and 2015 on my computer,  and use 2013 to open my visual studio web project and doing my test/run at first without problem, and then use 2015 to open the same project and choose to debug, there is error "Process with an ID #### is not running" on Visual Studio 2015. I google the issue and there is following solution:

  1. Delete the \Documents\IISExpress folder using the following console command:
  2. Delete the applicationhost.config file which is placed within the \.vs\Config\ folder in your Visual Studio project root folder.
I tried both and the issue is gone. I think it's because 2013 had created some files which was used by 2015 but have issues.

https://www.ryadel.com/en/process-id-not-running-visual-studio-2015-fix/

Tuesday, May 8, 2018

Git Bitbucket Stage VS Unstage

According to Johannes Kilian, "the staging area is a container where git collects all changes which will be part of the next commit". Inside bitbucket, when you staging the file, you put the uncommited file into the staging area. "The next git commit will transfer all items from staging into your repository".

When you have multiple bugs to be fixed, you don't want to commit all your changes, you select the code you want to commit and do staging them. Through this way you will have a clean commit specific for one bug.

You can choose complete files, hunks (part of one file), or single line for staging.

https://community.atlassian.com/t5/Sourcetree-questions/Staged-vs-Unstaged/qaq-p/127916

Monday, April 23, 2018

UNIT TEST in ASP.Net MVC Part II - How to handle App_GlobalResources


Traditional ASP.Net approaches to Localization and Internationaliazation is to use one special App_GlobalResources folder to hold resource files and Visual Studio will use GlobalResourceProxyGenerator to generate a strongly typed internal class to wrap all internal resources.

Note those global resources are not embeded inside the project dll file. They actually were stored inside one special assembly named App_GlobalResources.dll so that the ASP.Net views can reference them. This is fine but the problem is the special assembly was created by ASP.Net runtime, there is no such assembly during the compilation, thus when you tried to execute your unit testing against the resources, following error will happen:

"Could not load file or assembly 'App_GlobalResources' or one of its dependencies. The system cannot find the file specified.":"App_GlobalResources" System.IO.IOException {System.IO.FileNotFoundException}

The solution is to create one custom folder for your MVC project and put your resource files to that folder. For each resource file, update the file property as following:


The reason to change the Custom Tool to PublicResXFileCodeGenerator is because this tool will make the resource file available for your ASP.Net views. Alternately, you can also set the resource file to public inside the resource editor to do the same job.

Resource Files and ASP.NET MVC Projects

Friday, April 20, 2018

UNIT TEST in ASP.Net MVC Part I - How to add Unit Test project and testing private or internal properties

Unit testing can greatly improve your confidence on your own code. Visual Studio has included unit testing framework since version 2015. You can add unit test project through following steps: right click on your solution, and choose Add, and choose New Projcet, Visual Studio will popup a window of model project, choose test under c# project on the left pane,  an unit test project will be added into your solution.

After you added the test project, you need to Add Reference for your project which has the object to be tested. Note if the attributes of the object you want to test are set as Internal, when you are trying to reference your object to test, system will promot following error:

'MyTest' is inaccessible due to its protection level

You need a special trick because your test project can't see the internal attribute of another project.

using System.Runtime.CompilerServices;

[assembly:InternalsVisibleTo("MyTests")]
Add the last one to the project info file, e.g. Properties\AssemblyInfo.cs,for the project which has the object you want to test, while the "MyTests" is the name of your unit test project assembly. You can find your unit test project assembly name inside your unit test project info file, namely, AssemblyInfo.cs.

Note if you still experienced the error of "inaccessible due to protection level", you need to check the test project properties, in most cases, the project assembly name is different than what you thought. 

Now your unit test project can see the object to be tested.

Note if you still get
You can add

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting; 
using YourApplicationToTest;

namespace YourTest
{
    [TestClass]
    public class UnitTestl
    {
        [TestMethod]
        public void TestMethodl()
        {
            YourClassToTest ct= new YourClassToTest();
            Assert.AreEqual( ct.Name, "I need test!" );
        }
    }
}
Note if you want to test the private methods or attributes of your testing object, please use following:
Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual((int)obj.GetField("PrivateAttribute"), 0);
Assert.AreEqual(retVal, expectedVal);
For test the private static attributes of your object, use following:
PrivateType myType = new PrivateType(typeof(MyStaticClass));

myType.SetStaticFieldOrProperty("isMyTypeAttribute", false);
UNIT TESTING in Asp.Net: Complete Tutorial
C# “internal” access modifier when doing unit testing
InternalsVisibleTo attribute isn't working
Unit testing private methods in C#
InternalsVisibleToAttribute Class
How to access a Static Class Private fields to unit test its methods using Microsoft Fakes in C#
Unit testing in C# of private-static method accepting other private-static method as a delegate parameter

Wednesday, April 18, 2018

How to create a Restful API with authentication in Jwt

Asp.Net page usually do authentication and authorization with session. However, Restful means stateless, for doing so, we need to use Restful HTTP services to do authentication and authorization.

Sunday, March 12, 2017

A good start to learn Swift 3


I found Mac is much better to be used than windows system. The mouse is super good without wheel as the top part of mouse is actually touchable. To do copy/paste you need to use cmd key + C/V.

Following note are from what I learned for swift 3:

Swift simplified memory management using ARC - Automatic Reference Counting.

Using let for constant, var is used for variable:

let oneMillion = 1_000_000
You can use min and max properties for integer type to get maximum and minimum value.

// 定义类型别名 typealias
typealias AudioSample = UInt16 

// optional binding,只有当yyy是optional的时候才可以这样用。optional的yyy非空时为真,将yyy中的值取出赋给xxx,空时(nil)为假;

if let xxx = yyy {
     // do something
} else {
     // do other thing
}

// decompose一个tuple时,对于不想使用的元素用’_’接收
let http404Error = (404, "Not Found")
let (justTheStatusCode, _) = http404Error
println("The status code is \(justTheStatusCode)")
// prints "The status code is 404

let possibleNumber = "123"
let convertedNumber = possibleNumber.toInt()
// convertedNumber is inferred to be of type "Int?", or "optional Int”,因为toInt()可能会失败(比如“123a”)导致返回nil
!== and === are identity operators and are used to determine if two objects have the same reference.
var arr1 = [1, 2, 3]
var arr2 = arr1
arr2[0] = 10;
arr1     // [10, 2, 3]
arr2     // [10, 2, 3]
arr1 === arr2  // true


把《The Swift Programming Language》读薄

Introducing Firebase with Swift 3: Login and Sign Up

Monday, February 27, 2017

Chartjs V2 Add Custom Legend and Show/Hide Chart Data through Click

To add interactive feature for Chartjs version such as select/unselect group of data, you can use chart.metadata.hidden attribute. For doghnut chart, the hidden attribute can work with datasets and data both. However, for line and bar chart, only datasets.hidden is effective. To work with line and bar chart, you need to manually change data.

Following method can be used to add the interactive feature with Custom Legend for bar/line/doghnut charts:

//add custom legends
var addCustomLegends = function (canvas, data) {
    var legendLabel = canvas.replace('canvas', 'legendLabel');
    var legendNameBase = canvas.replace('canvas', 'legend');
    $('#' + canvas).before('<div id="' + legendLabel + '" style="width:100%;text-align:center;padding-left:25px;padding-right:25px;"></div>');
    var divWidth = 75 / data.labels.length;
    var innerHTMLStr = '';
    for (var i = 0; i < data.labels.length; i++) {
        var clickEvents = '';
        clickEvents += 'hideChartLabels(event,\'' + i + ',' + data.labels.length
            + ', \'#datalist\');'; //don't put space after comma inside the string presenting as array
    }
    innerHTMLStr += '<div style="display:inline-block; height:12px; text-align:center; vertical-align:middle; margin-right:5px; background:' + gbService.getColor('lightgray') + '; border:1px solid rgba(0, 0, 0, .2); width: '
        + '12px;"></div><div id="' + legendNameBase + i + '" style="display:inline-block; text-align:center; vertical-align:top; padding-right:10px; width=' + divWidth + '"% '
        + ' onclick ="' + clickEvents + '">'
        + data.labels[i] + '</div>';
    }
    document.getElementById(legendLabel).innerHTML = innerHTMLStr;
    if (legendStatus != null && legendStatus.length == data.labels.length) {
        for (var i = 0; i < data.labels.length; i++) {
            if (legendStatus[i] !== 'none') { //hidden the data
                $('#' + legendNameBase + i).click();
            }
        }
    }
}

var hideChartLabels = function (e, idName, index, length) {
    var idNameBase = idName;
    if (index < 10) {
        idNameBase = idNameBase.substring(0, idNameBase.length - 1);
    }
    else {
        idNameBase = idNameBase.substring(0, idNameBase.length - 2);
    }

    if (chart == null || chart == undefined) return;
    var data = chart.data;

    //draw the line-through to show the data is hidding
    if ($('#' + idNameBase + index).css("text-decoration") == "none") {
        $('#' + idNameBase + index).css({ "text-decoration": "line-through" });
    }
    else {
        $('#' + idNameBase + index).css({ "text-decoration": "none" });
    }

    //set hidden attribute for meta data
    for (var i = 0; i < data.datasets.length; i++) {
        updateChartDatasetItem(window[chart], i, index);
    }

    chart.update();
};

var updateChartDatasetItem = function (ci, index, elementIndex) {
    var meta = ci.getDatasetMeta(index);
    if (meta.dataset) {
        meta.hidden = !meta.hidden; //hidden whole dataset
    }
    else {
        meta.data[elementIndex].hidden = !meta.data[elementIndex].hidden;
    }
}

Reference:
https://github.com/chartjs/Chart.js/issues/2565


Friday, October 14, 2016

How to run R script under SQL Server 2016

SQL Server 2016 has one new feature: R services. This is a good news for data scientist. You can choose install SQL Server 2016 express version with advanced service, or you can also choose to upgrade your current SQL Server older express version to 2016 express to use the new feature. Note after you finished your installation, you need to manually install the feature using SQL Server 2016 installation program.

After you finished the installation, you need to lauch SQL Server Management Service 2016, and run the following script:
Exec sp_configure  'external scripts enabled', 1  
Reconfigure  with  override  
After running this script, you need to restart your SQL Server Database instance, and run the following script:
Exec sp_configure  'external scripts enabled'  
After running the above script, it should get the following result:
 
After finish above configuration, you need to manually restart SQL Server LauchPad service:
To restart the LauchPad service, please go to Windows Administrative Tools -> Services and find "SQL Server LauchPad (Your DB Name)"


Now the last step to test if you can run R script:
exec sp_execute_external_script  @language =N'R',  
@script=N'OutputDataSet<-InputDataSet',    
@input_data_1 =N'select 1 as hello'  
with result sets (([hello] int not null));  
go  
Expected Results:

hello 1

If experience the following error, you need to restart the LauchPad service.

Msg 39011, Level 16, State 1, Line 1
SQL Server was unable to communicate with the LaunchPad service. Please verify the configuration of the service.


Reference:
https://msdn.microsoft.com/en-us/library/mt696069.aspx
http://dba.stackexchange.com/questions/120205/msg-39011-sql-server-was-unable-to-communicate-with-the-launchpad-service

Wednesday, August 31, 2016

How to install window service


A window service need to be installed at first. Under Windows 10, click on window button, type dev inside the search input, and choose Developer Command Prompt for VS2012which is the service installer named installutil.exe.

Note, when you applying installutil to install window service, you need to run it with Administrator privilege.

Then go to the directory of compiled service, usually it's inside directory of bin/debug, and type
installutil yourservice.exe.

Your service will be installed successfully.

Reference:
http://www.c-sharpcorner.com/uploadfile/naresh.avari/develop-and-install-a-windows-service-in-c-sharp/

Tuesday, August 9, 2016

Integrated R.Net within your ASP.Net MVC applicaiton

R is a programming language used for statistical computing. For business intelligence, R is very useful for prediction marketing. To better integrating R into Microsoft .Net system, there is one tool named R.Net. You can use NuGet to install R.Net to your .net project. The latest version of R.Net is 1.6.5. You can use the following command to install the latest R.Net:
Install-Package R.NET.Community
Note R.Net didn't set the environment variable correctly. So the first issue you suffered for R.Net would be package installation.
REngine r = REngine.GetInstance();
r.Evaluate("library(RODBC)");
An unhandled exception of type 'RDotNet.EvaluationException' occurred in RDotNet.dll
Additional information: Error: package or namespace load failed for 'RODBC'
This is happened because R.net can't find r.dll as the Path doesn't included. To set the correct Path under windows 10, click on windows button, choose Settings, click on System, choose About from the left menu, scroll down to the bottom, click on System Info, choose Advanced System Setting from the left menu, choose the Tab of Advanced, click on Environment Variables button, then click on Edit button, click on New button, add following path:
C:\\Program Files\\R\\R-3.3.1\\bin\\i386
After this, restart windows 10, you should resolve this issue. Note other resources prefer to add more paths such as library, it's wrong!!! It will affect some packages installation.


Reference:
https://www.nuget.org/packages/R.NET.Community/
http://jmp75.github.io/rdotnet/ts_asp_dot_net/




Friday, June 24, 2016

How to align an image center inside div


HTML Blocks are elements established as a block-level element, which are created by using the <div> element.

The most common way to center blocks with CSS is to set both the left and right margins to auto. Here is the CSS:

<style>
  div.center {
    margin: auto;
  }
</style>
The most common way to center blocks with CSS is to set both the left and right margins to auto. Here is the CSS:

Reference:
https://www.w3.org/Style/Examples/007/center.en.html

Thursday, June 23, 2016

How to vertical align an image inside an anchor

To vertical align an image inside an anchor, there are 3 necessary parts inside the CSS:

1. Your anchor should have same  height and inline-height.
2. Your image should have vertical-align: middle;
3. Your image should have display: inline-block;

<style>
    .thumbnail {
        width: 150px;
        height: 150px;
        text-align: center;
        line-height: 150px;
    }
    .thumbnail img {
        margin: auto;
        vertical-align: middle; 
    }  
</style>
Reference:
http://stackoverflow.com/questions/20700475/vertical-align-image-inside-an-anchor-with-css
http://plnkr.co/edit/b5jEtK5EWVglrhEkj14e?p=preview

Thursday, June 16, 2016

jsPDF AutoTable Plugin examples

Bundle.cfg
bundles.Add(new ScriptBundle("~/bundles/jsPDF").Include(
    "~/Scripts/plugins/jsPDF/jspdf.js",
    "~/Scripts/plugins/jsPDF/jspdf.plugin.text-align.js",
    "~/Scripts/plugins/jsPDF/jspdf.plugin.autotable.js"));
jspdf.plugin.text-align.js
(function (api, $) {
    'use strict';
    api.writeText = function (x, y, text, options) {
        options = options || {};
        var defaults = {
            align: 'left',
            width: this.internal.pageSize.width
        }

        var settings = $.extend({}, defaults, options);

        // Get current font size
        var fontSize = this.internal.getFontSize();

        // Get the actual text's width
        /* You multiply the unit width of your string by your font size and divide
         * by the internal scale factor. The division is necessary
         * for the case where you use units other than 'pt' in the constructor
         * of jsPDF.
         */
        var txtWidth = this.getStringUnitWidth(text) * fontSize / this.internal.scaleFactor;

        if (settings.align === 'center')
            x += (settings.width - txtWidth) / 2;
        else if (settings.align === 'right')
            x += (settings.width - txtWidth);

        //default is 'left' alignment
        this.text(text, x, y);
    }
})(jsPDF.API, jQuery);
jsPDF example
/*
 |--------------------------------------------------------------------------
 | This file contains examples of how to use this plugin
 |--------------------------------------------------------------------------
 |
 | To see what the pdfs generated by these examples looks like you can open
 | ´examples.html´ or go to http://simonbengtsson.github.io/jsPDF-AutoTable.
 |
 | To make it possible to view each example in examples.html some extra code
 | are added to the examples below. For example they return their jspdf
 | doc instance and gets generated data from the library faker.js. However you
 | can of course use this plugin how you wish and the simplest first example
 | below would look like this without any extras:
 |
 | var columns = ["ID", "Name", "Age", "City"];
 |
 | var data = [
 |     [1, "Jonatan", 25, "Gothenburg"],
 |     [2, "Simon", 23, "Gothenburg"],
 |     [3, "Hanna", 21, "Stockholm"]
 | ];
 |
 | var doc = new jsPDF('p', 'pt');
 | doc.autoTable(columns, data);
 | doc.save("table.pdf");
 |
 */

var examples = {};

// Default - shows what a default table looks like
examples.auto = function () {
    var doc = new jsPDF('p', 'pt');
    doc.autoTable(getColumns(), getData());
    return doc;
};

// Minimal - shows how compact tables can be drawn
examples.minimal = function () {
    var doc = new jsPDF('p', 'pt');
    doc.autoTable(getColumns(), getData(), {
        tableWidth: 'wrap',
        styles: {cellPadding: 2},
        headerStyles: {rowHeight: 15, fontSize: 8},
        bodyStyles: {rowHeight: 12, fontSize: 8, valign: 'middle'}
    });
    return doc;
};

// Long data - shows how the overflow features looks and can be used
examples.long = function () {
    var doc = new jsPDF('l', 'pt');
    var columnsLong = getColumns().concat([
        {title: shuffleSentence(), dataKey: "text"},
        {title: "Text with a\nlinebreak", dataKey: "text2"}
    ]);

    doc.text("Overflow 'ellipsize' (default)", 10, 40);
    doc.autoTable(columnsLong, getData(), {
        startY: 55,
        margin: {horizontal: 10},
        columnStyles: {text: {columnWidth: 250}}
    });

    doc.text("Overflow 'hidden'", 10, doc.autoTableEndPosY() + 30);
    doc.autoTable(columnsLong, getData(), {
        startY: doc.autoTableEndPosY() + 45,
        margin: {horizontal: 10},
        styles: {overflow: 'hidden'},
        columnStyles: {email: {columnWidth: 160}}
    });

    doc.text("Overflow 'linebreak'", 10, doc.autoTableEndPosY() + 30);
    doc.autoTable(columnsLong, getData(3), {
        startY: doc.autoTableEndPosY() + 45,
        margin: {horizontal: 10},
        styles: {overflow: 'linebreak'},
        bodyStyles: {valign: 'top'},
        columnStyles: {email: {columnWidth: 'wrap'}},
    });

    return doc;
};

// Content - shows how tables can be integrated with any other pdf content
examples.content = function () {
    var doc = new jsPDF('p', 'pt');

    doc.setFontSize(18);
    doc.text('A story about Miusov', 40, 60);
    doc.setFontSize(11);
    doc.setTextColor(100);
    var text = doc.splitTextToSize(shuffleSentence(faker.lorem.words(55)) + '.', doc.internal.pageSize.width - 80, {});
    doc.text(text, 40, 80);

    var cols = getColumns();
    cols.splice(0, 2);
    doc.autoTable(cols, getData(40), {startY: 150});

    doc.text(text, 40, doc.autoTableEndPosY() + 30);

    return doc;
};

// Multiple - shows how multiple tables can be drawn both horizontally and vertically
examples.multiple = function () {
    var doc = new jsPDF('p', 'pt');
    doc.setFontSize(22);
    doc.text("Multiple tables", 40, 60);
    doc.setFontSize(12);

    doc.autoTable(getColumns().slice(0, 3), getData(), {
        startY: 90,
        pageBreak: 'avoid',
        margin: {right: 305}
    });

    doc.autoTable(getColumns().slice(0, 3), getData(), {
        startY: 90,
        pageBreak: 'avoid',
        margin: {left: 305}
    });

    for (var j = 0; j < 6; j++) {
        doc.autoTable(getColumns(), getData(9), {
            startY: doc.autoTableEndPosY() + 30,
            pageBreak: 'avoid',
        });
    }

    return doc;
};

// From html - shows how pdf tables can be be drawn from html tables
examples.html = function () {
    var doc = new jsPDF('p', 'pt');
    doc.text("From HTML", 40, 50);
    var res = doc.autoTableHtmlToJson(document.getElementById("basic-table"));
    doc.autoTable(res.columns, res.data, {startY: 60});
    return doc;
};

// Header and footers - shows how header and footers can be drawn
examples['header-footer'] = function () {
    var doc = new jsPDF('p', 'pt');

    var header = function (data) {
        doc.setFontSize(20);
        doc.setTextColor(40);
        doc.setFontStyle('normal');
        doc.addImage(headerImgData, 'JPEG', data.settings.margin.left, 40, 25, 25);
        doc.text("Report", data.settings.margin.left + 35, 60);
    };

    var totalPagesExp = "{total_pages_count_string}";
    var footer = function (data) {
        var str = "Page " + data.pageCount;
        // Total page number plugin only available in jspdf v1.0+
        if (typeof doc.putTotalPages === 'function') {
            str = str + " of " + totalPagesExp;
        }
        doc.text(str, data.settings.margin.left, doc.internal.pageSize.height - 30);
    };

    var options = {
        beforePageContent: header,
        afterPageContent: footer,
        margin: {top: 80}
    };
    doc.autoTable(getColumns(), getData(40), options);

    // Total page number plugin only available in jspdf v1.0+
    if (typeof doc.putTotalPages === 'function') {
        doc.putTotalPages(totalPagesExp);
    }

    return doc;
};

// Themes - shows how the different themes looks
examples.themes = function () {
    var doc = new jsPDF('p', 'pt');
    doc.setFontSize(12);
    doc.setFontStyle('bold');

    doc.text('Theme "striped"', 40, 50);
    doc.autoTable(getColumns(), getData(), {startY: 60});

    doc.text('Theme "grid"', 40, doc.autoTableEndPosY() + 30);
    doc.autoTable(getColumns(), getData(), {startY: doc.autoTableEndPosY() + 40, theme: 'grid'});

    doc.text('Theme "plain"', 40, doc.autoTableEndPosY() + 30);
    doc.autoTable(getColumns(), getData(), {startY: doc.autoTableEndPosY() + 40, theme: 'plain'});

    return doc;
};

// Horizontal - shows how tables can be drawn with horizontal headers
examples.horizontal = function () {
    var doc = new jsPDF('p', 'pt');
    doc.autoTable(getColumns().splice(1,4), getData(), {
        drawHeaderRow: function() {
            // Don't draw header row
            return false;
        },
        columnStyles: {
            first_name: {fillColor: [41, 128, 185], textColor: 255, fontStyle: 'bold'}
        }
    });
    return doc;
};


// Custom style - shows how custom styles can be applied to tables
examples.custom = function () {
    var doc = new jsPDF('p', 'pt');
    doc.autoTable(getColumns().slice(2, 6), getData(20), {
        styles: {
            font: 'courier',
            fillStyle: 'DF',
            lineColor: [44, 62, 80],
            lineWidth: 2
        },
        headerStyles: {
            fillColor: [44, 62, 80],
            fontSize: 15,
            rowHeight: 30
        },
        bodyStyles: {
            fillColor: [52, 73, 94],
            textColor: 240
        },
        alternateRowStyles: {
            fillColor: [74, 96, 117]
        },
        columnStyles: {
            email: {
                fontStyle: 'bold'
            }
        },
        createdCell: function (cell, data) {
            if (data.column.dataKey === 'expenses') {
                cell.styles.halign = 'right';
                if (cell.raw > 600) {
                    cell.styles.textColor = [255, 100, 100];
                    cell.styles.fontStyle = 'bolditalic';
                }
                cell.text = '$' + cell.text;
            } else if (data.column.dataKey === 'country') {
                cell.text = cell.raw.split(' ')[0];
            }
        }
    });
    return doc;
};

// Custom style - shows how custom styles can be applied to tables
examples.spans = function () {
    var doc = new jsPDF('p', 'pt');
    doc.setFontSize(12);
    doc.setTextColor(0);
    doc.setFontStyle('bold');
    doc.text('Col and row span', 40, 50);
    var data = getData(20);
    data.sort(function (a, b) {
        return parseFloat(b.expenses) - parseFloat(a.expenses);
    });
    doc.autoTable(getColumns(), data, {
        theme: 'grid',
        startY: 60,
        drawRow: function (row, data) {
            // Colspan
            doc.setFontStyle('bold');
            doc.setFontSize(10);
            if (row.index === 0) {
                doc.setTextColor(200, 0, 0);
                doc.rect(data.settings.margin.left, row.y, data.table.width, 20, 'S');
                doc.autoTableText("Priority Group", data.settings.margin.left + data.table.width / 2, row.y + row.height / 2, {
                    halign: 'center',
                    valign: 'middle'
                });
                data.cursor.y += 20;
            } else if (row.index === 5) {
                doc.rect(data.settings.margin.left, row.y, data.table.width, 20, 'S');
                doc.autoTableText("Other Groups", data.settings.margin.left + data.table.width / 2, row.y + row.height / 2, {
                    halign: 'center',
                    valign: 'middle'
                });
                data.cursor.y += 20;
            }
        },
        drawCell: function (cell, data) {
            // Rowspan
            if (data.column.dataKey === 'id') {
                if (data.row.index % 5 === 0) {
                    doc.rect(cell.x, cell.y, data.table.width, cell.height * 5, 'S');
                    doc.autoTableText(data.row.index / 5 + 1 + '', cell.x + cell.width / 2, cell.y + cell.height * 5 / 2, {
                        halign: 'center',
                        valign: 'middle'
                    });
                }
                return false;
            }
        }
    });
    return doc;
};

/*
 |--------------------------------------------------------------------------
 | Below is some helper functions for the examples
 |--------------------------------------------------------------------------
 */

// Returns a new array each time to avoid pointer issues
var getColumns = function () {
    return [
        {title: "ID", dataKey: "id"},
        {title: "Name", dataKey: "first_name"},
        {title: "Email", dataKey: "email"},
        {title: "City", dataKey: "city"},
        {title: "Country", dataKey: "country"},
        {title: "Expenses", dataKey: "expenses"}
    ];
};

// Uses the faker.js library to get random data.
function getData(rowCount) {
    rowCount = rowCount || 4;
    var sentence = faker.lorem.words(12);
    var data = [];
    for (var j = 1; j <= rowCount; j++) {
        data.push({
            id: j,
            first_name: faker.name.findName(),
            email: faker.internet.email(),
            country: faker.address.country(),
            city: faker.address.city(),
            expenses: faker.finance.amount(),
            text: shuffleSentence(sentence),
            text2: shuffleSentence(sentence)
        });
    }
    return data;
}

function shuffleSentence(words) {
    words = words || faker.lorem.words(8);
    var str = faker.helpers.shuffle(words).join(' ').trim();
    return str.charAt(0).toUpperCase() + str.slice(1);
}

// Use http://dopiaza.org/tools/datauri or similar service to convert an image into image data
var headerImgData = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4g';
References:
https://simonbengtsson.github.io/jsPDF-AutoTable/#custom
https://github.com/simonbengtsson/jsPDF-AutoTable/tree/master/examples

Monday, June 13, 2016

How to use PDFMake to generate PDF under IE, Safari, and Android

PDFMake can be used to create pdf on client side. It worked nice under Chrome and Firefox, however, it doesn't work under IE, Safari, and Android. The reason it doesn't work because PDFMake simply created the pdf using a popup window to display it.

So to make it work, another javascript lib called PDF.js can be used. The PDF.js itself is a pdf viewer, and we can create a modal dialog to contain the pdf viewer, instead of using popup window.

Note PDF.js is not working with Safari, to get it working on Safari,
  1. compatibility.js must be included.
  2. PDFJS.workerSrc must be assigned.
<script type="text/javascript" src="compatibility.js"></script>
<script type="text/javascript" src="pdf.js"></script>

<!-- NEED THIS for Safari Mac to render work -->
<script type="text/javascript">
    // Specify the main script used to create a new PDF.JS web worker.  
    // In production, change this to point to the combined `pdf.js` file.  
    PDFJS.workerSrc = 'pdf.worker.js';  
</script>
HTML part:
The pdf.js actually use modal to display pdf content.
<div id="containerPDFViewer" class="modal modal-info fade" role="dialog">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
  <button type="button" class="close" data-dismiss="modal">&times;</button>
  <h4 class="modal-title">@GeneralResources.PDFViewer</h4>
            </div>
            <div class="modal-body">
  <canvas id="pdfviewer"></canvas>
            </div>
        </div>
        <!-- /.modal-content -->
    </div>
    <!-- /.modal-dialog -->
</div>
Javascript part:
//save the pdf into base64 strings
var pdfstr;
try {
    pdfMake.createPdf(docDefinition).getDataUrl(function (result) {   
        pdfstr = result;
        var pdfAsArray = convertDataURIToBinary(pdfstr);
        PDFJS.getDocument(pdfAsArray).then(function getPdf(pdf) {
            //  
            // Fetch the first page
            //  
            pdf.getPage(1).then(function getPdfPage(page) {
                var scale = 1.4;
                var viewport = page.getViewport(scale);
                //
                // Prepare canvas using PDF page dimensions
                //
                var canvas = $("#pdfviewer").get(0);
                var context = canvas.getContext('2d');
                canvas.height = viewport.height;
                canvas.width = viewport.width;           
                //
                // Render PDF page into canvas context
                //
                var renderContext = {
                    canvasContext: context,
                    viewport: viewport
                };
                page.render(renderContext);
                $('#containerPDFViewer').modal({ backdrop: 'static' }); //disable backdrop so user need to make choice
            });
        });
    });
}
catch (e) {     
    throw e;
}

var BASE64_MARKER = ';base64,';
function convertDataURIToBinary(dataURI) {
    var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
    var base64 = dataURI.substring(base64Index);
    var raw = window.atob(base64);
    var rawLength = raw.length;
    var array = new Uint8Array(new ArrayBuffer(rawLength));

    for (var i = 0; i < rawLength; i++) {
        array[i] = raw.charCodeAt(i);
    }
    return array;
}

Note the 1st parameter of getDataUrl is a call back function, will will be executed after getDataUrl is done. We need to put pdf.js code inside the call back function.

Here we didn't use getBase64 because it only return Base64 string, and getDataUrl actually returned Base64 string with leading pdf string. I have tested the code, the getBase64 failed to call convertDataURIToBinary() as there is base64 code error, but getDataUrl is OK.

Note as I tested, the pdf content will be displayed within the modal, but the effect is not as good as normal pdf viewer. In addition, even pdf.js still has issue in IE and Safari.

As result, for supporting IE and Safari, we should not use PDFMake and pdf.js.


References:
http://gonehybrid.com/how-to-create-and-display-a-pdf-file-in-your-ionic-app/
http://stackoverflow.com/questions/17022052/pdf-js-not-working-with-safari
http://stackoverflow.com/questions/12092633/pdf-js-rendering-a-pdf-file-using-a-base64-file-source-instead-of-url
https://developer.tizen.org/community/tip-tech/displaying-pdf-files-pdf.js-library