diff --git a/Hot.xcodeproj/project.pbxproj b/Hot.xcodeproj/project.pbxproj index 283d30b..70af8c9 100644 --- a/Hot.xcodeproj/project.pbxproj +++ b/Hot.xcodeproj/project.pbxproj @@ -51,6 +51,10 @@ 05B0DFAA267150F40068FF9B /* TCXCTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 05B0DFA8267150F40068FF9B /* TCXCTemplate.pdf */; }; 05BA0BAA26F342DA00AA7BFC /* PressureToString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA0BA926F342DA00AA7BFC /* PressureToString.swift */; }; 05BA0BAD26F347A500AA7BFC /* PressureTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 05BA0BAC26F347A500AA7BFC /* PressureTemplate.pdf */; }; + 382CF88F2C4FF77400D9DB3C /* FanSpeedToString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382CF88E2C4FF77400D9DB3C /* FanSpeedToString.swift */; }; + 388C8F752C5522FC0057EA05 /* FanViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 388C8F732C5522FC0057EA05 /* FanViewController.xib */; }; + 388C8F772C5523260057EA05 /* FanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388C8F762C5523260057EA05 /* FanViewController.swift */; }; + 38B72A8D2C53DAFF00AD0B87 /* FanTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 38B72A8A2C53DAFF00AD0B87 /* FanTemplate.pdf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -193,6 +197,10 @@ 05B0DFA8267150F40068FF9B /* TCXCTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = TCXCTemplate.pdf; sourceTree = ""; }; 05BA0BA926F342DA00AA7BFC /* PressureToString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureToString.swift; sourceTree = ""; }; 05BA0BAC26F347A500AA7BFC /* PressureTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = PressureTemplate.pdf; sourceTree = ""; }; + 382CF88E2C4FF77400D9DB3C /* FanSpeedToString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanSpeedToString.swift; sourceTree = ""; }; + 388C8F742C5522FC0057EA05 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/FanViewController.xib; sourceTree = ""; }; + 388C8F762C5523260057EA05 /* FanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanViewController.swift; sourceTree = ""; }; + 38B72A8A2C53DAFF00AD0B87 /* FanTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = FanTemplate.pdf; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -227,6 +235,7 @@ 057F8AFF251D22360048F1E1 /* StatusIconTemplate.pdf */, 05BA0BAC26F347A500AA7BFC /* PressureTemplate.pdf */, 052FBCD2251F842C00C4322E /* TemperatureTemplate.pdf */, + 38B72A8A2C53DAFF00AD0B87 /* FanTemplate.pdf */, ); path = Images; sourceTree = ""; @@ -414,6 +423,7 @@ 057F8B0B251D273E0048F1E1 /* AboutWindowController.swift */, 05AA4079251CF3D200106CEA /* ApplicationDelegate.swift */, 05221E87267009A8007246FF /* SensorViewController.swift */, + 388C8F762C5523260057EA05 /* FanViewController.swift */, 055A932A252780FD0070F771 /* GraphView.swift */, 057F8B0E251D274C0048F1E1 /* Hot-Bridging-Header.h */, 055A931F252778C10070F771 /* InfoViewController.swift */, @@ -428,6 +438,7 @@ 05AA408C251CFA0600106CEA /* ThermalLog.swift */, 05AD458729084FEF00A791C8 /* SelectSensorsWindowController.swift */, 05566A74299195F400E7A5F1 /* GraphWindowController.swift */, + 382CF88E2C4FF77400D9DB3C /* FanSpeedToString.swift */, ); path = Classes; sourceTree = ""; @@ -441,6 +452,7 @@ 05AA407D251CF3D600106CEA /* MainMenu.xib */, 052FBCA5251EE46700C4322E /* PreferencesWindowController.xib */, 05221E8926700A57007246FF /* SensorViewController.xib */, + 388C8F732C5522FC0057EA05 /* FanViewController.xib */, 05AD458E2908505900A791C8 /* SelectSensorsWindowController.xib */, 05566A772991963400E7A5F1 /* GraphWindowController.xib */, ); @@ -544,12 +556,14 @@ files = ( 052FBCD4251F842C00C4322E /* SpeedTemplate.pdf in Resources */, 05BA0BAD26F347A500AA7BFC /* PressureTemplate.pdf in Resources */, + 388C8F752C5522FC0057EA05 /* FanViewController.xib in Resources */, 052FBCD6251F842C00C4322E /* TemperatureTemplate.pdf in Resources */, 057F8B00251D22360048F1E1 /* StatusIconTemplate.pdf in Resources */, 057F8B1B251D2A970048F1E1 /* Icon.icns in Resources */, 052FBCD5251F842C00C4322E /* CPUTemplate.pdf in Resources */, 05221E8B26700A57007246FF /* SensorViewController.xib in Resources */, 05AD459229092CEB00A791C8 /* TCalTemplate.pdf in Resources */, + 38B72A8D2C53DAFF00AD0B87 /* FanTemplate.pdf in Resources */, 05AD458C2908505900A791C8 /* SelectSensorsWindowController.xib in Resources */, 055A9323252778D00070F771 /* InfoViewController.xib in Resources */, 05B0DFA526714A830068FF9B /* UnknownTemplate.pdf in Resources */, @@ -594,6 +608,8 @@ buildActionMask = 2147483647; files = ( 055A932525277E9E0070F771 /* IntegerToString.swift in Sources */, + 388C8F772C5523260057EA05 /* FanViewController.swift in Sources */, + 382CF88F2C4FF77400D9DB3C /* FanSpeedToString.swift in Sources */, 055A932725277F6E0070F771 /* TemperatureToString.swift in Sources */, 05221E8626700772007246FF /* DictionaryIsEmpty.swift in Sources */, 052FBCA3251EE43B00C4322E /* PreferencesWindowController.swift in Sources */, @@ -670,6 +686,14 @@ name = SelectSensorsWindowController.xib; sourceTree = ""; }; + 388C8F732C5522FC0057EA05 /* FanViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 388C8F742C5522FC0057EA05 /* Base */, + ); + name = FanViewController.xib; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ diff --git a/Hot/Classes/ApplicationDelegate.swift b/Hot/Classes/ApplicationDelegate.swift index 5407c93..0b3a363 100644 --- a/Hot/Classes/ApplicationDelegate.swift +++ b/Hot/Classes/ApplicationDelegate.swift @@ -35,11 +35,13 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate private var sensorsWindowController: SensorsWindowController? private var selectSensorsWindowController: SelectSensorsWindowController? private var sensorViewControllers: [ SensorViewController ] = [] + private var fanViewControllers: [ FanViewController ] = [] private var graphWindowController: GraphWindowController? private var exiting = false @IBOutlet private var menu: NSMenu! @IBOutlet private var sensorsMenu: NSMenu! + @IBOutlet private var fansMenu: NSMenu! @IBOutlet private var updater: GitHubUpdater! @objc public private( set ) dynamic var infoViewController: InfoViewController? @@ -82,6 +84,7 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate self?.graphWindowController?.availableCPUs = self?.infoViewController?.availableCPUs ?? 0 self?.graphWindowController?.speedLimit = self?.infoViewController?.speedLimit ?? 0 self?.graphWindowController?.temperature = self?.infoViewController?.temperature ?? 0 + self?.graphWindowController?.fanSpeed = self?.infoViewController?.fanSpeed ?? 0 self?.graphWindowController?.thermalPressure = self?.infoViewController?.thermalPressure ?? 0 } @@ -357,18 +360,30 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { self.sensorsMenu.removeAllItems() } + if self.fanViewControllers.isEmpty + { + self.fansMenu.removeAllItems() + } guard let sensors = self.infoViewController?.log.sensors else { return } + guard let fans = self.infoViewController?.log.fans + else + { + return + } var controllers = self.sensorViewControllers + var fanControllers = self.fanViewControllers var items = self.sensorsMenu.items + var fanItems = self.fansMenu.items controllers.removeAll { item in sensors.contains { $0.key == item.name } == false } items.removeAll { item in sensors.contains { $0.key == item.title } == false } + fanItems.removeAll { item in fans.contains { $0.key == item.title } == false } sensors.forEach { @@ -403,6 +418,41 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { self.sensorsMenu.addItem( $0 ) } + + fans.forEach + { + fan in + + if let controller = self.fanViewControllers.first( where: { $0.name == fan.key } ) + { + controller.value = Int( fan.value ) + } + else + { + let controller = FanViewController() + controller.name = fan.key + controller.value = Int( fan.value ) + let item = NSMenuItem( title: fan.key, action: nil, keyEquivalent: "" ) + item.view = controller.view + + fanItems.append( item ) + fanControllers.append( controller ) + } + } + + self.fanViewControllers = fanControllers + + self.fansMenu.removeAllItems() + + + fanItems.sorted + { + $0.title.compare( $1.title, options: [ .numeric, .caseInsensitive ], range: nil, locale: nil ) == .orderedAscending + } + .forEach + { + self.fansMenu.addItem( $0 ) + } } @IBAction diff --git a/Hot/Classes/FanSpeedToString.swift b/Hot/Classes/FanSpeedToString.swift new file mode 100644 index 0000000..7e6b837 --- /dev/null +++ b/Hot/Classes/FanSpeedToString.swift @@ -0,0 +1,64 @@ +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2022, Jean-David Gadina - www.xs-labs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the Software), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +import Cocoa + +@objc( FanSpeedToString ) +public class FanSpeedToString: ValueTransformer +{ + public override class func transformedValueClass() -> AnyClass + { + return NSString.self + } + + public override class func allowsReverseTransformation() -> Bool + { + return false + } + + public override func transformedValue( _ value: Any? ) -> Any? + { + let n: Int? = + { + if let n = value as? Int + { + return n + } + else if let n = value as? Double + { + return Int( n ) + } + + return nil + }() + + guard let n = n, n > 0 + else + { + return "Off" as NSString + } + + return "\( n ) RPM" as NSString + } +} diff --git a/Hot/Classes/FanViewController.swift b/Hot/Classes/FanViewController.swift new file mode 100644 index 0000000..c10db06 --- /dev/null +++ b/Hot/Classes/FanViewController.swift @@ -0,0 +1,46 @@ +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2022, Jean-David Gadina - www.xs-labs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the Software), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +import Cocoa + +public class FanViewController: NSViewController +{ + @objc private dynamic var icon = NSImage( named: "Unknown" ) + @objc private dynamic var label = "Unknown:" + @objc public dynamic var value = 0 + @objc public dynamic var name = "Unknown" + { + didSet + { + self.label = self.name.hasSuffix( ":" ) ? self.name : "\( self.name ):" + + self.icon = NSImage( named: "FanTemplate" ) + } + } + + public override var nibName: NSNib.Name? + { + "FanViewController" + } +} diff --git a/Hot/Classes/GraphWindowController.swift b/Hot/Classes/GraphWindowController.swift index 9932b63..f96d65c 100644 --- a/Hot/Classes/GraphWindowController.swift +++ b/Hot/Classes/GraphWindowController.swift @@ -33,6 +33,7 @@ public class GraphWindowController: NSWindowController @objc public dynamic var availableCPUs: Int = 0 @objc public dynamic var speedLimit: Int = 0 @objc public dynamic var temperature: Int = 0 + @objc public dynamic var fanSpeed: Int = 0 @objc public dynamic var thermalPressure: Int = 0 @objc public dynamic var showOnAllSpaces: Bool = false { diff --git a/Hot/Classes/InfoViewController.swift b/Hot/Classes/InfoViewController.swift index 22f1944..41a29e5 100644 --- a/Hot/Classes/InfoViewController.swift +++ b/Hot/Classes/InfoViewController.swift @@ -33,8 +33,10 @@ public class InfoViewController: NSViewController @objc public private( set ) dynamic var availableCPUs: Int = 0 @objc public private( set ) dynamic var speedLimit: Int = 0 @objc public private( set ) dynamic var temperature: Int = 0 + @objc public private( set ) dynamic var fanSpeed: Int = 0 @objc public private( set ) dynamic var thermalPressure: Int = 0 @objc public private( set ) dynamic var hasSensors: Bool = false + @objc public private( set ) dynamic var hasFans: Bool = false public var onUpdate: ( () -> Void )? @@ -117,6 +119,7 @@ public class InfoViewController: NSViewController private func update() { self.hasSensors = self.log.sensors.isEmpty == false + self.hasFans = self.log.fans.isEmpty == false if let n = self.log.schedulerLimit?.intValue { @@ -138,6 +141,11 @@ public class InfoViewController: NSViewController self.temperature = n } + if let n = self.log.fanSpeed?.intValue + { + self.fanSpeed = n + } + if let n = self.log.thermalPressure?.intValue { self.thermalPressure = n diff --git a/Hot/Classes/ThermalLog.swift b/Hot/Classes/ThermalLog.swift index 06cb820..c520bb6 100644 --- a/Hot/Classes/ThermalLog.swift +++ b/Hot/Classes/ThermalLog.swift @@ -32,19 +32,24 @@ public class ThermalLog: NSObject @objc public dynamic var availableCPUs: NSNumber? @objc public dynamic var speedLimit: NSNumber? @objc public dynamic var temperature: NSNumber? + @objc public dynamic var fanSpeed: NSNumber? @objc public dynamic var thermalPressure: NSNumber? @objc public dynamic var sensors: [ String: Double ] = [ : ] + @objc public dynamic var fans: [ String: Double ] = [ : ] private var refreshing = false private static var queue = DispatchQueue( label: "com.xs-labs.Hot.ThermalLog", qos: .background, attributes: [], autoreleaseFrequency: .workItem, target: nil ) + private var regexFanRPM: NSRegularExpression + public override init() { + self.regexFanRPM = try! NSRegularExpression( pattern: "F[0-9]Ac" ) super.init() } - private func readSensors() -> [ String: ( temperature: Double, isCPU: Bool ) ] + private func readSensors() -> [ String: ( value: Double, isCPU: Bool, isFan: Bool ) ] { let ioHID = IOHID.shared.readTemperatureSensors().compactMap { @@ -53,29 +58,26 @@ public class ThermalLog: NSObject let smc = SMC.shared.readAllKeys { - $0 >> 24 == 84 // T prefix (four char code) + $0 >> 24 == 84 || // T prefix (four char code) + $0 >> 24 == 70 // F prefix } .compactMap { self.sensorValue( data: $0 ) } - let all = [ ioHID, smc ].flatMap { $0 }.filter - { - $0.1.temperature > 0 && $0.1.temperature < 120 - } - + let all = [ ioHID, smc ].flatMap { $0 } return Dictionary( uniqueKeysWithValues: all ) } - private func sensorValue( data: IOHIDData ) -> ( String, ( temperature: Double, isCPU: Bool ) )? + private func sensorValue( data: IOHIDData ) -> ( String, ( value: Double, isCPU: Bool, isFan: Bool ) )? { let isCPU = data.name.hasPrefix( "pACC" ) || data.name.hasPrefix( "eACC" ) - return ( data.name, ( temperature: data.value, isCPU: isCPU ) ) + return ( data.name, ( value: data.value, isCPU: isCPU, isFan: false ) ) } - private func sensorValue( data: SMCData ) -> ( String, ( temperature: Double, isCPU: Bool ) )? + private func sensorValue( data: SMCData ) -> ( String, ( value: Double, isCPU: Bool, isFan: Bool ) )? { let value: Double @@ -92,7 +94,7 @@ public class ThermalLog: NSObject return nil } - return ( data.keyName, ( temperature: value, isCPU: false ) ) + return ( data.keyName, ( value: value, isCPU: false, isFan: regexFanRPM.firstMatch(in: data.keyName, range: NSMakeRange(0, data.keyName.count)) != nil ) ) } public func refresh( completion: @escaping () -> Void ) @@ -116,9 +118,11 @@ public class ThermalLog: NSObject #endif let sensors = self.readSensors() - let cpu = sensors.filter { $0.value.isCPU }.mapValues { $0.temperature } - let all = sensors.mapValues { $0.temperature } - self.sensors = all + let cpu = sensors.filter { $0.value.isCPU }.mapValues { $0.value } + let allTemperatures = sensors.filter { $0.value.isFan == false && ($0.value.value > 0 && $0.value.value < 120) }.mapValues { $0.value } + let allFans = sensors.filter { $0.value.isFan == true }.mapValues { $0.value } + self.sensors = allTemperatures + self.fans = allFans let names = UserDefaults.standard.object( forKey: "selectedSensors" ) as? [ String ] ?? [] let selectionMode = UserDefaults.standard.integer( forKey: "selectedSensorsMode" ) let selected = sensors.filter @@ -127,7 +131,7 @@ public class ThermalLog: NSObject } .mapValues { - $0.temperature + $0.value } let temperatures: [ Double ] = @@ -142,9 +146,9 @@ public class ThermalLog: NSObject } else { - let tcal = all.first { $0.key.lowercased().hasSuffix( "tcal" ) } + let tcal = allTemperatures.first { $0.key.lowercased().hasSuffix( "tcal" ) } - return all.filter + return allTemperatures.filter { if $0.key.lowercased().hasSuffix( "tcal" ) { @@ -186,6 +190,28 @@ public class ThermalLog: NSObject self.temperature = NSNumber( value: temp ) } + let fanSpeeds: [ Double ] = + { + return allFans.filter + {_ in + true + } + .values.map { $0 } + }() + + let fanSpeed: Double = + { + return fanSpeeds.reduce( 0.0 ) + { + r, v in max( r, v ) + } + }() + + if !fanSpeed.isNaN + { + self.fanSpeed = NSNumber( value: fanSpeed ) + } + let pipe = Pipe() let task = Process() task.launchPath = "/usr/bin/pmset" diff --git a/Hot/UI/Base.lproj/FanViewController.xib b/Hot/UI/Base.lproj/FanViewController.xib new file mode 100644 index 0000000..0738d3b --- /dev/null +++ b/Hot/UI/Base.lproj/FanViewController.xib @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FanSpeedToString + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hot/UI/Base.lproj/InfoViewController.xib b/Hot/UI/Base.lproj/InfoViewController.xib index 29fe902..23a9abb 100644 --- a/Hot/UI/Base.lproj/InfoViewController.xib +++ b/Hot/UI/Base.lproj/InfoViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -16,13 +16,13 @@ - + - + - + @@ -71,7 +71,7 @@ - + @@ -120,7 +120,7 @@ - + @@ -169,7 +169,7 @@ - + @@ -222,7 +222,7 @@ - + @@ -267,6 +267,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FanSpeedToString + + + + + + + + + + + + + + + + @@ -286,6 +332,7 @@ + @@ -294,6 +341,7 @@ + @@ -308,6 +356,7 @@ + diff --git a/Hot/UI/Base.lproj/MainMenu.xib b/Hot/UI/Base.lproj/MainMenu.xib index 175f552..be210de 100644 --- a/Hot/UI/Base.lproj/MainMenu.xib +++ b/Hot/UI/Base.lproj/MainMenu.xib @@ -2,7 +2,7 @@ - + @@ -14,6 +14,7 @@ + @@ -711,6 +712,23 @@ + + + + + + + + + + + + + NSNegateBoolean + + + + diff --git a/Hot/UI/GraphWindowController.xib b/Hot/UI/GraphWindowController.xib index e84ab69..c715d4f 100644 --- a/Hot/UI/GraphWindowController.xib +++ b/Hot/UI/GraphWindowController.xib @@ -1,8 +1,8 @@ - + - + @@ -18,7 +18,7 @@ - + @@ -27,26 +27,26 @@ - + - + - + @@ -95,7 +95,7 @@ - + @@ -144,7 +144,7 @@ - + @@ -197,7 +197,7 @@ - + @@ -242,6 +242,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FanSpeedToString + + + + + + + + + + + + + + + + @@ -252,12 +298,14 @@ + + @@ -277,6 +325,7 @@ + diff --git a/Hot/UI/Images/FanTemplate.pdf b/Hot/UI/Images/FanTemplate.pdf new file mode 100644 index 0000000..574fed8 Binary files /dev/null and b/Hot/UI/Images/FanTemplate.pdf differ