Problem with Viewier (HTML) mode

I am using Groupdocs.Viewer 20.8 and till now I had set htmlMode: false in my configuration.yml file but one of the client required html mode for viewing the document we changed it to htmlMode: true but the problem is the viewer is not stable in html mode for example I had uploaded a pdf document
Akash Choudhary resume.pdf (187.7 KB)

and it is fine but when I upload the document in the viewer it is not showing properly, below is the image for your reference.
Viewer problem_1.png (124.4 KB)

Viewer problem.png (122.7 KB)

and I am also sharing the temp file generated
Docs.zip (296.5 KB)

Please have a look and please help me on this.

thanks in advance

@Niteen_Jadhav

Thank you for adding screenshots and output files. This issue happens because HTML document external resources (fonts, images, css) can’t be loaded due to incorrect links in the HTML file:

while you’re using different endpoint for your application.

There are a couple of solutions here:

  1. Enable rendering to HTML with embedded resources. In your code find ForExternalResources and replace it with ForEmbeddedResources e.g.

    from

HtmlViewOptions htmlViewOptions = HtmlViewOptions.ForExternalResources(
    pageNumber =>
    {
        string fileName = $"p{pageNumber}.html";
        string cacheFilePath = this.cache.GetCacheFilePath(fileName);

        return File.Create(cacheFilePath);
    },
    (pageNumber, resource) =>
    {
        string fileName = $"p{pageNumber}_{resource.FileName}";
        string cacheFilePath = this.cache.GetCacheFilePath(fileName);

        return File.Create(cacheFilePath);
    },
    (pageNumber, resource) =>
    {
        var urlPrefix = "/viewer/resources/" + Path.GetFileName(this.filePath).Replace(".", "_");
        return $"{urlPrefix}/p{ pageNumber}_{ resource.FileName}";
    });

to

HtmlViewOptions htmlViewOptions = HtmlViewOptions.ForEmbeddedResources(
    pageNumber =>
    {
        string fileName = $"p{pageNumber}.html";
        string cacheFilePath = this.cache.GetCacheFilePath(fileName);

        return File.Create(cacheFilePath);
    });
  1. Adjust resource path. Same as in previous case navigate to ForExternalResources and adjust the third delegate that controls resource path.
(pageNumber, resource) =>
{
    var urlPrefix = "/viewer/resources/" + Path.GetFileName(this.filePath).Replace(".", "_");
    return $"{urlPrefix}/p{ pageNumber}_{ resource.FileName}";
}

Please make sure to cleanup the cache after you applied the changes.

Let us know if it works for you.

Thank you for your timely response below is my current code

private HtmlViewOptions CreateHtmlViewOptions(int passedPageNumber = -1, int newAngle = 0)
{
    HtmlViewOptions htmlViewOptions = HtmlViewOptions.ForExternalResources(
        pageNumber =>
        {
            string fileName = $"p{pageNumber}.html";
            string cacheFilePath = this.cache.GetCacheFilePath(fileName);

            return File.Create(cacheFilePath);
        },
        (pageNumber, resource) =>
        {
            string fileName = $"p{pageNumber}_{resource.FileName}";
            string cacheFilePath = this.cache.GetCacheFilePath(fileName);

            return File.Create(cacheFilePath);
        },
        (pageNumber, resource) =>
        {
            var urlPrefix = "/viewer/resources/" + Path.GetFileName(this.filePath).Replace(".", "_");
            return $"{urlPrefix}/p{ pageNumber}_{ resource.FileName}";
        });

    htmlViewOptions.SpreadsheetOptions.TextOverflowMode = TextOverflowMode.HideText;
    htmlViewOptions.SpreadsheetOptions.SkipEmptyColumns = true;
    htmlViewOptions.SpreadsheetOptions.SkipEmptyRows = true;
    SetWatermarkOptions(htmlViewOptions);

    if (passedPageNumber >= 0 && newAngle != 0)
    {
        Rotation rotationAngle = GetRotationByAngle(newAngle);
        htmlViewOptions.RotatePage(passedPageNumber, rotationAngle);
    }

    return htmlViewOptions;
}

