Reduce file size for charts pasted from excel into word - vba

I have been creating reports by copying some charts and data from an excel document into a word document. I am pasting into a content control, so i use ChartObject.CopyPicture in excel and ContentControl.Range.Paste in word. This is done in a loop:
Set ws = ThisWorkbook.Worksheets("Charts")
With ws
For Each cc In wordDocument.ContentControls
If cc.Range.InlineShapes.Count > 0 Then
scaleHeight = cc.Range.InlineShapes(1).scaleHeight
scaleWidth = cc.Range.InlineShapes(1).scaleWidth
cc.Range.InlineShapes(1).Delete
.ChartObjects(cc.Tag).CopyPicture Appearance:=xlScreen, Format:=xlPicture
cc.Range.Paste
cc.Range.InlineShapes(1).scaleHeight = scaleHeight
cc.Range.InlineShapes(1).scaleWidth = scaleWidth
ElseIf ...
Next cc
End With
Creating these reports using Office 2007 yielded files that were around 6MB, but creating them (using the same worksheet and document) in Office 2010 yields a file that is around 10 times as large.
After unzipping the docx, I found that the extra size comes from emf files that correspond to charts that are pasted in using VBA. Where they range from 360 to 900 KB before, they are 5-18 MB. And the graphics are not visibly better.
Even further, it seems to be related to the Chart Style. I created a new spreadsheet and inserted 7 data points and a corresponding 2D pie chart. With the default style, it copied as a 79 KB emf, and with style 26 it copies as a 10 MB emf. When I was using Office 2007, the chart would copy as a 700 KB emf. This is the code:
Sub CopyAndPaste()
ThisWorkbook.Worksheets("Charts").ChartObjects("Chart 1").CopyPicture Appearance:=xlScreen, Format:=xlPicture
GetObject(, Class:="Word.Application").ActiveDocument.Range.Paste
End Sub
I am able to CopyPicture with the format xlBitmap, and while that is somewhat smaller, it is larger than the emf generated by Office 2007 and noticeably poorer quality. Are there any other options for reducing the file size? Ideally, I would like to produce a file with the same resolution for the charts as I did using Office 2007. Is there any way that uses VBA only (without modifying the charts in the spreadsheet)? Any way I can easily copy as an object without linking the documents?

"It's an older code, sir, but it checks out."
It's an old question and I have an even older (possible) solution: you can compress your .EMF files as .EMZ by gzipping it. This will reduce your file size while keeping the image quality.
On VB6 I used zlib.dll and the code below. I renamed the function names to english but I kept all comments in portuguese:
Option Explicit
' Declaração das interfaces com a ZLIB
Private Declare Function gzopen Lib "zlib.dll" (ByVal file As String, ByVal mode As String) As Long
Private Declare Function gzwrite Lib "zlib.dll" (ByVal file As Long, ByRef uncompr As Byte, ByVal uncomprLen As Long) As Long
Private Declare Function gzclose Lib "zlib.dll" (ByVal file As Long) As Long
Private Declare Function Compress Lib "zlib.dll" Alias "compress" (ByRef dest As Any, ByRef destLen As Any, ByRef src As Any, ByVal srcLen As Long) As Long
Private Declare Function Uncompress Lib "zlib.dll" Alias "uncompress" (ByRef dest As Any, ByRef destLen As Any, ByRef src As Any, ByVal srcLen As Long) As Long
' Ler o conteúdo de um arquivo
Public Function FileRead(ByVal strNomeArquivo As String) As Byte()
Dim intHandle As Integer
Dim lngTamanho As Long
Dim bytConteudo() As Byte
On Error GoTo FileReadError
' Abrir o documento indicado
intHandle = FreeFile
Open strNomeArquivo For Binary Access Read As intHandle
' Obter o tamanho do arquivo
lngTamanho = LOF(intHandle)
ReDim bytConteudo(lngTamanho)
' Obter o conteúdo e liberar o arquivo
Get intHandle, , bytConteudo()
Close intHandle
FileRead = bytConteudo
On Error GoTo 0
Exit Function
FileReadError:
objLogger.GravarEvento "modZLib.FileRead: " & Err.Description & " (" & Err.Number & " - " & Err.Source & ")", logTipoEvento.Erro
End Function
'Compactar um arquivo com o padrão gzip
Public Sub FileCompress(ByVal strArquivoOrigem As String, ByVal strArquivoDestino As String)
Dim gzFile As Long
Dim bytConteudo() As Byte
On Error GoTo FileCompressError
' Ler o conteúdo do arquivo
bytConteudo = FileRead(strArquivoOrigem)
' Compactar o conteúdo
gzFile = gzopen(strArquivoDestino, "wb")
gzwrite gzFile, bytConteudo(0), UBound(bytConteudo)
gzclose gzFile
On Error GoTo 0
Exit Sub
FileCompressError:
objLogger.GravarEvento "modZLib.FileCompress:" & Err.Description & " (" & Err.Number & " - " & Err.Source & ")", logTipoEvento.Erro
End Sub

