├── .github └── FUNDING.yml ├── Logo.png ├── .gitignore ├── AppSourceCop.json ├── JavaScript ├── Startup.js ├── stylesheet.css └── script.js ├── Volodymyr Dvernytskyi_PDFViewer_1.0.0.1.app ├── PermissionSet80000.PDFViewer.al ├── settings.json ├── ControlAddIn └── ControlAddIn.PDFVPDFViewer.al ├── app.json ├── Page Extensions ├── Pag1173-Ext80002.PDFVDocumentAttachmentDetai.al ├── Pag21-Ext80001.PDFVCustomerCard.al └── Pag22-Ext80000.PDFVCustomerList.al ├── LICENSE ├── Pages ├── Pag80000.PDFVPDFViewer.al ├── Pag80004.PDFVPDFViewerDocAttachament.al ├── Pag80002.PDFVPDFStorage.al ├── Pag80001.PDFVPDFViewerFactbox.al └── Pag80003.PDFVPDFViewerMatrix.al ├── ruleset.json ├── README.md ├── Tables └── Tab80000.PDFVPDFStorage.al └── Translations └── PDFViewer.g.xlf /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Drakonian] 2 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drakonian/bc-pdf-viewer/HEAD/Logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .alpackages 3 | .alcache 4 | .altemplates 5 | .vs/ 6 | .vscode/ -------------------------------------------------------------------------------- /AppSourceCop.json: -------------------------------------------------------------------------------- 1 | { 2 | "mandatoryAffixes": [ "PDFV",""], 3 | "supportedCountries": ["AU","NZ","US","GB","CA"] 4 | } -------------------------------------------------------------------------------- /JavaScript/Startup.js: -------------------------------------------------------------------------------- 1 | InitializeControl('controlAddIn'); 2 | Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ControlAddInReady', null); -------------------------------------------------------------------------------- /Volodymyr Dvernytskyi_PDFViewer_1.0.0.1.app: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drakonian/bc-pdf-viewer/HEAD/Volodymyr Dvernytskyi_PDFViewer_1.0.0.1.app -------------------------------------------------------------------------------- /PermissionSet80000.PDFViewer.al: -------------------------------------------------------------------------------- 1 | permissionset 80000 "PDFViewer" 2 | { 3 | Assignable = true; 4 | Caption = 'PDFViewer', MaxLength = 30; 5 | Permissions = 6 | table "PDFV PDF Storage" = X, 7 | tabledata "PDFV PDF Storage" = RMID, 8 | page "PDFV PDF Viewer" = X, 9 | page "PDFV PDF Viewer Factbox" = X, 10 | page "PDFV PDF Storage" = X, 11 | page "PDFV PDF Viewer Matrix" = X, 12 | page "PDFV PDFViewerDocAttachament" = X; 13 | } 14 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CRS.FileNamePattern": "..al", 3 | "CRS.FileNamePatternExtensions": "-Ext..al", 4 | "CRS.OnSaveAlFileAction": "Rename", 5 | "al.ruleSetPath": "ruleset.json", 6 | "al.enableCodeAnalysis": true, 7 | "al.codeAnalyzers": [ 8 | "${CodeCop}", 9 | "${PerTenantExtensionCop}", 10 | "${AppSourceCop}", 11 | "${UICop}" 12 | ] 13 | } -------------------------------------------------------------------------------- /JavaScript/stylesheet.css: -------------------------------------------------------------------------------- 1 | #the-canvas { 2 | border: 1px solid black; 3 | direction: ltr; 4 | } 5 | 6 | 7 | #pdf-buttons { 8 | float: left; 9 | } 10 | 11 | #page-count-container { 12 | float: right; 13 | } 14 | 15 | #page_num { 16 | display: inline; 17 | } 18 | 19 | #page_count { 20 | display: inline; 21 | } 22 | 23 | #pdf-meta { 24 | overflow: hidden; 25 | margin: 0 0 20px 0; 26 | } 27 | 28 | #pdf-contents { 29 | width: fit-content; 30 | margin: 20px auto; 31 | } -------------------------------------------------------------------------------- /ControlAddIn/ControlAddIn.PDFVPDFViewer.al: -------------------------------------------------------------------------------- 1 | controladdin "PDFV PDF Viewer" 2 | { 3 | Scripts = 'https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.8.335/pdf.min.js', 'JavaScript/script.js'; 4 | StartupScript = 'JavaScript/Startup.js'; 5 | StyleSheets = 'JavaScript/stylesheet.css'; 6 | 7 | MinimumHeight = 1; 8 | MinimumWidth = 1; 9 | MaximumHeight = 2000; 10 | HorizontalStretch = true; 11 | VerticalStretch = true; 12 | VerticalShrink = true; 13 | HorizontalShrink = true; 14 | event ControlAddinReady(); 15 | event onView() 16 | procedure LoadPDF(PDFDocument: Text; IsFactbox: Boolean) 17 | procedure SetVisible(IsVisible: Boolean) 18 | } -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "630af0aa-a687-46a3-b0d1-69e5522ed65c", 3 | "name": "PDFViewer", 4 | "publisher": "Volodymyr Dvernytskyi", 5 | "version": "1.0.0.1", 6 | "brief": "https://vld-nav.com/", 7 | "description": "https://vld-nav.com/", 8 | "privacyStatement": "https://vld-nav.com/", 9 | "EULA": "https://vld-nav.com/", 10 | "help": "https://vld-nav.com/", 11 | "url": "https://vld-nav.com/", 12 | "logo": "./logo.png", 13 | "dependencies": [], 14 | "screenshots": [], 15 | "platform": "20.0.0.0", 16 | "application": "20.0.0.0", 17 | "idRanges": [ 18 | { 19 | "from": 80000, 20 | "to": 80500 21 | } 22 | ], 23 | "contextSensitiveHelpUrl": "https://vld-nav.com/", 24 | "runtime": "9.0", 25 | "features": ["TranslationFile"], 26 | "resourceExposurePolicy": 27 | { 28 | "allowDebugging": true, 29 | "allowDownloadingSource": true, 30 | "includeSourceInSymbolFile": true 31 | } 32 | } -------------------------------------------------------------------------------- /Page Extensions/Pag1173-Ext80002.PDFVDocumentAttachmentDetai.al: -------------------------------------------------------------------------------- 1 | pageextension 80002 "PDFV Document Attachment Detai" extends "Document Attachment Details" //1173 2 | { 3 | actions 4 | { 5 | addlast(processing) 6 | { 7 | action("PDFV View PDF") 8 | { 9 | ApplicationArea = All; 10 | Image = Text; 11 | Caption = 'View PDF'; 12 | ToolTip = 'View PDF'; 13 | Promoted = true; 14 | PromotedOnly = true; 15 | PromotedCategory = Process; 16 | Enabled = Rec."File Extension" = 'pdf'; 17 | trigger OnAction() 18 | var 19 | PDFViewerDocAttachment: Page "PDFV PDFViewerDocAttachament"; 20 | begin 21 | PDFViewerDocAttachment.SetRecord(Rec); 22 | PDFViewerDocAttachment.SetTableView(Rec); 23 | PDFViewerDocAttachment.Run(); 24 | end; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Volodymyr Dvernytskyi https://vld-nav.com/bc-pdf-viewer/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pages/Pag80000.PDFVPDFViewer.al: -------------------------------------------------------------------------------- 1 | page 80000 "PDFV PDF Viewer" 2 | { 3 | 4 | Caption = 'PDF Viewer'; 5 | PageType = Card; 6 | UsageCategory = None; 7 | SourceTable = "PDFV PDF Storage"; 8 | layout 9 | { 10 | area(content) 11 | { 12 | group(General) 13 | { 14 | ShowCaption = false; 15 | usercontrol(PDFViewer; "PDFV PDF Viewer") 16 | { 17 | ApplicationArea = All; 18 | 19 | trigger ControlAddinReady() 20 | begin 21 | SetPDFDocument(); 22 | end; 23 | } 24 | } 25 | } 26 | } 27 | local procedure SetPDFDocument() 28 | var 29 | Base64Convert: Codeunit "Base64 Convert"; 30 | TempBlob: Codeunit "Temp Blob"; 31 | InStreamVar: InStream; 32 | PDFAsTxt: Text; 33 | begin 34 | Rec.CalcFields("PDF Value"); 35 | 36 | CurrPage.PDFViewer.SetVisible(Rec."PDF Value".HasValue()); 37 | if not Rec."PDF Value".HasValue() then 38 | exit; 39 | 40 | TempBlob.FromRecord(Rec, Rec.FieldNo("PDF Value")); 41 | TempBlob.CreateInStream(InStreamVar); 42 | 43 | PDFAsTxt := Base64Convert.ToBase64(InStreamVar); 44 | 45 | CurrPage.PDFViewer.LoadPDF(PDFAsTxt, false); 46 | end; 47 | } 48 | -------------------------------------------------------------------------------- /Pages/Pag80004.PDFVPDFViewerDocAttachament.al: -------------------------------------------------------------------------------- 1 | page 80004 "PDFV PDFViewerDocAttachament" 2 | { 3 | 4 | Caption = 'PDF Viewer'; 5 | PageType = Card; 6 | UsageCategory = None; 7 | SourceTable = "Document Attachment"; 8 | layout 9 | { 10 | area(content) 11 | { 12 | group(General) 13 | { 14 | ShowCaption = false; 15 | usercontrol(PDFViewer; "PDFV PDF Viewer") 16 | { 17 | ApplicationArea = All; 18 | 19 | trigger ControlAddinReady() 20 | begin 21 | SetPDFDocument(); 22 | end; 23 | } 24 | } 25 | } 26 | } 27 | local procedure SetPDFDocument() 28 | var 29 | Base64Convert: Codeunit "Base64 Convert"; 30 | TempBlob: Codeunit "Temp Blob"; 31 | InStreamVar: InStream; 32 | OutStreamVar: OutStream; 33 | PDFAsTxt: Text; 34 | begin 35 | CurrPage.PDFViewer.SetVisible(Rec."Document Reference ID".HasValue()); 36 | if not Rec."Document Reference ID".HasValue() then 37 | exit; 38 | 39 | TempBlob.CreateInStream(InStreamVar); 40 | TempBlob.CreateOutStream(OutStreamVar); 41 | Rec."Document Reference ID".ExportStream(OutStreamVar); 42 | 43 | PDFAsTxt := Base64Convert.ToBase64(InStreamVar); 44 | 45 | CurrPage.PDFViewer.LoadPDF(PDFAsTxt, false); 46 | end; 47 | } 48 | -------------------------------------------------------------------------------- /Page Extensions/Pag21-Ext80001.PDFVCustomerCard.al: -------------------------------------------------------------------------------- 1 | pageextension 80001 "PDFV Customer Card" extends "Customer Card"//21 2 | { 3 | layout 4 | { 5 | addfirst(factboxes) 6 | { 7 | part(PDFViewerMatrix; "PDFV PDF Viewer Matrix") 8 | { 9 | ApplicationArea = All; 10 | } 11 | } 12 | } 13 | actions 14 | { 15 | addfirst(processing) 16 | { 17 | action(PDFVPDFStorage) 18 | { 19 | ApplicationArea = All; 20 | Image = Database; 21 | Caption = 'PDF Storage'; 22 | ToolTip = 'PDF Storage'; 23 | Promoted = true; 24 | PromotedCategory = Process; 25 | PromotedIsBig = true; 26 | PromotedOnly = true; 27 | trigger OnAction() 28 | var 29 | PDFStorage: Record "PDFV PDF Storage"; 30 | PDFStorageList: Page "PDFV PDF Storage"; 31 | begin 32 | Clear(PDFStorageList); 33 | PDFStorage.SetRange("Source Record ID", Rec.RecordId()); 34 | PDFStorageList.SetTableView(PDFStorage); 35 | PDFStorageList.Run(); 36 | end; 37 | } 38 | } 39 | } 40 | trigger OnAfterGetCurrRecord() 41 | begin 42 | CurrPage.PDFViewerMatrix.Page.SetRecord(Rec.RecordId()); 43 | end; 44 | 45 | } -------------------------------------------------------------------------------- /Page Extensions/Pag22-Ext80000.PDFVCustomerList.al: -------------------------------------------------------------------------------- 1 | pageextension 80000 "PDFV Customer List" extends "Customer List" //22 2 | { 3 | layout 4 | { 5 | addfirst(factboxes) 6 | { 7 | part(PDFViewerMatrix; "PDFV PDF Viewer Matrix") 8 | { 9 | ApplicationArea = All; 10 | } 11 | } 12 | } 13 | actions 14 | { 15 | addfirst(processing) 16 | { 17 | action(PDFVPDFStorage) 18 | { 19 | ApplicationArea = All; 20 | Image = Database; 21 | Caption = 'PDF Storage'; 22 | ToolTip = 'PDF Storage'; 23 | Promoted = true; 24 | PromotedCategory = Process; 25 | PromotedIsBig = true; 26 | PromotedOnly = true; 27 | trigger OnAction() 28 | var 29 | PDFStorage: Record "PDFV PDF Storage"; 30 | PDFStorageList: Page "PDFV PDF Storage"; 31 | begin 32 | Clear(PDFStorageList); 33 | PDFStorage.SetRange("Source Record ID", Rec.RecordId()); 34 | PDFStorageList.SetTableView(PDFStorage); 35 | PDFStorageList.Run(); 36 | end; 37 | } 38 | } 39 | } 40 | trigger OnAfterGetCurrRecord() 41 | begin 42 | CurrPage.PDFViewerMatrix.Page.SetRecord(Rec.RecordId()); 43 | end; 44 | } 45 | -------------------------------------------------------------------------------- /ruleset.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ruleset", 3 | "description": "Adjusted Rules", 4 | "rules": [ 5 | { 6 | "id": "AA0215", 7 | "action": "Hidden", 8 | "justification": "Use XPL standard file naming convention" 9 | }, 10 | { 11 | "id": "AA0214", 12 | "action": "Hidden", 13 | "justification": "Erroneous error where local var used for lookup gets: record should be modified before saving" 14 | }, 15 | { 16 | "id": "AL0603", 17 | "action": "Hidden", 18 | "justification": "Implicit conversion from Enum to Integer. Hide warning until the bug is fixed by MS" 19 | }, 20 | { 21 | "id": "AA0005", 22 | "action": "Hidden", 23 | "justification": "This is OK -> Avoid AA0005 warning notification for BEGIN .. END only with compound statements" 24 | }, 25 | { 26 | "id": "AS0084", 27 | "action": "Hidden", 28 | "justification": "This is OK -> Avoid AA0005 warning notification for BEGIN .. END only with compound statements" 29 | }, 30 | { 31 | "id": "AA0072", 32 | "action": "Hidden", 33 | "justification": "This is OK -> Avoid AA0005 warning notification for BEGIN .. END only with compound statements" 34 | }, 35 | { 36 | "id": "AL0604", 37 | "action": "Hidden", 38 | "justification": "Not actual rule?" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Light PDF Viewer for Business Central (PDF.js) 2 | 3 | I have prepared a lightweight ready to use free extension for viewing PDF documents in Business Central. I've heard requests for Microsoft's built-in PDF Viewer on more than one occasion. But since Microsoft is still working on this problem, I found it helpful to make a simplified version. This extension doesn't need any additional servers. 4 | 5 | https://vld-nav.com/bc-pdf-viewer 6 | 7 | ![PDF Viewer](https://static.tildacdn.com/tild3330-6363-4261-a536-653938363531/FirstDemoScreen_NEW2.gif) 8 | 9 | --- 10 | 11 | # How to Install a Per Tenant Extension (.app file) in Business Central 12 | 13 | This guide provides step-by-step instructions for installing a Per Tenant Extension (PTE) `.app` file in Microsoft Dynamics 365 Business Central via the Extension Management page. 14 | 15 | ## Prerequisites 16 | 17 | - **Permissions**: Ensure you have the permissions to manage extensions in Business Central. 18 | - **.app File**: Have the `.app` file you wish to install ready. 19 | 20 | ## Installation Steps 21 | 22 | ### 1. Access the Extension Management Page 23 | 24 | - Go to **Extension Management** from the Search. 25 | 26 | ### 2. Upload the Extension 27 | 28 | - Click **Upload Extension** on the Extension Management page. 29 | - Press **Select .app file DrillDown**, navigate to your `.app` file, select it, and click **Open**. 30 | 31 | ### 3. Install the Extension 32 | 33 | - Click **Deploy** 34 | - Accept any terms and conditions, if prompted. 35 | - Confirm the installation by clicking **Yes**. 36 | 37 | ### 4. Verify Installation 38 | 39 | - The installation process may take a few minutes. You can monitor the progress on the Extension Management page, where the status will change to **Installed** once completed. 40 | 41 | ## Troubleshooting 42 | 43 | - **Installation Errors**: Refer to the error message details and consult the extension's documentation or support resources. 44 | - **Permissions Issues**: Confirm you have the necessary permissions to install extensions. Contact your system administrator if you're unsure. 45 | 46 | 47 | --- 48 | -------------------------------------------------------------------------------- /Tables/Tab80000.PDFVPDFStorage.al: -------------------------------------------------------------------------------- 1 | table 80000 "PDFV PDF Storage" 2 | { 3 | Caption = 'PDF Storage'; 4 | DataClassification = CustomerContent; 5 | 6 | fields 7 | { 8 | field(1; "Entry No."; BigInteger) 9 | { 10 | Caption = 'Entry No.'; 11 | DataClassification = CustomerContent; 12 | AutoIncrement = true; 13 | } 14 | field(2; "Source Record ID"; RecordId) 15 | { 16 | Caption = 'Source Record ID'; 17 | DataClassification = CustomerContent; 18 | } 19 | field(3; "PDF Value"; Blob) 20 | { 21 | Caption = 'PDF Value'; 22 | DataClassification = CustomerContent; 23 | } 24 | field(4; Description; Text[200]) 25 | { 26 | Caption = 'Description'; 27 | DataClassification = CustomerContent; 28 | } 29 | } 30 | keys 31 | { 32 | key(PK; "Entry No.") 33 | { 34 | Clustered = true; 35 | } 36 | } 37 | 38 | procedure UploadContent(SourceRecordID: RecordId) 39 | var 40 | InStreamVar: InStream; 41 | OutStreamVar: OutStream; 42 | FileName: Text; 43 | begin 44 | Rec.Init(); 45 | Rec.CalcFields("PDF Value"); 46 | 47 | Rec."PDF Value".CreateOutStream(OutStreamVar); 48 | if not UploadIntoStream(UploadTitleLbl, '', FileFilterLbl, FileName, InStreamVar) then 49 | exit; 50 | CopyStream(OutStreamVar, InStreamVar); 51 | Rec.Description := CopyStr(FileName, 1, MaxStrLen(Rec.Description)); 52 | if SourceRecordID.TableNo() <> 0 then 53 | Rec."Source Record ID" := SourceRecordID; 54 | Rec.Insert(true); 55 | end; 56 | 57 | procedure DownloadContent() 58 | var 59 | FileManagement: Codeunit "File Management"; 60 | InStreamVar: InStream; 61 | FileName: Text; 62 | begin 63 | Rec.CalcFields("PDF Value"); 64 | 65 | if not Rec."PDF Value".HasValue() then 66 | exit; 67 | Rec."PDF Value".CreateInStream(InStreamVar); 68 | FileName := Rec.Description; 69 | if FileManagement.GetExtension(FileName) <> DelStr(PDFExtLbl, 1) then 70 | FileName += PDFExtLbl; 71 | DownloadFromStream(InStreamVar, DownloadTitleLbl, '', FileFilterLbl, FileName); 72 | end; 73 | 74 | var 75 | FileFilterLbl: Label 'PDF file(*.pdf)|*.pdf', Locked = true; 76 | PDFExtLbl: Label '.pdf', Locked = true; 77 | UploadTitleLbl: Label 'Upload PDF File'; 78 | DownloadTitleLbl: Label 'Download PDF File'; 79 | } 80 | -------------------------------------------------------------------------------- /Pages/Pag80002.PDFVPDFStorage.al: -------------------------------------------------------------------------------- 1 | page 80002 "PDFV PDF Storage" 2 | { 3 | 4 | ApplicationArea = All; 5 | Caption = 'PDF Storage'; 6 | PageType = List; 7 | SourceTable = "PDFV PDF Storage"; 8 | UsageCategory = Lists; 9 | InsertAllowed = false; 10 | 11 | layout 12 | { 13 | area(content) 14 | { 15 | repeater(General) 16 | { 17 | field(Description; Rec.Description) 18 | { 19 | ToolTip = 'Specifies the value of the Description field'; 20 | ApplicationArea = All; 21 | } 22 | } 23 | } 24 | area(FactBoxes) 25 | { 26 | part(PDFViewerFactbox; "PDFV PDF Viewer Factbox") 27 | { 28 | ApplicationArea = All; 29 | Caption = 'View'; 30 | } 31 | } 32 | } 33 | actions 34 | { 35 | area(Processing) 36 | { 37 | action(UploadContent) 38 | { 39 | ApplicationArea = All; 40 | Image = MoveUp; 41 | Caption = 'Upload'; 42 | ToolTip = 'Upload'; 43 | Promoted = true; 44 | PromotedCategory = Process; 45 | PromotedIsBig = true; 46 | PromotedOnly = true; 47 | trigger OnAction() 48 | var 49 | PDFStorage: Record "PDFV PDF Storage"; 50 | SourceRecordID: RecordId; 51 | begin 52 | if Rec.GetFilter("Source Record ID") <> '' then 53 | Evaluate(SourceRecordID, Rec.GetFilter("Source Record ID")); 54 | PDFStorage.UploadContent(SourceRecordID); 55 | CurrPage.Update(true); 56 | end; 57 | } 58 | 59 | action(DownloadContent) 60 | { 61 | ApplicationArea = All; 62 | Image = MoveDown; 63 | Caption = 'Download'; 64 | ToolTip = 'Download'; 65 | Promoted = true; 66 | PromotedCategory = Process; 67 | PromotedIsBig = true; 68 | PromotedOnly = true; 69 | 70 | trigger OnAction() 71 | begin 72 | Rec.DownloadContent(); 73 | CurrPage.Update(true); 74 | end; 75 | } 76 | } 77 | 78 | } 79 | 80 | trigger OnDeleteRecord(): Boolean 81 | begin 82 | CurrPage.Update(true); 83 | end; 84 | 85 | trigger OnAfterGetCurrRecord() 86 | begin 87 | CurrPage.PDFViewerFactbox.Page.SetRecord(Rec."Entry No."); 88 | end; 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Pages/Pag80001.PDFVPDFViewerFactbox.al: -------------------------------------------------------------------------------- 1 | page 80001 "PDFV PDF Viewer Factbox" 2 | { 3 | 4 | Caption = 'PDF Viewer'; 5 | PageType = CardPart; 6 | SourceTable = "PDFV PDF Storage"; 7 | DeleteAllowed = false; 8 | InsertAllowed = false; 9 | LinksAllowed = false; 10 | 11 | layout 12 | { 13 | area(content) 14 | { 15 | group(General) 16 | { 17 | ShowCaption = false; 18 | usercontrol(PDFViewer; "PDFV PDF Viewer") 19 | { 20 | ApplicationArea = All; 21 | 22 | trigger ControlAddinReady() 23 | begin 24 | SetPDFDocument(); 25 | end; 26 | 27 | trigger onView() 28 | begin 29 | RunFullView(); 30 | end; 31 | } 32 | } 33 | } 34 | } 35 | actions 36 | { 37 | area(Processing) 38 | { 39 | action(PDFVViewFullDocument) 40 | { 41 | ApplicationArea = All; 42 | Image = View; 43 | Caption = 'View'; 44 | ToolTip = 'View'; 45 | PromotedOnly = true; 46 | PromotedIsBig = true; 47 | PromotedCategory = Process; 48 | Promoted = true; 49 | trigger OnAction() 50 | begin 51 | RunFullView(); 52 | end; 53 | } 54 | } 55 | } 56 | local procedure SetPDFDocument() 57 | var 58 | Base64Convert: Codeunit "Base64 Convert"; 59 | TempBlob: Codeunit "Temp Blob"; 60 | InStreamVar: InStream; 61 | PDFAsTxt: Text; 62 | begin 63 | Rec.CalcFields("PDF Value"); 64 | CurrPage.PDFViewer.SetVisible(Rec."PDF Value".HasValue()); 65 | if not Rec."PDF Value".HasValue() then 66 | exit; 67 | 68 | TempBlob.FromRecord(Rec, Rec.FieldNo("PDF Value")); 69 | TempBlob.CreateInStream(InStreamVar); 70 | 71 | PDFAsTxt := Base64Convert.ToBase64(InStreamVar); 72 | 73 | CurrPage.PDFViewer.LoadPDF(PDFAsTxt, true); 74 | end; 75 | 76 | procedure SetRecord(EntryNo: BigInteger) 77 | begin 78 | Rec.SetRange("Entry No.", EntryNo); 79 | if not Rec.FindFirst() then 80 | exit; 81 | SetPDFDocument(); 82 | CurrPage.Update(false); 83 | end; 84 | 85 | local procedure RunFullView() 86 | var 87 | PDFViewerCard: Page "PDFV PDF Viewer"; 88 | begin 89 | if Rec.IsEmpty() then 90 | exit; 91 | PDFViewerCard.SetRecord(Rec); 92 | PDFViewerCard.SetTableView(Rec); 93 | PDFViewerCard.Run(); 94 | end; 95 | } 96 | -------------------------------------------------------------------------------- /JavaScript/script.js: -------------------------------------------------------------------------------- 1 | var pdfDoc = null, 2 | pageNum = 1, 3 | pageRendering = false, 4 | pageNumPending = null, 5 | IsFirstLoad = true; 6 | 7 | function InitializeControl(controlId) { 8 | var controlAddIn = document.getElementById(controlId); 9 | controlAddIn.innerHTML ='
Page: /
'; 10 | } 11 | 12 | function SetVisible(IsVisible) { 13 | if (IsVisible){ 14 | document.querySelector("#pdf-contents").style.display = 'block'; 15 | }else{ 16 | document.querySelector("#pdf-contents").style.display = 'none'; 17 | } 18 | 19 | } 20 | 21 | function LoadPDF(PDFDocument,IsFactbox){ 22 | 23 | var canvas = document.getElementById('the-canvas'), 24 | pdfcontents = document.getElementById('pdf-contents'), 25 | ctx = canvas.getContext('2d'), 26 | iframe = window.frameElement, 27 | factboxarea = window.frameElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement, 28 | scale = 1.3; 29 | 30 | 31 | pageRendering = false; 32 | pageNum = 1; 33 | pageNumPending = null; 34 | 35 | PDFDocument = atob(PDFDocument); 36 | 37 | if (IsFactbox) { 38 | if (factboxarea.className = "ms-nav-layout-factbox-content-area ms-nav-scrollable"){ 39 | factboxarea.style.paddingLeft = "5px"; 40 | factboxarea.style.paddingRight = "0px"; 41 | factboxarea.style.overflowY = "scroll"; 42 | } 43 | scale = 0.6; 44 | }else{ 45 | document.querySelector("#pdf-view").style.display = 'none'; 46 | } 47 | 48 | 49 | requestAnimationFrame(() => { 50 | 51 | /** 52 | * Get page info from document, resize canvas accordingly, and render page. 53 | * @param num Page number. 54 | */ 55 | function renderPage(num) { 56 | pageRendering = true; 57 | // Using promise to fetch the page 58 | pdfDoc.getPage(num).then(function(page) { 59 | var viewport = page.getViewport({scale: scale}); 60 | canvas.height = viewport.height; 61 | canvas.width = viewport.width; 62 | 63 | pdfcontents.height = viewport.height; 64 | pdfcontents.width = viewport.width; 65 | iframe.style.height = viewport.height + 100 + "px"; 66 | iframe.parentElement.style.height = viewport.height + 100 + "px"; 67 | iframe.style.maxHeight = "2500px"; 68 | 69 | // Render PDF page into canvas context 70 | var renderContext = { 71 | canvasContext: ctx, 72 | viewport: viewport 73 | }; 74 | var renderTask = page.render(renderContext); 75 | 76 | // Wait for rendering to finish 77 | renderTask.promise.then(function() { 78 | pageRendering = false; 79 | if (pageNumPending !== null) { 80 | // New page rendering is pending 81 | renderPage(pageNumPending); 82 | pageNumPending = null; 83 | } 84 | }); 85 | }); 86 | 87 | // Update page counters 88 | document.getElementById('page_num').textContent = num; 89 | } 90 | 91 | 92 | /** 93 | * If another page rendering in progress, waits until the rendering is 94 | * finised. Otherwise, executes rendering immediately. 95 | */ 96 | function queueRenderPage(num) { 97 | if (pageRendering) { 98 | pageNumPending = num; 99 | } else { 100 | renderPage(num); 101 | } 102 | } 103 | 104 | /** 105 | * Displays previous page. 106 | */ 107 | function onPrevPage() { 108 | if (pageNum <= 1) { 109 | return; 110 | } 111 | pageNum--; 112 | queueRenderPage(pageNum); 113 | } 114 | if (IsFirstLoad){ 115 | document.getElementById('prev').addEventListener('click', onPrevPage); 116 | } 117 | 118 | /** 119 | * Displays next page. 120 | */ 121 | function onNextPage() { 122 | if (pageNum >= pdfDoc.numPages) { 123 | return; 124 | } 125 | pageNum++; 126 | queueRenderPage(pageNum); 127 | } 128 | if (IsFirstLoad){ 129 | document.getElementById('next').addEventListener('click', onNextPage); 130 | } 131 | 132 | /** 133 | * Displays full page. 134 | */ 135 | function onView() { 136 | Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('onView'); 137 | } 138 | if (IsFirstLoad){ 139 | document.getElementById('pdf-view').addEventListener('click', onView); 140 | } 141 | 142 | IsFirstLoad = false; 143 | 144 | /** 145 | * Asynchronously downloads PDF. 146 | */ 147 | pdfjsLib.getDocument({data: PDFDocument}).promise.then(function(pdfDoc_) { 148 | pdfDoc = pdfDoc_; 149 | document.getElementById('page_count').textContent = pdfDoc.numPages; 150 | 151 | // Initial/first page rendering 152 | renderPage(pageNum); 153 | }); 154 | 155 | 156 | }); 157 | } 158 | 159 | -------------------------------------------------------------------------------- /Pages/Pag80003.PDFVPDFViewerMatrix.al: -------------------------------------------------------------------------------- 1 | page 80003 "PDFV PDF Viewer Matrix" 2 | { 3 | 4 | Caption = 'PDF Documents'; 5 | PageType = CardPart; 6 | DeleteAllowed = false; 7 | InsertAllowed = false; 8 | LinksAllowed = false; 9 | RefreshOnActivate = true; 10 | 11 | layout 12 | { 13 | area(content) 14 | { 15 | group(Group1) 16 | { 17 | ShowCaption = false; 18 | Visible = VisibleControl1; 19 | usercontrol(PDFViewer1; "PDFV PDF Viewer") 20 | { 21 | ApplicationArea = All; 22 | 23 | trigger ControlAddinReady() 24 | begin 25 | IsControlAddInReady := true; 26 | SetRecord(); 27 | end; 28 | 29 | trigger onView() 30 | begin 31 | RunFullView(PDFStorageArray[1]); 32 | end; 33 | } 34 | } 35 | group(Group2) 36 | { 37 | ShowCaption = false; 38 | Visible = VisibleControl2; 39 | usercontrol(PDFViewer2; "PDFV PDF Viewer") 40 | { 41 | ApplicationArea = All; 42 | 43 | trigger ControlAddinReady() 44 | begin 45 | IsControlAddInReady := true; 46 | SetRecord(); 47 | end; 48 | 49 | trigger onView() 50 | begin 51 | RunFullView(PDFStorageArray[2]); 52 | end; 53 | } 54 | } 55 | group(Group3) 56 | { 57 | ShowCaption = false; 58 | Visible = VisibleControl3; 59 | usercontrol(PDFViewer3; "PDFV PDF Viewer") 60 | { 61 | ApplicationArea = All; 62 | 63 | trigger ControlAddinReady() 64 | begin 65 | IsControlAddInReady := true; 66 | SetRecord(); 67 | end; 68 | 69 | trigger onView() 70 | begin 71 | RunFullView(PDFStorageArray[3]); 72 | end; 73 | } 74 | } 75 | group(Group4) 76 | { 77 | ShowCaption = false; 78 | Visible = VisibleControl4; 79 | usercontrol(PDFViewer4; "PDFV PDF Viewer") 80 | { 81 | ApplicationArea = All; 82 | 83 | trigger ControlAddinReady() 84 | begin 85 | IsControlAddInReady := true; 86 | SetRecord(); 87 | end; 88 | 89 | trigger onView() 90 | begin 91 | RunFullView(PDFStorageArray[4]); 92 | end; 93 | } 94 | } 95 | group(Group5) 96 | { 97 | ShowCaption = false; 98 | Visible = VisibleControl5; 99 | usercontrol(PDFViewer5; "PDFV PDF Viewer") 100 | { 101 | ApplicationArea = All; 102 | 103 | trigger ControlAddinReady() 104 | begin 105 | IsControlAddInReady := true; 106 | SetRecord(); 107 | end; 108 | 109 | trigger onView() 110 | begin 111 | RunFullView(PDFStorageArray[5]); 112 | end; 113 | } 114 | } 115 | } 116 | } 117 | trigger OnAfterGetCurrRecord() 118 | begin 119 | 120 | end; 121 | 122 | local procedure GetPDFAsTxt(PDFStorage: Record "PDFV PDF Storage"): Text 123 | var 124 | Base64Convert: Codeunit "Base64 Convert"; 125 | TempBlob: Codeunit "Temp Blob"; 126 | InStreamVar: InStream; 127 | begin 128 | PDFStorage.CalcFields("PDF Value"); 129 | if not PDFStorage."PDF Value".HasValue() then 130 | exit; 131 | 132 | TempBlob.FromRecord(PDFStorage, PDFStorage.FieldNo("PDF Value")); 133 | TempBlob.CreateInStream(InStreamVar); 134 | 135 | exit(Base64Convert.ToBase64(InStreamVar)); 136 | end; 137 | 138 | local procedure SetPDFDocument(PDFAsTxt: Text; i: Integer); 139 | var 140 | IsVisible: Boolean; 141 | begin 142 | IsVisible := PDFAsTxt <> ''; 143 | case i of 144 | 1: 145 | begin 146 | VisibleControl1 := IsVisible; 147 | if not IsVisible or not IsControlAddInReady then 148 | exit; 149 | CurrPage.PDFViewer1.SetVisible(IsVisible); 150 | CurrPage.PDFViewer1.LoadPDF(PDFAsTxt, true); 151 | end; 152 | 2: 153 | begin 154 | 155 | VisibleControl2 := IsVisible; 156 | if not IsVisible or not IsControlAddInReady then 157 | exit; 158 | CurrPage.PDFViewer2.SetVisible(IsVisible); 159 | CurrPage.PDFViewer2.LoadPDF(PDFAsTxt, true); 160 | end; 161 | 3: 162 | begin 163 | VisibleControl3 := IsVisible; 164 | if not IsVisible or not IsControlAddInReady then 165 | exit; 166 | CurrPage.PDFViewer3.SetVisible(IsVisible); 167 | CurrPage.PDFViewer3.LoadPDF(PDFAsTxt, true); 168 | end; 169 | 4: 170 | begin 171 | VisibleControl4 := IsVisible; 172 | if not IsVisible or not IsControlAddInReady then 173 | exit; 174 | CurrPage.PDFViewer4.SetVisible(IsVisible); 175 | CurrPage.PDFViewer4.LoadPDF(PDFAsTxt, true); 176 | end; 177 | 5: 178 | begin 179 | VisibleControl5 := IsVisible; 180 | if not IsVisible or not IsControlAddInReady then 181 | exit; 182 | CurrPage.PDFViewer5.SetVisible(IsVisible); 183 | CurrPage.PDFViewer5.LoadPDF(PDFAsTxt, true); 184 | end; 185 | end; 186 | 187 | end; 188 | 189 | local procedure SetRecord() 190 | var 191 | PDFStorage: Record "PDFV PDF Storage"; 192 | HandleErr: Boolean; 193 | i: Integer; 194 | begin 195 | Clear(PDFStorageArray); 196 | Clear(VisibleControl1); 197 | Clear(VisibleControl2); 198 | Clear(VisibleControl3); 199 | Clear(VisibleControl4); 200 | Clear(VisibleControl5); 201 | PDFStorage.SetRange("Source Record ID", gSourceRecordId); 202 | if PDFStorage.FindSet() then 203 | repeat 204 | i += 1; 205 | Clear(PDFStorageArray[i]); 206 | HandleErr := PDFStorageArray[i].Get(PDFStorage."Entry No."); 207 | until (PDFStorage.Next() = 0) or (i = ArrayLen(PDFStorageArray)); 208 | 209 | SetPDFDocument(GetPDFAsTxt(PDFStorageArray[1]), 1); 210 | SetPDFDocument(GetPDFAsTxt(PDFStorageArray[2]), 2); 211 | SetPDFDocument(GetPDFAsTxt(PDFStorageArray[3]), 3); 212 | SetPDFDocument(GetPDFAsTxt(PDFStorageArray[4]), 4); 213 | SetPDFDocument(GetPDFAsTxt(PDFStorageArray[5]), 5); 214 | end; 215 | 216 | procedure SetRecord(SourceRecordID: RecordId) 217 | begin 218 | gSourceRecordId := SourceRecordID; 219 | SetRecord(); 220 | CurrPage.Update(false); 221 | end; 222 | 223 | local procedure RunFullView(PDFStorage: Record "PDFV PDF Storage") 224 | var 225 | PDFViewerCard: Page "PDFV PDF Viewer"; 226 | begin 227 | if PDFStorage.IsEmpty() then 228 | exit; 229 | PDFViewerCard.SetRecord(PDFStorage); 230 | PDFViewerCard.SetTableView(PDFStorage); 231 | PDFViewerCard.Run(); 232 | end; 233 | 234 | var 235 | PDFStorageArray: array[5] of Record "PDFV PDF Storage"; 236 | gSourceRecordId: RecordId; 237 | [InDataSet] 238 | VisibleControl1: Boolean; 239 | [InDataSet] 240 | VisibleControl2: Boolean; 241 | [InDataSet] 242 | VisibleControl3: Boolean; 243 | [InDataSet] 244 | VisibleControl4: Boolean; 245 | [InDataSet] 246 | VisibleControl5: Boolean; 247 | [InDataSet] 248 | IsControlAddInReady: Boolean; 249 | } 250 | -------------------------------------------------------------------------------- /Translations/PDFViewer.g.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PDF Storage 8 | 9 | Table PDFV PDF Storage - Property Caption 10 | 11 | 12 | Download PDF File 13 | 14 | Table PDFV PDF Storage - NamedType DownloadTitleLbl 15 | 16 | 17 | Upload PDF File 18 | 19 | Table PDFV PDF Storage - NamedType UploadTitleLbl 20 | 21 | 22 | Description 23 | 24 | Table PDFV PDF Storage - Field Description - Property Caption 25 | 26 | 27 | Entry No. 28 | 29 | Table PDFV PDF Storage - Field Entry No. - Property Caption 30 | 31 | 32 | PDF Value 33 | 34 | Table PDFV PDF Storage - Field PDF Value - Property Caption 35 | 36 | 37 | Source Record ID 38 | 39 | Table PDFV PDF Storage - Field Source Record ID - Property Caption 40 | 41 | 42 | PDF Storage 43 | 44 | Page PDFV PDF Storage - Property Caption 45 | 46 | 47 | Specifies the value of the Description field 48 | 49 | Page PDFV PDF Storage - Control Description - Property ToolTip 50 | 51 | 52 | View 53 | 54 | Page PDFV PDF Storage - Control PDFViewerFactbox - Property Caption 55 | 56 | 57 | Download 58 | 59 | Page PDFV PDF Storage - Action DownloadContent - Property ToolTip 60 | 61 | 62 | Download 63 | 64 | Page PDFV PDF Storage - Action DownloadContent - Property Caption 65 | 66 | 67 | Upload 68 | 69 | Page PDFV PDF Storage - Action UploadContent - Property ToolTip 70 | 71 | 72 | Upload 73 | 74 | Page PDFV PDF Storage - Action UploadContent - Property Caption 75 | 76 | 77 | PDF Viewer 78 | 79 | Page PDFV PDF Viewer - Property Caption 80 | 81 | 82 | PDF Viewer 83 | 84 | Page PDFV PDF Viewer Factbox - Property Caption 85 | 86 | 87 | View 88 | 89 | Page PDFV PDF Viewer Factbox - Action PDFVViewFullDocument - Property ToolTip 90 | 91 | 92 | View 93 | 94 | Page PDFV PDF Viewer Factbox - Action PDFVViewFullDocument - Property Caption 95 | 96 | 97 | PDF Documents 98 | 99 | Page PDFV PDF Viewer Matrix - Property Caption 100 | 101 | 102 | PDF Viewer 103 | 104 | Page PDFV PDFViewerDocAttachament - Property Caption 105 | 106 | 107 | PDF Storage 108 | 109 | PageExtension PDFV Customer Card - Action PDFVPDFStorage - Property ToolTip 110 | 111 | 112 | PDF Storage 113 | 114 | PageExtension PDFV Customer Card - Action PDFVPDFStorage - Property Caption 115 | 116 | 117 | PDF Storage 118 | 119 | PageExtension PDFV Customer List - Action PDFVPDFStorage - Property ToolTip 120 | 121 | 122 | PDF Storage 123 | 124 | PageExtension PDFV Customer List - Action PDFVPDFStorage - Property Caption 125 | 126 | 127 | View PDF 128 | 129 | PageExtension PDFV Document Attachment Detai - Action PDFV View PDF - Property ToolTip 130 | 131 | 132 | View PDF 133 | 134 | PageExtension PDFV Document Attachment Detai - Action PDFV View PDF - Property Caption 135 | 136 | 137 | PDFViewer 138 | 139 | PermissionSet PDFViewer - Property Caption 140 | 141 | 142 | 143 | 144 | --------------------------------------------------------------------------------