Can you please help me about what should be done here?

Hello I tried with you example and I am still facing the same problem

My updated code →

var urlPrefix = GD.Utilities.StoragePath + "/cache/" + Path.GetFileName(this.filePath).Replace(".", "_");
return $"{urlPrefix}/p{pageNumber}_{fileName}";

Now the problem is when I open my .html file it is looking fine(Images and css executing)

Working on html.png (140.8 KB)

but when I open it from viewer still same problem

Here is the updated zip file.

cache.zip (296.8 KB)

@Niteen_Jadhav

Instead of using GD.Utilities.StoragePath you want to use your application’s URL. In your case it looks like to be /Viewer/. Make sure that you have this method that is going to return a resource file e.g.

[HttpGet]
[Route("resources/{guid}/{resourceName}")]
public HttpResponseMessage GetResource(string guid, string resourceName)

Do you have this method in your code?

No I don’t have this method

@Niteen_Jadhav

See this method as an example. Or switch to rendering to HTML with embedded resources:

HtmlViewOptions htmlViewOptions = HtmlViewOptions.ForEmbeddedResources(
    pageNumber =>
    {
        string fileName = $"p{pageNumber}.html";
        string cacheFilePath = this.cache.GetCacheFilePath(fileName);

        return File.Create(cacheFilePath);
    });

Can you please explain me what is the problem with GD.Utilities.StoragePath code as it is properly redirecting to the particular resource file. and how can I use GetResource as my method CreateHtmlViewOptions is located in class HtmlViewer : IDisposable, ICustomViewer class

@Niteen_Jadhav

When you’re using GD.Utilities.StoragePath for external resources in HTML document browser security will block the requests from your web application to the local disc. Resources are loaded correctly when you open HTML document directly because it is loaded from a local disc, so the resources can also be loaded from a local disc.
You can check this by opening the developer console in a browser, in Chrome you can press F12 and check the Network tab.

The method GetResource solves this issue as it is responsible for serving the resources from your application’s endpoint which is considered safe from a browser’s perspective.

I understood what you are trying to say but the problem is this is how the method are getting executed →

[HttpPost]
[Route("viewer/loadDocumentPage")]
public HttpResponseMessage LoadDocumentPage(PostedDataEntity postedData)
{
	string password = string.Empty;
	try
	{
		string documentGuid = GetDocumentPath(postedData.guid);
		int pageNumber = postedData.page;
		password = string.IsNullOrEmpty(postedData.password) ? null : postedData.password;

		var fileFolderName = Path.GetFileName(documentGuid).Replace(".", "_");
		string fileCacheSubFolder = Path.Combine(cachePath, fileFolderName);

		IViewerCache cache = new FileViewerCache(cachePath, fileCacheSubFolder);

		PageDescriptionEntity page;
		if (globalConfiguration.GetViewerConfiguration().GetIsHtmlMode())
		{
			using (HtmlViewer htmlViewer = new HtmlViewer(documentGuid, cache, GetLoadOptions(password)))
			{
				page = this.GetPageDescritpionEntity(htmlViewer, documentGuid, pageNumber, fileCacheSubFolder);
			}
		}
		else
		{
			using (PngViewer pngViewer = new PngViewer(documentGuid, cache, GetLoadOptions(password)))
			{
				page = this.GetPageDescritpionEntity(pngViewer, documentGuid, pageNumber, fileCacheSubFolder);
			}
		}

		return this.Request.CreateResponse(HttpStatusCode.OK, page);
	}
	catch (System.Exception ex)
	{
		// set exception message
		return Request.CreateResponse(HttpStatusCode.Forbidden, new Common.Resources.Resource().GenerateException(ex, password));
	}
}