I've dealt with something like this before
Instead of using
Document.Range.Paste
Try Using
Document.Range.PasteSpecial DataType:= wdPasteMetafilePicture
or
Document.Range.PasteSpecial DataType:= wdPasteShape
This will paste the chart as a picture or drawing as opposed to an embedded excel object
Equivelant to using "Paste Special..." from the menu.
Other DataTypes are available
http://msdn.microsoft.com/en-us/library/office/aa220339(v=office.11).aspx

This is possibly happening because the .emf files are getting scaled incorrectly. Using PNG may resolve the size issue (as mentioned in the comments above), but will still be an issue because they will not be vector images.
If you use AddPicture to add images to your file, then the following page shows a solution wherein you can change the scale and reduce filesize from whatever default is being used. So it might solve your issue.
http://social.msdn.microsoft.com/Forums/en-US/f1c9c753-77fd-4c17-9ca8-02908debca5d/emf-file-added-using-the-addpicture-method-looks-bigger-in-excel-20072010-vs-excel-2003?forum=exceldev

Related

Download a text file from a url in VBA

I need to do a one time download of a pipe deliminated text file in VBA. I have tried many of the solutions in other stack overflow questions but I can't seem to make any of the solutions work. It's from the internal wiki page of my firm.
The file is something like: https://wiki.somecompany/downloads/attachments/data.txt
Note: that is not a real url
Edit: I am working within excel.
I am extremely new to VBA, so the solutions I read will probably work but they were not idiot proof.
I tried many things, but the most promising looking were the solutions posted here: EXCEL VBA - To open a text file from a website
I stopped working with the first one because it seemed like you needed Mozilla for that one, and I did not know how to specify Chrome.
I messed around with the open workbook option, but I kept getting a compile error that said "Expected: =" but I don't know what the problem is or where it should be.
Edited: #Tim Williams - your solution is the closest to have anything at all happen besides just VBA errors. I got as far as turning my spreadsheet into a log in page, so I guess I need to pass a username and password somehow
You should be able to turn on the Macro Recorder and get what you want pretty quickly. In fact, you probably spent 10x more time describing the scenario, then it would take to record the code you need. Although, it is possible that you actually can't import the data using the Macro Recorder. You should still be able to import the data by referencing a CSV, which I believe is the exact same thing as a Text file.
Sub Import_CSV_File_From_URL()
Dim URL As String
Dim destCell As Range
URL = "http://www.test.com/test.csv"
Set destCell = Worksheets("test").Range("A1")
With destCell.Parent.QueryTables.Add(Connection:="TEXT;" & URL, Destination:=destCell)
.TextFileStartRow = 1
.TextFileParseType = xlDelimited
.TextFileCommaDelimiter = True
.Refresh BackgroundQuery:=False
End With
destCell.Parent.QueryTables(1).Delete
End Sub
If that doesn't work for you, simply download the file, and do the import from your hard-drive.
Private Declare Function URLDownloadToFile Lib "urlmon" Alias _
"URLDownloadToFileA" (ByVal pCaller As Long, ByVal szURL As String, ByVal _
szFileName As String, ByVal dwReserved As Long, ByVal lpfnCB As Long) As Long
Sub DownloadFilefromWeb()
Dim strSavePath As String
Dim URL As String, ext As String
Dim buf, ret As Long
URL = Worksheets("Sheet1").Range("A2").Value
buf = Split(URL, ".")
ext = buf(UBound(buf))
strSavePath = "C:\Users\rshuell\Desktop\Downloads\" & "DownloadedFile." & ext
ret = URLDownloadToFile(0, URL, strSavePath, 0, 0)
If ret = 0 Then
MsgBox "Download has been succeed!"
Else
MsgBox "Error"
End If
End Sub

Excel VBA bug accessing HelpFile property from macro-disabled instance?

I think I've stumbled upon a bug in Excel - I'd really like to verify it with someone else though.
The bug occurs when reading the Workbook.VBProject.HelpFile property when the workbook has been opened with the opening application's .AutomationSecurity property set to ForceDisable. In that case this string property returns a (probably) malformed Unicode string, which VBA in turn displays with question marks. Running StrConv(..., vbUnicode) on it makes it readable again, but it sometimes looses the last character this way; this might indicate that the unicode string is indeed malformed or such, and that VBA therefore tries to convert it first and fails.
Steps to reproduce this behaviour:
Create a new Excel workbook
Go to it's VBA project (Alt-F11)
Add a new code module and add some code to it (like e.g. Dim a As Long)
Enter the project's properties (menu Tools... properties)
Enter "description" as Project description and "abc.hlp" as Help file name
Save the workbook as a .xlsb or .xlsm
Close the workbook
Create a new Excel workbook
Go to it's VBA project (Alt-F11)
Add a fresh new code module
Paste the code below in it
Adjust the path on the 1st line so it points to the file you created above
Run the Test routine
The code to use:
Const csFilePath As String = "<path to your test workbook>"
Sub TestSecurity(testType As String, secondExcel As Application, security As MsoAutomationSecurity)
Dim theWorkbook As Workbook
secondExcel.AutomationSecurity = security
Set theWorkbook = secondExcel.Workbooks.Open(csFilePath)
Call MsgBox(testType & " - helpfile: " & theWorkbook.VBProject.HelpFile)
Call MsgBox(testType & " - helpfile converted: " & StrConv(theWorkbook.VBProject.HelpFile, vbUnicode))
Call MsgBox(testType & " - description: " & theWorkbook.VBProject.Description)
Call theWorkbook.Close(False)
End Sub
Sub Test()
Dim secondExcel As Excel.Application
Set secondExcel = New Excel.Application
Dim oldSecurity As MsoAutomationSecurity
oldSecurity = secondExcel.AutomationSecurity
Call TestSecurity("enabled macros", secondExcel, msoAutomationSecurityLow)
Call TestSecurity("disabled macros", secondExcel, msoAutomationSecurityForceDisable)
secondExcel.AutomationSecurity = oldSecurity
Call secondExcel.Quit
Set secondExcel = Nothing
End Sub
Conclusion when working from Excel 2010:
.Description is always readable, no matter what (so it's not like all string properties behave this way)
xlsb and xlsm files result in an unreadable .HelpFile only when macros are disabled
xls files result in an unreadable .HelpFile in all cases (!)
It might be even weirder than that, since I swear I once even saw the questionmarks-version pop up in the VBE GUI when looking at such a project's properties, though I'm unable to reproduce that now.
I realize this is an edge case if ever there was one (except for the .xls treatment though), so it might just have been overlooked by Microsoft's QA department, but for my current project I have to get this working properly and consistently across Excel versions and workbook formats...
Could anyone else test this as well to verify my Excel installation isn't hosed? Preferably also with another Excel version, to see if that makes a difference?
Hopefully this won't get to be a tumbleweed like some of my other posts here :) Maybe "Tumbleweed generator" might be a nice badge to add...
UPDATE
I've expanded the list of properties to test just to see what else I could find, and of all the VBProject's properties (BuildFileName, Description, Filename, HelpContextID, HelpFile, Mode, Name, Protection and Type) only .HelpFile has this problem of being mangled when macros are off.
UPDATE 2
Porting the sample code to Word 2010 and running that exhibits exactly the same behaviour - the .HelpFile property is malformed when macros are disabled. Seems like the code responsible for this is Office-wide, probably in a shared VBA library module (as was to be expected TBH).
UPDATE 3
Just tested it on Excel 2007 and 2003, and both contain this bug as well. I haven't got an Excel XP installation to test it out on, but I can safely say that this issue already has a long history :)
I've messed with the underlying binary representation of the strings in question, and found out that the .HelpFile string property indeed returns a malformed string.
The BSTR representation (underwater binary representation for VB(A) strings) returned by the .HelpFile property lists the string size in the 4 bytes in front of the string, but the following content is filled with the ASCII representation and not the Unicode (UTF16) representation as VBA expects.
Parsing the content of the BSTR returned and deciding for ourselves which format is most likely used fixes this issue in some circumstances. Another issue is unfortunately at play here as well: it only works for even-length strings... Odd-length strings get their last character chopped off, their BSTR size is reported one short, and the ASCII representation just doesn't include the last character either... In that case, the string cannot be recovered fully.
The following code is the example code in the question augmented with this fix. The same usage instructions apply to it as for the original sample code. The RecoverString function performs the needed magic to, well, recover the string ;) DumpMem returns a 50-byte memory dump of the string you pass to it; use this one to see how the memory is layed out exactly for the passed-in string.
Const csFilePath As String = "<path to your test workbook>"
Private Declare Sub CopyMemoryByte Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Byte, ByVal Source As Long, ByVal Length As Integer)
Private Declare Sub CopyMemoryWord Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Integer, ByVal Source As Long, ByVal Length As Integer)
Private Declare Sub CopyMemoryDWord Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Function DumpMem(text As String) As String
Dim textAddress As LongPtr
textAddress = StrPtr(text)
Dim dump As String
Dim offset As Long
For offset = -4 To 50
Dim nextByte As Byte
Call CopyMemoryByte(nextByte, textAddress + offset, 1)
dump = dump & Right("00" & Hex(nextByte), 2) & " "
Next
DumpMem = dump
End Function
Function RecoverString(text As String) As String
Dim textAddress As LongPtr
textAddress = StrPtr(text)
If textAddress <> 0 Then
Dim textSize As Long
Call CopyMemoryDWord(textSize, textAddress - 4, 4)
Dim recovered As String
Dim foundNulls As Boolean
foundNulls = False
Dim offset As Long
For offset = 0 To textSize - 1
Dim nextByte As Byte
Call CopyMemoryByte(nextByte, textAddress + offset, 1)
recovered = recovered & Chr(CLng(nextByte) + IIf(nextByte < 0, &H80, 0))
If nextByte = 0 Then
foundNulls = True
End If
Next
Dim isNotUnicode As Boolean
isNotUnicode = isNotUnicode Mod 2 = 1
If foundNulls And Not isNotUnicode Then
recovered = ""
For offset = 0 To textSize - 1 Step 2
Dim nextWord As Integer
Call CopyMemoryWord(nextWord, textAddress + offset, 2)
recovered = recovered & ChrW(CLng(nextWord) + IIf(nextWord < 0, &H8000, 0))
Next
End If
End If
RecoverString = recovered
End Function
Sub TestSecurity(testType As String, secondExcel As Application, security As MsoAutomationSecurity)
Dim theWorkbook As Workbook
secondExcel.AutomationSecurity = security
Set theWorkbook = secondExcel.Workbooks.Open(csFilePath)
Call MsgBox(testType & " - helpfile: " & theWorkbook.VBProject.HelpFile & " - " & RecoverString(theWorkbook.VBProject.HelpFile))
Call MsgBox(testType & " - description: " & theWorkbook.VBProject.Description & " - " & RecoverString(theWorkbook.VBProject.Description))
Call theWorkbook.Close(False)
End Sub
Sub Test()
Dim secondExcel As Excel.Application
Set secondExcel = New Excel.Application
Dim oldSecurity As MsoAutomationSecurity
oldSecurity = secondExcel.AutomationSecurity
Call TestSecurity("disabled macros", secondExcel, msoAutomationSecurityForceDisable)
Call TestSecurity("enabled macros", secondExcel, msoAutomationSecurityLow)
secondExcel.AutomationSecurity = oldSecurity
Call secondExcel.Quit
Set secondExcel = Nothing
End Sub