class HtmlViewer : IDisposable, ICustomViewer
{
    private readonly string filePath;
    private readonly IViewerCache cache;

    private readonly GroupDocs.Viewer.Viewer viewer;
    private readonly HtmlViewOptions viewOptions;
    private readonly ViewInfoOptions viewInfoOptions;
    private static readonly Common.Config.GlobalConfiguration globalConfiguration = new Common.Config.GlobalConfiguration();

    public HtmlViewer(string filePath, IViewerCache cache, LoadOptions loadOptions, int pageNumber = -1, int newAngle = 0)
    {
        this.cache = cache;
        this.filePath = filePath;
        this.viewer = new GroupDocs.Viewer.Viewer(filePath, loadOptions);
        this.viewOptions = this.CreateHtmlViewOptions(pageNumber, newAngle);
        this.viewInfoOptions = ViewInfoOptions.FromHtmlViewOptions(this.viewOptions);
    }

    public GroupDocs.Viewer.Viewer GetViewer()
    {
        return this.viewer;
    }

    private string GetHtmlFilePath(int pageNumber, string fileName)
    {
        var urlPrefix = GD.Utilities.StoragePath + "/cache/" + Path.GetFileName(this.filePath).Replace(".", "_");
        return $"{urlPrefix}/p{pageNumber}_{fileName}";
    }

    private HtmlViewOptions CreateHtmlViewOptions(int passedPageNumber = -1, int newAngle = 0)
    {
        HtmlViewOptions htmlViewOptions = HtmlViewOptions.ForExternalResources(
            pageNumber =>
            {
                string fileName = $"p{pageNumber}.html";
                string cacheFilePath = this.cache.GetCacheFilePath(fileName);

                return File.Create(cacheFilePath);
            },
            (pageNumber, resource) =>
            {
                string fileName = $"p{pageNumber}_{resource.FileName}";
                string cacheFilePath = this.cache.GetCacheFilePath(fileName);

                return File.Create(cacheFilePath);
            },
            (pageNumber, resource) =>
            {
                return GetHtmlFilePath(pageNumber, resource.FileName);
                //var baseFolderPath = Server.MapPath("~/Resources/Viewer/resources");
                //var urlPrefix = GD.Utilities.StoragePath + "/cache/" + Path.GetFileName(this.filePath).Replace(".", "_");
                //return $"{urlPrefix}/p{ pageNumber}_{ resource.FileName}";
            });

        htmlViewOptions.SpreadsheetOptions.TextOverflowMode = TextOverflowMode.HideText;
        htmlViewOptions.SpreadsheetOptions.SkipEmptyColumns = true;
        htmlViewOptions.SpreadsheetOptions.SkipEmptyRows = true;
        SetWatermarkOptions(htmlViewOptions);

        if (passedPageNumber >= 0 && newAngle != 0)
        {
            Rotation rotationAngle = GetRotationByAngle(newAngle);
            htmlViewOptions.RotatePage(passedPageNumber, rotationAngle);
        }

        return htmlViewOptions;
    }
}

now, how can I use that api in the current scenario.

@Niteen_Jadhav

Let me prepare a simplified version of an MVC application to show how it is expected to work.

Thanks a lot

@Niteen_Jadhav

Please try downloading and running this sample application that has two API methods where the first one returns a page and the second one is responsible for serving resources. It demonstrates how resources should be handled and served when viewing a file.

The current solution version is 23.1 and my version is 20.8… Will it work?

@Niteen_Jadhav

Yes, here is updated version where 20.8 is used - WebApiExternalResources-20-8.zip.

We encourage you to update to the latest version as there are a lot of issues were fixed, and overall rendering quality improved since 20.8.

Hello, the files which you shared looks same.

@Niteen_Jadhav

Yes, they are almost the same except version of GroupDocs.Viewer for .NET which is 20.8 in the second file. Also, I have added a couple of minor changes.