Identify icon overlay in VBA

I'm on a quest to figure out how to identify different icon overlays through Excel VBA.
There is a cloud syncing software and I am trying to identify whenever the syncing of my excel file has finished or still in progress. I was able to achieve a basic level of reliability by following the modification date of some meta(?) files but there is not enough consistency to fully rely on this method.
The result of my searches is a big punch in the face, since there is not much info about it in VBA. Basically all I have found that everyone uses advanced languages like C++ to handle these things.
The closest source I've got in VBA does something similar with the System Tray and uses the shell32.dll calling the appropiate windows api (link). But I have no idea how to make it to the Shell Icon Overlay Identifier.
What do you guys think, is there a possible way to make it through VBA or I have to learn C++?
Awesome! It is possible! The SHGetFileInfo method works!
It gives me values according to the current overlays. Here is the code for any other crazy people who wanna mess around with it:
Const SHGFI_ICON = &H100
Const SHGFI_OVERLAYINDEX = &H40
Const MAX_PATH = 260
Const SYNCED = 100664316 'own specific value
Const UNDSYNC = 117442532 'own specific value
Private Type SHFILEINFO
hIcon As Long 'icon
iIcon As Long 'icon index
dwAttributes As Long 'SFGAO_ flags
szDisplayName As String * MAX_PATH 'display name (or path)
szTypeName As String * 80 'type name
End Type
Private Declare Function SHGetFileInfo Lib "shell32.dll" Alias "SHGetFileInfoA" _
(ByVal pszPath As String, _
ByVal dwFileAttributes As Long, _
psfi As SHFILEINFO, _
ByVal cbFileInfo As Long, _
ByVal uFlags As Long) As Long
Private Sub GetThatInfo()
Dim FI As SHFILEINFO
SHGetFileInfo "E:\Test.xlsm", 0, FI, Len(FI), SHGFI_ICON Or SHGFI_OVERLAYINDEX
Select Case FI.iIcon
Case SYNCED
Debug.Print "Synchronized"
Case UNDSYNC
Debug.Print "Synchronization in progress"
Case Else
Debug.Print "Some shady stuff is going on!"
End Select
End Sub
Thanks for the tip again!

Export Excel data to INI file

Summary: Export values from a column to an INI file.
I have a spreadsheet that has data in column M and I want to copy the data from column M to a an .ini file. Each cell in the column contains a file name of a person's progress report. This data would be like:
Smith, John Progress Report 2015-07-30.doc
The .ini file would be something like:
[Settings]
smith=Smith, John Progress Report 2015-07-30
I would prefer to have the key in lowercase and remove the ".doc" from the filename.
I want to export those filenames to the .ini and use the last name as the key. I don't really know where to start on this. Excel doesn't have its own access to writing to .ini files, but it can use Word or Windows API from what I read on the internet. Also, I guess Excel can just simply append the data to the .ini as a text file.
So I have some ideas, but I didn't see anything else on StackOverflow that specifically worked on this task to get me started. If you find something that does this specifically then that would be great. Otherwise, if someone can get me started on the code to complete this task, I would really appreciate it.
This should get you started. It uses the value from M1 and writes it to an .ini file. Just add a loop to iterate all your rows and you should be all set.
Private Declare Function WritePrivateProfileString Lib "kernel32" Alias "WritePrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpString As Any, ByVal lpFileName As String) As Long
Public Sub ExportToIni()
Dim re As Object
Set re = CreateObject("VBScript.RegExp")
re.IgnoreCase = True
re.Pattern = "^(([^,]+),.+)\.doc$"
Dim strText As String
strText = Trim$(Cells(1, 13)) ' [M1]
Dim m As Object, c As Object, strLastName As String, strFileName As String
If re.Test(strText) Then
Set c = re.Execute(strText)
strFileName = c(0).SubMatches(0)
strLastName = LCase$(c(0).SubMatches(1))
WritePrivateProfileString "Settings", strLastName, strFileName, "c:\path\to\your.ini"
End If
End Sub
The .ini file:
[Settings]
smith=Smith, John Progress Report 2015-07-30
This will do what you want:
Sub SaveIni()
Dim c as range
Open "C:\Temp\Output.ini" For Output As #1
For Each c In Range("M1:M" & Range("M" & Rows.Count).End(xlUp).Row)
Print #1, LCase(Split(c, ",")(0)) & "=" & Split(c, ".doc")(0)
Next
Close #1
End Sub
Uses split on the comma to get the name and lower cases it then adds an = then splits the initial string on .doc to drop that off, we could have used replace here if you so wish.
Here is my test input:
Smith, John Progress Report 2015-07-30.doc
davis, andrew Progress Report 2015-07-30.doc
MALCOLMSON, Aaron File 20jio32ejo23oji32eojie23oj23eoje23joe3e23oji-07-30.doc
Green, Frank Report 20-07-30.doc
Here is my ini file:
smith=Smith, John Progress Report 2015-07-30
davis=davis, andrew Progress Report 2015-07-30
malcolmson=MALCOLMSON, Aaron File 20jio32ejo23oji32eojie23oj23eoje23joe3e23oji-07-30
green=Green, Frank Report 20-07-30
Note, this will OVERWRITE your ini file if it already exists If you wish to append then change this:
Open "C:\Temp\Output.ini" For Output As #1
to this:
Open "C:\Temp\Output.ini" For Append As #1

WinAPI FTPGetFile Conversion From ANSI to Unicode

Premise: Copying files from Linux to Windows over FTP using WinInet FtpGetFile.
Objective: The files originate as ANSI and are needed in Unicode.
Progress:
The only issue I am having is that I need LF characters from the original file to be CRLF characters in the destination file.
I have tried:
Public Declare Function FtpGetFile Lib "wininet.dll" Alias "FtpGetFileW" (ByVal hFTP As Long, ByVal sRemoteFile As String, ByVal sNewFile As String, ByVal bFailIfExists As Boolean, ByVal lFlagsAndAttributes As Long, ByVal lFlags As Long, ByVal lContext As Long) As Boolean
Public Sub test(hConn as Long, strSrcPath as String, strDestPath as String)
'All code works other than the file not converting to having CR chars
ftpGetFile(hConn, StrConv(strSrcPath, vbUnicode), StrConv(strDestPath, vbUnicode), True, 0, 0, 0)
End Sub
(FAILS to convert) using the Unicode version of the FtpGetFile method (Alias FtpGetFileW), passing the arguments using StrConv(<string>, vbUnicode). The files show up with only LF chars at the end of the lines.
(WORKS, manually) copying files manually using WinSCP. It automatically makes the output files Unicode but I can't find the method/settings associated with this. I cannot use the WinSCP.dll at my work as I cannot register it.
(WORKS, slowly) using a work-around. using the either version of the FtpGetFile. Opening file, reading to variable, closing file and then opening file for write, writing Replace(variable,Chr(10),Chr(13)&Chr(10)). Also, files appear to ~double in size.
How do I get a file using the WinAPI functions and have it convert in one shot (if possible)?
Related articles:
Unicode turns ANSI after FTP transfer
Writing ANSI string to Unicode file over FTP
Source Info:
How to Create FTP Components CodeGuru
MSDN for WinInet
The following appears to be working near instantaneously. If anyone has any better suggestions on how to automate this (preferably without this work-around or to make my work-around better) please provide them. Otherwise, I'll probably be choosing this as the answer in a few days. ftpReadFile is a custom function that uses InternetReadFile and spits out the entire file as a string.
Public Function ftpGetFileToUnicode(hConn As Long, strFromPath As String, strDestPath As String) As Boolean
Dim hFile As Long
Dim objFS As New FileSystemObject, objFile As TextStream
If Not objFS.FileExists(strDestPath) Then
Set objFile = objFS.CreateTextFile(strDestPath, ForWriting)
objFile.Write Replace(ftpReadFile(hConn, strFromPath), Chr(10), Chr(13) & Chr(10))
objFile.Close
If objFS.GetFile(strDestPath).Size > 0 Then
ftpGetFileToUnicode = True
Exit Function
End If
End If
ftpGetFileToUnicode = False
End Function
Note: Creates a 0 byte file if the file doesn't exist. Can easily be changed to not do that.
Disclaimer: I know nothing about VB. But FtpGetFile says it supports ASCII mode transfers, which have implicit line ending conversion:
ftpGetFile(hConn, StrConv(strSrcPath, vbUnicode), StrConv(strDestPath, vbUnicode),
True, 0, FTP_TRANSFER_TYPE_ASCII, 0)